From e2b943799a39fc4593a577b28bf6dd348ab15a1d Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 11 Aug 2022 13:35:59 +0300 Subject: [PATCH 001/806] feat: Initial support for parsing MySQL show variables (#559) Co-authored-by: Alex Vasilev --- src/ast/mod.rs | 11 +++++++++++ src/keywords.rs | 1 + src/parser.rs | 7 +++++++ tests/sqlparser_mysql.rs | 7 +++++++ 4 files changed, 26 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 312a86813..e45ebfcd8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -996,6 +996,10 @@ pub enum Statement { /// /// Note: this is a PostgreSQL-specific statement. ShowVariable { variable: Vec }, + /// SHOW VARIABLES + /// + /// Note: this is a MySQL-specific statement. + ShowVariables { filter: Option }, /// SHOW CREATE TABLE /// /// Note: this is a MySQL-specific statement. @@ -1787,6 +1791,13 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::ShowVariables { filter } => { + write!(f, "SHOW VARIABLES")?; + if filter.is_some() { + write!(f, " {}", filter.as_ref().unwrap())?; + } + Ok(()) + } Statement::ShowCreate { obj_type, obj_name } => { write!( f, diff --git a/src/keywords.rs b/src/keywords.rs index 7643298e4..af8bf2f5e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -543,6 +543,7 @@ define_keywords!( VALUE_OF, VARBINARY, VARCHAR, + VARIABLES, VARYING, VAR_POP, VAR_SAMP, diff --git a/src/parser.rs b/src/parser.rs index 76190feb9..a72529b14 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3694,6 +3694,13 @@ impl<'a> Parser<'a> { Ok(self.parse_show_columns()?) } else if self.parse_one_of_keywords(&[Keyword::CREATE]).is_some() { Ok(self.parse_show_create()?) + } else if self.parse_keyword(Keyword::VARIABLES) + && dialect_of!(self is MySqlDialect | GenericDialect) + { + // TODO: Support GLOBAL|SESSION + Ok(Statement::ShowVariables { + filter: self.parse_show_statement_filter()?, + }) } else { Ok(Statement::ShowVariable { variable: self.parse_identifiers()?, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b3c02de30..004b2c876 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -820,6 +820,13 @@ fn parse_substring_in_select() { } } +#[test] +fn parse_show_variables() { + mysql_and_generic().verified_stmt("SHOW VARIABLES"); + mysql_and_generic().verified_stmt("SHOW VARIABLES LIKE 'admin%'"); + mysql_and_generic().verified_stmt("SHOW VARIABLES WHERE value = '3306'"); +} + #[test] fn parse_kill() { let stmt = mysql_and_generic().verified_stmt("KILL CONNECTION 5"); From 71c3ec057b4701363ebecff0f2ac34b98e456f63 Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Thu, 11 Aug 2022 14:42:08 +0400 Subject: [PATCH 002/806] Support `SHOW COLUMNS FROM tbl FROM db` (#562) --- src/parser.rs | 14 ++++++++++---- tests/sqlparser_mysql.rs | 10 ++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index a72529b14..e0324182e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3739,10 +3739,16 @@ impl<'a> Parser<'a> { let full = self.parse_keyword(Keyword::FULL); self.expect_one_of_keywords(&[Keyword::COLUMNS, Keyword::FIELDS])?; self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; - let table_name = self.parse_object_name()?; - // MySQL also supports FROM here. In other words, MySQL - // allows both FROM FROM and FROM .
, - // while we only support the latter for now. + let object_name = self.parse_object_name()?; + let table_name = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { + Some(_) => { + let db_name = vec![self.parse_identifier()?]; + let ObjectName(table_name) = object_name; + let object_name = db_name.into_iter().chain(table_name.into_iter()).collect(); + ObjectName(object_name) + } + None => object_name, + }; let filter = self.parse_show_statement_filter()?; Ok(Statement::ShowColumns { extended, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 004b2c876..9ac775bdb 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -110,12 +110,10 @@ fn parse_show_columns() { .one_statement_parses_to("SHOW COLUMNS IN mytable", "SHOW COLUMNS FROM mytable"); mysql_and_generic() .one_statement_parses_to("SHOW FIELDS IN mytable", "SHOW COLUMNS FROM mytable"); - - // unhandled things are truly unhandled - match mysql_and_generic().parse_sql_statements("SHOW COLUMNS FROM mytable FROM mydb") { - Err(_) => {} - Ok(val) => panic!("unexpected successful parse: {:?}", val), - } + mysql_and_generic().one_statement_parses_to( + "SHOW COLUMNS FROM mytable FROM mydb", + "SHOW COLUMNS FROM mydb.mytable", + ); } #[test] From a9db6ed1395066c500ba2a8eddb4f5cca736a96a Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Thu, 11 Aug 2022 14:49:20 +0400 Subject: [PATCH 003/806] Support `USE db` (#565) --- src/ast/mod.rs | 8 ++++++++ src/keywords.rs | 1 + src/parser.rs | 6 ++++++ tests/sqlparser_mysql.rs | 10 ++++++++++ 4 files changed, 25 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e45ebfcd8..97b647bc9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1016,6 +1016,10 @@ pub enum Statement { table_name: ObjectName, filter: Option, }, + /// USE + /// + /// Note: This is a MySQL-specific statement. + Use { db_name: Ident }, /// `{ BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...` StartTransaction { modes: Vec }, /// `SET TRANSACTION ...` @@ -1825,6 +1829,10 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::Use { db_name } => { + write!(f, "USE {}", db_name)?; + Ok(()) + } Statement::StartTransaction { modes } => { write!(f, "START TRANSACTION")?; if !modes.is_empty() { diff --git a/src/keywords.rs b/src/keywords.rs index af8bf2f5e..7eb6b420c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -535,6 +535,7 @@ define_keywords!( UPDATE, UPPER, USAGE, + USE, USER, USING, UUID, diff --git a/src/parser.rs b/src/parser.rs index e0324182e..c59c88e8e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -177,6 +177,7 @@ impl<'a> Parser<'a> { Keyword::CLOSE => Ok(self.parse_close()?), Keyword::SET => Ok(self.parse_set()?), Keyword::SHOW => Ok(self.parse_show()?), + Keyword::USE => Ok(self.parse_use()?), Keyword::GRANT => Ok(self.parse_grant()?), Keyword::REVOKE => Ok(self.parse_revoke()?), Keyword::START => Ok(self.parse_start_transaction()?), @@ -3776,6 +3777,11 @@ impl<'a> Parser<'a> { } } + pub fn parse_use(&mut self) -> Result { + let db_name = self.parse_identifier()?; + Ok(Statement::Use { db_name }) + } + pub fn parse_table_and_joins(&mut self) -> Result { let relation = self.parse_table_factor()?; // Note that for keywords to be properly handled here, they need to be diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 9ac775bdb..ab28b879b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -138,6 +138,16 @@ fn parse_show_create() { } } +#[test] +fn parse_use() { + assert_eq!( + mysql_and_generic().verified_stmt("USE mydb"), + Statement::Use { + db_name: Ident::new("mydb") + } + ); +} + #[test] fn parse_create_table_auto_increment() { let sql = "CREATE TABLE foo (bar INT PRIMARY KEY AUTO_INCREMENT)"; From 8176561100d2279f3415534fb39d1f9330de7cc5 Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Thu, 11 Aug 2022 14:50:18 +0400 Subject: [PATCH 004/806] Support expressions in `LIMIT`/`OFFSET` (#567) --- src/parser.rs | 4 ++-- tests/sqlparser_common.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index c59c88e8e..be705efc6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4421,13 +4421,13 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::ALL) { Ok(None) } else { - Ok(Some(Expr::Value(self.parse_number_value()?))) + Ok(Some(self.parse_expr()?)) } } /// Parse an OFFSET clause pub fn parse_offset(&mut self) -> Result { - let value = Expr::Value(self.parse_number_value()?); + let value = self.parse_expr()?; let rows = if self.parse_keyword(Keyword::ROW) { OffsetRows::Row } else if self.parse_keyword(Keyword::ROWS) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ef6011841..0f818e5fc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5010,6 +5010,20 @@ fn test_placeholder() { right: Box::new(Expr::Value(Value::Placeholder("$Id1".into()))) }) ); + + 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()))) + ); + assert_eq!( + ast.offset, + Some(Offset { + value: Expr::Value(Value::Placeholder("$2".into())), + rows: OffsetRows::None, + }), + ); } #[test] @@ -5058,6 +5072,29 @@ fn parse_offset_and_limit() { // different order is OK one_statement_parses_to("SELECT foo FROM bar OFFSET 2 LIMIT 2", sql); + // 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 { + left: Box::new(Expr::Value(number("1"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Value(number("2"))), + }), + ); + assert_eq!( + ast.offset, + Some(Offset { + value: Expr::BinaryOp { + left: Box::new(Expr::Value(number("3"))), + op: BinaryOperator::Multiply, + right: Box::new(Expr::Value(number("4"))), + }, + rows: OffsetRows::None, + }), + ); + // Can't repeat OFFSET / LIMIT let res = parse_sql_statements("SELECT foo FROM bar OFFSET 2 OFFSET 2"); assert_eq!( From aabafc9fc8a1bb3153d8c7b1bf04a09eb4ae3c70 Mon Sep 17 00:00:00 2001 From: Yoshiyuki Komazaki Date: Thu, 11 Aug 2022 19:54:04 +0900 Subject: [PATCH 005/806] feat: Support trailing commas (#557) --- src/parser.rs | 29 ++++++++++++++++++++++++++++- tests/sqlparser_bigquery.rs | 15 +++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index be705efc6..50f8a2d53 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1639,6 +1639,33 @@ impl<'a> Parser<'a> { } } + /// Parse a comma-separated list of 1+ SelectItem + pub fn parse_projection(&mut self) -> Result, ParserError> { + let mut values = vec![]; + loop { + values.push(self.parse_select_item()?); + if !self.consume_token(&Token::Comma) { + break; + } else if dialect_of!(self is BigQueryDialect) { + // BigQuery allows trailing commas. + // e.g. `SELECT 1, 2, FROM t` + // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#trailing_commas + match self.peek_token() { + Token::Word(kw) + if keywords::RESERVED_FOR_COLUMN_ALIAS + .iter() + .any(|d| kw.keyword == *d) => + { + break + } + Token::RParen | Token::EOF => break, + _ => continue, + } + } + } + Ok(values) + } + /// Parse a comma-separated list of 1+ items accepted by `F` pub fn parse_comma_separated(&mut self, mut f: F) -> Result, ParserError> where @@ -3486,7 +3513,7 @@ impl<'a> Parser<'a> { None }; - let projection = self.parse_comma_separated(Parser::parse_select_item)?; + let projection = self.parse_projection()?; let into = if self.parse_keyword(Keyword::INTO) { let temporary = self diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index d866fc2f5..0a606c3ec 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -94,6 +94,21 @@ fn parse_table_identifiers() { test_table_ident("abc5.GROUP", vec![Ident::new("abc5"), Ident::new("GROUP")]); } +#[test] +fn parse_trailing_comma() { + for (sql, canonical) in [ + ("SELECT a,", "SELECT a"), + ("SELECT a, b,", "SELECT a, b"), + ("SELECT a, b AS c,", "SELECT a, b AS c"), + ("SELECT a, b AS c, FROM t", "SELECT a, b AS c FROM t"), + ("SELECT a, b, FROM t", "SELECT a, b FROM t"), + ("SELECT a, b, LIMIT 1", "SELECT a, b LIMIT 1"), + ("SELECT a, (SELECT 1, )", "SELECT a, (SELECT 1)"), + ] { + bigquery().one_statement_parses_to(sql, canonical); + } +} + #[test] fn parse_cast_type() { let sql = r#"SELECT SAFE_CAST(1 AS INT64)"#; From 221e9c2bab18319119256460f40a37957e13d118 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 11 Aug 2022 13:55:55 +0300 Subject: [PATCH 006/806] feat: Support SET NAMES literal [COLLATE literal] (#558) * feat: Support SET NAMES literal [COLLATE literal] * feat: Support SET NAMES DEFAULT * clippy --- src/ast/mod.rs | 30 ++++++++++++++++++++++++++++++ src/parser.rs | 20 +++++++++++++++++++- tests/sqlparser_mysql.rs | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 97b647bc9..86c0f2e92 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -992,6 +992,17 @@ pub enum Statement { variable: ObjectName, value: Vec, }, + /// SET NAMES 'charset_name' [COLLATE 'collation_name'] + /// + /// Note: this is a MySQL-specific statement. + SetNames { + charset_name: String, + collation_name: Option, + }, + /// SET NAMES DEFAULT + /// + /// Note: this is a MySQL-specific statement. + SetNamesDefault {}, /// SHOW /// /// Note: this is a PostgreSQL-specific statement. @@ -1788,6 +1799,25 @@ impl fmt::Display for Statement { value = display_comma_separated(value) ) } + Statement::SetNames { + charset_name, + collation_name, + } => { + f.write_str("SET NAMES ")?; + f.write_str(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(()) + } Statement::ShowVariable { variable } => { write!(f, "SHOW")?; if !variable.is_empty() { diff --git a/src/parser.rs b/src/parser.rs index 50f8a2d53..5bea35e79 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3652,7 +3652,25 @@ impl<'a> Parser<'a> { } let variable = self.parse_object_name()?; - if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { + if variable.to_string().eq_ignore_ascii_case("NAMES") + && dialect_of!(self is MySqlDialect | GenericDialect) + { + if self.parse_keyword(Keyword::DEFAULT) { + return Ok(Statement::SetNamesDefault {}); + } + + let charset_name = self.parse_literal_string()?; + let collation_name = if self.parse_one_of_keywords(&[Keyword::COLLATE]).is_some() { + Some(self.parse_literal_string()?) + } else { + None + }; + + Ok(Statement::SetNames { + charset_name, + collation_name, + }) + } else if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { let mut values = vec![]; loop { let token = self.peek_token(); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ab28b879b..a514b1e8e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -890,6 +890,41 @@ fn parse_table_colum_option_on_update() { } } +#[test] +fn parse_set_names() { + let stmt = mysql_and_generic().verified_stmt("SET NAMES utf8mb4"); + assert_eq!( + stmt, + Statement::SetNames { + charset_name: "utf8mb4".to_string(), + collation_name: None, + } + ); + + let stmt = mysql_and_generic().verified_stmt("SET NAMES utf8mb4 COLLATE bogus"); + assert_eq!( + stmt, + Statement::SetNames { + charset_name: "utf8mb4".to_string(), + collation_name: Some("bogus".to_string()), + } + ); + + let stmt = mysql_and_generic() + .parse_sql_statements("set names utf8mb4 collate bogus") + .unwrap(); + assert_eq!( + stmt, + vec![Statement::SetNames { + charset_name: "utf8mb4".to_string(), + collation_name: Some("bogus".to_string()), + }] + ); + + let stmt = mysql_and_generic().verified_stmt("SET NAMES DEFAULT"); + assert_eq!(stmt, Statement::SetNamesDefault {}); +} + fn mysql() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})], From b6e36ad760292de29b299e90779cfdb57fb1dc58 Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Thu, 11 Aug 2022 16:24:40 +0400 Subject: [PATCH 007/806] Support `SHOW TABLES` (#563) --- src/ast/mod.rs | 29 ++++++++++++++++ src/parser.rs | 45 +++++++++++++++++------- tests/sqlparser_mysql.rs | 75 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 86c0f2e92..eeead0de0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1027,6 +1027,15 @@ pub enum Statement { table_name: ObjectName, filter: Option, }, + /// SHOW TABLES + /// + /// Note: this is a MySQL-specific statement. + ShowTables { + extended: bool, + full: bool, + db_name: Option, + filter: Option, + }, /// USE /// /// Note: This is a MySQL-specific statement. @@ -1859,6 +1868,26 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::ShowTables { + extended, + full, + db_name, + filter, + } => { + write!( + f, + "SHOW {extended}{full}TABLES", + extended = if *extended { "EXTENDED " } else { "" }, + full = if *full { "FULL " } else { "" }, + )?; + if let Some(db_name) = db_name { + write!(f, " FROM {}", db_name)?; + } + if let Some(filter) = filter { + write!(f, " {}", filter)?; + } + Ok(()) + } Statement::Use { db_name } => { write!(f, "USE {}", db_name)?; Ok(()) diff --git a/src/parser.rs b/src/parser.rs index 5bea35e79..412d3eb0a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3727,17 +3727,19 @@ impl<'a> Parser<'a> { } pub fn parse_show(&mut self) -> Result { + let extended = self.parse_keyword(Keyword::EXTENDED); + let full = self.parse_keyword(Keyword::FULL); if self - .parse_one_of_keywords(&[ - Keyword::EXTENDED, - Keyword::FULL, - Keyword::COLUMNS, - Keyword::FIELDS, - ]) + .parse_one_of_keywords(&[Keyword::COLUMNS, Keyword::FIELDS]) .is_some() { - self.prev_token(); - Ok(self.parse_show_columns()?) + Ok(self.parse_show_columns(extended, full)?) + } else if self.parse_keyword(Keyword::TABLES) { + Ok(self.parse_show_tables(extended, full)?) + } else if extended || full { + Err(ParserError::ParserError( + "EXTENDED/FULL are not supported with this type of SHOW query".to_string(), + )) } else if self.parse_one_of_keywords(&[Keyword::CREATE]).is_some() { Ok(self.parse_show_create()?) } else if self.parse_keyword(Keyword::VARIABLES) @@ -3780,10 +3782,11 @@ impl<'a> Parser<'a> { Ok(Statement::ShowCreate { obj_type, obj_name }) } - pub fn parse_show_columns(&mut self) -> Result { - let extended = self.parse_keyword(Keyword::EXTENDED); - let full = self.parse_keyword(Keyword::FULL); - self.expect_one_of_keywords(&[Keyword::COLUMNS, Keyword::FIELDS])?; + pub fn parse_show_columns( + &mut self, + extended: bool, + full: bool, + ) -> Result { self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; let object_name = self.parse_object_name()?; let table_name = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { @@ -3804,6 +3807,24 @@ impl<'a> Parser<'a> { }) } + pub fn parse_show_tables( + &mut self, + extended: bool, + full: bool, + ) -> Result { + let db_name = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { + Some(_) => Some(self.parse_identifier()?), + None => None, + }; + let filter = self.parse_show_statement_filter()?; + Ok(Statement::ShowTables { + extended, + full, + db_name, + filter, + }) + } + pub fn parse_show_statement_filter( &mut self, ) -> Result, ParserError> { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a514b1e8e..a72ffbb07 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -116,6 +116,81 @@ fn parse_show_columns() { ); } +#[test] +fn parse_show_tables() { + assert_eq!( + mysql_and_generic().verified_stmt("SHOW TABLES"), + Statement::ShowTables { + extended: false, + full: false, + db_name: None, + filter: None, + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW TABLES FROM mydb"), + Statement::ShowTables { + extended: false, + full: false, + db_name: Some(Ident::new("mydb")), + filter: None, + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW EXTENDED TABLES"), + Statement::ShowTables { + extended: true, + full: false, + db_name: None, + filter: None, + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW FULL TABLES"), + Statement::ShowTables { + extended: false, + full: true, + db_name: None, + filter: None, + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW TABLES LIKE 'pattern'"), + Statement::ShowTables { + extended: false, + full: false, + db_name: None, + filter: Some(ShowStatementFilter::Like("pattern".into())), + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW TABLES WHERE 1 = 2"), + Statement::ShowTables { + extended: false, + full: false, + db_name: None, + filter: Some(ShowStatementFilter::Where( + mysql_and_generic().verified_expr("1 = 2") + )), + } + ); + mysql_and_generic().one_statement_parses_to("SHOW TABLES IN mydb", "SHOW TABLES FROM mydb"); +} + +#[test] +fn parse_show_extended_full() { + assert!(mysql_and_generic() + .parse_sql_statements("SHOW EXTENDED FULL TABLES") + .is_ok()); + assert!(mysql_and_generic() + .parse_sql_statements("SHOW EXTENDED FULL COLUMNS FROM mytable") + .is_ok()); + // SHOW EXTENDED/FULL can only be used with COLUMNS and TABLES + assert!(mysql_and_generic() + .parse_sql_statements("SHOW EXTENDED FULL CREATE TABLE mytable") + .is_err()); +} + #[test] fn parse_show_create() { let obj_name = ObjectName(vec![Ident::new("myident")]); From 54a29e872d2f752daa48ce0ede5d48ea682ef0cc Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 11 Aug 2022 15:30:06 +0300 Subject: [PATCH 008/806] feat: Parse special keywords as functions (current_user, user, etc) (#561) * feat: Parse special keywors as functions (current_user, user, etc) * explain special field --- src/ast/mod.rs | 27 +++++++++++++------- src/parser.rs | 16 ++++++++++++ tests/sqlparser_common.rs | 19 ++++++++++++-- tests/sqlparser_mysql.rs | 15 +++++++---- tests/sqlparser_postgres.rs | 47 +++++++++++++++++++++++++++++++++++ tests/sqpparser_clickhouse.rs | 4 ++- 6 files changed, 111 insertions(+), 17 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eeead0de0..5e8296f88 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2321,20 +2321,29 @@ pub struct Function { pub over: Option, // aggregate functions may specify eg `COUNT(DISTINCT x)` pub distinct: bool, + // Some functions must be called without trailing parentheses, for example Postgres + // do it for current_catalog, current_schema, etc. This flags is used for formatting. + pub special: bool, } impl fmt::Display for Function { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}({}{})", - self.name, - if self.distinct { "DISTINCT " } else { "" }, - display_comma_separated(&self.args), - )?; - if let Some(o) = &self.over { - write!(f, " OVER ({})", o)?; + if self.special { + write!(f, "{}", self.name)?; + } else { + write!( + f, + "{}({}{})", + self.name, + if self.distinct { "DISTINCT " } else { "" }, + display_comma_separated(&self.args), + )?; + + if let Some(o) = &self.over { + write!(f, " OVER ({})", o)?; + } } + Ok(()) } } diff --git a/src/parser.rs b/src/parser.rs index 412d3eb0a..11f14ecf4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -421,6 +421,20 @@ impl<'a> Parser<'a> { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } + Keyword::CURRENT_CATALOG + | Keyword::CURRENT_USER + | Keyword::SESSION_USER + | Keyword::USER + if dialect_of!(self is PostgreSqlDialect | GenericDialect) => + { + Ok(Expr::Function(Function { + name: ObjectName(vec![w.to_ident()]), + args: vec![], + over: None, + distinct: false, + special: true, + })) + } Keyword::CURRENT_TIMESTAMP | Keyword::CURRENT_TIME | Keyword::CURRENT_DATE => { self.parse_time_functions(ObjectName(vec![w.to_ident()])) } @@ -598,6 +612,7 @@ impl<'a> Parser<'a> { args, over, distinct, + special: false, })) } @@ -612,6 +627,7 @@ impl<'a> Parser<'a> { args, over: None, distinct: false, + special: false, })) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0f818e5fc..27b8879bf 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -572,6 +572,7 @@ fn parse_select_count_wildcard() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], over: None, distinct: false, + special: false, }), expr_from_projection(only(&select.projection)) ); @@ -590,6 +591,7 @@ fn parse_select_count_distinct() { }))], over: None, distinct: true, + special: false, }), expr_from_projection(only(&select.projection)) ); @@ -1414,6 +1416,7 @@ fn parse_select_having() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], over: None, distinct: false, + special: false, })), op: BinaryOperator::Gt, right: Box::new(Expr::Value(number("1"))) @@ -1445,7 +1448,8 @@ fn parse_select_qualify() { }], window_frame: None }), - distinct: false + distinct: false, + special: false })), op: BinaryOperator::Eq, right: Box::new(Expr::Value(number("1"))) @@ -2532,6 +2536,7 @@ fn parse_scalar_function_in_projection() { ))], over: None, distinct: false, + special: false, }), expr_from_projection(only(&select.projection)) ); @@ -2610,6 +2615,7 @@ fn parse_named_argument_function() { ], over: None, distinct: false, + special: false, }), expr_from_projection(only(&select.projection)) ); @@ -2643,6 +2649,7 @@ fn parse_window_functions() { window_frame: None, }), distinct: false, + special: false, }), expr_from_projection(&select.projection[0]) ); @@ -2906,7 +2913,8 @@ fn parse_at_timezone() { }]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero.clone()))], over: None, - distinct: false + distinct: false, + special: false, })), time_zone: "UTC-06:00".to_string() }, @@ -2932,6 +2940,7 @@ fn parse_at_timezone() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero,),),], over: None, distinct: false, + special: false },)), time_zone: "UTC-06:00".to_string(), },),), @@ -2941,6 +2950,7 @@ fn parse_at_timezone() { ], over: None, distinct: false, + special: false },), alias: Ident { value: "hour".to_string(), @@ -2976,6 +2986,7 @@ fn parse_table_function() { )))], over: None, distinct: false, + special: false, }); assert_eq!(expr, expected_expr); assert_eq!(alias, table_alias("a")) @@ -3148,6 +3159,7 @@ fn parse_delimited_identifiers() { args: vec![], over: None, distinct: false, + special: false, }), expr_from_projection(&select.projection[1]), ); @@ -5125,6 +5137,7 @@ fn parse_time_functions() { args: vec![], over: None, distinct: false, + special: false, }), expr_from_projection(&select.projection[0]) ); @@ -5140,6 +5153,7 @@ fn parse_time_functions() { args: vec![], over: None, distinct: false, + special: false, }), expr_from_projection(&select.projection[0]) ); @@ -5155,6 +5169,7 @@ fn parse_time_functions() { args: vec![], over: None, distinct: false, + special: false, }), expr_from_projection(&select.projection[0]) ); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a72ffbb07..16d0daf8d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -686,7 +686,8 @@ fn parse_insert_with_on_duplicate_update() { Expr::Identifier(Ident::new("description")) ))], over: None, - distinct: false + distinct: false, + special: false, }) }, Assignment { @@ -697,7 +698,8 @@ fn parse_insert_with_on_duplicate_update() { Expr::Identifier(Ident::new("perm_create")) ))], over: None, - distinct: false + distinct: false, + special: false, }) }, Assignment { @@ -708,7 +710,8 @@ fn parse_insert_with_on_duplicate_update() { Expr::Identifier(Ident::new("perm_read")) ))], over: None, - distinct: false + distinct: false, + special: false, }) }, Assignment { @@ -719,7 +722,8 @@ fn parse_insert_with_on_duplicate_update() { Expr::Identifier(Ident::new("perm_update")) ))], over: None, - distinct: false + distinct: false, + special: false, }) }, Assignment { @@ -730,7 +734,8 @@ fn parse_insert_with_on_duplicate_update() { Expr::Identifier(Ident::new("perm_delete")) ))], over: None, - distinct: false + distinct: false, + special: false, }) }, ])), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 632a8bf34..d227df626 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1400,6 +1400,7 @@ fn test_composite_value() { )))], over: None, distinct: false, + special: false })))) }), select.projection[0] @@ -1542,6 +1543,52 @@ fn parse_declare() { pg_and_generic().verified_stmt("DECLARE \"SQL_CUR0x7fa44801bc00\" BINARY INSENSITIVE SCROLL CURSOR WITH HOLD FOR SELECT * FROM table_name LIMIT 2222"); } +#[test] +fn parse_current_functions() { + let sql = "SELECT CURRENT_CATALOG, CURRENT_USER, SESSION_USER, USER"; + let select = pg_and_generic().verified_only_select(sql); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::new("CURRENT_CATALOG")]), + args: vec![], + over: None, + distinct: false, + special: true, + }), + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::new("CURRENT_USER")]), + args: vec![], + over: None, + distinct: false, + special: true, + }), + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::new("SESSION_USER")]), + args: vec![], + over: None, + distinct: false, + special: true, + }), + expr_from_projection(&select.projection[2]) + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::new("USER")]), + args: vec![], + over: None, + distinct: false, + special: true, + }), + expr_from_projection(&select.projection[3]) + ); +} + #[test] fn parse_fetch() { pg_and_generic().verified_stmt("FETCH 2048 IN \"SQL_CUR0x7fa44801bc00\""); diff --git a/tests/sqpparser_clickhouse.rs b/tests/sqpparser_clickhouse.rs index 72a1a0556..af0f59fe6 100644 --- a/tests/sqpparser_clickhouse.rs +++ b/tests/sqpparser_clickhouse.rs @@ -51,6 +51,7 @@ fn parse_map_access_expr() { ], over: None, distinct: false, + special: false, })], })], into: None, @@ -85,7 +86,8 @@ fn parse_map_access_expr() { ))), ], over: None, - distinct: false + distinct: false, + special: false, })] }), op: BinaryOperator::NotEq, From 18881f8fcf611cb5dabfacf4d1b76c680c846b81 Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Thu, 11 Aug 2022 17:28:34 +0400 Subject: [PATCH 009/806] Support `SHOW COLLATION` (#564) --- src/ast/mod.rs | 11 +++++++++++ src/keywords.rs | 1 + src/parser.rs | 7 +++++++ tests/sqlparser_mysql.rs | 28 ++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5e8296f88..99b93b25c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1036,6 +1036,10 @@ pub enum Statement { db_name: Option, filter: Option, }, + /// SHOW COLLATION + /// + /// Note: this is a MySQL-specific statement. + ShowCollation { filter: Option }, /// USE /// /// Note: This is a MySQL-specific statement. @@ -1892,6 +1896,13 @@ impl fmt::Display for Statement { write!(f, "USE {}", db_name)?; Ok(()) } + Statement::ShowCollation { filter } => { + write!(f, "SHOW COLLATION")?; + if let Some(filter) = filter { + write!(f, " {}", filter)?; + } + Ok(()) + } Statement::StartTransaction { modes } => { write!(f, "START TRANSACTION")?; if !modes.is_empty() { diff --git a/src/keywords.rs b/src/keywords.rs index 7eb6b420c..a06ddc48d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -131,6 +131,7 @@ define_keywords!( CLUSTER, COALESCE, COLLATE, + COLLATION, COLLECT, COLUMN, COLUMNS, diff --git a/src/parser.rs b/src/parser.rs index 11f14ecf4..784010efb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3758,6 +3758,8 @@ impl<'a> Parser<'a> { )) } else if self.parse_one_of_keywords(&[Keyword::CREATE]).is_some() { Ok(self.parse_show_create()?) + } else if self.parse_keyword(Keyword::COLLATION) { + Ok(self.parse_show_collation()?) } else if self.parse_keyword(Keyword::VARIABLES) && dialect_of!(self is MySqlDialect | GenericDialect) { @@ -3841,6 +3843,11 @@ impl<'a> Parser<'a> { }) } + pub fn parse_show_collation(&mut self) -> Result { + let filter = self.parse_show_statement_filter()?; + Ok(Statement::ShowCollation { filter }) + } + pub fn parse_show_statement_filter( &mut self, ) -> Result, ParserError> { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 16d0daf8d..c1cfa2876 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -189,6 +189,12 @@ fn parse_show_extended_full() { assert!(mysql_and_generic() .parse_sql_statements("SHOW EXTENDED FULL CREATE TABLE mytable") .is_err()); + assert!(mysql_and_generic() + .parse_sql_statements("SHOW EXTENDED FULL COLLATION") + .is_err()); + assert!(mysql_and_generic() + .parse_sql_statements("SHOW EXTENDED FULL VARIABLES") + .is_err()); } #[test] @@ -213,6 +219,28 @@ fn parse_show_create() { } } +#[test] +fn parse_show_collation() { + assert_eq!( + mysql_and_generic().verified_stmt("SHOW COLLATION"), + Statement::ShowCollation { filter: None } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW COLLATION LIKE 'pattern'"), + Statement::ShowCollation { + filter: Some(ShowStatementFilter::Like("pattern".into())), + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW COLLATION WHERE 1 = 2"), + Statement::ShowCollation { + filter: Some(ShowStatementFilter::Where( + mysql_and_generic().verified_expr("1 = 2") + )), + } + ); +} + #[test] fn parse_use() { assert_eq!( From f07063f0cdf06c56df7c3dd65f9062a2ce439c1c Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Thu, 11 Aug 2022 12:44:26 -0700 Subject: [PATCH 010/806] Support for `SIMILAR TO` syntax, change `Like` and `ILike` to `Expr` variants, allow escape char for like/ilike (#569) * Remove [not]like,[not]ilike from binary operator list * Add like, ilike and similar as an expr variant. Also adds support for escape char to like/ilike * Add parsing logic for similar to, update parsing logic for like/ilike * Add tests for similar to, update tests for like/ilike * Fix linter warnings * remove additional copyright license from files * Add more coverage w/wo escape char for like,ilike,similar to --- src/ast/mod.rs | 87 ++++++++++++++++++++++ src/ast/operator.rs | 8 -- src/parser.rs | 50 ++++++++++--- tests/sqlparser_common.rs | 149 ++++++++++++++++++++++++++++---------- 4 files changed, 237 insertions(+), 57 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 99b93b25c..aa8118033 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -268,6 +268,27 @@ pub enum Expr { op: BinaryOperator, right: Box, }, + /// LIKE + Like { + negated: bool, + expr: Box, + pattern: Box, + escape_char: Option, + }, + /// ILIKE (case-insensitive LIKE) + ILike { + negated: bool, + expr: Box, + pattern: Box, + escape_char: Option, + }, + /// SIMILAR TO regex + SimilarTo { + negated: bool, + expr: Box, + pattern: Box, + escape_char: Option, + }, /// Any operation e.g. `1 ANY (1)` or `foo > ANY(bar)`, It will be wrapped in the right side of BinaryExpr AnyOp(Box), /// ALL operation e.g. `1 ALL (1)` or `foo > ALL(bar)`, It will be wrapped in the right side of BinaryExpr @@ -438,6 +459,72 @@ impl fmt::Display for Expr { high ), Expr::BinaryOp { left, op, right } => write!(f, "{} {} {}", left, op, right), + Expr::Like { + negated, + expr, + pattern, + escape_char, + } => match escape_char { + Some(ch) => write!( + f, + "{} {}LIKE {} ESCAPE '{}'", + expr, + if *negated { "NOT " } else { "" }, + pattern, + ch + ), + _ => write!( + f, + "{} {}LIKE {}", + expr, + if *negated { "NOT " } else { "" }, + pattern + ), + }, + Expr::ILike { + negated, + expr, + pattern, + escape_char, + } => match escape_char { + Some(ch) => write!( + f, + "{} {}ILIKE {} ESCAPE '{}'", + expr, + if *negated { "NOT " } else { "" }, + pattern, + ch + ), + _ => write!( + f, + "{} {}ILIKE {}", + expr, + if *negated { "NOT " } else { "" }, + pattern + ), + }, + Expr::SimilarTo { + negated, + expr, + pattern, + escape_char, + } => match escape_char { + Some(ch) => write!( + f, + "{} {}SIMILAR TO {} ESCAPE '{}'", + expr, + if *negated { "NOT " } else { "" }, + pattern, + ch + ), + _ => write!( + f, + "{} {}SIMILAR TO {}", + expr, + if *negated { "NOT " } else { "" }, + pattern + ), + }, Expr::AnyOp(expr) => write!(f, "ANY({})", expr), Expr::AllOp(expr) => write!(f, "ALL({})", expr), Expr::UnaryOp { op, expr } => { diff --git a/src/ast/operator.rs b/src/ast/operator.rs index f7a63a4a4..1c96ebbcb 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -76,10 +76,6 @@ pub enum BinaryOperator { And, Or, Xor, - Like, - NotLike, - ILike, - NotILike, BitwiseOr, BitwiseAnd, BitwiseXor, @@ -116,10 +112,6 @@ impl fmt::Display for BinaryOperator { BinaryOperator::And => f.write_str("AND"), BinaryOperator::Or => f.write_str("OR"), BinaryOperator::Xor => f.write_str("XOR"), - BinaryOperator::Like => f.write_str("LIKE"), - BinaryOperator::NotLike => f.write_str("NOT LIKE"), - BinaryOperator::ILike => f.write_str("ILIKE"), - BinaryOperator::NotILike => f.write_str("NOT ILIKE"), BinaryOperator::BitwiseOr => f.write_str("|"), BinaryOperator::BitwiseAnd => f.write_str("&"), BinaryOperator::BitwiseXor => f.write_str("^"), diff --git a/src/parser.rs b/src/parser.rs index 784010efb..4241c7667 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1178,17 +1178,6 @@ impl<'a> Parser<'a> { Token::Word(w) => match w.keyword { Keyword::AND => Some(BinaryOperator::And), Keyword::OR => Some(BinaryOperator::Or), - Keyword::LIKE => Some(BinaryOperator::Like), - Keyword::ILIKE => Some(BinaryOperator::ILike), - Keyword::NOT => { - if self.parse_keyword(Keyword::LIKE) { - Some(BinaryOperator::NotLike) - } else if self.parse_keyword(Keyword::ILIKE) { - Some(BinaryOperator::NotILike) - } else { - None - } - } Keyword::XOR => Some(BinaryOperator::Xor), Keyword::OPERATOR if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { self.expect_token(&Token::LParen)?; @@ -1282,13 +1271,39 @@ impl<'a> Parser<'a> { self.expected("Expected Token::Word after AT", tok) } } - Keyword::NOT | Keyword::IN | Keyword::BETWEEN => { + Keyword::NOT + | Keyword::IN + | Keyword::BETWEEN + | Keyword::LIKE + | Keyword::ILIKE + | Keyword::SIMILAR => { self.prev_token(); let negated = self.parse_keyword(Keyword::NOT); if self.parse_keyword(Keyword::IN) { self.parse_in(expr, negated) } else if self.parse_keyword(Keyword::BETWEEN) { self.parse_between(expr, negated) + } else if self.parse_keyword(Keyword::LIKE) { + Ok(Expr::Like { + negated, + expr: Box::new(expr), + pattern: Box::new(self.parse_value()?), + escape_char: self.parse_escape_char()?, + }) + } else if self.parse_keyword(Keyword::ILIKE) { + Ok(Expr::ILike { + negated, + expr: Box::new(expr), + pattern: Box::new(self.parse_value()?), + escape_char: self.parse_escape_char()?, + }) + } else if self.parse_keywords(&[Keyword::SIMILAR, Keyword::TO]) { + Ok(Expr::SimilarTo { + negated, + expr: Box::new(expr), + pattern: Box::new(self.parse_value()?), + escape_char: self.parse_escape_char()?, + }) } else { self.expected("IN or BETWEEN after NOT", self.peek_token()) } @@ -1333,6 +1348,15 @@ impl<'a> Parser<'a> { } } + /// parse the ESCAPE CHAR portion of LIKE, ILIKE, and SIMILAR TO + pub fn parse_escape_char(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::ESCAPE) { + Ok(Some(self.parse_literal_char()?)) + } else { + Ok(None) + } + } + pub fn parse_array_index(&mut self, expr: Expr) -> Result { let index = self.parse_expr()?; self.expect_token(&Token::RBracket)?; @@ -1463,6 +1487,7 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::BETWEEN_PREC), _ => Ok(0), }, Token::Word(w) if w.keyword == Keyword::IS => Ok(17), @@ -1471,6 +1496,7 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(Self::BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::BETWEEN_PREC), Token::Eq | Token::Lt | Token::LtEq diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 27b8879bf..2a6bcdfb3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -859,10 +859,11 @@ fn parse_not_precedence() { verified_expr(sql), Expr::UnaryOp { op: UnaryOperator::Not, - expr: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), - op: BinaryOperator::NotLike, - right: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), + expr: Box::new(Expr::Like { + expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), + negated: true, + pattern: Box::new(Value::SingleQuotedString("b".into())), + escape_char: None }), }, ); @@ -891,14 +892,27 @@ fn parse_like() { ); let select = verified_only_select(sql); assert_eq!( - Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("name"))), - op: if negated { - BinaryOperator::NotLike - } else { - BinaryOperator::Like - }, - right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('\\') }, select.selection.unwrap() ); @@ -911,14 +925,11 @@ fn parse_like() { ); let select = verified_only_select(sql); assert_eq!( - Expr::IsNull(Box::new(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("name"))), - op: if negated { - BinaryOperator::NotLike - } else { - BinaryOperator::Like - }, - right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None })), select.selection.unwrap() ); @@ -936,19 +947,32 @@ fn parse_ilike() { ); let select = verified_only_select(sql); assert_eq!( - Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("name"))), - op: if negated { - BinaryOperator::NotILike - } else { - BinaryOperator::ILike - }, - right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + Expr::ILike { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None }, select.selection.unwrap() ); - // This statement tests that LIKE and NOT LIKE have the same precedence. + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}ILIKE '%a' ESCAPE '^'", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::ILike { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('^') + }, + select.selection.unwrap() + ); + + // This statement tests that ILIKE and NOT ILIKE have the same precedence. // This was previously mishandled (#81). let sql = &format!( "SELECT * FROM customers WHERE name {}ILIKE '%a' IS NULL", @@ -956,14 +980,65 @@ fn parse_ilike() { ); let select = verified_only_select(sql); assert_eq!( - Expr::IsNull(Box::new(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("name"))), - op: if negated { - BinaryOperator::NotILike - } else { - BinaryOperator::ILike - }, - right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + Expr::IsNull(Box::new(Expr::ILike { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + 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(Value::SingleQuotedString("%a".to_string())), + escape_char: None + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + 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(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('\\') + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('\\') })), select.selection.unwrap() ); From 36de9a69c69a8a24e2b291640ca6693e77bd8015 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 11 Aug 2022 17:18:46 -0400 Subject: [PATCH 011/806] Update for new clippy ints (#571) --- src/parser.rs | 8 ++++---- src/tokenizer.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 4241c7667..4d5070d7a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -29,7 +29,7 @@ use crate::dialect::*; use crate::keywords::{self, Keyword}; use crate::tokenizer::*; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ParserError { TokenizerError(String), ParserError(String), @@ -51,7 +51,7 @@ macro_rules! return_ok_if_some { }}; } -#[derive(PartialEq)] +#[derive(PartialEq, Eq)] pub enum IsOptional { Optional, Mandatory, @@ -848,7 +848,7 @@ impl<'a> Parser<'a> { r#in: Box::new(from), }) } else { - return parser_err!("Position function must include IN keyword".to_string()); + parser_err!("Position function must include IN keyword".to_string()) } } @@ -1745,7 +1745,7 @@ impl<'a> Parser<'a> { let all = self.parse_keyword(Keyword::ALL); let distinct = self.parse_keyword(Keyword::DISTINCT); if all && distinct { - return parser_err!("Cannot specify both ALL and DISTINCT".to_string()); + parser_err!("Cannot specify both ALL and DISTINCT".to_string()) } else { Ok(distinct) } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 57ec57d46..fdd066f61 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -301,7 +301,7 @@ impl fmt::Display for Whitespace { } /// Tokenizer error -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub struct TokenizerError { pub message: String, pub line: u64, From 61dc3e9bf9be42b0a338ff126d713e0217585e01 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 11 Aug 2022 17:29:27 -0400 Subject: [PATCH 012/806] Clarify contribution licensing (#570) * Clarify contribution licensing * less whitespace --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 9437775aa..dee097e56 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,15 @@ of various features from various contributors, but not to provide the implementations ourselves, as we simply don't have the resources. +## Licensing + +All code in this repository is licensed under the [Apache Software License 2.0](LICENSE.txt). + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +licensed as above, without any additional terms or conditions. + + [tdop-tutorial]: https://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing [`cargo fmt`]: https://github.com/rust-lang/rustfmt#on-the-stable-toolchain [current issues]: https://github.com/sqlparser-rs/sqlparser-rs/issues From 31ba0012f747c9e2fc551b95fd2ee3bfa46b12e6 Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Fri, 12 Aug 2022 22:08:56 +0400 Subject: [PATCH 013/806] Support PostgreSQL array subquery constructor (#566) --- src/ast/mod.rs | 3 + src/parser.rs | 16 ++++- ..._clickhouse.rs => sqlparser_clickhouse.rs} | 19 ++++++ tests/sqlparser_common.rs | 2 +- tests/sqlparser_postgres.rs | 63 +++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) rename tests/{sqpparser_clickhouse.rs => sqlparser_clickhouse.rs} (88%) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index aa8118033..36b17c309 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -376,6 +376,8 @@ pub enum Expr { /// A parenthesized subquery `(SELECT ...)`, used in expression like /// `SELECT (subquery) AS x` or `WHERE (subquery) = x` Subquery(Box), + /// An array subquery constructor, e.g. `SELECT ARRAY(SELECT 1 UNION SELECT 2)` + ArraySubquery(Box), /// The `LISTAGG` function `SELECT LISTAGG(...) WITHIN GROUP (ORDER BY ...)` ListAgg(ListAgg), /// The `GROUPING SETS` expr. @@ -573,6 +575,7 @@ impl fmt::Display for Expr { subquery ), Expr::Subquery(s) => write!(f, "({})", s), + Expr::ArraySubquery(s) => write!(f, "ARRAY({})", s), Expr::ListAgg(listagg) => write!(f, "{}", listagg), Expr::GroupingSets(sets) => { write!(f, "GROUPING SETS (")?; diff --git a/src/parser.rs b/src/parser.rs index 4d5070d7a..c803a3bbf 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -449,11 +449,18 @@ impl<'a> Parser<'a> { Keyword::TRIM => self.parse_trim_expr(), Keyword::INTERVAL => self.parse_literal_interval(), Keyword::LISTAGG => self.parse_listagg_expr(), - // Treat ARRAY[1,2,3] as an array [1,2,3], otherwise try as function call + // Treat ARRAY[1,2,3] as an array [1,2,3], otherwise try as subquery or a function call Keyword::ARRAY if self.peek_token() == Token::LBracket => { self.expect_token(&Token::LBracket)?; self.parse_array_expr(true) } + Keyword::ARRAY + if self.peek_token() == Token::LParen + && !dialect_of!(self is ClickHouseDialect) => + { + self.expect_token(&Token::LParen)?; + self.parse_array_subquery() + } Keyword::NOT => self.parse_not(), // Here `w` is a word, check if it's a part of a multi-part // identifier, a function call, or a simple identifier: @@ -927,6 +934,13 @@ impl<'a> Parser<'a> { } } + // Parses an array constructed from a subquery + pub fn parse_array_subquery(&mut self) -> Result { + let query = self.parse_query()?; + self.expect_token(&Token::RParen)?; + Ok(Expr::ArraySubquery(Box::new(query))) + } + /// Parse a SQL LISTAGG expression, e.g. `LISTAGG(...) WITHIN GROUP (ORDER BY ...)`. pub fn parse_listagg_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; diff --git a/tests/sqpparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs similarity index 88% rename from tests/sqpparser_clickhouse.rs rename to tests/sqlparser_clickhouse.rs index af0f59fe6..a61df73cc 100644 --- a/tests/sqpparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -121,6 +121,25 @@ fn parse_array_expr() { ) } +#[test] +fn parse_array_fn() { + let sql = "SELECT array(x1, x2) FROM foo"; + let select = clickhouse().verified_only_select(sql); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::new("array")]), + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new("x1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new("x2")))), + ], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(only(&select.projection)) + ); +} + #[test] fn parse_kill() { let stmt = clickhouse().verified_stmt("KILL MUTATION 5"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2a6bcdfb3..da8b544ee 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2597,7 +2597,7 @@ fn parse_bad_constraint() { #[test] fn parse_scalar_function_in_projection() { - let names = vec!["sqrt", "array", "foo"]; + let names = vec!["sqrt", "foo"]; for function_name in names { // like SELECT sqrt(id) FROM foo diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d227df626..e3c9332dc 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1241,6 +1241,69 @@ fn parse_array_index_expr() { ); } +#[test] +fn parse_array_subquery_expr() { + let sql = "SELECT ARRAY(SELECT 1 UNION SELECT 2)"; + let select = pg().verified_only_select(sql); + assert_eq!( + &Expr::ArraySubquery(Box::new(Query { + with: None, + body: Box::new(SetExpr::SetOperation { + op: SetOperator::Union, + all: false, + left: Box::new(SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::Value(Value::Number( + #[cfg(not(feature = "bigdecimal"))] + "1".to_string(), + #[cfg(feature = "bigdecimal")] + bigdecimal::BigDecimal::from(1), + false, + )))], + into: None, + from: vec![], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + qualify: None, + }))), + right: Box::new(SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::Value(Value::Number( + #[cfg(not(feature = "bigdecimal"))] + "2".to_string(), + #[cfg(feature = "bigdecimal")] + bigdecimal::BigDecimal::from(2), + false, + )))], + into: None, + from: vec![], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + qualify: None, + }))), + }), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + lock: None, + })), + expr_from_projection(only(&select.projection)), + ); +} + #[test] fn test_transaction_statement() { let statement = pg().verified_stmt("SET TRANSACTION SNAPSHOT '000003A1-1'"); From 42c5d43b45d3e7a573ac24dd5c927c43bbd3768c Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Mon, 15 Aug 2022 14:58:43 -0700 Subject: [PATCH 014/806] Support `TRIM` from with optional `FROM` clause (#573) * Split trimwhereFlag and trim_char expr to seperate members of Expr::Trim * update trim parsing logic to handle no flag + char from expr combo * add tests * Allow trim flag without trim_what expr * Add trim flag without trim_what test --- src/ast/mod.rs | 20 ++++++++++++++------ src/parser.rs | 32 ++++++++++++++++++++------------ tests/sqlparser_common.rs | 8 ++++++++ 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 36b17c309..b91aae43c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -331,13 +331,14 @@ pub enum Expr { substring_from: Option>, substring_for: Option>, }, - /// TRIM([BOTH | LEADING | TRAILING] [FROM ])\ + /// TRIM([BOTH | LEADING | TRAILING] [ FROM] )\ /// Or\ /// TRIM() Trim { expr: Box, - // ([BOTH | LEADING | TRAILING], ) - trim_where: Option<(TrimWhereField, Box)>, + // ([BOTH | LEADING | TRAILING] + trim_where: Option, + trim_what: Option>, }, /// `expr COLLATE collation` Collate { @@ -632,10 +633,17 @@ impl fmt::Display for Expr { } Expr::IsDistinctFrom(a, b) => write!(f, "{} IS DISTINCT FROM {}", a, b), Expr::IsNotDistinctFrom(a, b) => write!(f, "{} IS NOT DISTINCT FROM {}", a, b), - Expr::Trim { expr, trim_where } => { + Expr::Trim { + expr, + trim_where, + trim_what, + } => { write!(f, "TRIM(")?; - if let Some((ident, trim_char)) = trim_where { - write!(f, "{} {} FROM {}", ident, trim_char, expr)?; + if let Some(ident) = trim_where { + write!(f, "{} ", ident)?; + } + if let Some(trim_char) = trim_what { + write!(f, "{} FROM {}", trim_char, expr)?; } else { write!(f, "{}", expr)?; } diff --git a/src/parser.rs b/src/parser.rs index c803a3bbf..3a90b3ccb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -881,29 +881,37 @@ impl<'a> Parser<'a> { }) } - /// TRIM (WHERE 'text' FROM 'text')\ + /// TRIM ([WHERE] ['text' FROM] 'text')\ /// TRIM ('text') pub fn parse_trim_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; - let mut where_expr = None; + let mut trim_where = None; if let Token::Word(word) = self.peek_token() { if [Keyword::BOTH, Keyword::LEADING, Keyword::TRAILING] .iter() .any(|d| word.keyword == *d) { - let trim_where = self.parse_trim_where()?; - let sub_expr = self.parse_expr()?; - self.expect_keyword(Keyword::FROM)?; - where_expr = Some((trim_where, Box::new(sub_expr))); + trim_where = Some(self.parse_trim_where()?); } } let expr = self.parse_expr()?; - self.expect_token(&Token::RParen)?; - - Ok(Expr::Trim { - expr: Box::new(expr), - trim_where: where_expr, - }) + if self.parse_keyword(Keyword::FROM) { + let trim_what = Box::new(expr); + let expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Ok(Expr::Trim { + expr: Box::new(expr), + trim_where, + trim_what: Some(trim_what), + }) + } else { + self.expect_token(&Token::RParen)?; + Ok(Expr::Trim { + expr: Box::new(expr), + trim_where, + trim_what: None, + }) + } } pub fn parse_trim_where(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index da8b544ee..c87b0c5e8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3928,7 +3928,15 @@ fn parse_trim() { "SELECT TRIM(TRAILING 'xyz' FROM 'xyzfooxyz')", ); + one_statement_parses_to( + "SELECT TRIM('xyz' FROM 'xyzfooxyz')", + "SELECT TRIM('xyz' FROM 'xyzfooxyz')", + ); one_statement_parses_to("SELECT TRIM(' foo ')", "SELECT TRIM(' foo ')"); + one_statement_parses_to( + "SELECT TRIM(LEADING ' foo ')", + "SELECT TRIM(LEADING ' foo ')", + ); assert_eq!( ParserError::ParserError("Expected ), found: 'xyz'".to_owned()), From 50aafa8dc129980e801eeb5385c1cbb8020bdb63 Mon Sep 17 00:00:00 2001 From: Seo Sanghyeon Date: Thu, 18 Aug 2022 03:26:45 +0900 Subject: [PATCH 015/806] Update Ballista link in README (#576) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dee097e56..6e9f6290d 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ licensed as above, without any additional terms or conditions. [current issues]: https://github.com/sqlparser-rs/sqlparser-rs/issues [DataFusion]: https://github.com/apache/arrow-datafusion [LocustDB]: https://github.com/cswinter/LocustDB -[Ballista]: https://github.com/apache/arrow-datafusion/tree/master/ballista +[Ballista]: https://github.com/apache/arrow-ballista [GlueSQL]: https://github.com/gluesql/gluesql [Pratt Parser]: https://tdop.github.io/ [sql-2016-grammar]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html From fc71719719faeac60921746f4ed76b9a8f855cdd Mon Sep 17 00:00:00 2001 From: Sarah Yurick <53962159+sarahyurick@users.noreply.github.com> Date: Thu, 18 Aug 2022 06:44:18 -0700 Subject: [PATCH 016/806] add unknown, is not true/false/unknown (#583) --- src/ast/mod.rs | 12 ++++++++++++ src/parser.rs | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b91aae43c..8015f9c73 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -227,12 +227,20 @@ pub enum Expr { CompositeAccess { expr: Box, key: Ident }, /// `IS FALSE` operator IsFalse(Box), + /// `IS NOT FALSE` operator + IsNotFalse(Box), /// `IS TRUE` operator IsTrue(Box), + /// `IS NOT TRUE` operator + IsNotTrue(Box), /// `IS NULL` operator IsNull(Box), /// `IS NOT NULL` operator IsNotNull(Box), + /// `IS UNKNOWN` operator + IsUnknown(Box), + /// `IS NOT UNKNOWN` operator + IsNotUnknown(Box), /// `IS DISTINCT FROM` operator IsDistinctFrom(Box, Box), /// `IS NOT DISTINCT FROM` operator @@ -412,9 +420,13 @@ impl fmt::Display for Expr { } Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), Expr::IsTrue(ast) => write!(f, "{} IS TRUE", ast), + Expr::IsNotTrue(ast) => write!(f, "{} IS NOT TRUE", ast), Expr::IsFalse(ast) => write!(f, "{} IS FALSE", ast), + Expr::IsNotFalse(ast) => write!(f, "{} IS NOT FALSE", ast), Expr::IsNull(ast) => write!(f, "{} IS NULL", ast), Expr::IsNotNull(ast) => write!(f, "{} IS NOT NULL", ast), + Expr::IsUnknown(ast) => write!(f, "{} IS UNKNOWN", ast), + Expr::IsNotUnknown(ast) => write!(f, "{} IS NOT UNKNOWN", ast), Expr::InList { expr, list, diff --git a/src/parser.rs b/src/parser.rs index 3a90b3ccb..5cd6ae14b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1255,8 +1255,16 @@ impl<'a> Parser<'a> { Ok(Expr::IsNotNull(Box::new(expr))) } else if self.parse_keywords(&[Keyword::TRUE]) { Ok(Expr::IsTrue(Box::new(expr))) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::TRUE]) { + Ok(Expr::IsNotTrue(Box::new(expr))) } else if self.parse_keywords(&[Keyword::FALSE]) { Ok(Expr::IsFalse(Box::new(expr))) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::FALSE]) { + Ok(Expr::IsNotFalse(Box::new(expr))) + } else if self.parse_keywords(&[Keyword::UNKNOWN]) { + Ok(Expr::IsUnknown(Box::new(expr))) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::UNKNOWN]) { + Ok(Expr::IsNotUnknown(Box::new(expr))) } else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::FROM]) { let expr2 = self.parse_expr()?; Ok(Expr::IsDistinctFrom(Box::new(expr), Box::new(expr2))) From eb7f1b005eb339735760a7c4b1cbf44e251a8c95 Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Thu, 18 Aug 2022 10:02:54 -0600 Subject: [PATCH 017/806] Parse LIKE patterns as Expr not Value (#579) --- src/ast/mod.rs | 6 ++-- src/parser.rs | 36 ++++++++++++++---------- tests/sqlparser_common.rs | 59 ++++++++++++++++++++++++++++++++------- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8015f9c73..832b09bcc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -280,21 +280,21 @@ pub enum Expr { Like { negated: bool, expr: Box, - pattern: Box, + pattern: Box, escape_char: Option, }, /// ILIKE (case-insensitive LIKE) ILike { negated: bool, expr: Box, - pattern: Box, + pattern: Box, escape_char: Option, }, /// SIMILAR TO regex SimilarTo { negated: bool, expr: Box, - pattern: Box, + pattern: Box, escape_char: Option, }, /// Any operation e.g. `1 ANY (1)` or `foo > ANY(bar)`, It will be wrapped in the right side of BinaryExpr diff --git a/src/parser.rs b/src/parser.rs index 5cd6ae14b..a0d6fd9ad 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1317,21 +1317,21 @@ impl<'a> Parser<'a> { Ok(Expr::Like { negated, expr: Box::new(expr), - pattern: Box::new(self.parse_value()?), + pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), escape_char: self.parse_escape_char()?, }) } else if self.parse_keyword(Keyword::ILIKE) { Ok(Expr::ILike { negated, expr: Box::new(expr), - pattern: Box::new(self.parse_value()?), + pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), escape_char: self.parse_escape_char()?, }) } else if self.parse_keywords(&[Keyword::SIMILAR, Keyword::TO]) { Ok(Expr::SimilarTo { negated, expr: Box::new(expr), - pattern: Box::new(self.parse_value()?), + pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), escape_char: self.parse_escape_char()?, }) } else { @@ -1478,10 +1478,16 @@ impl<'a> Parser<'a> { }) } - const UNARY_NOT_PREC: u8 = 15; - const BETWEEN_PREC: u8 = 20; + // use https://www.postgresql.org/docs/7.0/operators.htm#AEN2026 as a reference const PLUS_MINUS_PREC: u8 = 30; + const XOR_PREC: u8 = 24; const TIME_ZONE_PREC: u8 = 20; + const BETWEEN_PREC: u8 = 20; + const LIKE_PREC: u8 = 19; + const IS_PREC: u8 = 17; + const UNARY_NOT_PREC: u8 = 15; + const AND_PREC: u8 = 10; + const OR_PREC: u8 = 5; /// Get the precedence of the next token pub fn get_next_precedence(&self) -> Result { @@ -1492,9 +1498,9 @@ impl<'a> Parser<'a> { let token_2 = self.peek_nth_token(2); debug!("0: {token_0} 1: {token_1} 2: {token_2}"); match token { - Token::Word(w) if w.keyword == Keyword::OR => Ok(5), - Token::Word(w) if w.keyword == Keyword::AND => Ok(10), - Token::Word(w) if w.keyword == Keyword::XOR => Ok(24), + Token::Word(w) if w.keyword == Keyword::OR => Ok(Self::OR_PREC), + Token::Word(w) if w.keyword == Keyword::AND => Ok(Self::AND_PREC), + Token::Word(w) if w.keyword == Keyword::XOR => Ok(Self::XOR_PREC), Token::Word(w) if w.keyword == Keyword::AT => { match (self.peek_nth_token(1), self.peek_nth_token(2)) { @@ -1515,18 +1521,18 @@ impl<'a> Parser<'a> { // precedence. Token::Word(w) if w.keyword == Keyword::IN => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), _ => Ok(0), }, - Token::Word(w) if w.keyword == Keyword::IS => Ok(17), + Token::Word(w) if w.keyword == Keyword::IS => Ok(Self::IS_PREC), Token::Word(w) if w.keyword == Keyword::IN => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::BETWEEN_PREC), Token::Eq | Token::Lt | Token::LtEq diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c87b0c5e8..5f4b48567 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -862,7 +862,7 @@ fn parse_not_precedence() { expr: Box::new(Expr::Like { expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), negated: true, - pattern: Box::new(Value::SingleQuotedString("b".into())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), escape_char: None }), }, @@ -895,7 +895,7 @@ fn parse_like() { Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None }, select.selection.unwrap() @@ -911,7 +911,7 @@ fn parse_like() { Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('\\') }, select.selection.unwrap() @@ -928,7 +928,7 @@ fn parse_like() { Expr::IsNull(Box::new(Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None })), select.selection.unwrap() @@ -938,6 +938,45 @@ fn parse_like() { chk(true); } +#[test] +fn parse_null_like() { + let sql = "SELECT \ + column1 LIKE NULL AS col_null, \ + NULL LIKE column1 AS null_col \ + FROM customers"; + let select = verified_only_select(sql); + assert_eq!( + SelectItem::ExprWithAlias { + expr: Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("column1"))), + negated: false, + pattern: Box::new(Expr::Value(Value::Null)), + escape_char: None + }, + alias: Ident { + value: "col_null".to_owned(), + quote_style: None + } + }, + select.projection[0] + ); + assert_eq!( + SelectItem::ExprWithAlias { + expr: Expr::Like { + expr: Box::new(Expr::Value(Value::Null)), + negated: false, + pattern: Box::new(Expr::Identifier(Ident::new("column1"))), + escape_char: None + }, + alias: Ident { + value: "null_col".to_owned(), + quote_style: None + } + }, + select.projection[1] + ); +} + #[test] fn parse_ilike() { fn chk(negated: bool) { @@ -950,7 +989,7 @@ fn parse_ilike() { Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None }, select.selection.unwrap() @@ -966,7 +1005,7 @@ fn parse_ilike() { Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('^') }, select.selection.unwrap() @@ -983,7 +1022,7 @@ fn parse_ilike() { Expr::IsNull(Box::new(Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None })), select.selection.unwrap() @@ -1005,7 +1044,7 @@ fn parse_similar_to() { Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None }, select.selection.unwrap() @@ -1021,7 +1060,7 @@ fn parse_similar_to() { Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('\\') }, select.selection.unwrap() @@ -1037,7 +1076,7 @@ fn parse_similar_to() { Expr::IsNull(Box::new(Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('\\') })), select.selection.unwrap() From 6d8aacd85b63840c506f0d52f0c1d45845ee14b9 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 18 Aug 2022 20:29:55 +0300 Subject: [PATCH 018/806] feat: Support expression in SET statement (#574) Co-authored-by: Alex Vasilev --- src/ast/mod.rs | 23 ++++------------------- src/dialect/mysql.rs | 1 + src/parser.rs | 20 +++++--------------- tests/sqlparser_common.rs | 8 ++++---- tests/sqlparser_hive.rs | 9 ++++++--- tests/sqlparser_mysql.rs | 20 ++++++++++++++++++++ tests/sqlparser_postgres.rs | 34 +++++++++++++++++++++++----------- 7 files changed, 63 insertions(+), 52 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 832b09bcc..838594da7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -545,8 +545,10 @@ impl fmt::Display for Expr { Expr::UnaryOp { op, expr } => { if op == &UnaryOperator::PGPostfixFactorial { write!(f, "{}{}", expr, op) - } else { + } else if op == &UnaryOperator::Not { write!(f, "{} {}", op, expr) + } else { + write!(f, "{}{}", op, expr) } } Expr::Cast { expr, data_type } => write!(f, "CAST({} AS {})", expr, data_type), @@ -1100,7 +1102,7 @@ pub enum Statement { local: bool, hivevar: bool, variable: ObjectName, - value: Vec, + value: Vec, }, /// SET NAMES 'charset_name' [COLLATE 'collation_name'] /// @@ -2745,23 +2747,6 @@ impl fmt::Display for ShowStatementFilter { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum SetVariableValue { - Ident(Ident), - Literal(Value), -} - -impl fmt::Display for SetVariableValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use SetVariableValue::*; - match self { - Ident(ident) => write!(f, "{}", ident), - Literal(literal) => write!(f, "{}", literal), - } - } -} - /// Sqlite specific syntax /// /// https://sqlite.org/lang_conflict.html diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 6581195b8..d6095262c 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -24,6 +24,7 @@ impl Dialect for MySqlDialect { || ('A'..='Z').contains(&ch) || ch == '_' || ch == '$' + || ch == '@' || ('\u{0080}'..='\u{ffff}').contains(&ch) } diff --git a/src/parser.rs b/src/parser.rs index a0d6fd9ad..715843e9d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3751,22 +3751,12 @@ impl<'a> Parser<'a> { } else if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { let mut values = vec![]; loop { - let token = self.peek_token(); - let value = match (self.parse_value(), token) { - (Ok(value), _) => SetVariableValue::Literal(value), - (Err(_), Token::Word(ident)) => SetVariableValue::Ident(ident.to_ident()), - (Err(_), Token::Minus) => { - let next_token = self.next_token(); - match next_token { - Token::Word(ident) => SetVariableValue::Ident(Ident { - quote_style: ident.quote_style, - value: format!("-{}", ident.value), - }), - _ => self.expected("word", next_token)?, - } - } - (Err(_), unexpected) => self.expected("variable value", unexpected)?, + let value = 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; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5f4b48567..65d699b3f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -580,7 +580,7 @@ fn parse_select_count_wildcard() { #[test] fn parse_select_count_distinct() { - let sql = "SELECT COUNT(DISTINCT + x) FROM customer"; + let sql = "SELECT COUNT(DISTINCT +x) FROM customer"; let select = verified_only_select(sql); assert_eq!( &Expr::Function(Function { @@ -597,8 +597,8 @@ fn parse_select_count_distinct() { ); one_statement_parses_to( - "SELECT COUNT(ALL + x) FROM customer", - "SELECT COUNT(+ x) FROM customer", + "SELECT COUNT(ALL +x) FROM customer", + "SELECT COUNT(+x) FROM customer", ); let sql = "SELECT COUNT(ALL DISTINCT + x) FROM customer"; @@ -754,7 +754,7 @@ fn parse_compound_expr_2() { #[test] fn parse_unary_math() { use self::Expr::*; - let sql = "- a + - b"; + let sql = "-a + -b"; assert_eq!( BinaryOp { left: Box::new(UnaryOp { diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index fa2486120..4223ad5fa 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -15,7 +15,7 @@ //! Test SQL syntax specific to Hive. The parser based on the generic dialect //! is also tested (on the inputs it can handle). -use sqlparser::ast::{CreateFunctionUsing, Ident, ObjectName, SetVariableValue, Statement}; +use sqlparser::ast::{CreateFunctionUsing, Expr, Ident, ObjectName, Statement, UnaryOperator}; use sqlparser::dialect::{GenericDialect, HiveDialect}; use sqlparser::parser::ParserError; use sqlparser::test_utils::*; @@ -220,14 +220,17 @@ fn set_statement_with_minus() { Ident::new("java"), Ident::new("opts") ]), - value: vec![SetVariableValue::Ident("-Xmx4g".into())], + value: vec![Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Identifier(Ident::new("Xmx4g"))) + }], } ); assert_eq!( hive().parse_sql_statements("SET hive.tez.java.opts = -"), Err(ParserError::ParserError( - "Expected word, found: EOF".to_string() + "Expected variable value, found: EOF".to_string() )) ) } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index c1cfa2876..f46d5d23e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -251,6 +251,26 @@ fn parse_use() { ); } +#[test] +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 { + local: true, + hivevar: false, + variable: ObjectName(vec!["autocommit".into()]), + value: vec![Expr::Value(Value::Number( + #[cfg(not(feature = "bigdecimal"))] + "1".to_string(), + #[cfg(feature = "bigdecimal")] + bigdecimal::BigDecimal::from(1), + false + ))], + } + ); +} + #[test] fn parse_create_table_auto_increment() { let sql = "CREATE TABLE foo (bar INT PRIMARY KEY AUTO_INCREMENT)"; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e3c9332dc..3aaabc9e3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -18,7 +18,6 @@ mod test_utils; use test_utils::*; -use sqlparser::ast::Value::Boolean; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; @@ -782,7 +781,10 @@ fn parse_set() { local: false, hivevar: false, variable: ObjectName(vec![Ident::new("a")]), - value: vec![SetVariableValue::Ident("b".into())], + value: vec![Expr::Identifier(Ident { + value: "b".into(), + quote_style: None + })], } ); @@ -793,9 +795,7 @@ fn parse_set() { local: false, hivevar: false, variable: ObjectName(vec![Ident::new("a")]), - value: vec![SetVariableValue::Literal(Value::SingleQuotedString( - "b".into() - ))], + value: vec![Expr::Value(Value::SingleQuotedString("b".into()))], } ); @@ -806,7 +806,13 @@ fn parse_set() { local: false, hivevar: false, variable: ObjectName(vec![Ident::new("a")]), - value: vec![SetVariableValue::Literal(number("0"))], + value: vec![Expr::Value(Value::Number( + #[cfg(not(feature = "bigdecimal"))] + "0".to_string(), + #[cfg(feature = "bigdecimal")] + bigdecimal::BigDecimal::from(0), + false, + ))], } ); @@ -817,7 +823,10 @@ fn parse_set() { local: false, hivevar: false, variable: ObjectName(vec![Ident::new("a")]), - value: vec![SetVariableValue::Ident("DEFAULT".into())], + value: vec![Expr::Identifier(Ident { + value: "DEFAULT".into(), + quote_style: None + })], } ); @@ -828,7 +837,7 @@ fn parse_set() { local: true, hivevar: false, variable: ObjectName(vec![Ident::new("a")]), - value: vec![SetVariableValue::Ident("b".into())], + value: vec![Expr::Identifier("b".into())], } ); @@ -839,7 +848,10 @@ fn parse_set() { local: false, hivevar: false, variable: ObjectName(vec![Ident::new("a"), Ident::new("b"), Ident::new("c")]), - value: vec![SetVariableValue::Ident("b".into())], + value: vec![Expr::Identifier(Ident { + value: "b".into(), + quote_style: None + })], } ); @@ -859,7 +871,7 @@ fn parse_set() { Ident::new("reducer"), Ident::new("parallelism") ]), - value: vec![SetVariableValue::Literal(Boolean(false))], + value: vec![Expr::Value(Value::Boolean(false))], } ); @@ -1107,7 +1119,7 @@ fn parse_pg_unary_ops() { ]; for (str_op, op) in pg_unary_ops { - let select = pg().verified_only_select(&format!("SELECT {} a", &str_op)); + let select = pg().verified_only_select(&format!("SELECT {}a", &str_op)); assert_eq!( SelectItem::UnnamedExpr(Expr::UnaryOp { op: op.clone(), From dcb005e17aef46858fe19eeac3004ae779b9f3aa Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 18 Aug 2022 13:55:36 -0400 Subject: [PATCH 019/806] Update changelog (#584) --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 327cd9094..ac602c042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,30 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.21.0] 2022-08-18 + +### Added +* Support `IS [NOT] TRUE`, `IS [NOT] FALSE`, and `IS [NOT] UNKNOWN` - Thanks (#583) @sarahyurick +* Support `SIMILAR TO` syntax (#569) - Thanks @ayushdg +* Support `SHOW COLLATION` (#564) - Thanks @MazterQyou +* Support `SHOW TABLES` (#563) - Thanks @MazterQyou +* Support `SET NAMES literal [COLLATE literal]` (#558) - Thanks @ovr +* Support trailing commas (#557) in `BigQuery` dialect - Thanks @komukomo +* Support `USE ` (#565) - Thanks @MazterQyou +* Support `SHOW COLUMNS FROM tbl FROM db` (#562) - Thanks @MazterQyou +* Support `SHOW VARIABLES` for `MySQL` dialect (#559) - Thanks @ovr and @vasilev-alex + +### Changed +* Support arbitrary expression in `SET` statement (#574) - Thanks @ovr and @vasilev-alex +* Parse LIKE patterns as Expr not Value (#579) - Thanks @andygrove +* Update Ballista link in README (#576) - Thanks @sanxiyn +* Parse `TRIM` from with optional expr and `FROM` expr (#573) - Thanks @ayushdg +* Support PostgreSQL array subquery constructor (#566) - Thanks @MazterQyou +* Clarify contribution licensing (#570) - Thanks @alamb +* Update for new clippy ints (#571) - Thanks @alamb +* Change `Like` and `ILike` to `Expr` variants, allow escape char (#569) - Thanks @ayushdg +* Parse special keywords as functions (`current_user`, `user`, etc) (#561) - Thanks @ovr +* Support expressions in `LIMIT`/`OFFSET` (#567) - Thanks @MazterQyou ## [0.20.0] 2022-08-05 From 7c0247715121e253dcd7b90555f9f8f3068a6281 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 18 Aug 2022 14:00:13 -0400 Subject: [PATCH 020/806] (cargo-release) version 0.21.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a3576159c..1204ebec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.20.0" +version = "0.21.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 72559e9b6298f39c9ab2a084b5e1cd11d3fb6c6d Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Fri, 19 Aug 2022 05:44:14 -0600 Subject: [PATCH 021/806] Add ability for dialects to override prefix, infix, and statement parsing (#581) --- src/dialect/mod.rs | 27 ++++++ src/dialect/postgresql.rs | 41 +++++++++ src/dialect/sqlite.rs | 12 +++ src/parser.rs | 58 +++++-------- tests/sqlparser_custom_dialect.rs | 138 ++++++++++++++++++++++++++++++ 5 files changed, 239 insertions(+), 37 deletions(-) create mode 100644 tests/sqlparser_custom_dialect.rs diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 63821dd74..46e8dda2c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -22,6 +22,7 @@ mod redshift; mod snowflake; mod sqlite; +use crate::ast::{Expr, Statement}; use core::any::{Any, TypeId}; use core::fmt::Debug; use core::iter::Peekable; @@ -39,6 +40,7 @@ pub use self::redshift::RedshiftSqlDialect; pub use self::snowflake::SnowflakeDialect; pub use self::sqlite::SQLiteDialect; pub use crate::keywords; +use crate::parser::{Parser, ParserError}; /// `dialect_of!(parser is SQLiteDialect | GenericDialect)` evaluates /// to `true` if `parser.dialect` is one of the `Dialect`s specified. @@ -65,6 +67,31 @@ pub trait Dialect: Debug + Any { fn is_identifier_start(&self, ch: char) -> bool; /// Determine if a character is a valid unquoted identifier character fn is_identifier_part(&self, ch: char) -> bool; + /// Dialect-specific prefix parser override + fn parse_prefix(&self, _parser: &mut Parser) -> Option> { + // return None to fall back to the default behavior + None + } + /// Dialect-specific infix parser override + fn parse_infix( + &self, + _parser: &mut Parser, + _expr: &Expr, + _precendence: u8, + ) -> Option> { + // return None to fall back to the default behavior + None + } + /// Dialect-specific precedence override + fn get_next_precedence(&self, _parser: &Parser) -> Option> { + // return None to fall back to the default behavior + None + } + /// Dialect-specific statement parser override + fn parse_statement(&self, _parser: &mut Parser) -> Option> { + // return None to fall back to the default behavior + None + } } impl dyn Dialect { diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 0c2eb99f0..04d64b9bf 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -10,7 +10,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::ast::{CommentObject, Statement}; use crate::dialect::Dialect; +use crate::keywords::Keyword; +use crate::parser::{Parser, ParserError}; +use crate::tokenizer::Token; #[derive(Debug)] pub struct PostgreSqlDialect {} @@ -30,4 +34,41 @@ impl Dialect for PostgreSqlDialect { || ch == '$' || ch == '_' } + + fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keyword(Keyword::COMMENT) { + Some(parse_comment(parser)) + } else { + None + } + } +} + +pub fn parse_comment(parser: &mut Parser) -> Result { + parser.expect_keyword(Keyword::ON)?; + let token = parser.next_token(); + + let (object_type, object_name) = match token { + Token::Word(w) if w.keyword == Keyword::COLUMN => { + let object_name = parser.parse_object_name()?; + (CommentObject::Column, object_name) + } + Token::Word(w) if w.keyword == Keyword::TABLE => { + let object_name = parser.parse_object_name()?; + (CommentObject::Table, object_name) + } + _ => parser.expected("comment object_type", token)?, + }; + + parser.expect_keyword(Keyword::IS)?; + let comment = if parser.parse_keyword(Keyword::NULL) { + None + } else { + Some(parser.parse_literal_string()?) + }; + Ok(Statement::Comment { + object_type, + object_name, + comment, + }) } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 4ce2f834b..64d7f62fd 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -10,7 +10,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::ast::Statement; use crate::dialect::Dialect; +use crate::keywords::Keyword; +use crate::parser::{Parser, ParserError}; #[derive(Debug)] pub struct SQLiteDialect {} @@ -35,4 +38,13 @@ impl Dialect for SQLiteDialect { fn is_identifier_part(&self, ch: char) -> bool { self.is_identifier_start(ch) || ('0'..='9').contains(&ch) } + + fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keyword(Keyword::REPLACE) { + parser.prev_token(); + Some(parser.parse_insert()) + } else { + None + } + } } diff --git a/src/parser.rs b/src/parser.rs index 715843e9d..4cfbdd23b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -152,6 +152,11 @@ impl<'a> Parser<'a> { /// Parse a single top-level statement (such as SELECT, INSERT, CREATE, etc.), /// stopping before the statement separator, if any. pub fn parse_statement(&mut self) -> Result { + // allow the dialect to override statement parsing + if let Some(statement) = self.dialect.parse_statement(self) { + return statement; + } + match self.next_token() { Token::Word(w) => match w.keyword { Keyword::KILL => Ok(self.parse_kill()?), @@ -195,13 +200,6 @@ impl<'a> Parser<'a> { Keyword::EXECUTE => Ok(self.parse_execute()?), Keyword::PREPARE => Ok(self.parse_prepare()?), Keyword::MERGE => Ok(self.parse_merge()?), - Keyword::REPLACE if dialect_of!(self is SQLiteDialect ) => { - self.prev_token(); - Ok(self.parse_insert()?) - } - Keyword::COMMENT if dialect_of!(self is PostgreSqlDialect) => { - Ok(self.parse_comment()?) - } _ => self.expected("an SQL statement", Token::Word(w)), }, Token::LParen => { @@ -381,6 +379,11 @@ impl<'a> Parser<'a> { /// Parse an expression prefix pub fn parse_prefix(&mut self) -> Result { + // allow the dialect to override prefix parsing + if let Some(prefix) = self.dialect.parse_prefix(self) { + return prefix; + } + // PostgreSQL allows any string literal to be preceded by a type name, indicating that the // string literal represents a literal of that type. Some examples: // @@ -1164,6 +1167,11 @@ impl<'a> Parser<'a> { /// Parse an operator following an expression pub fn parse_infix(&mut self, expr: Expr, precedence: u8) -> Result { + // allow the dialect to override infix parsing + if let Some(infix) = self.dialect.parse_infix(self, &expr, precedence) { + return infix; + } + let tok = self.next_token(); let regular_binary_operator = match &tok { @@ -1491,6 +1499,11 @@ impl<'a> Parser<'a> { /// Get the precedence of the next token pub fn get_next_precedence(&self) -> Result { + // allow the dialect to override precedence logic + if let Some(precedence) = self.dialect.get_next_precedence(self) { + return precedence; + } + let token = self.peek_token(); debug!("get_next_precedence() {:?}", token); let token_0 = self.peek_nth_token(0); @@ -1618,7 +1631,7 @@ impl<'a> Parser<'a> { } /// Report unexpected token - fn expected(&self, expected: &str, found: Token) -> Result { + pub fn expected(&self, expected: &str, found: Token) -> Result { parser_err!(format!("Expected {}, found: {}", expected, found)) } @@ -4735,35 +4748,6 @@ impl<'a> Parser<'a> { }) } - pub fn parse_comment(&mut self) -> Result { - self.expect_keyword(Keyword::ON)?; - let token = self.next_token(); - - let (object_type, object_name) = match token { - Token::Word(w) if w.keyword == Keyword::COLUMN => { - let object_name = self.parse_object_name()?; - (CommentObject::Column, object_name) - } - Token::Word(w) if w.keyword == Keyword::TABLE => { - let object_name = self.parse_object_name()?; - (CommentObject::Table, object_name) - } - _ => self.expected("comment object_type", token)?, - }; - - self.expect_keyword(Keyword::IS)?; - let comment = if self.parse_keyword(Keyword::NULL) { - None - } else { - Some(self.parse_literal_string()?) - }; - Ok(Statement::Comment { - object_type, - object_name, - comment, - }) - } - pub fn parse_merge_clauses(&mut self) -> Result, ParserError> { let mut clauses: Vec = vec![]; loop { diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs new file mode 100644 index 000000000..c0fe4c1dd --- /dev/null +++ b/tests/sqlparser_custom_dialect.rs @@ -0,0 +1,138 @@ +// Licensed 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. + +//! Test the ability for dialects to override parsing + +use sqlparser::{ + ast::{BinaryOperator, Expr, Statement, Value}, + dialect::Dialect, + keywords::Keyword, + parser::{Parser, ParserError}, + tokenizer::Token, +}; + +#[test] +fn custom_prefix_parser() -> Result<(), ParserError> { + #[derive(Debug)] + struct MyDialect {} + + impl Dialect for MyDialect { + fn is_identifier_start(&self, ch: char) -> bool { + is_identifier_start(ch) + } + + fn is_identifier_part(&self, ch: char) -> bool { + is_identifier_part(ch) + } + + fn parse_prefix(&self, parser: &mut Parser) -> Option> { + if parser.consume_token(&Token::Number("1".to_string(), false)) { + Some(Ok(Expr::Value(Value::Null))) + } else { + None + } + } + } + + let dialect = MyDialect {}; + let sql = "SELECT 1 + 2"; + let ast = Parser::parse_sql(&dialect, sql)?; + let query = &ast[0]; + assert_eq!("SELECT NULL + 2", &format!("{}", query)); + Ok(()) +} + +#[test] +fn custom_infix_parser() -> Result<(), ParserError> { + #[derive(Debug)] + struct MyDialect {} + + impl Dialect for MyDialect { + fn is_identifier_start(&self, ch: char) -> bool { + is_identifier_start(ch) + } + + fn is_identifier_part(&self, ch: char) -> bool { + is_identifier_part(ch) + } + + fn parse_infix( + &self, + parser: &mut Parser, + expr: &Expr, + _precendence: u8, + ) -> Option> { + if parser.consume_token(&Token::Plus) { + Some(Ok(Expr::BinaryOp { + left: Box::new(expr.clone()), + op: BinaryOperator::Multiply, // translate Plus to Multiply + right: Box::new(parser.parse_expr().unwrap()), + })) + } else { + None + } + } + } + + let dialect = MyDialect {}; + let sql = "SELECT 1 + 2"; + let ast = Parser::parse_sql(&dialect, sql)?; + let query = &ast[0]; + assert_eq!("SELECT 1 * 2", &format!("{}", query)); + Ok(()) +} + +#[test] +fn custom_statement_parser() -> Result<(), ParserError> { + #[derive(Debug)] + struct MyDialect {} + + impl Dialect for MyDialect { + fn is_identifier_start(&self, ch: char) -> bool { + is_identifier_start(ch) + } + + fn is_identifier_part(&self, ch: char) -> bool { + is_identifier_part(ch) + } + + fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keyword(Keyword::SELECT) { + for _ in 0..3 { + let _ = parser.next_token(); + } + Some(Ok(Statement::Commit { chain: false })) + } else { + None + } + } + } + + let dialect = MyDialect {}; + let sql = "SELECT 1 + 2"; + let ast = Parser::parse_sql(&dialect, sql)?; + let query = &ast[0]; + assert_eq!("COMMIT", &format!("{}", query)); + Ok(()) +} + +fn is_identifier_start(ch: char) -> bool { + ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' +} + +fn is_identifier_part(ch: char) -> bool { + ('a'..='z').contains(&ch) + || ('A'..='Z').contains(&ch) + || ('0'..='9').contains(&ch) + || ch == '$' + || ch == '_' +} From 95fbb55f2c4cbc3ada0bff0b1ad0245e287eb35d Mon Sep 17 00:00:00 2001 From: Wei-Ting Kuo Date: Sat, 27 Aug 2022 05:11:21 +0800 Subject: [PATCH 022/806] add with/without time zone (#589) --- src/ast/data_type.rs | 5 ++++- src/keywords.rs | 1 + src/parser.rs | 11 ++++++++--- tests/sqlparser_common.rs | 25 ++++++++++++++++++++++++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index da9ff1ad1..3a6ebf4cd 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -77,8 +77,10 @@ pub enum DataType { Time, /// Datetime Datetime, - /// Timestamp + /// Timestamp [Without Time Zone] Timestamp, + /// Timestamp With Time Zone + TimestampTz, /// Interval Interval, /// Regclass used in postgresql serial @@ -157,6 +159,7 @@ impl fmt::Display for DataType { DataType::Time => write!(f, "TIME"), DataType::Datetime => write!(f, "DATETIME"), DataType::Timestamp => write!(f, "TIMESTAMP"), + DataType::TimestampTz => write!(f, "TIMESTAMPTZ"), DataType::Interval => write!(f, "INTERVAL"), DataType::Regclass => write!(f, "REGCLASS"), DataType::Text => write!(f, "TEXT"), diff --git a/src/keywords.rs b/src/keywords.rs index a06ddc48d..3a7157204 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -505,6 +505,7 @@ define_keywords!( TIES, TIME, TIMESTAMP, + TIMESTAMPTZ, TIMEZONE, TIMEZONE_HOUR, TIMEZONE_MINUTE, diff --git a/src/parser.rs b/src/parser.rs index 4cfbdd23b..e9aeeda5a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3119,12 +3119,17 @@ impl<'a> Parser<'a> { Keyword::DATE => Ok(DataType::Date), Keyword::DATETIME => Ok(DataType::Datetime), Keyword::TIMESTAMP => { - // TBD: we throw away "with/without timezone" information - if self.parse_keyword(Keyword::WITH) || self.parse_keyword(Keyword::WITHOUT) { + if self.parse_keyword(Keyword::WITH) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::TimestampTz) + } else if self.parse_keyword(Keyword::WITHOUT) { + self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::Timestamp) + } else { + Ok(DataType::Timestamp) } - Ok(DataType::Timestamp) } + Keyword::TIMESTAMPTZ => Ok(DataType::TimestampTz), Keyword::TIME => { // TBD: we throw away "with/without timezone" information if self.parse_keyword(Keyword::WITH) || self.parse_keyword(Keyword::WITHOUT) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 65d699b3f..53ec83106 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2854,7 +2854,7 @@ fn parse_literal_datetime() { } #[test] -fn parse_literal_timestamp() { +fn parse_literal_timestamp_without_time_zone() { let sql = "SELECT TIMESTAMP '1999-01-01 01:23:34'"; let select = verified_only_select(sql); assert_eq!( @@ -2864,6 +2864,29 @@ fn parse_literal_timestamp() { }, expr_from_projection(only(&select.projection)), ); + + one_statement_parses_to( + "SELECT TIMESTAMP WITHOUT TIME ZONE '1999-01-01 01:23:34'", + sql, + ); +} + +#[test] +fn parse_literal_timestamp_with_time_zone() { + let sql = "SELECT TIMESTAMPTZ '1999-01-01 01:23:34Z'"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::TimestampTz, + value: "1999-01-01 01:23:34Z".into() + }, + expr_from_projection(only(&select.projection)), + ); + + one_statement_parses_to( + "SELECT TIMESTAMP WITH TIME ZONE '1999-01-01 01:23:34Z'", + sql, + ); } #[test] From 6f55dead530420ec7962aab5c6a7022715cebb6b Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Fri, 26 Aug 2022 14:13:25 -0700 Subject: [PATCH 023/806] Add overlay expr (#594) * Add PLACING keyword to keywords list * Add high level overlay expr to Expr enum * Add parsing logic for overlay op * add ovleray and is not true/false/unknown tests --- src/ast/mod.rs | 28 +++++++++++++++-- src/keywords.rs | 1 + src/parser.rs | 23 ++++++++++++++ tests/sqlparser_common.rs | 65 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 838594da7..4f5fdb2eb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -348,6 +348,13 @@ pub enum Expr { trim_where: Option, trim_what: Option>, }, + /// OVERLAY( PLACING FROM [ FOR ] + Overlay { + expr: Box, + overlay_what: Box, + overlay_from: Box, + overlay_for: Option>, + }, /// `expr COLLATE collation` Collate { expr: Box, @@ -639,8 +646,25 @@ impl fmt::Display for Expr { if let Some(from_part) = substring_from { write!(f, " FROM {}", from_part)?; } - if let Some(from_part) = substring_for { - write!(f, " FOR {}", from_part)?; + if let Some(for_part) = substring_for { + write!(f, " FOR {}", for_part)?; + } + + write!(f, ")") + } + Expr::Overlay { + expr, + overlay_what, + overlay_from, + overlay_for, + } => { + write!( + f, + "OVERLAY({} PLACING {} FROM {}", + expr, overlay_what, overlay_from + )?; + if let Some(for_part) = overlay_for { + write!(f, " FOR {}", for_part)?; } write!(f, ")") diff --git a/src/keywords.rs b/src/keywords.rs index 3a7157204..30ec735f7 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -382,6 +382,7 @@ define_keywords!( PERCENTILE_DISC, PERCENT_RANK, PERIOD, + PLACING, PLANS, PORTION, POSITION, diff --git a/src/parser.rs b/src/parser.rs index e9aeeda5a..fdcf19f18 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -449,6 +449,7 @@ impl<'a> Parser<'a> { Keyword::EXTRACT => self.parse_extract_expr(), Keyword::POSITION => self.parse_position_expr(), Keyword::SUBSTRING => self.parse_substring_expr(), + Keyword::OVERLAY => self.parse_overlay_expr(), Keyword::TRIM => self.parse_trim_expr(), Keyword::INTERVAL => self.parse_literal_interval(), Keyword::LISTAGG => self.parse_listagg_expr(), @@ -884,6 +885,28 @@ impl<'a> Parser<'a> { }) } + pub fn parse_overlay_expr(&mut self) -> Result { + // PARSE OVERLAY (EXPR PLACING EXPR FROM 1 [FOR 3]) + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + self.expect_keyword(Keyword::PLACING)?; + let what_expr = self.parse_expr()?; + self.expect_keyword(Keyword::FROM)?; + let from_expr = self.parse_expr()?; + let mut for_expr = None; + if self.parse_keyword(Keyword::FOR) { + for_expr = Some(self.parse_expr()?); + } + self.expect_token(&Token::RParen)?; + + Ok(Expr::Overlay { + expr: Box::new(expr), + overlay_what: Box::new(what_expr), + overlay_from: Box::new(from_expr), + overlay_for: for_expr.map(Box::new), + }) + } + /// TRIM ([WHERE] ['text' FROM] 'text')\ /// TRIM ('text') pub fn parse_trim_expr(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 53ec83106..8d652ab0a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3973,6 +3973,38 @@ fn parse_substring() { one_statement_parses_to("SELECT SUBSTRING('1' FOR 3)", "SELECT SUBSTRING('1' FOR 3)"); } +#[test] +fn parse_overlay() { + one_statement_parses_to( + "SELECT OVERLAY('abccccde' PLACING 'abc' FROM 3)", + "SELECT OVERLAY('abccccde' PLACING 'abc' FROM 3)", + ); + one_statement_parses_to( + "SELECT OVERLAY('abccccde' PLACING 'abc' FROM 3 FOR 12)", + "SELECT OVERLAY('abccccde' PLACING 'abc' FROM 3 FOR 12)", + ); + assert_eq!( + ParserError::ParserError("Expected PLACING, found: FROM".to_owned()), + parse_sql_statements("SELECT OVERLAY('abccccde' FROM 3)").unwrap_err(), + ); + + let sql = "SELECT OVERLAY('abcdef' PLACING name FROM 3 FOR id + 1) FROM CUSTOMERS"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Overlay { + expr: Box::new(Expr::Value(Value::SingleQuotedString("abcdef".to_string()))), + overlay_what: Box::new(Expr::Identifier(Ident::new("name"))), + overlay_from: Box::new(Expr::Value(number("3"))), + overlay_for: Some(Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("id"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Value(number("1"))), + })) + }, + expr_from_projection(only(&select.projection)) + ); +} + #[test] fn parse_trim() { one_statement_parses_to( @@ -5357,21 +5389,50 @@ fn parse_position_negative() { fn parse_is_boolean() { use self::Expr::*; + let sql = "a IS TRUE"; + assert_eq!( + IsTrue(Box::new(Identifier(Ident::new("a")))), + verified_expr(sql) + ); + + let sql = "a IS NOT TRUE"; + assert_eq!( + IsNotTrue(Box::new(Identifier(Ident::new("a")))), + verified_expr(sql) + ); + let sql = "a IS FALSE"; assert_eq!( IsFalse(Box::new(Identifier(Ident::new("a")))), verified_expr(sql) ); - let sql = "a IS TRUE"; + let sql = "a IS NOT FALSE"; assert_eq!( - IsTrue(Box::new(Identifier(Ident::new("a")))), + IsNotFalse(Box::new(Identifier(Ident::new("a")))), + verified_expr(sql) + ); + + let sql = "a IS UNKNOWN"; + assert_eq!( + IsUnknown(Box::new(Identifier(Ident::new("a")))), + verified_expr(sql) + ); + + let sql = "a IS NOT UNKNOWN"; + assert_eq!( + IsNotUnknown(Box::new(Identifier(Ident::new("a")))), verified_expr(sql) ); verified_stmt("SELECT f FROM foo WHERE field IS TRUE"); + verified_stmt("SELECT f FROM foo WHERE field IS NOT TRUE"); verified_stmt("SELECT f FROM foo WHERE field IS FALSE"); + verified_stmt("SELECT f FROM foo WHERE field IS NOT FALSE"); + + verified_stmt("SELECT f FROM foo WHERE field IS UNKNOWN"); + verified_stmt("SELECT f FROM foo WHERE field IS NOT UNKNOWN"); let sql = "SELECT f from foo where field is 0"; let res = parse_sql_statements(sql); From 019183917686d6310e0be5c18a3b481e444e2f74 Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Fri, 26 Aug 2022 16:42:46 -0600 Subject: [PATCH 024/806] update changelog for 0.22 (#595) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac602c042..66208c541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.22.0] 2022-08-26 + +### Added +* Support `OVERLAY` expressions - Thanks (#594) @ayushg +* Support `WITH TIMEZONE` and `WITHOUT TIMEZONE` when parsing `TIMESTAMP` expressions - Thanks (#589) @waitingkuo +* Add ability for dialects to override prefix, infix, and statement parsing - Thanks (#581) @andygrove + ## [0.21.0] 2022-08-18 ### Added From bccd63ea072645c75f4ea5444e8b93fc67db0aa9 Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Fri, 26 Aug 2022 17:03:42 -0600 Subject: [PATCH 025/806] (cargo-release) version 0.22.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1204ebec5..02ac9a53f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.21.0" +version = "0.22.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 0bb49cea9908e0d7389a46519e368efeeab89e79 Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Tue, 30 Aug 2022 20:15:39 +0400 Subject: [PATCH 026/806] feat: Support `LOCALTIME` and `LOCALTIMESTAMP` time functions (#592) --- src/parser.rs | 6 +++++- tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index fdcf19f18..877c47303 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -438,7 +438,11 @@ impl<'a> Parser<'a> { special: true, })) } - Keyword::CURRENT_TIMESTAMP | Keyword::CURRENT_TIME | Keyword::CURRENT_DATE => { + Keyword::CURRENT_TIMESTAMP + | Keyword::CURRENT_TIME + | Keyword::CURRENT_DATE + | Keyword::LOCALTIME + | Keyword::LOCALTIMESTAMP => { self.parse_time_functions(ObjectName(vec![w.to_ident()])) } Keyword::CASE => self.parse_case_expr(), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8d652ab0a..6fee5e88b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5353,6 +5353,38 @@ fn parse_time_functions() { // Validating Parenthesis one_statement_parses_to("SELECT CURRENT_DATE", sql); + + let sql = "SELECT LOCALTIME()"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::new("LOCALTIME")]), + args: vec![], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(&select.projection[0]) + ); + + // Validating Parenthesis + one_statement_parses_to("SELECT LOCALTIME", sql); + + let sql = "SELECT LOCALTIMESTAMP()"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::new("LOCALTIMESTAMP")]), + args: vec![], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(&select.projection[0]) + ); + + // Validating Parenthesis + one_statement_parses_to("SELECT LOCALTIMESTAMP", sql); } #[test] From 303f80f168a1360f432431d9d8757a42cd00acca Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Thu, 8 Sep 2022 13:08:45 -0600 Subject: [PATCH 027/806] Add support for aggregate expressions with filters (#585) --- src/ast/mod.rs | 5 +++++ src/dialect/hive.rs | 4 ++++ src/dialect/mod.rs | 4 ++++ src/dialect/postgresql.rs | 4 ++++ src/parser.rs | 31 +++++++++++++++++++++++++------ tests/sqlparser_hive.rs | 25 +++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4f5fdb2eb..6c279518e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -375,6 +375,8 @@ pub enum Expr { MapAccess { column: Box, keys: Vec }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), + /// Aggregate function with filter + AggregateExpressionWithFilter { expr: Box, filter: Box }, /// `CASE [] WHEN THEN ... [ELSE ] END` /// /// Note we only recognize a complete single expression as ``, @@ -571,6 +573,9 @@ impl fmt::Display for Expr { write!(f, " '{}'", &value::escape_single_quote_string(value)) } Expr::Function(fun) => write!(f, "{}", fun), + Expr::AggregateExpressionWithFilter { expr, filter } => { + write!(f, "{} FILTER (WHERE {})", expr, filter) + } Expr::Case { operand, conditions, diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index 9b42857ec..ceb5488ef 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -36,4 +36,8 @@ impl Dialect for HiveDialect { || ch == '{' || ch == '}' } + + fn supports_filter_during_aggregation(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 46e8dda2c..1d3c9cf5f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -67,6 +67,10 @@ pub trait Dialect: Debug + Any { fn is_identifier_start(&self, ch: char) -> bool; /// Determine if a character is a valid unquoted identifier character fn is_identifier_part(&self, ch: char) -> bool; + /// Does the dialect support `FILTER (WHERE expr)` for aggregate queries? + fn supports_filter_during_aggregation(&self) -> bool { + false + } /// Dialect-specific prefix parser override fn parse_prefix(&self, _parser: &mut Parser) -> Option> { // return None to fall back to the default behavior diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 04d64b9bf..b1f261b2e 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -42,6 +42,10 @@ impl Dialect for PostgreSqlDialect { None } } + + fn supports_filter_during_aggregation(&self) -> bool { + true + } } pub fn parse_comment(parser: &mut Parser) -> Result { diff --git a/src/parser.rs b/src/parser.rs index 877c47303..894bb84f1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4542,12 +4542,31 @@ impl<'a> Parser<'a> { /// Parse a comma-delimited list of projections after SELECT pub fn parse_select_item(&mut self) -> Result { match self.parse_wildcard_expr()? { - WildcardExpr::Expr(expr) => self - .parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) - .map(|alias| match alias { - Some(alias) => SelectItem::ExprWithAlias { expr, alias }, - None => SelectItem::UnnamedExpr(expr), - }), + WildcardExpr::Expr(expr) => { + let expr: Expr = if self.dialect.supports_filter_during_aggregation() + && self.parse_keyword(Keyword::FILTER) + { + let i = self.index - 1; + if self.consume_token(&Token::LParen) && self.parse_keyword(Keyword::WHERE) { + let filter = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Expr::AggregateExpressionWithFilter { + expr: Box::new(expr), + filter: Box::new(filter), + } + } else { + self.index = i; + expr + } + } else { + expr + }; + self.parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) + .map(|alias| match alias { + Some(alias) => SelectItem::ExprWithAlias { expr, alias }, + None => SelectItem::UnnamedExpr(expr), + }) + } WildcardExpr::QualifiedWildcard(prefix) => Ok(SelectItem::QualifiedWildcard(prefix)), WildcardExpr::Wildcard => Ok(SelectItem::Wildcard), } diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 4223ad5fa..8839cea2b 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -276,6 +276,31 @@ fn parse_create_function() { ); } +#[test] +fn filtering_during_aggregation() { + let rename = "SELECT \ + array_agg(name) FILTER (WHERE name IS NOT NULL), \ + array_agg(name) FILTER (WHERE name LIKE 'a%') \ + FROM region"; + println!("{}", hive().verified_stmt(rename)); +} + +#[test] +fn filtering_during_aggregation_aliased() { + let rename = "SELECT \ + array_agg(name) FILTER (WHERE name IS NOT NULL) AS agg1, \ + array_agg(name) FILTER (WHERE name LIKE 'a%') AS agg2 \ + FROM region"; + println!("{}", hive().verified_stmt(rename)); +} + +#[test] +fn filter_as_alias() { + let sql = "SELECT name filter FROM region"; + let expected = "SELECT name AS filter FROM region"; + println!("{}", hive().one_statement_parses_to(sql, expected)); +} + fn hive() -> TestedDialects { TestedDialects { dialects: vec![Box::new(HiveDialect {})], From fd07a17101e1d3045baca5ecf719d58ac0feee34 Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Thu, 8 Sep 2022 15:15:40 -0600 Subject: [PATCH 028/806] (cargo-release) version 0.23.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 02ac9a53f..c1ae73546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.22.0" +version = "0.23.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 48fa79d744fdc9c3aa1060c7eb35ea962eeb6837 Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Thu, 8 Sep 2022 20:13:11 -0600 Subject: [PATCH 029/806] update changelog (#607) --- CHANGELOG.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66208c541..237b00af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,18 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.23.0] 2022-09-08 + +### Added +* Add support for aggregate expressions with filters (#585) - Thanks @andygrove +* Support `LOCALTIME` and `LOCALTIMESTAMP` time functions (#592) - Thanks @MazterQyou + ## [0.22.0] 2022-08-26 ### Added -* Support `OVERLAY` expressions - Thanks (#594) @ayushg -* Support `WITH TIMEZONE` and `WITHOUT TIMEZONE` when parsing `TIMESTAMP` expressions - Thanks (#589) @waitingkuo -* Add ability for dialects to override prefix, infix, and statement parsing - Thanks (#581) @andygrove +* Support `OVERLAY` expressions (#594) - Thanks @ayushg +* Support `WITH TIMEZONE` and `WITHOUT TIMEZONE` when parsing `TIMESTAMP` expressions (#589) - Thanks @waitingkuo +* Add ability for dialects to override prefix, infix, and statement parsing (#581) - Thanks @andygrove ## [0.21.0] 2022-08-18 From fccae77c5e3056214b2d06902577925ec8d5549d Mon Sep 17 00:00:00 2001 From: Wei-Ting Kuo Date: Tue, 20 Sep 2022 21:44:51 +0800 Subject: [PATCH 030/806] support "set time zone to 'some-timezone'" (#617) * support "set time zone" * fix clippy * fix test cases --- src/parser.rs | 7 +++++- tests/sqlparser_common.rs | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index 894bb84f1..968b1a5dd 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3774,7 +3774,12 @@ impl<'a> Parser<'a> { }); } - let variable = self.parse_object_name()?; + let variable = if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) { + ObjectName(vec!["TIMEZONE".into()]) + } else { + self.parse_object_name()? + }; + if variable.to_string().eq_ignore_ascii_case("NAMES") && dialect_of!(self is MySqlDialect | GenericDialect) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6fee5e88b..d2d2646cd 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4663,6 +4663,52 @@ fn parse_set_transaction() { } } +#[test] +fn parse_set_variable() { + match verified_stmt("SET SOMETHING = '1'") { + Statement::SetVariable { + local, + hivevar, + variable, + value, + } => { + assert!(!local); + assert!(!hivevar); + assert_eq!(variable, ObjectName(vec!["SOMETHING".into()])); + assert_eq!( + value, + vec![Expr::Value(Value::SingleQuotedString("1".into()))] + ); + } + _ => unreachable!(), + } + + one_statement_parses_to("SET SOMETHING TO '1'", "SET SOMETHING = '1'"); +} + +#[test] +fn parse_set_time_zone() { + match verified_stmt("SET TIMEZONE = 'UTC'") { + Statement::SetVariable { + local, + hivevar, + variable, + value, + } => { + assert!(!local); + assert!(!hivevar); + assert_eq!(variable, ObjectName(vec!["TIMEZONE".into()])); + assert_eq!( + value, + vec![Expr::Value(Value::SingleQuotedString("UTC".into()))] + ); + } + _ => unreachable!(), + } + + one_statement_parses_to("SET TIME ZONE TO 'UTC'", "SET TIMEZONE = 'UTC'"); +} + #[test] fn parse_commit() { match verified_stmt("COMMIT") { From 495ab59aadf4b3a08ecd7643e0cb3c972f45507c Mon Sep 17 00:00:00 2001 From: Justin Joyce Date: Mon, 26 Sep 2022 07:33:53 +0100 Subject: [PATCH 031/806] Support `SHOW FUNCTIONS` (#620) * support SHOW FUNCTIONS * Update keywords.rs * Update mod.rs * Update sqlparser_common.rs * Fix CI issues --- src/ast/mod.rs | 11 +++++++++++ src/keywords.rs | 1 + src/parser.rs | 7 +++++++ tests/sqlparser_common.rs | 10 ++++++++++ 4 files changed, 29 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6c279518e..a6654be0e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1144,6 +1144,10 @@ pub enum Statement { /// /// Note: this is a MySQL-specific statement. SetNamesDefault {}, + /// SHOW FUNCTIONS + /// + /// Note: this is a Presto-specific statement. + ShowFunctions { filter: Option }, /// SHOW /// /// Note: this is a PostgreSQL-specific statement. @@ -2033,6 +2037,13 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::ShowFunctions { filter } => { + write!(f, "SHOW FUNCTIONS")?; + if let Some(filter) = filter { + write!(f, " {}", filter)?; + } + Ok(()) + } Statement::Use { db_name } => { write!(f, "USE {}", db_name)?; Ok(()) diff --git a/src/keywords.rs b/src/keywords.rs index 30ec735f7..2fa79855b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -250,6 +250,7 @@ define_keywords!( FROM, FULL, FUNCTION, + FUNCTIONS, FUSION, GET, GLOBAL, diff --git a/src/parser.rs b/src/parser.rs index 968b1a5dd..a9ee5b0e4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3854,6 +3854,8 @@ impl<'a> Parser<'a> { Ok(self.parse_show_columns(extended, full)?) } else if self.parse_keyword(Keyword::TABLES) { Ok(self.parse_show_tables(extended, full)?) + } else if self.parse_keyword(Keyword::FUNCTIONS) { + Ok(self.parse_show_functions()?) } else if extended || full { Err(ParserError::ParserError( "EXTENDED/FULL are not supported with this type of SHOW query".to_string(), @@ -3945,6 +3947,11 @@ impl<'a> Parser<'a> { }) } + pub fn parse_show_functions(&mut self) -> Result { + let filter = self.parse_show_statement_filter()?; + Ok(Statement::ShowFunctions { filter }) + } + pub fn parse_show_collation(&mut self) -> Result { let filter = self.parse_show_statement_filter()?; Ok(Statement::ShowCollation { filter }) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d2d2646cd..7fb71ea94 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5569,3 +5569,13 @@ fn parse_cursor() { _ => unreachable!(), } } + +#[test] +fn parse_show_functions() { + assert_eq!( + verified_stmt("SHOW FUNCTIONS LIKE 'pattern'"), + Statement::ShowFunctions { + filter: Some(ShowStatementFilter::Like("pattern".into())), + } + ); +} From 39761b05991ec7b0431bff0a41eff6284c0bb34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Heres?= Date: Mon, 26 Sep 2022 13:22:03 +0200 Subject: [PATCH 032/806] Add optional format for explain (#621) * Add format for explain * Add comment --- src/ast/mod.rs | 25 ++++++++++++++++++++ src/keywords.rs | 2 ++ src/parser.rs | 17 ++++++++++++++ tests/sqlparser_common.rs | 49 +++++++++++++++++++++++++++++++++++---- 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a6654be0e..dbf43b838 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1294,6 +1294,8 @@ pub enum Statement { verbose: bool, /// A SQL query that specifies what to explain statement: Box, + /// Optional output format of explain + format: Option, }, /// SAVEPOINT -- define a new savepoint within the current transaction Savepoint { name: Ident }, @@ -1344,6 +1346,7 @@ impl fmt::Display for Statement { verbose, analyze, statement, + format, } => { if *describe_alias { write!(f, "DESCRIBE ")?; @@ -1359,6 +1362,10 @@ impl fmt::Display for Statement { write!(f, "VERBOSE ")?; } + if let Some(format) = format { + write!(f, "FORMAT {} ", format)?; + } + write!(f, "{}", statement) } Statement::Query(s) => write!(f, "{}", s), @@ -2489,6 +2496,24 @@ pub struct Function { pub special: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum AnalyzeFormat { + TEXT, + GRAPHVIZ, + JSON, +} + +impl fmt::Display for AnalyzeFormat { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(match self { + AnalyzeFormat::TEXT => "TEXT", + AnalyzeFormat::GRAPHVIZ => "GRAPHVIZ", + AnalyzeFormat::JSON => "JSON", + }) + } +} + impl fmt::Display for Function { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.special { diff --git a/src/keywords.rs b/src/keywords.rs index 2fa79855b..23e4fee22 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -256,6 +256,7 @@ define_keywords!( GLOBAL, GRANT, GRANTED, + GRAPHVIZ, GROUP, GROUPING, GROUPS, @@ -288,6 +289,7 @@ define_keywords!( ISOYEAR, JAR, JOIN, + JSON, JSONFILE, JULIAN, KEY, diff --git a/src/parser.rs b/src/parser.rs index a9ee5b0e4..41b2f7b2a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2023,6 +2023,18 @@ impl<'a> Parser<'a> { } } + pub fn parse_analyze_format(&mut self) -> Result { + match self.next_token() { + Token::Word(w) => match w.keyword { + Keyword::TEXT => Ok(AnalyzeFormat::TEXT), + Keyword::GRAPHVIZ => Ok(AnalyzeFormat::GRAPHVIZ), + Keyword::JSON => Ok(AnalyzeFormat::JSON), + _ => self.expected("fileformat", Token::Word(w)), + }, + unexpected => self.expected("fileformat", unexpected), + } + } + pub fn parse_create_view(&mut self, or_replace: bool) -> Result { let materialized = self.parse_keyword(Keyword::MATERIALIZED); self.expect_keyword(Keyword::VIEW)?; @@ -3432,6 +3444,10 @@ impl<'a> Parser<'a> { pub fn parse_explain(&mut self, describe_alias: bool) -> Result { let analyze = self.parse_keyword(Keyword::ANALYZE); let verbose = self.parse_keyword(Keyword::VERBOSE); + let mut format = None; + if self.parse_keyword(Keyword::FORMAT) { + format = Some(self.parse_analyze_format()?); + } if let Some(statement) = self.maybe_parse(|parser| parser.parse_statement()) { Ok(Statement::Explain { @@ -3439,6 +3455,7 @@ impl<'a> Parser<'a> { analyze, verbose, statement: Box::new(statement), + format, }) } else { let table_name = self.parse_object_name()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7fb71ea94..9cdd492e6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2657,16 +2657,23 @@ fn parse_scalar_function_in_projection() { } } -fn run_explain_analyze(query: &str, expected_verbose: bool, expected_analyze: bool) { +fn run_explain_analyze( + query: &str, + expected_verbose: bool, + expected_analyze: bool, + expected_format: Option, +) { match verified_stmt(query) { Statement::Explain { describe_alias: _, analyze, verbose, statement, + format, } => { assert_eq!(verbose, expected_verbose); assert_eq!(analyze, expected_analyze); + assert_eq!(format, expected_format); assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); } _ => panic!("Unexpected Statement, must be Explain"), @@ -2693,15 +2700,47 @@ fn parse_explain_table() { #[test] fn parse_explain_analyze_with_simple_select() { // Describe is an alias for EXPLAIN - run_explain_analyze("DESCRIBE SELECT sqrt(id) FROM foo", false, false); + run_explain_analyze("DESCRIBE SELECT sqrt(id) FROM foo", false, false, None); - run_explain_analyze("EXPLAIN SELECT sqrt(id) FROM foo", false, false); - run_explain_analyze("EXPLAIN VERBOSE SELECT sqrt(id) FROM foo", true, false); - run_explain_analyze("EXPLAIN ANALYZE SELECT sqrt(id) FROM foo", false, true); + run_explain_analyze("EXPLAIN SELECT sqrt(id) FROM foo", false, false, None); + run_explain_analyze( + "EXPLAIN VERBOSE SELECT sqrt(id) FROM foo", + true, + false, + None, + ); + run_explain_analyze( + "EXPLAIN ANALYZE SELECT sqrt(id) FROM foo", + false, + true, + None, + ); run_explain_analyze( "EXPLAIN ANALYZE VERBOSE SELECT sqrt(id) FROM foo", true, true, + None, + ); + + run_explain_analyze( + "EXPLAIN ANALYZE FORMAT GRAPHVIZ SELECT sqrt(id) FROM foo", + false, + true, + Some(AnalyzeFormat::GRAPHVIZ), + ); + + run_explain_analyze( + "EXPLAIN ANALYZE VERBOSE FORMAT JSON SELECT sqrt(id) FROM foo", + true, + true, + Some(AnalyzeFormat::JSON), + ); + + run_explain_analyze( + "EXPLAIN VERBOSE FORMAT TEXT SELECT sqrt(id) FROM foo", + true, + false, + Some(AnalyzeFormat::TEXT), ); } From 0724ef13a437ee13e936b58b6ed5b63e9ec77472 Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Tue, 27 Sep 2022 17:03:58 +0400 Subject: [PATCH 033/806] Box `Query` in `Cte` (#572) --- src/ast/query.rs | 2 +- src/parser.rs | 4 ++-- tests/sqlparser_common.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index bc5af9e5f..011d4658b 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -271,7 +271,7 @@ impl fmt::Display for With { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Cte { pub alias: TableAlias, - pub query: Query, + pub query: Box, pub from: Option, } diff --git a/src/parser.rs b/src/parser.rs index 41b2f7b2a..280483c13 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3550,7 +3550,7 @@ impl<'a> Parser<'a> { let mut cte = if self.parse_keyword(Keyword::AS) { self.expect_token(&Token::LParen)?; - let query = self.parse_query()?; + let query = Box::new(self.parse_query()?); self.expect_token(&Token::RParen)?; let alias = TableAlias { name, @@ -3565,7 +3565,7 @@ impl<'a> Parser<'a> { let columns = self.parse_parenthesized_column_list(Optional)?; self.expect_keyword(Keyword::AS)?; self.expect_token(&Token::LParen)?; - let query = self.parse_query()?; + let query = Box::new(self.parse_query()?); self.expect_token(&Token::RParen)?; let alias = TableAlias { name, columns }; Cte { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9cdd492e6..5ac093a89 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3861,7 +3861,7 @@ fn parse_recursive_cte() { quote_style: None, }], }, - query: cte_query, + query: Box::new(cte_query), from: None, }; assert_eq!(with.cte_tables.first().unwrap(), &expected); From d4e5b4d5e89f8b847f3bcf9d4c6de86d4c9937dd Mon Sep 17 00:00:00 2001 From: Maciej Skrzypkowski Date: Tue, 27 Sep 2022 15:07:13 +0200 Subject: [PATCH 034/806] Support National string literal with lower case `n` (#612) * National string literal with lower case n It's used by Snowflake * Corrected DB name Co-authored-by: Maciej Skrzypkowski --- src/tokenizer.rs | 5 +++-- tests/sqlparser_common.rs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index fdd066f61..f128c9d43 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -387,7 +387,8 @@ impl<'a> Tokenizer<'a> { } Ok(Some(Token::Whitespace(Whitespace::Newline))) } - 'N' => { + // Redshift uses lower case n for national string literal + n @ 'N' | n @ 'n' => { chars.next(); // consume, to check the next char match chars.peek() { Some('\'') => { @@ -397,7 +398,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "N" - let s = self.tokenize_word('N', chars); + let s = self.tokenize_word(n, chars); Ok(Some(Token::make_word(&s, None))) } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5ac093a89..324c22c3d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2851,6 +2851,7 @@ fn parse_literal_string() { ); one_statement_parses_to("SELECT x'deadBEEF'", "SELECT X'deadBEEF'"); + one_statement_parses_to("SELECT n'national string'", "SELECT N'national string'"); } #[test] From d971a029ddd8ac4948899c749643bcf5bc0b76d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Sep 2022 09:07:59 -0400 Subject: [PATCH 035/806] Update criterion requirement from 0.3 to 0.4 in /sqlparser_bench (#611) Updates the requirements on [criterion](https://github.com/bheisler/criterion.rs) to permit the latest version. - [Release notes](https://github.com/bheisler/criterion.rs/releases) - [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/bheisler/criterion.rs/compare/0.3.0...0.4.0) --- updated-dependencies: - dependency-name: criterion dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sqlparser_bench/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlparser_bench/Cargo.toml b/sqlparser_bench/Cargo.toml index d98ff156c..f5dc85e4a 100644 --- a/sqlparser_bench/Cargo.toml +++ b/sqlparser_bench/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" sqlparser = { path = "../" } [dev-dependencies] -criterion = "0.3" +criterion = "0.4" [[bench]] name = "sqlparser_bench" From 3ac1bb5b80ef18102d2ba4cd3e5082b224efd2cc Mon Sep 17 00:00:00 2001 From: ding-young Date: Tue, 27 Sep 2022 22:30:48 +0900 Subject: [PATCH 036/806] Move `Value::Interval` to `Expr::Interval` (#609) * refactor(value): convert Value::Interval to Expr::Interval * test(sqlparser_common): modify test case * refactor(parser): rename func parse_interval * refactor(tests) rename parse_interval test func --- src/ast/mod.rs | 57 +++++++++++++++++++++++++++++++++++++ src/ast/value.rs | 59 --------------------------------------- src/parser.rs | 14 +++++----- tests/sqlparser_common.rs | 34 +++++++++++----------- 4 files changed, 81 insertions(+), 83 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dbf43b838..76781d0c6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -410,6 +410,25 @@ pub enum Expr { ArrayIndex { obj: Box, indexes: Vec }, /// An array expression e.g. `ARRAY[1, 2]` Array(Array), + /// INTERVAL literals, roughly in the following format: + /// `INTERVAL '' [ [ () ] ] + /// [ TO [ () ] ]`, + /// e.g. `INTERVAL '123:45.67' MINUTE(3) TO SECOND(2)`. + /// + /// The parser does not validate the ``, nor does it ensure + /// that the `` units >= the units in ``, + /// so the user will have to reject intervals like `HOUR TO YEAR`. + Interval { + value: Box, + leading_field: Option, + leading_precision: Option, + last_field: Option, + /// The seconds precision can be specified in SQL source as + /// `INTERVAL '__' SECOND(_, x)` (in which case the `leading_field` + /// will be `Second` and the `last_field` will be `None`), + /// or as `__ TO SECOND(x)`. + fractional_seconds_precision: Option, + }, } impl fmt::Display for Expr { @@ -722,6 +741,44 @@ impl fmt::Display for Expr { } => { write!(f, "{} AT TIME ZONE '{}'", timestamp, time_zone) } + Expr::Interval { + value, + leading_field: Some(DateTimeField::Second), + leading_precision: Some(leading_precision), + last_field, + fractional_seconds_precision: Some(fractional_seconds_precision), + } => { + // When the leading field is SECOND, the parser guarantees that + // the last field is None. + assert!(last_field.is_none()); + write!( + f, + "INTERVAL {} SECOND ({}, {})", + value, leading_precision, fractional_seconds_precision + ) + } + Expr::Interval { + value, + leading_field, + leading_precision, + last_field, + fractional_seconds_precision, + } => { + write!(f, "INTERVAL {}", value)?; + if let Some(leading_field) = leading_field { + write!(f, " {}", leading_field)?; + } + if let Some(leading_precision) = leading_precision { + write!(f, " ({})", leading_precision)?; + } + if let Some(last_field) = last_field { + write!(f, " TO {}", last_field)?; + } + if let Some(fractional_seconds_precision) = fractional_seconds_precision { + write!(f, " ({})", fractional_seconds_precision)?; + } + Ok(()) + } } } } diff --git a/src/ast/value.rs b/src/ast/value.rs index 1260f3f15..6f5953c4b 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -21,8 +21,6 @@ use bigdecimal::BigDecimal; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use super::Expr; - /// Primitive SQL values such as number and string #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -45,25 +43,6 @@ pub enum Value { DoubleQuotedString(String), /// Boolean value true or false Boolean(bool), - /// INTERVAL literals, roughly in the following format: - /// `INTERVAL '' [ [ () ] ] - /// [ TO [ () ] ]`, - /// e.g. `INTERVAL '123:45.67' MINUTE(3) TO SECOND(2)`. - /// - /// The parser does not validate the ``, nor does it ensure - /// that the `` units >= the units in ``, - /// so the user will have to reject intervals like `HOUR TO YEAR`. - Interval { - value: Box, - leading_field: Option, - leading_precision: Option, - last_field: Option, - /// The seconds precision can be specified in SQL source as - /// `INTERVAL '__' SECOND(_, x)` (in which case the `leading_field` - /// will be `Second` and the `last_field` will be `None`), - /// or as `__ TO SECOND(x)`. - fractional_seconds_precision: Option, - }, /// `NULL` value Null, /// `?` or `$` Prepared statement arg placeholder @@ -80,44 +59,6 @@ impl fmt::Display for Value { Value::NationalStringLiteral(v) => write!(f, "N'{}'", v), Value::HexStringLiteral(v) => write!(f, "X'{}'", v), Value::Boolean(v) => write!(f, "{}", v), - Value::Interval { - value, - leading_field: Some(DateTimeField::Second), - leading_precision: Some(leading_precision), - last_field, - fractional_seconds_precision: Some(fractional_seconds_precision), - } => { - // When the leading field is SECOND, the parser guarantees that - // the last field is None. - assert!(last_field.is_none()); - write!( - f, - "INTERVAL {} SECOND ({}, {})", - value, leading_precision, fractional_seconds_precision - ) - } - Value::Interval { - value, - leading_field, - leading_precision, - last_field, - fractional_seconds_precision, - } => { - write!(f, "INTERVAL {}", value)?; - if let Some(leading_field) = leading_field { - write!(f, " {}", leading_field)?; - } - if let Some(leading_precision) = leading_precision { - write!(f, " ({})", leading_precision)?; - } - if let Some(last_field) = last_field { - write!(f, " TO {}", last_field)?; - } - if let Some(fractional_seconds_precision) = fractional_seconds_precision { - write!(f, " ({})", fractional_seconds_precision)?; - } - Ok(()) - } Value::Null => write!(f, "NULL"), Value::Placeholder(v) => write!(f, "{}", v), } diff --git a/src/parser.rs b/src/parser.rs index 280483c13..fd752142a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -402,7 +402,7 @@ impl<'a> Parser<'a> { // expression that should parse as the column name "date". return_ok_if_some!(self.maybe_parse(|parser| { match parser.parse_data_type()? { - DataType::Interval => parser.parse_literal_interval(), + DataType::Interval => parser.parse_interval(), // PostgreSQL allows almost any identifier to be used as custom data type name, // and we support that in `parse_data_type()`. But unlike Postgres we don't // have a list of globally reserved keywords (since they vary across dialects), @@ -455,7 +455,7 @@ impl<'a> Parser<'a> { Keyword::SUBSTRING => self.parse_substring_expr(), Keyword::OVERLAY => self.parse_overlay_expr(), Keyword::TRIM => self.parse_trim_expr(), - Keyword::INTERVAL => self.parse_literal_interval(), + Keyword::INTERVAL => self.parse_interval(), Keyword::LISTAGG => self.parse_listagg_expr(), // Treat ARRAY[1,2,3] as an array [1,2,3], otherwise try as subquery or a function call Keyword::ARRAY if self.peek_token() == Token::LBracket => { @@ -1096,7 +1096,7 @@ impl<'a> Parser<'a> { } } - /// Parse an INTERVAL literal. + /// Parse an INTERVAL expression. /// /// Some syntactically valid intervals: /// @@ -1109,7 +1109,7 @@ impl<'a> Parser<'a> { /// 7. (MySql and BigQuey only):`INTERVAL 1 DAY` /// /// Note that we do not currently attempt to parse the quoted value. - pub fn parse_literal_interval(&mut self) -> Result { + pub fn parse_interval(&mut self) -> Result { // The SQL standard allows an optional sign before the value string, but // it is not clear if any implementations support that syntax, so we // don't currently try to parse it. (The sign can instead be included @@ -1183,13 +1183,13 @@ impl<'a> Parser<'a> { } }; - Ok(Expr::Value(Value::Interval { + Ok(Expr::Interval { value: Box::new(value), leading_field, leading_precision, last_field, fractional_seconds_precision: fsec_precision, - })) + }) } /// Parse an operator following an expression @@ -3178,7 +3178,7 @@ impl<'a> Parser<'a> { } // Interval types can be followed by a complicated interval // qualifier that we don't currently support. See - // parse_interval_literal for a taste. + // parse_interval for a taste. Keyword::INTERVAL => Ok(DataType::Interval), Keyword::REGCLASS => Ok(DataType::Regclass), Keyword::STRING => Ok(DataType::String), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 324c22c3d..8323d139c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2930,24 +2930,24 @@ fn parse_literal_timestamp_with_time_zone() { } #[test] -fn parse_literal_interval() { +fn parse_interval() { let sql = "SELECT INTERVAL '1-1' YEAR TO MONTH"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Interval { + &Expr::Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("1-1")))), leading_field: Some(DateTimeField::Year), leading_precision: None, last_field: Some(DateTimeField::Month), fractional_seconds_precision: None, - }), + }, expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '01:01.01' MINUTE (5) TO SECOND (5)"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Interval { + &Expr::Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( "01:01.01" )))), @@ -2955,53 +2955,53 @@ fn parse_literal_interval() { leading_precision: Some(5), last_field: Some(DateTimeField::Second), fractional_seconds_precision: Some(5), - }), + }, expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '1' SECOND (5, 4)"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Interval { + &Expr::Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("1")))), leading_field: Some(DateTimeField::Second), leading_precision: Some(5), last_field: None, fractional_seconds_precision: Some(4), - }), + }, expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '10' HOUR"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Interval { + &Expr::Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("10")))), leading_field: Some(DateTimeField::Hour), leading_precision: None, last_field: None, fractional_seconds_precision: None, - }), + }, expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL 5 DAY"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Interval { + &Expr::Interval { value: Box::new(Expr::Value(number("5"))), leading_field: Some(DateTimeField::Day), leading_precision: None, last_field: None, fractional_seconds_precision: None, - }), + }, expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL 1 + 1 DAY"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Interval { + &Expr::Interval { value: Box::new(Expr::BinaryOp { left: Box::new(Expr::Value(number("1"))), op: BinaryOperator::Plus, @@ -3011,27 +3011,27 @@ fn parse_literal_interval() { leading_precision: None, last_field: None, fractional_seconds_precision: None, - }), + }, expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '10' HOUR (1)"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Interval { + &Expr::Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("10")))), leading_field: Some(DateTimeField::Hour), leading_precision: Some(1), last_field: None, fractional_seconds_precision: None, - }), + }, expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '1 DAY'"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Interval { + &Expr::Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( "1 DAY" )))), @@ -3039,7 +3039,7 @@ fn parse_literal_interval() { leading_precision: None, last_field: None, fractional_seconds_precision: None, - }), + }, expr_from_projection(only(&select.projection)), ); From 604f755a59087a92342f80e2bb0291cd6a38a71b Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 27 Sep 2022 15:58:26 +0200 Subject: [PATCH 037/806] Fix parse error on some prepared statement placeholders (#604) sqlparser can now parse all the prepared statement placeholders supported by SQLite: - ? - ?NNN - @VVV - :VVV - $VVV See: https://www.sqlite.org/lang_expr.html#varparam This does not break existing support for postgresql's '@' operator Fixes #603 --- src/parser.rs | 13 +++++++++---- src/tokenizer.rs | 11 ++++++----- tests/sqlparser_common.rs | 12 ++++++++++++ tests/sqlparser_sqlite.rs | 21 ++++++++++++++++++--- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index fd752142a..c8533a237 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -570,7 +570,7 @@ impl<'a> Parser<'a> { }) } } - Token::Placeholder(_) => { + Token::Placeholder(_) | Token::Colon | Token::AtSign => { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } @@ -1774,7 +1774,7 @@ impl<'a> Parser<'a> { .iter() .any(|d| kw.keyword == *d) => { - break + break; } Token::RParen | Token::EOF => break, _ => continue, @@ -3038,6 +3038,11 @@ impl<'a> Parser<'a> { Token::EscapedStringLiteral(ref s) => Ok(Value::EscapedStringLiteral(s.to_string())), Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), Token::Placeholder(ref s) => Ok(Value::Placeholder(s.to_string())), + tok @ Token::Colon | tok @ Token::AtSign => { + let ident = self.parse_identifier()?; + let placeholder = tok.to_string() + &ident.value; + Ok(Value::Placeholder(placeholder)) + } unexpected => self.expected("a value", unexpected), } } @@ -4892,12 +4897,12 @@ impl<'a> Parser<'a> { Some(_) => { return Err(ParserError::ParserError( "expected UPDATE, DELETE or INSERT in merge clause".to_string(), - )) + )); } None => { return Err(ParserError::ParserError( "expected UPDATE, DELETE or INSERT in merge clause".to_string(), - )) + )); } }, ); diff --git a/src/tokenizer.rs b/src/tokenizer.rs index f128c9d43..4253dd80a 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -678,13 +678,14 @@ impl<'a> Tokenizer<'a> { } } '@' => self.consume_and_return(chars, Token::AtSign), - '?' => self.consume_and_return(chars, Token::Placeholder(String::from("?"))), + '?' => { + chars.next(); + let s = peeking_take_while(chars, |ch| ch.is_numeric()); + Ok(Some(Token::Placeholder(String::from("?") + &s))) + } '$' => { chars.next(); - let s = peeking_take_while( - chars, - |ch| matches!(ch, '0'..='9' | 'A'..='Z' | 'a'..='z'), - ); + let s = peeking_take_while(chars, |ch| ch.is_alphanumeric() || ch == '_'); Ok(Some(Token::Placeholder(String::from("$") + &s))) } //whitespace check (including unicode chars) should be last as it covers some of the chars above diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8323d139c..b019b71e5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22,6 +22,7 @@ mod test_utils; use matches::assert_matches; +use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::*; use sqlparser::dialect::{ AnsiDialect, BigQueryDialect, ClickHouseDialect, GenericDialect, HiveDialect, MsSqlDialect, @@ -5299,6 +5300,17 @@ fn test_placeholder() { rows: OffsetRows::None, }), ); + + let sql = "SELECT $fromage_français, :x, ?123"; + let ast = dialects.verified_only_select(sql); + assert_eq!( + ast.projection, + vec![ + UnnamedExpr(Expr::Value(Value::Placeholder("$fromage_français".into()))), + UnnamedExpr(Expr::Value(Value::Placeholder(":x".into()))), + UnnamedExpr(Expr::Value(Value::Placeholder("?123".into()))), + ] + ); } #[test] diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 61436cb51..3f60e16d1 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -16,8 +16,10 @@ #[macro_use] mod test_utils; + use test_utils::*; +use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SQLiteDialect}; use sqlparser::tokenizer::Token; @@ -73,14 +75,14 @@ fn parse_create_table_auto_increment() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { is_primary: true } + option: ColumnOption::Unique { is_primary: true }, }, ColumnOptionDef { name: None, option: ColumnOption::DialectSpecific(vec![Token::make_keyword( "AUTOINCREMENT" - )]) - } + )]), + }, ], }], columns @@ -118,6 +120,19 @@ fn parse_create_sqlite_quote() { } } +#[test] +fn test_placeholder() { + // In postgres, this would be the absolute value operator '@' applied to the column 'xxx' + // But in sqlite, this is a named parameter. + // see https://www.sqlite.org/lang_expr.html#varparam + let sql = "SELECT @xxx"; + let ast = sqlite().verified_only_select(sql); + assert_eq!( + ast.projection[0], + UnnamedExpr(Expr::Value(Value::Placeholder("@xxx".into()))), + ); +} + fn sqlite() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SQLiteDialect {})], From 91087fcba0f9c2356e3dcb6397ae352850cbdaca Mon Sep 17 00:00:00 2001 From: Ben Cook Date: Tue, 27 Sep 2022 06:58:36 -0700 Subject: [PATCH 038/806] Support CREATE ROLE and DROP ROLE (#598) * Parse GRANT ROLE and DROP ROLE * Gate create role on dialect * cargo fmt * clippy * no-std * clippy again --- src/ast/mod.rs | 114 +++++++++++++++++++ src/keywords.rs | 18 +++ src/parser.rs | 213 +++++++++++++++++++++++++++++++++++- src/test_utils.rs | 7 ++ tests/sqlparser_common.rs | 110 ++++++++++++------- tests/sqlparser_mssql.rs | 22 ++++ tests/sqlparser_postgres.rs | 101 +++++++++++++++++ 7 files changed, 543 insertions(+), 42 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 76781d0c6..73895d069 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -954,6 +954,13 @@ impl fmt::Display for CommentObject { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Password { + Password(Expr), + NullPassword, +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1109,6 +1116,27 @@ pub enum Statement { unique: bool, if_not_exists: bool, }, + /// CREATE ROLE + CreateRole { + names: Vec, + if_not_exists: bool, + // Postgres + login: Option, + inherit: Option, + bypassrls: Option, + password: Option, + superuser: Option, + create_db: Option, + create_role: Option, + replication: Option, + connection_limit: Option, + valid_until: Option, + in_role: Vec, + role: Vec, + admin: Vec, + // MSSQL + authorization_owner: Option, + }, /// ALTER TABLE AlterTable { /// Table name @@ -1963,6 +1991,90 @@ impl fmt::Display for Statement { table_name = table_name, columns = display_separated(columns, ",") ), + Statement::CreateRole { + names, + if_not_exists, + inherit, + login, + bypassrls, + password, + create_db, + create_role, + superuser, + replication, + connection_limit, + valid_until, + in_role, + role, + admin, + authorization_owner, + } => { + write!( + f, + "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + names = display_separated(names, ", "), + superuser = match *superuser { + Some(true) => " SUPERUSER", + Some(false) => " NOSUPERUSER", + None => "" + }, + create_db = match *create_db { + Some(true) => " CREATEDB", + Some(false) => " NOCREATEDB", + None => "" + }, + create_role = match *create_role { + Some(true) => " CREATEROLE", + Some(false) => " NOCREATEROLE", + None => "" + }, + inherit = match *inherit { + Some(true) => " INHERIT", + Some(false) => " NOINHERIT", + None => "" + }, + login = match *login { + Some(true) => " LOGIN", + Some(false) => " NOLOGIN", + None => "" + }, + replication = match *replication { + Some(true) => " REPLICATION", + Some(false) => " NOREPLICATION", + None => "" + }, + bypassrls = match *bypassrls { + Some(true) => " BYPASSRLS", + Some(false) => " NOBYPASSRLS", + None => "" + } + )?; + if let Some(limit) = connection_limit { + write!(f, " CONNECTION LIMIT {}", limit)?; + } + match password { + Some(Password::Password(pass)) => write!(f, " PASSWORD {}", pass), + Some(Password::NullPassword) => write!(f, " PASSWORD NULL"), + None => Ok(()), + }?; + if let Some(until) = valid_until { + write!(f, " VALID UNTIL {}", until)?; + } + if !in_role.is_empty() { + write!(f, " IN ROLE {}", display_comma_separated(in_role))?; + } + if !role.is_empty() { + write!(f, " ROLE {}", display_comma_separated(role))?; + } + if !admin.is_empty() { + write!(f, " ADMIN {}", display_comma_separated(admin))?; + } + if let Some(owner) = authorization_owner { + write!(f, " AUTHORIZATION {}", owner)?; + } + Ok(()) + } Statement::AlterTable { name, operation } => { write!(f, "ALTER TABLE {} {}", name, operation) } @@ -2701,6 +2813,7 @@ pub enum ObjectType { View, Index, Schema, + Role, } impl fmt::Display for ObjectType { @@ -2710,6 +2823,7 @@ impl fmt::Display for ObjectType { ObjectType::View => "VIEW", ObjectType::Index => "INDEX", ObjectType::Schema => "SCHEMA", + ObjectType::Role => "ROLE", }) } } diff --git a/src/keywords.rs b/src/keywords.rs index 23e4fee22..35d98eef7 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -70,6 +70,7 @@ define_keywords!( ABSOLUTE, ACTION, ADD, + ADMIN, ALL, ALLOCATE, ALTER, @@ -105,6 +106,7 @@ define_keywords!( BOOLEAN, BOTH, BY, + BYPASSRLS, BYTEA, CACHE, CALL, @@ -152,6 +154,8 @@ define_keywords!( COVAR_POP, COVAR_SAMP, CREATE, + CREATEDB, + CREATEROLE, CROSS, CSV, CUBE, @@ -272,6 +276,7 @@ define_keywords!( IN, INDEX, INDICATOR, + INHERIT, INNER, INOUT, INPUTFORMAT, @@ -313,6 +318,7 @@ define_keywords!( LOCALTIME, LOCALTIMESTAMP, LOCATION, + LOGIN, LOWER, MANAGEDLOCATION, MATCH, @@ -342,9 +348,16 @@ define_keywords!( NEW, NEXT, NO, + NOBYPASSRLS, + NOCREATEDB, + NOCREATEROLE, + NOINHERIT, + NOLOGIN, NONE, + NOREPLICATION, NORMALIZE, NOSCAN, + NOSUPERUSER, NOT, NTH_VALUE, NTILE, @@ -380,6 +393,7 @@ define_keywords!( PARTITION, PARTITIONED, PARTITIONS, + PASSWORD, PERCENT, PERCENTILE_CONT, PERCENTILE_DISC, @@ -432,6 +446,7 @@ define_keywords!( REPAIR, REPEATABLE, REPLACE, + REPLICATION, RESTRICT, RESULT, RETURN, @@ -492,6 +507,7 @@ define_keywords!( SUCCEEDS, SUM, SUPER, + SUPERUSER, SYMMETRIC, SYNC, SYSTEM, @@ -538,6 +554,7 @@ define_keywords!( UNLOGGED, UNNEST, UNSIGNED, + UNTIL, UPDATE, UPPER, USAGE, @@ -545,6 +562,7 @@ define_keywords!( USER, USING, UUID, + VALID, VALUE, VALUES, VALUE_OF, diff --git a/src/parser.rs b/src/parser.rs index c8533a237..9289cdbb1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1866,6 +1866,8 @@ impl<'a> Parser<'a> { self.parse_create_database() } else if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::FUNCTION) { self.parse_create_function(temporary) + } else if self.parse_keyword(Keyword::ROLE) { + self.parse_create_role() } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -2056,6 +2058,207 @@ impl<'a> Parser<'a> { }) } + pub fn parse_create_role(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let names = self.parse_comma_separated(Parser::parse_object_name)?; + + let _ = self.parse_keyword(Keyword::WITH); + + let optional_keywords = if dialect_of!(self is MsSqlDialect) { + vec![Keyword::AUTHORIZATION] + } else if dialect_of!(self is PostgreSqlDialect) { + vec![ + Keyword::LOGIN, + Keyword::NOLOGIN, + Keyword::INHERIT, + Keyword::NOINHERIT, + Keyword::BYPASSRLS, + Keyword::NOBYPASSRLS, + Keyword::PASSWORD, + Keyword::CREATEDB, + Keyword::NOCREATEDB, + Keyword::CREATEROLE, + Keyword::NOCREATEROLE, + Keyword::SUPERUSER, + Keyword::NOSUPERUSER, + Keyword::REPLICATION, + Keyword::NOREPLICATION, + Keyword::CONNECTION, + Keyword::VALID, + Keyword::IN, + Keyword::ROLE, + Keyword::ADMIN, + Keyword::USER, + ] + } else { + vec![] + }; + + // MSSQL + let mut authorization_owner = None; + // Postgres + let mut login = None; + let mut inherit = None; + let mut bypassrls = None; + let mut password = None; + let mut create_db = None; + let mut create_role = None; + let mut superuser = None; + let mut replication = None; + let mut connection_limit = None; + let mut valid_until = None; + let mut in_role = vec![]; + let mut roles = vec![]; + let mut admin = vec![]; + + while let Some(keyword) = self.parse_one_of_keywords(&optional_keywords) { + match keyword { + Keyword::AUTHORIZATION => { + if authorization_owner.is_some() { + parser_err!("Found multiple AUTHORIZATION") + } else { + authorization_owner = Some(self.parse_object_name()?); + Ok(()) + } + } + Keyword::LOGIN | Keyword::NOLOGIN => { + if login.is_some() { + parser_err!("Found multiple LOGIN or NOLOGIN") + } else { + login = Some(keyword == Keyword::LOGIN); + Ok(()) + } + } + Keyword::INHERIT | Keyword::NOINHERIT => { + if inherit.is_some() { + parser_err!("Found multiple INHERIT or NOINHERIT") + } else { + inherit = Some(keyword == Keyword::INHERIT); + Ok(()) + } + } + Keyword::BYPASSRLS | Keyword::NOBYPASSRLS => { + if bypassrls.is_some() { + parser_err!("Found multiple BYPASSRLS or NOBYPASSRLS") + } else { + bypassrls = Some(keyword == Keyword::BYPASSRLS); + Ok(()) + } + } + Keyword::CREATEDB | Keyword::NOCREATEDB => { + if create_db.is_some() { + parser_err!("Found multiple CREATEDB or NOCREATEDB") + } else { + create_db = Some(keyword == Keyword::CREATEDB); + Ok(()) + } + } + Keyword::CREATEROLE | Keyword::NOCREATEROLE => { + if create_role.is_some() { + parser_err!("Found multiple CREATEROLE or NOCREATEROLE") + } else { + create_role = Some(keyword == Keyword::CREATEROLE); + Ok(()) + } + } + Keyword::SUPERUSER | Keyword::NOSUPERUSER => { + if superuser.is_some() { + parser_err!("Found multiple SUPERUSER or NOSUPERUSER") + } else { + superuser = Some(keyword == Keyword::SUPERUSER); + Ok(()) + } + } + Keyword::REPLICATION | Keyword::NOREPLICATION => { + if replication.is_some() { + parser_err!("Found multiple REPLICATION or NOREPLICATION") + } else { + replication = Some(keyword == Keyword::REPLICATION); + Ok(()) + } + } + Keyword::PASSWORD => { + if password.is_some() { + parser_err!("Found multiple PASSWORD") + } else { + password = if self.parse_keyword(Keyword::NULL) { + Some(Password::NullPassword) + } else { + Some(Password::Password(Expr::Value(self.parse_value()?))) + }; + Ok(()) + } + } + Keyword::CONNECTION => { + self.expect_keyword(Keyword::LIMIT)?; + if connection_limit.is_some() { + parser_err!("Found multiple CONNECTION LIMIT") + } else { + connection_limit = Some(Expr::Value(self.parse_number_value()?)); + Ok(()) + } + } + Keyword::VALID => { + self.expect_keyword(Keyword::UNTIL)?; + if valid_until.is_some() { + parser_err!("Found multiple VALID UNTIL") + } else { + valid_until = Some(Expr::Value(self.parse_value()?)); + Ok(()) + } + } + Keyword::IN => { + if self.parse_keyword(Keyword::ROLE) || self.parse_keyword(Keyword::GROUP) { + if !in_role.is_empty() { + parser_err!("Found multiple IN ROLE or IN GROUP") + } else { + in_role = self.parse_comma_separated(Parser::parse_identifier)?; + Ok(()) + } + } else { + self.expected("ROLE or GROUP after IN", self.peek_token()) + } + } + Keyword::ROLE | Keyword::USER => { + if !roles.is_empty() { + parser_err!("Found multiple ROLE or USER") + } else { + roles = self.parse_comma_separated(Parser::parse_identifier)?; + Ok(()) + } + } + Keyword::ADMIN => { + if !admin.is_empty() { + parser_err!("Found multiple ADMIN") + } else { + admin = self.parse_comma_separated(Parser::parse_identifier)?; + Ok(()) + } + } + _ => break, + }? + } + + Ok(Statement::CreateRole { + names, + if_not_exists, + login, + inherit, + bypassrls, + password, + create_db, + create_role, + replication, + superuser, + connection_limit, + valid_until, + in_role, + role: roles, + admin, + authorization_owner, + }) + } + pub fn parse_drop(&mut self) -> Result { let object_type = if self.parse_keyword(Keyword::TABLE) { ObjectType::Table @@ -2063,10 +2266,15 @@ impl<'a> Parser<'a> { ObjectType::View } else if self.parse_keyword(Keyword::INDEX) { ObjectType::Index + } else if self.parse_keyword(Keyword::ROLE) { + ObjectType::Role } else if self.parse_keyword(Keyword::SCHEMA) { ObjectType::Schema } else { - return self.expected("TABLE, VIEW, INDEX or SCHEMA after DROP", self.peek_token()); + return self.expected( + "TABLE, VIEW, INDEX, ROLE, or SCHEMA after DROP", + self.peek_token(), + ); }; // Many dialects support the non standard `IF EXISTS` clause and allow // specifying multiple objects to delete in a single statement @@ -2078,6 +2286,9 @@ impl<'a> Parser<'a> { if cascade && restrict { return parser_err!("Cannot specify both CASCADE and RESTRICT in DROP"); } + if object_type == ObjectType::Role && (cascade || restrict || purge) { + return parser_err!("Cannot specify CASCADE, RESTRICT, or PURGE in DROP ROLE"); + } Ok(Statement::Drop { object_type, if_exists, diff --git a/src/test_utils.rs b/src/test_utils.rs index c5ea62085..c3d60ee62 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -146,6 +146,13 @@ pub fn all_dialects() -> TestedDialects { } } +pub fn assert_eq_vec(expected: &[&str], actual: &[T]) { + assert_eq!( + expected, + actual.iter().map(ToString::to_string).collect::>() + ); +} + pub fn only(v: impl IntoIterator) -> T { let mut iter = v.into_iter(); if let (Some(item), None) = (iter.next(), iter.next()) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b019b71e5..e304f38c3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -32,7 +32,8 @@ use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError}; use test_utils::{ - all_dialects, expr_from_projection, join, number, only, table, table_alias, TestedDialects, + all_dialects, assert_eq_vec, expr_from_projection, join, number, only, table, table_alias, + TestedDialects, }; #[test] @@ -4850,6 +4851,63 @@ fn parse_drop_index() { } } +#[test] +fn parse_create_role() { + let sql = "CREATE ROLE consultant"; + match verified_stmt(sql) { + Statement::CreateRole { names, .. } => { + assert_eq_vec(&["consultant"], &names); + } + _ => unreachable!(), + } + + let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; + match verified_stmt(sql) { + Statement::CreateRole { + names, + if_not_exists, + .. + } => { + assert_eq_vec(&["mysql_a", "mysql_b"], &names); + assert!(if_not_exists); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_drop_role() { + let sql = "DROP ROLE abc"; + match verified_stmt(sql) { + Statement::Drop { + names, + object_type, + if_exists, + .. + } => { + assert_eq_vec(&["abc"], &names); + assert_eq!(ObjectType::Role, object_type); + assert!(!if_exists); + } + _ => unreachable!(), + }; + + let sql = "DROP ROLE IF EXISTS def, magician, quaternion"; + match verified_stmt(sql) { + Statement::Drop { + names, + object_type, + if_exists, + .. + } => { + assert_eq_vec(&["def", "magician", "quaternion"], &names); + assert_eq!(ObjectType::Role, object_type); + assert!(if_exists); + } + _ => unreachable!(), + } +} + #[test] fn parse_grant() { let sql = "GRANT SELECT, INSERT, UPDATE (shape, size), USAGE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CONNECT, CREATE, EXECUTE, TEMPORARY ON abc, def TO xyz, m WITH GRANT OPTION GRANTED BY jj"; @@ -4891,14 +4949,8 @@ fn parse_grant() { ], actions ); - assert_eq!( - vec!["abc", "def"], - objects.iter().map(ToString::to_string).collect::>() - ); - assert_eq!( - vec!["xyz", "m"], - grantees.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["abc", "def"], &objects); + assert_eq_vec(&["xyz", "m"], &grantees); assert!(with_grant_option); assert_eq!("jj", granted_by.unwrap().to_string()); } @@ -4918,14 +4970,8 @@ fn parse_grant() { } => match (privileges, objects) { (Privileges::Actions(actions), GrantObjects::AllTablesInSchema { schemas }) => { assert_eq!(vec![Action::Insert { columns: None }], actions); - assert_eq!( - vec!["public"], - schemas.iter().map(ToString::to_string).collect::>() - ); - assert_eq!( - vec!["browser"], - grantees.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["public"], &schemas); + assert_eq_vec(&["browser"], &grantees); assert!(!with_grant_option); } _ => unreachable!(), @@ -4947,14 +4993,8 @@ fn parse_grant() { vec![Action::Usage, Action::Select { columns: None }], actions ); - assert_eq!( - vec!["p"], - objects.iter().map(ToString::to_string).collect::>() - ); - assert_eq!( - vec!["u"], - grantees.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["p"], &objects); + assert_eq_vec(&["u"], &grantees); } _ => unreachable!(), }, @@ -4988,10 +5028,7 @@ fn parse_grant() { GrantObjects::Schemas(schemas), ) => { assert!(!with_privileges_keyword); - assert_eq!( - vec!["aa", "b"], - schemas.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["aa", "b"], &schemas); } _ => unreachable!(), }, @@ -5007,10 +5044,7 @@ fn parse_grant() { } => match (privileges, objects) { (Privileges::Actions(actions), GrantObjects::AllSequencesInSchema { schemas }) => { assert_eq!(vec![Action::Usage], actions); - assert_eq!( - vec!["bus"], - schemas.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["bus"], &schemas); } _ => unreachable!(), }, @@ -5035,14 +5069,8 @@ fn test_revoke() { }, privileges ); - assert_eq!( - vec!["users", "auth"], - tables.iter().map(ToString::to_string).collect::>() - ); - assert_eq!( - vec!["analyst"], - grantees.iter().map(ToString::to_string).collect::>() - ); + assert_eq_vec(&["users", "auth"], &tables); + assert_eq_vec(&["analyst"], &grantees); assert!(cascade); assert_eq!(None, granted_by); } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index c613b8b16..de2376f92 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -118,6 +118,28 @@ fn parse_mssql_bin_literal() { let _ = ms_and_generic().one_statement_parses_to("SELECT 0xdeadBEEF", "SELECT X'deadBEEF'"); } +#[test] +fn parse_mssql_create_role() { + let sql = "CREATE ROLE mssql AUTHORIZATION helena"; + match ms().verified_stmt(sql) { + Statement::CreateRole { + names, + authorization_owner, + .. + } => { + assert_eq_vec(&["mssql"], &names); + assert_eq!( + authorization_owner, + Some(ObjectName(vec![Ident { + value: "helena".into(), + quote_style: None + }])) + ); + } + _ => unreachable!(), + } +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3aaabc9e3..ecc41a4d3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1739,3 +1739,104 @@ fn parse_custom_operator() { }) ); } + +#[test] +fn parse_create_role() { + let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; + match pg().verified_stmt(sql) { + Statement::CreateRole { + names, + if_not_exists, + .. + } => { + assert_eq_vec(&["mysql_a", "mysql_b"], &names); + assert!(if_not_exists); + } + _ => unreachable!(), + } + + let sql = "CREATE ROLE abc LOGIN PASSWORD NULL"; + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::CreateRole { + names, + login, + password, + .. + }], + ) => { + assert_eq_vec(&["abc"], names); + assert_eq!(*login, Some(true)); + assert_eq!(*password, Some(Password::NullPassword)); + } + err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + } + + let sql = "CREATE ROLE magician WITH SUPERUSER CREATEROLE NOCREATEDB BYPASSRLS INHERIT PASSWORD 'abcdef' LOGIN VALID UNTIL '2025-01-01' IN ROLE role1, role2 ROLE role3 ADMIN role4, role5 REPLICATION"; + // Roundtrip order of optional parameters is not preserved + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::CreateRole { + names, + if_not_exists, + bypassrls, + login, + inherit, + password, + superuser, + create_db, + create_role, + replication, + connection_limit, + valid_until, + in_role, + role, + admin, + authorization_owner, + }], + ) => { + assert_eq_vec(&["magician"], names); + assert!(!*if_not_exists); + assert_eq!(*login, Some(true)); + assert_eq!(*inherit, Some(true)); + assert_eq!(*bypassrls, Some(true)); + assert_eq!( + *password, + Some(Password::Password(Expr::Value(Value::SingleQuotedString( + "abcdef".into() + )))) + ); + assert_eq!(*superuser, Some(true)); + assert_eq!(*create_db, Some(false)); + assert_eq!(*create_role, Some(true)); + assert_eq!(*replication, Some(true)); + assert_eq!(*connection_limit, None); + assert_eq!( + *valid_until, + Some(Expr::Value(Value::SingleQuotedString("2025-01-01".into()))) + ); + assert_eq_vec(&["role1", "role2"], in_role); + assert_eq_vec(&["role3"], role); + assert_eq_vec(&["role4", "role5"], admin); + assert_eq!(*authorization_owner, None); + } + err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + } + + let negatables = vec![ + "BYPASSRLS", + "CREATEDB", + "CREATEROLE", + "INHERIT", + "LOGIN", + "REPLICATION", + "SUPERUSER", + ]; + + for negatable_kw in negatables.iter() { + let sql = format!("CREATE ROLE abc {kw} NO{kw}", kw = negatable_kw); + if pg().parse_sql_statements(&sql).is_ok() { + panic!("Should not be able to parse CREATE ROLE containing both negated and non-negated versions of the same keyword: {}", negatable_kw) + } + } +} From 2b21da243948bf74ac345302320b91af4ddf1aad Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 27 Sep 2022 10:23:33 -0400 Subject: [PATCH 039/806] Add test for optional WITH in `CREATE ROLE` (#627) --- src/ast/mod.rs | 2 +- src/parser.rs | 1 + tests/sqlparser_postgres.rs | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 73895d069..22762168f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1229,7 +1229,7 @@ pub enum Statement { /// /// Note: this is a MySQL-specific statement. SetNamesDefault {}, - /// SHOW FUNCTIONS + /// SHOW FUNCTIONS /// /// Note: this is a Presto-specific statement. ShowFunctions { filter: Option }, diff --git a/src/parser.rs b/src/parser.rs index 9289cdbb1..503ef07db 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2062,6 +2062,7 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let names = self.parse_comma_separated(Parser::parse_object_name)?; + // Parse optional WITH let _ = self.parse_keyword(Keyword::WITH); let optional_keywords = if dialect_of!(self is MsSqlDialect) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ecc41a4d3..07e87770c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1772,6 +1772,23 @@ fn parse_create_role() { err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), } + let sql = "CREATE ROLE abc WITH LOGIN PASSWORD NULL"; + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::CreateRole { + names, + login, + password, + .. + }], + ) => { + assert_eq_vec(&["abc"], names); + assert_eq!(*login, Some(true)); + assert_eq!(*password, Some(Password::NullPassword)); + } + err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + } + let sql = "CREATE ROLE magician WITH SUPERUSER CREATEROLE NOCREATEDB BYPASSRLS INHERIT PASSWORD 'abcdef' LOGIN VALID UNTIL '2025-01-01' IN ROLE role1, role2 ROLE role3 ADMIN role4, role5 REPLICATION"; // Roundtrip order of optional parameters is not preserved match pg().parse_sql_statements(sql).as_deref() { From 6afd194e947cfd800376f424ff7c300ee385cd9e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 27 Sep 2022 13:56:29 -0400 Subject: [PATCH 040/806] Disambiguate CREATE ROLE ... USER and GROUP (#628) --- src/ast/mod.rs | 11 +++++++++++ src/parser.rs | 35 +++++++++++++++++++++++++++-------- tests/sqlparser_postgres.rs | 18 ++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 22762168f..330af3d33 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1117,6 +1117,7 @@ pub enum Statement { if_not_exists: bool, }, /// CREATE ROLE + /// See [postgres](https://www.postgresql.org/docs/current/sql-createrole.html) CreateRole { names: Vec, if_not_exists: bool, @@ -1132,7 +1133,9 @@ pub enum Statement { connection_limit: Option, valid_until: Option, in_role: Vec, + in_group: Vec, role: Vec, + user: Vec, admin: Vec, // MSSQL authorization_owner: Option, @@ -2005,7 +2008,9 @@ impl fmt::Display for Statement { connection_limit, valid_until, in_role, + in_group, role, + user, admin, authorization_owner, } => { @@ -2064,9 +2069,15 @@ impl fmt::Display for Statement { if !in_role.is_empty() { write!(f, " IN ROLE {}", display_comma_separated(in_role))?; } + if !in_group.is_empty() { + write!(f, " IN GROUP {}", display_comma_separated(in_group))?; + } if !role.is_empty() { write!(f, " ROLE {}", display_comma_separated(role))?; } + if !user.is_empty() { + write!(f, " USER {}", display_comma_separated(user))?; + } if !admin.is_empty() { write!(f, " ADMIN {}", display_comma_separated(admin))?; } diff --git a/src/parser.rs b/src/parser.rs index 503ef07db..e1f0c87a1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2109,7 +2109,9 @@ impl<'a> Parser<'a> { let mut connection_limit = None; let mut valid_until = None; let mut in_role = vec![]; - let mut roles = vec![]; + let mut in_group = vec![]; + let mut role = vec![]; + let mut user = vec![]; let mut admin = vec![]; while let Some(keyword) = self.parse_one_of_keywords(&optional_keywords) { @@ -2209,22 +2211,37 @@ impl<'a> Parser<'a> { } } Keyword::IN => { - if self.parse_keyword(Keyword::ROLE) || self.parse_keyword(Keyword::GROUP) { + if self.parse_keyword(Keyword::ROLE) { if !in_role.is_empty() { - parser_err!("Found multiple IN ROLE or IN GROUP") + parser_err!("Found multiple IN ROLE") } else { in_role = self.parse_comma_separated(Parser::parse_identifier)?; Ok(()) } + } else if self.parse_keyword(Keyword::GROUP) { + if !in_group.is_empty() { + parser_err!("Found multiple IN GROUP") + } else { + in_group = self.parse_comma_separated(Parser::parse_identifier)?; + Ok(()) + } } else { self.expected("ROLE or GROUP after IN", self.peek_token()) } } - Keyword::ROLE | Keyword::USER => { - if !roles.is_empty() { - parser_err!("Found multiple ROLE or USER") + Keyword::ROLE => { + if !role.is_empty() { + parser_err!("Found multiple ROLE") + } else { + role = self.parse_comma_separated(Parser::parse_identifier)?; + Ok(()) + } + } + Keyword::USER => { + if !user.is_empty() { + parser_err!("Found multiple USER") } else { - roles = self.parse_comma_separated(Parser::parse_identifier)?; + user = self.parse_comma_separated(Parser::parse_identifier)?; Ok(()) } } @@ -2254,7 +2271,9 @@ impl<'a> Parser<'a> { connection_limit, valid_until, in_role, - role: roles, + in_group, + role, + user, admin, authorization_owner, }) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 07e87770c..171624c0b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1807,7 +1807,9 @@ fn parse_create_role() { connection_limit, valid_until, in_role, + in_group, role, + user, admin, authorization_owner, }], @@ -1833,13 +1835,29 @@ fn parse_create_role() { Some(Expr::Value(Value::SingleQuotedString("2025-01-01".into()))) ); assert_eq_vec(&["role1", "role2"], in_role); + assert!(in_group.is_empty()); assert_eq_vec(&["role3"], role); + assert!(user.is_empty()); assert_eq_vec(&["role4", "role5"], admin); assert_eq!(*authorization_owner, None); } err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), } + let sql = "CREATE ROLE abc WITH USER foo, bar ROLE baz "; + match pg().parse_sql_statements(sql).as_deref() { + Ok( + [Statement::CreateRole { + names, user, role, .. + }], + ) => { + assert_eq_vec(&["abc"], names); + assert_eq_vec(&["foo", "bar"], user); + assert_eq_vec(&["baz"], role); + } + err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + } + let negatables = vec![ "BYPASSRLS", "CREATEDB", From 6c8f31c367ac75ea29be030bad244c0c6a2fafee Mon Sep 17 00:00:00 2001 From: ding-young Date: Wed, 28 Sep 2022 20:02:30 +0900 Subject: [PATCH 041/806] Fix parsing CLOB, BINARY, VARBINARY, BLOB data type (#618) * fix(parser): parse clob, binary, varbinary, blob data type * feat(tests): parse_cast tests for LOB, BINARY datatype --- src/parser.rs | 11 +++++++++++ tests/sqlparser_common.rs | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/parser.rs b/src/parser.rs index e1f0c87a1..76ffa1661 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3390,6 +3390,10 @@ impl<'a> Parser<'a> { Ok(DataType::Char(self.parse_optional_precision()?)) } } + Keyword::CLOB => Ok(DataType::Clob(self.parse_precision()?)), + Keyword::BINARY => Ok(DataType::Binary(self.parse_precision()?)), + Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_precision()?)), + Keyword::BLOB => Ok(DataType::Blob(self.parse_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), Keyword::DATETIME => Ok(DataType::Datetime), @@ -3603,6 +3607,13 @@ impl<'a> Parser<'a> { } } + pub fn parse_precision(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let n = self.parse_literal_uint()?; + self.expect_token(&Token::RParen)?; + Ok(n) + } + pub fn parse_optional_precision(&mut self) -> Result, ParserError> { if self.consume_token(&Token::LParen) { let n = self.parse_literal_uint()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e304f38c3..048e3517f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1649,6 +1649,46 @@ fn parse_cast() { }, expr_from_projection(only(&select.projection)) ); + + let sql = "SELECT CAST(id AS CLOB(50)) FROM customer"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Cast { + expr: Box::new(Expr::Identifier(Ident::new("id"))), + data_type: DataType::Clob(50) + }, + expr_from_projection(only(&select.projection)) + ); + + let sql = "SELECT CAST(id AS BINARY(50)) FROM customer"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Cast { + expr: Box::new(Expr::Identifier(Ident::new("id"))), + data_type: DataType::Binary(50) + }, + expr_from_projection(only(&select.projection)) + ); + + let sql = "SELECT CAST(id AS VARBINARY(50)) FROM customer"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Cast { + expr: Box::new(Expr::Identifier(Ident::new("id"))), + data_type: DataType::Varbinary(50) + }, + expr_from_projection(only(&select.projection)) + ); + + let sql = "SELECT CAST(id AS BLOB(50)) FROM customer"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Cast { + expr: Box::new(Expr::Identifier(Ident::new("id"))), + data_type: DataType::Blob(50) + }, + expr_from_projection(only(&select.projection)) + ); } #[test] From 46b7c037880475e6bc70b9f1fb0b654654ab47cc Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 28 Sep 2022 17:26:11 -0300 Subject: [PATCH 042/806] Adding DOUBLE PRECISION to data types, parsing it, and tests (#629) --- src/ast/data_type.rs | 8 +++++++- src/parser.rs | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 3a6ebf4cd..f3fa5001f 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -67,8 +67,13 @@ pub enum DataType { UnsignedBigInt(Option), /// Floating point e.g. REAL Real, - /// Double e.g. DOUBLE PRECISION + /// Double Double, + /// Double PRECISION e.g. [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 + DoublePrecision, /// Boolean Boolean, /// Date @@ -154,6 +159,7 @@ impl fmt::Display for DataType { } DataType::Real => write!(f, "REAL"), DataType::Double => write!(f, "DOUBLE"), + DataType::DoublePrecision => write!(f, "DOUBLE PRECISION"), DataType::Boolean => write!(f, "BOOLEAN"), DataType::Date => write!(f, "DATE"), DataType::Time => write!(f, "TIME"), diff --git a/src/parser.rs b/src/parser.rs index 76ffa1661..b4e592d7e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3338,8 +3338,11 @@ impl<'a> Parser<'a> { Keyword::FLOAT => Ok(DataType::Float(self.parse_optional_precision()?)), Keyword::REAL => Ok(DataType::Real), Keyword::DOUBLE => { - let _ = self.parse_keyword(Keyword::PRECISION); - Ok(DataType::Double) + if self.parse_keyword(Keyword::PRECISION) { + Ok(DataType::DoublePrecision) + } else { + Ok(DataType::Double) + } } Keyword::TINYINT => { let optional_precision = self.parse_optional_precision(); @@ -5241,4 +5244,18 @@ mod tests { assert_eq!(ast.to_string(), sql.to_string()); }); } + + // TODO add tests for all data types? https://github.com/sqlparser-rs/sqlparser-rs/issues/2 + #[test] + fn test_parse_data_type() { + test_parse_data_type("DOUBLE PRECISION", "DOUBLE PRECISION"); + test_parse_data_type("DOUBLE", "DOUBLE"); + + fn test_parse_data_type(input: &str, expected: &str) { + all_dialects().run_parser_method(input, |parser| { + let data_type = parser.parse_data_type().unwrap().to_string(); + assert_eq!(data_type, expected); + }); + } + } } From e951cd5278acb1b262f02565f8e7ccea0ea56ddd Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 28 Sep 2022 17:26:17 -0300 Subject: [PATCH 043/806] #625 adding mediumint support (#630) --- src/ast/data_type.rs | 14 ++++++++++++++ src/keywords.rs | 1 + src/parser.rs | 8 ++++++++ tests/sqlparser_common.rs | 5 +++++ tests/sqlparser_mysql.rs | 16 ++++++++++++++-- 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index f3fa5001f..2343cf0cb 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -53,6 +53,14 @@ pub enum DataType { SmallInt(Option), /// Unsigned small integer with optional display width e.g. SMALLINT UNSIGNED or SMALLINT(5) UNSIGNED UnsignedSmallInt(Option), + /// 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 + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/integer-types.html + UnsignedMediumInt(Option), /// Integer with optional display width e.g. INT or INT(11) Int(Option), /// Integer with optional display width e.g. INTEGER or INTEGER(11) @@ -141,6 +149,12 @@ impl fmt::Display for DataType { DataType::UnsignedSmallInt(zerofill) => { format_type_with_optional_length(f, "SMALLINT", zerofill, true) } + DataType::MediumInt(zerofill) => { + format_type_with_optional_length(f, "MEDIUMINT", zerofill, false) + } + DataType::UnsignedMediumInt(zerofill) => { + format_type_with_optional_length(f, "MEDIUMINT", zerofill, true) + } DataType::Int(zerofill) => format_type_with_optional_length(f, "INT", zerofill, false), DataType::UnsignedInt(zerofill) => { format_type_with_optional_length(f, "INT", zerofill, true) diff --git a/src/keywords.rs b/src/keywords.rs index 35d98eef7..287dc14df 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -325,6 +325,7 @@ define_keywords!( MATCHED, MATERIALIZED, MAX, + MEDIUMINT, MEMBER, MERGE, METADATA, diff --git a/src/parser.rs b/src/parser.rs index b4e592d7e..7d8f2aba6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3360,6 +3360,14 @@ impl<'a> Parser<'a> { Ok(DataType::SmallInt(optional_precision?)) } } + Keyword::MEDIUMINT => { + let optional_precision = self.parse_optional_precision(); + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::UnsignedMediumInt(optional_precision?)) + } else { + Ok(DataType::MediumInt(optional_precision?)) + } + } Keyword::INT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 048e3517f..570dd4de7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1623,6 +1623,11 @@ fn parse_cast() { expr_from_projection(only(&select.projection)) ); + one_statement_parses_to( + "SELECT CAST(id AS MEDIUMINT) FROM customer", + "SELECT CAST(id AS MEDIUMINT) FROM customer", + ); + one_statement_parses_to( "SELECT CAST(id AS BIGINT) FROM customer", "SELECT CAST(id AS BIGINT) FROM customer", diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f46d5d23e..a91be9833 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -552,7 +552,7 @@ fn parse_escaped_string() { #[test] fn parse_create_table_with_minimum_display_width() { - let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3), bar_smallint SMALLINT(5), bar_int INT(11), bar_bigint BIGINT(20))"; + let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3), bar_smallint SMALLINT(5), bar_mediumint MEDIUMINT(6), bar_int INT(11), bar_bigint BIGINT(20))"; match mysql().verified_stmt(sql) { Statement::CreateTable { name, columns, .. } => { assert_eq!(name.to_string(), "foo"); @@ -570,6 +570,12 @@ fn parse_create_table_with_minimum_display_width() { collation: None, options: vec![], }, + ColumnDef { + name: Ident::new("bar_mediumint"), + data_type: DataType::MediumInt(Some(6)), + collation: None, + options: vec![], + }, ColumnDef { name: Ident::new("bar_int"), data_type: DataType::Int(Some(11)), @@ -592,7 +598,7 @@ fn parse_create_table_with_minimum_display_width() { #[test] fn parse_create_table_unsigned() { - let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3) UNSIGNED, bar_smallint SMALLINT(5) UNSIGNED, bar_int INT(11) UNSIGNED, bar_bigint BIGINT(20) UNSIGNED)"; + let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3) UNSIGNED, bar_smallint SMALLINT(5) UNSIGNED, bar_mediumint MEDIUMINT(13) UNSIGNED, bar_int INT(11) UNSIGNED, bar_bigint BIGINT(20) UNSIGNED)"; match mysql().verified_stmt(sql) { Statement::CreateTable { name, columns, .. } => { assert_eq!(name.to_string(), "foo"); @@ -610,6 +616,12 @@ fn parse_create_table_unsigned() { collation: None, options: vec![], }, + ColumnDef { + name: Ident::new("bar_mediumint"), + data_type: DataType::UnsignedMediumInt(Some(13)), + collation: None, + options: vec![], + }, ColumnDef { name: Ident::new("bar_int"), data_type: DataType::UnsignedInt(Some(11)), From d87408bdaf71856dceb533297d65b3069771f50d Mon Sep 17 00:00:00 2001 From: Sarah Yurick <53962159+sarahyurick@users.noreply.github.com> Date: Wed, 28 Sep 2022 13:26:29 -0700 Subject: [PATCH 044/806] Parse `MILLENNIUM` (#633) * add unknown, is not true/false/unknown * millennium --- src/ast/value.rs | 2 ++ src/keywords.rs | 1 + src/parser.rs | 2 ++ tests/sqlparser_common.rs | 1 + 4 files changed, 6 insertions(+) diff --git a/src/ast/value.rs b/src/ast/value.rs index 6f5953c4b..96d525ac6 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -85,6 +85,7 @@ pub enum DateTimeField { Julian, Microseconds, Millenium, + Millennium, Milliseconds, Quarter, Timezone, @@ -112,6 +113,7 @@ impl fmt::Display for DateTimeField { DateTimeField::Julian => "JULIAN", DateTimeField::Microseconds => "MICROSECONDS", DateTimeField::Millenium => "MILLENIUM", + DateTimeField::Millennium => "MILLENNIUM", DateTimeField::Milliseconds => "MILLISECONDS", DateTimeField::Quarter => "QUARTER", DateTimeField::Timezone => "TIMEZONE", diff --git a/src/keywords.rs b/src/keywords.rs index 287dc14df..bf9a7711e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -332,6 +332,7 @@ define_keywords!( METHOD, MICROSECONDS, MILLENIUM, + MILLENNIUM, MILLISECONDS, MIN, MINUTE, diff --git a/src/parser.rs b/src/parser.rs index 7d8f2aba6..f61ea4832 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1065,6 +1065,7 @@ impl<'a> Parser<'a> { Keyword::JULIAN => Ok(DateTimeField::Julian), Keyword::MICROSECONDS => Ok(DateTimeField::Microseconds), Keyword::MILLENIUM => Ok(DateTimeField::Millenium), + Keyword::MILLENNIUM => Ok(DateTimeField::Millennium), Keyword::MILLISECONDS => Ok(DateTimeField::Milliseconds), Keyword::QUARTER => Ok(DateTimeField::Quarter), Keyword::TIMEZONE => Ok(DateTimeField::Timezone), @@ -1144,6 +1145,7 @@ impl<'a> Parser<'a> { Keyword::JULIAN, Keyword::MICROSECONDS, Keyword::MILLENIUM, + Keyword::MILLENNIUM, Keyword::MILLISECONDS, Keyword::QUARTER, Keyword::TIMEZONE, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 570dd4de7..57fa35709 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1755,6 +1755,7 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(JULIAN FROM d)"); verified_stmt("SELECT EXTRACT(MICROSECONDS FROM d)"); verified_stmt("SELECT EXTRACT(MILLENIUM FROM d)"); + verified_stmt("SELECT EXTRACT(MILLENNIUM FROM d)"); verified_stmt("SELECT EXTRACT(MILLISECONDS FROM d)"); verified_stmt("SELECT EXTRACT(QUARTER FROM d)"); verified_stmt("SELECT EXTRACT(TIMEZONE FROM d)"); From e2a51ad6491e06f1b3f44581d6c31442af2bb929 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 28 Sep 2022 16:34:48 -0400 Subject: [PATCH 045/806] Release notes for 0.24.0 (#634) * Disambiguate CREATE ROLE ... USER and GROUP * Add changelog for 0.24 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 237b00af3..f5b4c9d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,29 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.24.0] 2022-09-29 + +### Added + +* Support `MILLENNIUM` (2 Ns) (#633) - Thanks @sarahyurick +* Support `MEDIUMINT` (#630) - Thanks @AugustoFKL +* Support `DOUBLE PRECISION` (#629) - Thanks @AugustoFKL +* Support precision in `CLOB`, `BINARY`, `VARBINARY`, `BLOB` data type (#618) - Thanks @ding-young +* Support `CREATE ROLE` and `DROP ROLE` (#598) - Thanks @blx +* Support full range of sqlite prepared statement placeholders (#604) - Thanks @lovasoa +* Support National string literal with lower case `n` (#612) - Thanks @mskrzypkows +* Support SHOW FUNCTIONS (#620) - Thanks @joocer +* Support `set time zone to 'some-timezone'` (#617) - Thanks @waitingkuo + +### Changed +* Move `Value::Interval` to `Expr::Interval` (#609) - Thanks @ding-young +* Update `criterion` dev-requirement from 0.3 to 0.4 in /sqlparser_bench (#611) - Thanks @dependabot +* Box `Query` in `Cte` (#572) - Thanks @MazterQyou + +### Other +* Disambiguate CREATE ROLE ... USER and GROUP (#628) - Thanks @alamb +* Add test for optional WITH in CREATE ROLE (#627) - Thanks @alamb + ## [0.23.0] 2022-09-08 ### Added From c2d68255d00e42779a2503d8676c15545af4c15a Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 28 Sep 2022 16:46:33 -0400 Subject: [PATCH 046/806] (cargo-release) version 0.24.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c1ae73546..1b153aedc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.23.0" +version = "0.24.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 300e28bd85834be2dca7f08f13387ac192f7fc84 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Sat, 1 Oct 2022 17:52:44 -0300 Subject: [PATCH 047/806] 636 Adjusting VARBINARY and BINARY with optional precision (#637) --- src/ast/data_type.rs | 20 ++++++++++++++------ src/parser.rs | 8 ++++++-- tests/sqlparser_common.rs | 4 ++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 2343cf0cb..c994645ba 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -35,10 +35,16 @@ pub enum DataType { Uuid, /// Large character object e.g. CLOB(1000) Clob(u64), - /// Fixed-length binary type e.g. BINARY(10) - Binary(u64), - /// Variable-length binary type e.g. VARBINARY(10) - Varbinary(u64), + /// Fixed-length binary type with optional length e.g. [standard], [MS SQL Server] + /// + /// [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] + /// + /// [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 e.g. BLOB(1000) Blob(u64), /// Decimal type with optional precision and scale e.g. DECIMAL(10,2) @@ -126,8 +132,10 @@ impl fmt::Display for DataType { } DataType::Uuid => write!(f, "UUID"), DataType::Clob(size) => write!(f, "CLOB({})", size), - DataType::Binary(size) => write!(f, "BINARY({})", size), - DataType::Varbinary(size) => write!(f, "VARBINARY({})", size), + DataType::Binary(size) => format_type_with_optional_length(f, "BINARY", size, false), + DataType::Varbinary(size) => { + format_type_with_optional_length(f, "VARBINARY", size, false) + } DataType::Blob(size) => write!(f, "BLOB({})", size), DataType::Decimal(precision, scale) => { if let Some(scale) = scale { diff --git a/src/parser.rs b/src/parser.rs index f61ea4832..09419cc29 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3404,8 +3404,8 @@ impl<'a> Parser<'a> { } } Keyword::CLOB => Ok(DataType::Clob(self.parse_precision()?)), - Keyword::BINARY => Ok(DataType::Binary(self.parse_precision()?)), - Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_precision()?)), + Keyword::BINARY => Ok(DataType::Binary(self.parse_optional_precision()?)), + Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_optional_precision()?)), Keyword::BLOB => Ok(DataType::Blob(self.parse_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), @@ -5260,6 +5260,10 @@ mod tests { fn test_parse_data_type() { test_parse_data_type("DOUBLE PRECISION", "DOUBLE PRECISION"); test_parse_data_type("DOUBLE", "DOUBLE"); + test_parse_data_type("VARBINARY", "VARBINARY"); + test_parse_data_type("VARBINARY(20)", "VARBINARY(20)"); + test_parse_data_type("BINARY", "BINARY"); + test_parse_data_type("BINARY(20)", "BINARY(20)"); fn test_parse_data_type(input: &str, expected: &str) { all_dialects().run_parser_method(input, |parser| { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 57fa35709..391eff134 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1670,7 +1670,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Binary(50) + data_type: DataType::Binary(Some(50)) }, expr_from_projection(only(&select.projection)) ); @@ -1680,7 +1680,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Varbinary(50) + data_type: DataType::Varbinary(Some(50)) }, expr_from_projection(only(&select.projection)) ); From fb5a9747ae4069736a6d8c08f16f3242648da312 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Sat, 1 Oct 2022 18:15:47 -0300 Subject: [PATCH 048/806] Support optional precision for `CLOB` and `BLOB` (#639) * 638 Adjusting CLOB and BLOB with optional precision * 638 Adding integration tests to increase coverage * 638 Adjusting BLOB test --- src/ast/data_type.rs | 18 ++++++++++++------ src/parser.rs | 8 ++++++-- tests/sqlparser_common.rs | 24 ++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index c994645ba..2f844d50e 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -33,8 +33,11 @@ pub enum DataType { Nvarchar(Option), /// Uuid type Uuid, - /// Large character object e.g. CLOB(1000) - Clob(u64), + /// Large character object with optional length e.g. CLOB, CLOB(1000), [standard], [Oracle] + /// + /// [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] /// /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-string-type @@ -45,8 +48,11 @@ pub enum DataType { /// [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 e.g. BLOB(1000) - Blob(u64), + /// Large binary object with optional length e.g. BLOB, BLOB(1000), [standard], [Oracle] + /// + /// [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), /// Decimal type with optional precision and scale e.g. DECIMAL(10,2) Decimal(Option, Option), /// Floating point with optional precision e.g. FLOAT(8) @@ -131,12 +137,12 @@ impl fmt::Display for DataType { format_type_with_optional_length(f, "NVARCHAR", size, false) } DataType::Uuid => write!(f, "UUID"), - DataType::Clob(size) => write!(f, "CLOB({})", size), + DataType::Clob(size) => format_type_with_optional_length(f, "CLOB", size, false), DataType::Binary(size) => format_type_with_optional_length(f, "BINARY", size, false), DataType::Varbinary(size) => { format_type_with_optional_length(f, "VARBINARY", size, false) } - DataType::Blob(size) => write!(f, "BLOB({})", size), + DataType::Blob(size) => format_type_with_optional_length(f, "BLOB", size, false), DataType::Decimal(precision, scale) => { if let Some(scale) = scale { write!(f, "NUMERIC({},{})", precision.unwrap(), scale) diff --git a/src/parser.rs b/src/parser.rs index 09419cc29..9ac5f89c6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3403,10 +3403,10 @@ impl<'a> Parser<'a> { Ok(DataType::Char(self.parse_optional_precision()?)) } } - Keyword::CLOB => Ok(DataType::Clob(self.parse_precision()?)), + Keyword::CLOB => Ok(DataType::Clob(self.parse_optional_precision()?)), Keyword::BINARY => Ok(DataType::Binary(self.parse_optional_precision()?)), Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_optional_precision()?)), - Keyword::BLOB => Ok(DataType::Blob(self.parse_precision()?)), + Keyword::BLOB => Ok(DataType::Blob(self.parse_optional_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), Keyword::DATETIME => Ok(DataType::Datetime), @@ -5258,6 +5258,10 @@ mod tests { // TODO add tests for all data types? https://github.com/sqlparser-rs/sqlparser-rs/issues/2 #[test] fn test_parse_data_type() { + test_parse_data_type("BLOB", "BLOB"); + test_parse_data_type("BLOB(50)", "BLOB(50)"); + test_parse_data_type("CLOB", "CLOB"); + test_parse_data_type("CLOB(50)", "CLOB(50)"); test_parse_data_type("DOUBLE PRECISION", "DOUBLE PRECISION"); test_parse_data_type("DOUBLE", "DOUBLE"); test_parse_data_type("VARBINARY", "VARBINARY"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 391eff134..956a62297 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1655,12 +1655,22 @@ fn parse_cast() { expr_from_projection(only(&select.projection)) ); + let sql = "SELECT CAST(id AS CLOB) FROM customer"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Cast { + expr: Box::new(Expr::Identifier(Ident::new("id"))), + data_type: DataType::Clob(None) + }, + expr_from_projection(only(&select.projection)) + ); + let sql = "SELECT CAST(id AS CLOB(50)) FROM customer"; let select = verified_only_select(sql); assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Clob(50) + data_type: DataType::Clob(Some(50)) }, expr_from_projection(only(&select.projection)) ); @@ -1685,12 +1695,22 @@ fn parse_cast() { expr_from_projection(only(&select.projection)) ); + let sql = "SELECT CAST(id AS BLOB) FROM customer"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Cast { + expr: Box::new(Expr::Identifier(Ident::new("id"))), + data_type: DataType::Blob(None) + }, + expr_from_projection(only(&select.projection)) + ); + let sql = "SELECT CAST(id AS BLOB(50)) FROM customer"; let select = verified_only_select(sql); assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Blob(50) + data_type: DataType::Blob(Some(50)) }, expr_from_projection(only(&select.projection)) ); From 3beecc0a7a06c8dae47a4440129d01553c28c2ee Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Sat, 1 Oct 2022 18:23:06 -0300 Subject: [PATCH 049/806] Correct order of arguments in `LIMIT x,y` , restrict to MySql and generic dialects (#642) * 613 Fixing MySQL LIMIT syntax * 613 Reducing logic to real case scenario * 613 Adding syntax to generic dialect --- src/parser.rs | 25 +++++++++++++++++-------- tests/sqlparser_common.rs | 8 -------- tests/sqlparser_mysql.rs | 8 ++++++++ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 9ac5f89c6..96d8ef327 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -24,6 +24,9 @@ use core::fmt; use log::debug; +use IsLateral::*; +use IsOptional::*; + use crate::ast::*; use crate::dialect::*; use crate::keywords::{self, Keyword}; @@ -57,15 +60,11 @@ pub enum IsOptional { Mandatory, } -use IsOptional::*; - pub enum IsLateral { Lateral, NotLateral, } -use IsLateral::*; - pub enum WildcardExpr { Expr(Expr), QualifiedWildcard(ObjectName), @@ -3762,9 +3761,18 @@ impl<'a> Parser<'a> { offset = Some(self.parse_offset()?) } - if offset.is_none() && self.consume_token(&Token::Comma) { - // mysql style LIMIT 10, offset 5 - offset = Some(self.parse_offset()?) + if dialect_of!(self is GenericDialect | MySqlDialect) + && 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()?); } } @@ -5197,9 +5205,10 @@ impl Word { #[cfg(test)] mod tests { - use super::*; use crate::test_utils::{all_dialects, TestedDialects}; + use super::*; + #[test] fn test_prev_index() { let sql = "SELECT version"; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 956a62297..553793ca5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1593,14 +1593,6 @@ fn parse_limit_accepts_all() { ); } -#[test] -fn parse_limit_my_sql_syntax() { - one_statement_parses_to( - "SELECT id, fname, lname FROM customer LIMIT 5, 10", - "SELECT id, fname, lname FROM customer LIMIT 5 OFFSET 10", - ); -} - #[test] fn parse_cast() { let sql = "SELECT CAST(id AS BIGINT) FROM customer"; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a91be9833..8b8754db4 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1065,6 +1065,14 @@ fn parse_set_names() { assert_eq!(stmt, Statement::SetNamesDefault {}); } +#[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", + ); +} + fn mysql() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})], From 95464ec72c8020f618d582564e54213d8c43edf8 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Mon, 3 Oct 2022 09:37:17 -0300 Subject: [PATCH 050/806] Change `TIMESTAMP` and `TIME` parsing so that time zone information is preserved (#641) * 640 Fixing time zone printing format for TIMESTAMP and TIME * 640 Removing unnecessary changes * Update src/ast/data_type.rs Fix comment typo Co-authored-by: Andrew Lamb --- src/ast/data_type.rs | 60 ++++++++++++++++++--- src/ast/mod.rs | 1 + src/keywords.rs | 1 + src/parser.rs | 102 ++++++++++++++++++++++++++++-------- tests/sqlparser_common.rs | 16 ++---- tests/sqlparser_postgres.rs | 4 +- 6 files changed, 142 insertions(+), 42 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 2f844d50e..c159c3b4d 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -99,13 +99,11 @@ pub enum DataType { /// Date Date, /// Time - Time, + Time(TimezoneInfo), /// Datetime Datetime, - /// Timestamp [Without Time Zone] - Timestamp, - /// Timestamp With Time Zone - TimestampTz, + /// Timestamp + Timestamp(TimezoneInfo), /// Interval Interval, /// Regclass used in postgresql serial @@ -190,10 +188,9 @@ impl fmt::Display for DataType { DataType::DoublePrecision => write!(f, "DOUBLE PRECISION"), DataType::Boolean => write!(f, "BOOLEAN"), DataType::Date => write!(f, "DATE"), - DataType::Time => write!(f, "TIME"), + DataType::Time(timezone_info) => write!(f, "TIME{}", timezone_info), DataType::Datetime => write!(f, "DATETIME"), - DataType::Timestamp => write!(f, "TIMESTAMP"), - DataType::TimestampTz => write!(f, "TIMESTAMPTZ"), + DataType::Timestamp(timezone_info) => write!(f, "TIMESTAMP{}", timezone_info), DataType::Interval => write!(f, "INTERVAL"), DataType::Regclass => write!(f, "REGCLASS"), DataType::Text => write!(f, "TEXT"), @@ -240,3 +237,50 @@ fn format_type_with_optional_length( } Ok(()) } + +/// Timestamp and Time data types information about TimeZone formatting. +/// +/// This is more related to a display information than real differences between each variant. To +/// guarantee compatibility with the input query we must maintain its exact information. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum TimezoneInfo { + /// No information about time zone. E.g., TIMESTAMP + None, + /// Temporal type 'WITH TIME ZONE'. E.g., TIMESTAMP WITH TIME ZONE, [standard], [Oracle] + /// + /// [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] + /// + /// [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]: https://www.postgresql.org/docs/current/datatype-datetime.html + Tz, +} + +impl fmt::Display for TimezoneInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TimezoneInfo::None => { + write!(f, "") + } + TimezoneInfo::WithTimeZone => { + write!(f, " WITH TIME ZONE") + } + TimezoneInfo::WithoutTimeZone => { + write!(f, " WITHOUT TIME ZONE") + } + TimezoneInfo::Tz => { + // TZ is the only one that is displayed BEFORE the precision, so the datatype display + // must be aware of that. Check + // for more information + write!(f, "TZ") + } + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 330af3d33..77101a873 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -29,6 +29,7 @@ use core::fmt; use serde::{Deserialize, Serialize}; pub use self::data_type::DataType; +pub use self::data_type::TimezoneInfo; pub use self::ddl::{ AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ReferentialAction, TableConstraint, diff --git a/src/keywords.rs b/src/keywords.rs index bf9a7711e..66b4c9f9f 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -528,6 +528,7 @@ define_keywords!( TIME, TIMESTAMP, TIMESTAMPTZ, + TIMETZ, TIMEZONE, TIMEZONE_HOUR, TIMEZONE_MINUTE, diff --git a/src/parser.rs b/src/parser.rs index 96d8ef327..2944b5586 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3412,22 +3412,27 @@ impl<'a> Parser<'a> { Keyword::TIMESTAMP => { if self.parse_keyword(Keyword::WITH) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; - Ok(DataType::TimestampTz) + Ok(DataType::Timestamp(TimezoneInfo::WithTimeZone)) } else if self.parse_keyword(Keyword::WITHOUT) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; - Ok(DataType::Timestamp) + Ok(DataType::Timestamp(TimezoneInfo::WithoutTimeZone)) } else { - Ok(DataType::Timestamp) + Ok(DataType::Timestamp(TimezoneInfo::None)) } } - Keyword::TIMESTAMPTZ => Ok(DataType::TimestampTz), + Keyword::TIMESTAMPTZ => Ok(DataType::Timestamp(TimezoneInfo::Tz)), Keyword::TIME => { - // TBD: we throw away "with/without timezone" information - if self.parse_keyword(Keyword::WITH) || self.parse_keyword(Keyword::WITHOUT) { + if self.parse_keyword(Keyword::WITH) { + self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::Time(TimezoneInfo::WithTimeZone)) + } else if self.parse_keyword(Keyword::WITHOUT) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::Time(TimezoneInfo::WithoutTimeZone)) + } else { + Ok(DataType::Time(TimezoneInfo::None)) } - Ok(DataType::Time) } + Keyword::TIMETZ => Ok(DataType::Time(TimezoneInfo::Tz)), // Interval types can be followed by a complicated interval // qualifier that we don't currently support. See // parse_interval for a taste. @@ -5265,23 +5270,78 @@ mod tests { } // TODO add tests for all data types? https://github.com/sqlparser-rs/sqlparser-rs/issues/2 + // TODO when we have dialect validation by data type parsing, split test #[test] fn test_parse_data_type() { - test_parse_data_type("BLOB", "BLOB"); - test_parse_data_type("BLOB(50)", "BLOB(50)"); - test_parse_data_type("CLOB", "CLOB"); - test_parse_data_type("CLOB(50)", "CLOB(50)"); - test_parse_data_type("DOUBLE PRECISION", "DOUBLE PRECISION"); - test_parse_data_type("DOUBLE", "DOUBLE"); - test_parse_data_type("VARBINARY", "VARBINARY"); - test_parse_data_type("VARBINARY(20)", "VARBINARY(20)"); - test_parse_data_type("BINARY", "BINARY"); - test_parse_data_type("BINARY(20)", "BINARY(20)"); - - fn test_parse_data_type(input: &str, expected: &str) { + // BINARY data type + test_parse_data_type("BINARY", DataType::Binary(None), "BINARY"); + test_parse_data_type("BINARY(20)", DataType::Binary(Some(20)), "BINARY(20)"); + + // BLOB data type + test_parse_data_type("BLOB", DataType::Blob(None), "BLOB"); + test_parse_data_type("BLOB(50)", DataType::Blob(Some(50)), "BLOB(50)"); + + // CLOB data type + test_parse_data_type("CLOB", DataType::Clob(None), "CLOB"); + test_parse_data_type("CLOB(50)", DataType::Clob(Some(50)), "CLOB(50)"); + + // Double data type + test_parse_data_type( + "DOUBLE PRECISION", + DataType::DoublePrecision, + "DOUBLE PRECISION", + ); + test_parse_data_type("DOUBLE", DataType::Double, "DOUBLE"); + + // Time data type + test_parse_data_type("TIME", DataType::Time(TimezoneInfo::None), "TIME"); + test_parse_data_type( + "TIME WITH TIME ZONE", + DataType::Time(TimezoneInfo::WithTimeZone), + "TIME WITH TIME ZONE", + ); + test_parse_data_type( + "TIME WITHOUT TIME ZONE", + DataType::Time(TimezoneInfo::WithoutTimeZone), + "TIME WITHOUT TIME ZONE", + ); + test_parse_data_type("TIMETZ", DataType::Time(TimezoneInfo::Tz), "TIMETZ"); + + // Timestamp data type + test_parse_data_type( + "TIMESTAMP", + DataType::Timestamp(TimezoneInfo::None), + "TIMESTAMP", + ); + test_parse_data_type( + "TIMESTAMP WITH TIME ZONE", + DataType::Timestamp(TimezoneInfo::WithTimeZone), + "TIMESTAMP WITH TIME ZONE", + ); + test_parse_data_type( + "TIMESTAMP WITHOUT TIME ZONE", + DataType::Timestamp(TimezoneInfo::WithoutTimeZone), + "TIMESTAMP WITHOUT TIME ZONE", + ); + test_parse_data_type( + "TIMESTAMPTZ", + DataType::Timestamp(TimezoneInfo::Tz), + "TIMESTAMPTZ", + ); + + // VARBINARY data type + test_parse_data_type("VARBINARY", DataType::Varbinary(None), "VARBINARY"); + test_parse_data_type( + "VARBINARY(20)", + DataType::Varbinary(Some(20)), + "VARBINARY(20)", + ); + + fn test_parse_data_type(input: &str, expected_type: DataType, expected_str: &str) { all_dialects().run_parser_method(input, |parser| { - let data_type = parser.parse_data_type().unwrap().to_string(); - assert_eq!(data_type, expected); + let data_type = parser.parse_data_type().unwrap(); + assert_eq!(data_type, expected_type); + assert_eq!(expected_type.to_string(), expected_str.to_string()); }); } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 553793ca5..ec2ab6e81 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2933,7 +2933,7 @@ fn parse_literal_time() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::Time, + data_type: DataType::Time(TimezoneInfo::None), value: "01:23:34".into() }, expr_from_projection(only(&select.projection)), @@ -2959,16 +2959,13 @@ fn parse_literal_timestamp_without_time_zone() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::Timestamp, + data_type: DataType::Timestamp(TimezoneInfo::None), value: "1999-01-01 01:23:34".into() }, expr_from_projection(only(&select.projection)), ); - one_statement_parses_to( - "SELECT TIMESTAMP WITHOUT TIME ZONE '1999-01-01 01:23:34'", - sql, - ); + one_statement_parses_to("SELECT TIMESTAMP '1999-01-01 01:23:34'", sql); } #[test] @@ -2977,16 +2974,13 @@ fn parse_literal_timestamp_with_time_zone() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::TimestampTz, + data_type: DataType::Timestamp(TimezoneInfo::Tz), value: "1999-01-01 01:23:34Z".into() }, expr_from_projection(only(&select.projection)), ); - one_statement_parses_to( - "SELECT TIMESTAMP WITH TIME ZONE '1999-01-01 01:23:34Z'", - sql, - ); + one_statement_parses_to("SELECT TIMESTAMPTZ '1999-01-01 01:23:34Z'", sql); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 171624c0b..c9cafe273 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -139,7 +139,7 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "last_update".into(), - data_type: DataType::Timestamp, + data_type: DataType::Timestamp(TimezoneInfo::WithoutTimeZone), collation: None, options: vec![ ColumnOptionDef { @@ -212,7 +212,7 @@ fn parse_create_table_from_pg_dump() { activebool BOOLEAN DEFAULT true NOT NULL, \ create_date DATE DEFAULT CAST(now() AS DATE) NOT NULL, \ create_date1 DATE DEFAULT CAST(CAST('now' AS TEXT) AS DATE) NOT NULL, \ - last_update TIMESTAMP DEFAULT now(), \ + last_update TIMESTAMP WITHOUT TIME ZONE DEFAULT now(), \ release_year public.year, \ active INT\ )"); From 1ced0f630451678d421bebe30b418e130071533f Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Mon, 3 Oct 2022 09:38:01 -0300 Subject: [PATCH 051/806] 645 New schema name structure (#646) --- src/ast/mod.rs | 33 +++++++++++++++++++++++- src/parser.rs | 54 ++++++++++++++++++++++++++++++++++++++- tests/sqlparser_common.rs | 24 +++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 77101a873..36ae59339 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1300,7 +1300,8 @@ pub enum Statement { Rollback { chain: bool }, /// CREATE SCHEMA CreateSchema { - schema_name: ObjectName, + /// ` | AUTHORIZATION | AUTHORIZATION ` + schema_name: SchemaName, if_not_exists: bool, }, /// CREATE DATABASE @@ -3275,6 +3276,36 @@ impl fmt::Display for CreateFunctionUsing { } } +/// Schema possible naming variants ([1]). +/// +/// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#schema-definition +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum SchemaName { + /// Only schema name specified: ``. + Simple(ObjectName), + /// Only authorization identifier specified: `AUTHORIZATION `. + UnnamedAuthorization(Ident), + /// Both schema name and authorization identifier specified: ` AUTHORIZATION `. + NamedAuthorization(ObjectName, Ident), +} + +impl fmt::Display for SchemaName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SchemaName::Simple(name) => { + write!(f, "{name}") + } + SchemaName::UnnamedAuthorization(authorization) => { + write!(f, "AUTHORIZATION {authorization}") + } + SchemaName::NamedAuthorization(name, authorization) => { + write!(f, "{name} AUTHORIZATION {authorization}") + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/parser.rs b/src/parser.rs index 2944b5586..897e74c66 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1896,13 +1896,32 @@ impl<'a> Parser<'a> { pub fn parse_create_schema(&mut self) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let schema_name = self.parse_object_name()?; + + let schema_name = self.parse_schema_name()?; + Ok(Statement::CreateSchema { schema_name, if_not_exists, }) } + fn parse_schema_name(&mut self) -> Result { + if self.parse_keyword(Keyword::AUTHORIZATION) { + Ok(SchemaName::UnnamedAuthorization(self.parse_identifier()?)) + } else { + let name = self.parse_object_name()?; + + if self.parse_keyword(Keyword::AUTHORIZATION) { + Ok(SchemaName::NamedAuthorization( + name, + self.parse_identifier()?, + )) + } else { + Ok(SchemaName::Simple(name)) + } + } + } + pub fn parse_create_database(&mut self) -> Result { let ine = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let db_name = self.parse_object_name()?; @@ -5345,4 +5364,37 @@ mod tests { }); } } + + #[test] + fn test_parse_schema_name() { + // The expected name should be identical as the input name, that's why I don't receive both + macro_rules! test_parse_schema_name { + ($input:expr, $expected_name:expr $(,)?) => {{ + all_dialects().run_parser_method(&*$input, |parser| { + let schema_name = parser.parse_schema_name().unwrap(); + // Validate that the structure is the same as expected + assert_eq!(schema_name, $expected_name); + // Validate that the input and the expected structure serialization are the same + assert_eq!(schema_name.to_string(), $input.to_string()); + }); + }}; + } + + let dummy_name = ObjectName(vec![Ident::new("dummy_name")]); + let dummy_authorization = Ident::new("dummy_authorization"); + + test_parse_schema_name!( + format!("{dummy_name}"), + SchemaName::Simple(dummy_name.clone()) + ); + + test_parse_schema_name!( + format!("AUTHORIZATION {dummy_authorization}"), + SchemaName::UnnamedAuthorization(dummy_authorization.clone()), + ); + test_parse_schema_name!( + format!("{dummy_name} AUTHORIZATION {dummy_authorization}"), + SchemaName::NamedAuthorization(dummy_name.clone(), dummy_authorization.clone()), + ); + } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ec2ab6e81..106502be1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2133,6 +2133,30 @@ fn parse_create_schema() { } } +#[test] +fn parse_create_schema_with_authorization() { + let sql = "CREATE SCHEMA AUTHORIZATION Y"; + + match verified_stmt(sql) { + Statement::CreateSchema { schema_name, .. } => { + assert_eq!(schema_name.to_string(), "AUTHORIZATION Y".to_owned()) + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_schema_with_name_and_authorization() { + let sql = "CREATE SCHEMA X AUTHORIZATION Y"; + + match verified_stmt(sql) { + Statement::CreateSchema { schema_name, .. } => { + assert_eq!(schema_name.to_string(), "X AUTHORIZATION Y".to_owned()) + } + _ => unreachable!(), + } +} + #[test] fn parse_drop_schema() { let sql = "DROP SCHEMA X"; From 97a52cb85b61a0fff77e2e372ed99fc8eb893b12 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 3 Oct 2022 09:01:26 -0400 Subject: [PATCH 052/806] Changelog for `0.25.0` release (#651) * Prepare for 0.25.0 release * fixip --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b4c9d51..542fa5e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.25.0] 2022-10-03 + +### Added + +* Support `AUTHORIZATION` clause in `CREATE SCHEMA` statements (#646) - Thanks @AugustoFKL +* Support optional precision for `CLOB` and `BLOB` (#639) - Thanks @AugustoFKL +* Support optional precision in `VARBINARY` and `BINARY` (#637) - Thanks @AugustoFKL + + +### Changed +* `TIMESTAMP` and `TIME` parsing preserve zone information (#646) - Thanks @AugustoFKL + +### Fixed + +* Correct order of arguments when parsing `LIMIT x,y` , restrict to `MySql` and `Generic` dialects - Thanks @AugustoFKL + + ## [0.24.0] 2022-09-29 ### Added From 977cdb2270ce34539684d7817ef7a6199cd24013 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 3 Oct 2022 09:11:58 -0400 Subject: [PATCH 053/806] (cargo-release) version 0.25.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1b153aedc..8dcc2acd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.24.0" +version = "0.25.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From f7f14df4b14256a05f1dd41c7b03552fd8ea0fa1 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Tue, 4 Oct 2022 07:42:29 -0300 Subject: [PATCH 054/806] 647 Adding all ansii character string types, parsing them, and differentiating between each one (#648) --- src/ast/data_type.rs | 17 +++- src/parser.rs | 161 ++++++++++++++++++++---------------- tests/sqlparser_common.rs | 8 +- tests/sqlparser_postgres.rs | 6 +- 4 files changed, 110 insertions(+), 82 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index c159c3b4d..97123d212 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -25,8 +25,14 @@ use super::value::escape_single_quote_string; #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum DataType { - /// Fixed-length character type e.g. CHAR(10) + /// Fixed-length character type e.g. CHARACTER(10) + Character(Option), + /// Fixed-length char type e.g. CHAR(10) Char(Option), + /// Character varying type e.g. CHARACTER VARYING(10) + CharacterVarying(Option), + /// Char varying type e.g. CHAR VARYING(10) + CharVarying(Option), /// Variable-length character type e.g. VARCHAR(10) Varchar(Option), /// Variable-length character type e.g. NVARCHAR(10) @@ -127,10 +133,17 @@ pub enum DataType { impl fmt::Display for DataType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + DataType::Character(size) => { + format_type_with_optional_length(f, "CHARACTER", size, false) + } DataType::Char(size) => format_type_with_optional_length(f, "CHAR", size, false), - DataType::Varchar(size) => { + DataType::CharacterVarying(size) => { format_type_with_optional_length(f, "CHARACTER VARYING", size, false) } + DataType::CharVarying(size) => { + format_type_with_optional_length(f, "CHAR VARYING", size, false) + } + DataType::Varchar(size) => format_type_with_optional_length(f, "VARCHAR", size, false), DataType::Nvarchar(size) => { format_type_with_optional_length(f, "NVARCHAR", size, false) } diff --git a/src/parser.rs b/src/parser.rs index 897e74c66..57ff4fe50 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3414,9 +3414,16 @@ impl<'a> Parser<'a> { } Keyword::VARCHAR => Ok(DataType::Varchar(self.parse_optional_precision()?)), Keyword::NVARCHAR => Ok(DataType::Nvarchar(self.parse_optional_precision()?)), - Keyword::CHAR | Keyword::CHARACTER => { + Keyword::CHARACTER => { if self.parse_keyword(Keyword::VARYING) { - Ok(DataType::Varchar(self.parse_optional_precision()?)) + Ok(DataType::CharacterVarying(self.parse_optional_precision()?)) + } else { + Ok(DataType::Character(self.parse_optional_precision()?)) + } + } + Keyword::CHAR => { + if self.parse_keyword(Keyword::VARYING) { + Ok(DataType::CharVarying(self.parse_optional_precision()?)) } else { Ok(DataType::Char(self.parse_optional_precision()?)) } @@ -5288,80 +5295,88 @@ mod tests { }); } - // TODO add tests for all data types? https://github.com/sqlparser-rs/sqlparser-rs/issues/2 - // TODO when we have dialect validation by data type parsing, split test - #[test] - fn test_parse_data_type() { - // BINARY data type - test_parse_data_type("BINARY", DataType::Binary(None), "BINARY"); - test_parse_data_type("BINARY(20)", DataType::Binary(Some(20)), "BINARY(20)"); - - // BLOB data type - test_parse_data_type("BLOB", DataType::Blob(None), "BLOB"); - test_parse_data_type("BLOB(50)", DataType::Blob(Some(50)), "BLOB(50)"); - - // CLOB data type - test_parse_data_type("CLOB", DataType::Clob(None), "CLOB"); - test_parse_data_type("CLOB(50)", DataType::Clob(Some(50)), "CLOB(50)"); - - // Double data type - test_parse_data_type( - "DOUBLE PRECISION", - DataType::DoublePrecision, - "DOUBLE PRECISION", - ); - test_parse_data_type("DOUBLE", DataType::Double, "DOUBLE"); - - // Time data type - test_parse_data_type("TIME", DataType::Time(TimezoneInfo::None), "TIME"); - test_parse_data_type( - "TIME WITH TIME ZONE", - DataType::Time(TimezoneInfo::WithTimeZone), - "TIME WITH TIME ZONE", - ); - test_parse_data_type( - "TIME WITHOUT TIME ZONE", - DataType::Time(TimezoneInfo::WithoutTimeZone), - "TIME WITHOUT TIME ZONE", - ); - test_parse_data_type("TIMETZ", DataType::Time(TimezoneInfo::Tz), "TIMETZ"); + #[cfg(test)] + mod test_parse_data_type { + use crate::ast::{DataType, TimezoneInfo}; + use crate::dialect::{AnsiDialect, GenericDialect}; + use crate::test_utils::TestedDialects; - // Timestamp data type - test_parse_data_type( - "TIMESTAMP", - DataType::Timestamp(TimezoneInfo::None), - "TIMESTAMP", - ); - test_parse_data_type( - "TIMESTAMP WITH TIME ZONE", - DataType::Timestamp(TimezoneInfo::WithTimeZone), - "TIMESTAMP WITH TIME ZONE", - ); - test_parse_data_type( - "TIMESTAMP WITHOUT TIME ZONE", - DataType::Timestamp(TimezoneInfo::WithoutTimeZone), - "TIMESTAMP WITHOUT TIME ZONE", - ); - test_parse_data_type( - "TIMESTAMPTZ", - DataType::Timestamp(TimezoneInfo::Tz), - "TIMESTAMPTZ", - ); + macro_rules! test_parse_data_type { + ($dialect:expr, $input:expr, $expected_type:expr $(,)?) => {{ + $dialect.run_parser_method(&*$input, |parser| { + let data_type = parser.parse_data_type().unwrap(); + assert_eq!(data_type, $expected_type); + assert_eq!(data_type.to_string(), $input.to_string()); + }); + }}; + } - // VARBINARY data type - test_parse_data_type("VARBINARY", DataType::Varbinary(None), "VARBINARY"); - test_parse_data_type( - "VARBINARY(20)", - DataType::Varbinary(Some(20)), - "VARBINARY(20)", - ); + #[test] + fn test_ansii_character_string_types() { + // Character string types: + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + }; - fn test_parse_data_type(input: &str, expected_type: DataType, expected_str: &str) { - all_dialects().run_parser_method(input, |parser| { - let data_type = parser.parse_data_type().unwrap(); - assert_eq!(data_type, expected_type); - assert_eq!(expected_type.to_string(), expected_str.to_string()); - }); + test_parse_data_type!(dialect, "CHARACTER", DataType::Character(None)); + + test_parse_data_type!(dialect, "CHARACTER(20)", DataType::Character(Some(20))); + + test_parse_data_type!(dialect, "CHAR", DataType::Char(None)); + + test_parse_data_type!(dialect, "CHAR(20)", DataType::Char(Some(20))); + + test_parse_data_type!( + dialect, + "CHARACTER VARYING(20)", + DataType::CharacterVarying(Some(20)) + ); + + test_parse_data_type!(dialect, "CHAR VARYING(20)", DataType::CharVarying(Some(20))); + + test_parse_data_type!(dialect, "VARCHAR(20)", DataType::Varchar(Some(20))); + } + + #[test] + fn test_ansii_datetime_types() { + // Datetime types: + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + }; + + test_parse_data_type!(dialect, "DATE", DataType::Date); + + test_parse_data_type!(dialect, "TIME", DataType::Time(TimezoneInfo::None)); + + test_parse_data_type!( + dialect, + "TIME WITH TIME ZONE", + DataType::Time(TimezoneInfo::WithTimeZone) + ); + + test_parse_data_type!( + dialect, + "TIME WITHOUT TIME ZONE", + DataType::Time(TimezoneInfo::WithoutTimeZone) + ); + + test_parse_data_type!( + dialect, + "TIMESTAMP", + DataType::Timestamp(TimezoneInfo::None) + ); + + test_parse_data_type!( + dialect, + "TIMESTAMP WITH TIME ZONE", + DataType::Timestamp(TimezoneInfo::WithTimeZone) + ); + + test_parse_data_type!( + dialect, + "TIMESTAMP WITHOUT TIME ZONE", + DataType::Timestamp(TimezoneInfo::WithoutTimeZone) + ); } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 106502be1..1cb466de3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1850,7 +1850,7 @@ fn parse_create_table() { let ast = one_statement_parses_to( sql, "CREATE TABLE uk_cities (\ - name CHARACTER VARYING(100) NOT NULL, \ + name VARCHAR(100) NOT NULL, \ lat DOUBLE NULL, \ lng DOUBLE, \ constrained INT NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0), \ @@ -2312,7 +2312,7 @@ fn parse_create_external_table() { let ast = one_statement_parses_to( sql, "CREATE EXTERNAL TABLE uk_cities (\ - name CHARACTER VARYING(100) NOT NULL, \ + name VARCHAR(100) NOT NULL, \ lat DOUBLE NULL, \ lng DOUBLE) \ STORED AS TEXTFILE LOCATION '/tmp/example.csv'", @@ -2382,7 +2382,7 @@ fn parse_create_or_replace_external_table() { let ast = one_statement_parses_to( sql, "CREATE OR REPLACE EXTERNAL TABLE uk_cities (\ - name CHARACTER VARYING(100) NOT NULL) \ + name VARCHAR(100) NOT NULL) \ STORED AS TEXTFILE LOCATION '/tmp/example.csv'", ); match ast { @@ -2435,7 +2435,7 @@ fn parse_create_external_table_lowercase() { let ast = one_statement_parses_to( sql, "CREATE EXTERNAL TABLE uk_cities (\ - name CHARACTER VARYING(100) NOT NULL, \ + name VARCHAR(100) NOT NULL, \ lat DOUBLE NULL, \ lng DOUBLE) \ STORED AS PARQUET LOCATION '/tmp/example.csv'", diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c9cafe273..c589feec5 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -74,7 +74,7 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "first_name".into(), - data_type: DataType::Varchar(Some(45)), + data_type: DataType::CharacterVarying(Some(45)), collation: None, options: vec![ColumnOptionDef { name: None, @@ -83,7 +83,7 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "last_name".into(), - data_type: DataType::Varchar(Some(45)), + data_type: DataType::CharacterVarying(Some(45)), collation: Some(ObjectName(vec![Ident::with_quote('"', "es_ES")])), options: vec![ColumnOptionDef { name: None, @@ -92,7 +92,7 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "email".into(), - data_type: DataType::Varchar(Some(50)), + data_type: DataType::CharacterVarying(Some(50)), collation: None, options: vec![], }, From cb397d19f99718b1e76e5cd5eebde93d5dc3a83e Mon Sep 17 00:00:00 2001 From: Sarah Yurick <53962159+sarahyurick@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:21:25 -0700 Subject: [PATCH 055/806] Support `CEIL(expr TO DateTimeField)` and `FLOOR(expr TO DateTimeField)` (#635) * support ceil/floor to datetime * Update mod.rs * Update parser.rs * murphys law * Update sqlparser_common.rs * possible fix? * remove question mark * ceil to floor * Update mod.rs * Apply suggestions from code review Co-authored-by: Andrew Lamb * refactor into parse_ceil_floor_expr Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 24 ++++++++++++++ src/ast/value.rs | 6 ++++ src/keywords.rs | 2 ++ src/parser.rs | 38 ++++++++++++++++++--- tests/sqlparser_common.rs | 70 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 134 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 36ae59339..4bc6ad1e9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -332,6 +332,16 @@ pub enum Expr { field: DateTimeField, expr: Box, }, + /// CEIL( [TO DateTimeField]) + Ceil { + expr: Box, + field: DateTimeField, + }, + /// FLOOR( [TO DateTimeField]) + Floor { + expr: Box, + field: DateTimeField, + }, /// POSITION( in ) Position { expr: Box, r#in: Box }, /// SUBSTRING( [FROM ] [FOR ]) @@ -584,6 +594,20 @@ impl fmt::Display for Expr { Expr::TryCast { expr, data_type } => write!(f, "TRY_CAST({} AS {})", expr, data_type), Expr::SafeCast { expr, data_type } => write!(f, "SAFE_CAST({} AS {})", expr, data_type), Expr::Extract { field, expr } => write!(f, "EXTRACT({} FROM {})", field, expr), + Expr::Ceil { expr, field } => { + if field == &DateTimeField::NoDateTime { + write!(f, "CEIL({})", expr) + } else { + write!(f, "CEIL({} TO {})", expr, field) + } + } + Expr::Floor { expr, field } => { + if field == &DateTimeField::NoDateTime { + write!(f, "FLOOR({})", expr) + } else { + write!(f, "FLOOR({} TO {})", expr, field) + } + } Expr::Position { expr, r#in } => write!(f, "POSITION({} IN {})", expr, r#in), Expr::Collate { expr, collation } => write!(f, "{} COLLATE {}", expr, collation), Expr::Nested(ast) => write!(f, "({})", ast), diff --git a/src/ast/value.rs b/src/ast/value.rs index 96d525ac6..2d273d031 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -83,14 +83,17 @@ pub enum DateTimeField { Isodow, Isoyear, Julian, + Microsecond, Microseconds, Millenium, Millennium, + Millisecond, Milliseconds, Quarter, Timezone, TimezoneHour, TimezoneMinute, + NoDateTime, } impl fmt::Display for DateTimeField { @@ -111,14 +114,17 @@ impl fmt::Display for DateTimeField { DateTimeField::Isodow => "ISODOW", DateTimeField::Isoyear => "ISOYEAR", DateTimeField::Julian => "JULIAN", + DateTimeField::Microsecond => "MICROSECOND", DateTimeField::Microseconds => "MICROSECONDS", DateTimeField::Millenium => "MILLENIUM", DateTimeField::Millennium => "MILLENNIUM", + DateTimeField::Millisecond => "MILLISECOND", DateTimeField::Milliseconds => "MILLISECONDS", DateTimeField::Quarter => "QUARTER", DateTimeField::Timezone => "TIMEZONE", DateTimeField::TimezoneHour => "TIMEZONE_HOUR", DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE", + DateTimeField::NoDateTime => "NODATETIME", }) } } diff --git a/src/keywords.rs b/src/keywords.rs index 66b4c9f9f..b84b4cf9d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -330,9 +330,11 @@ define_keywords!( MERGE, METADATA, METHOD, + MICROSECOND, MICROSECONDS, MILLENIUM, MILLENNIUM, + MILLISECOND, MILLISECONDS, MIN, MINUTE, diff --git a/src/parser.rs b/src/parser.rs index 57ff4fe50..36c577b05 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -450,6 +450,8 @@ impl<'a> Parser<'a> { Keyword::SAFE_CAST => self.parse_safe_cast_expr(), Keyword::EXISTS => self.parse_exists_expr(false), Keyword::EXTRACT => self.parse_extract_expr(), + Keyword::CEIL => self.parse_ceil_floor_expr(true), + Keyword::FLOOR => self.parse_ceil_floor_expr(false), Keyword::POSITION => self.parse_position_expr(), Keyword::SUBSTRING => self.parse_substring_expr(), Keyword::OVERLAY => self.parse_overlay_expr(), @@ -848,6 +850,30 @@ impl<'a> Parser<'a> { }) } + pub fn parse_ceil_floor_expr(&mut self, is_ceil: bool) -> Result { + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + // Parse `CEIL/FLOOR(expr)` + let mut field = DateTimeField::NoDateTime; + let keyword_to = self.parse_keyword(Keyword::TO); + if keyword_to { + // Parse `CEIL/FLOOR(expr TO DateTimeField)` + field = self.parse_date_time_field()?; + } + self.expect_token(&Token::RParen)?; + if is_ceil { + Ok(Expr::Ceil { + expr: Box::new(expr), + field, + }) + } else { + Ok(Expr::Floor { + expr: Box::new(expr), + field, + }) + } + } + pub fn parse_position_expr(&mut self) -> Result { // PARSE SELECT POSITION('@' in field) self.expect_token(&Token::LParen)?; @@ -1040,10 +1066,10 @@ impl<'a> Parser<'a> { })) } - // This function parses date/time fields for both the EXTRACT function-like - // operator and interval qualifiers. EXTRACT supports a wider set of - // date/time fields than interval qualifiers, so this function may need to - // be split in two. + // This function parses date/time fields for the EXTRACT function-like + // operator, interval qualifiers, and the ceil/floor operations. + // EXTRACT supports a wider set of date/time fields than interval qualifiers, + // so this function may need to be split in two. pub fn parse_date_time_field(&mut self) -> Result { match self.next_token() { Token::Word(w) => match w.keyword { @@ -1062,9 +1088,11 @@ impl<'a> Parser<'a> { Keyword::ISODOW => Ok(DateTimeField::Isodow), Keyword::ISOYEAR => Ok(DateTimeField::Isoyear), Keyword::JULIAN => Ok(DateTimeField::Julian), + Keyword::MICROSECOND => Ok(DateTimeField::Microsecond), Keyword::MICROSECONDS => Ok(DateTimeField::Microseconds), Keyword::MILLENIUM => Ok(DateTimeField::Millenium), Keyword::MILLENNIUM => Ok(DateTimeField::Millennium), + Keyword::MILLISECOND => Ok(DateTimeField::Millisecond), Keyword::MILLISECONDS => Ok(DateTimeField::Milliseconds), Keyword::QUARTER => Ok(DateTimeField::Quarter), Keyword::TIMEZONE => Ok(DateTimeField::Timezone), @@ -1142,9 +1170,11 @@ impl<'a> Parser<'a> { Keyword::ISODOW, Keyword::ISOYEAR, Keyword::JULIAN, + Keyword::MICROSECOND, Keyword::MICROSECONDS, Keyword::MILLENIUM, Keyword::MILLENNIUM, + Keyword::MILLISECOND, Keyword::MILLISECONDS, Keyword::QUARTER, Keyword::TIMEZONE, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1cb466de3..7654d677e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1765,18 +1765,84 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(ISODOW FROM d)"); verified_stmt("SELECT EXTRACT(ISOYEAR FROM d)"); verified_stmt("SELECT EXTRACT(JULIAN FROM d)"); + verified_stmt("SELECT EXTRACT(MICROSECOND FROM d)"); verified_stmt("SELECT EXTRACT(MICROSECONDS FROM d)"); verified_stmt("SELECT EXTRACT(MILLENIUM FROM d)"); verified_stmt("SELECT EXTRACT(MILLENNIUM FROM d)"); + verified_stmt("SELECT EXTRACT(MILLISECOND FROM d)"); verified_stmt("SELECT EXTRACT(MILLISECONDS FROM d)"); verified_stmt("SELECT EXTRACT(QUARTER FROM d)"); verified_stmt("SELECT EXTRACT(TIMEZONE FROM d)"); verified_stmt("SELECT EXTRACT(TIMEZONE_HOUR FROM d)"); verified_stmt("SELECT EXTRACT(TIMEZONE_MINUTE FROM d)"); - let res = parse_sql_statements("SELECT EXTRACT(MILLISECOND FROM d)"); + let res = parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)"); assert_eq!( - ParserError::ParserError("Expected date/time field, found: MILLISECOND".to_string()), + ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), + res.unwrap_err() + ); +} + +#[test] +fn parse_ceil_number() { + verified_stmt("SELECT CEIL(1.5)"); + verified_stmt("SELECT CEIL(float_column) FROM my_table"); +} + +#[test] +fn parse_floor_number() { + verified_stmt("SELECT FLOOR(1.5)"); + verified_stmt("SELECT FLOOR(float_column) FROM my_table"); +} + +#[test] +fn parse_ceil_datetime() { + let sql = "SELECT CEIL(d TO DAY)"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Ceil { + expr: Box::new(Expr::Identifier(Ident::new("d"))), + field: DateTimeField::Day, + }, + expr_from_projection(only(&select.projection)), + ); + + one_statement_parses_to("SELECT CEIL(d to day)", "SELECT CEIL(d TO DAY)"); + + verified_stmt("SELECT CEIL(d TO HOUR) FROM df"); + verified_stmt("SELECT CEIL(d TO MINUTE) FROM df"); + verified_stmt("SELECT CEIL(d TO SECOND) FROM df"); + verified_stmt("SELECT CEIL(d TO MILLISECOND) FROM df"); + + let res = parse_sql_statements("SELECT CEIL(d TO JIFFY) FROM df"); + assert_eq!( + ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), + res.unwrap_err() + ); +} + +#[test] +fn parse_floor_datetime() { + let sql = "SELECT FLOOR(d TO DAY)"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Floor { + expr: Box::new(Expr::Identifier(Ident::new("d"))), + field: DateTimeField::Day, + }, + expr_from_projection(only(&select.projection)), + ); + + one_statement_parses_to("SELECT FLOOR(d to day)", "SELECT FLOOR(d TO DAY)"); + + verified_stmt("SELECT FLOOR(d TO HOUR) FROM df"); + verified_stmt("SELECT FLOOR(d TO MINUTE) FROM df"); + verified_stmt("SELECT FLOOR(d TO SECOND) FROM df"); + verified_stmt("SELECT FLOOR(d TO MILLISECOND) FROM df"); + + let res = parse_sql_statements("SELECT FLOOR(d TO JIFFY) FROM df"); + assert_eq!( + ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), res.unwrap_err() ); } From e7fb3d4fa7041f54327689fd6512ba6b2832e05c Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 6 Oct 2022 15:21:56 -0400 Subject: [PATCH 056/806] Correct typo in readme (#658) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 542fa5e47..f63a963cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented ### Added -* Support `AUTHORIZATION` clause in `CREATE SCHEMA` statements (#646) - Thanks @AugustoFKL +* Support `AUTHORIZATION` clause in `CREATE SCHEMA` statements (#641) - Thanks @AugustoFKL * Support optional precision for `CLOB` and `BLOB` (#639) - Thanks @AugustoFKL * Support optional precision in `VARBINARY` and `BINARY` (#637) - Thanks @AugustoFKL From f2b0efec0c889e6c8578d4664fdb24b4954c2b2e Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Fri, 7 Oct 2022 22:41:33 +0200 Subject: [PATCH 057/806] clippy fixes (#656) --- src/tokenizer.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 4253dd80a..5a9afdbef 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -229,7 +229,7 @@ impl Token { Token::Word(Word { value: word.to_string(), quote_style, - keyword: if quote_style == None { + keyword: if quote_style.is_none() { let keyword = ALL_KEYWORDS.binary_search(&word_uppercase.as_str()); keyword.map_or(Keyword::NoKeyword, |x| ALL_KEYWORDS_INDEX[x]) } else { @@ -354,11 +354,11 @@ impl<'a> Tokenizer<'a> { } Token::Whitespace(Whitespace::Tab) => self.col += 4, - Token::Word(w) if w.quote_style == None => { - self.col += w.value.chars().count() as u64 - } - Token::Word(w) if w.quote_style != None => { - self.col += w.value.chars().count() as u64 + 2 + Token::Word(w) => { + self.col += w.value.chars().count() as u64; + if w.quote_style.is_some() { + self.col += 2 + } } Token::Number(s, _) => self.col += s.chars().count() as u64, Token::SingleQuotedString(s) => self.col += s.chars().count() as u64, From 6392a216f0b9b90dd4af3e2e25fc95ba92a6ac40 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Fri, 7 Oct 2022 17:56:27 -0300 Subject: [PATCH 058/806] Replacing 'disable-publish=true' flag by 'public=false', to allow compatibility with recent cargo-release versions (#660) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8dcc2acd2..60fe478c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,4 +42,4 @@ pretty_assertions = "1" # Instruct `cargo release` to not run `cargo publish` locally: # https://github.com/sunng87/cargo-release/blob/master/docs/reference.md#config-fields # See docs/releasing.md for details. -disable-publish = true +publish = false From a3194ddd52a4647351ef7b2b4d74eac0d7850eba Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Fri, 7 Oct 2022 17:57:10 -0300 Subject: [PATCH 059/806] Create table builder structure (#659) * Create table builder structure * Adding comments and examples and adjusting file structure --- src/ast/helpers/mod.rs | 1 + src/ast/helpers/stmt_create_table.rs | 323 +++++++++++++++++++++++++++ src/ast/mod.rs | 1 + src/ast/value.rs | 2 - src/parser.rs | 84 +++---- src/test_utils.rs | 2 +- 6 files changed, 360 insertions(+), 53 deletions(-) create mode 100644 src/ast/helpers/mod.rs create mode 100644 src/ast/helpers/stmt_create_table.rs diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs new file mode 100644 index 000000000..41098b014 --- /dev/null +++ b/src/ast/helpers/mod.rs @@ -0,0 +1 @@ +pub mod stmt_create_table; diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs new file mode 100644 index 000000000..97c567b83 --- /dev/null +++ b/src/ast/helpers/stmt_create_table.rs @@ -0,0 +1,323 @@ +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::ast::{ + ColumnDef, FileFormat, HiveDistributionStyle, HiveFormat, ObjectName, OnCommit, Query, + SqlOption, Statement, TableConstraint, +}; +use crate::parser::ParserError; + +/// Builder for create table statement variant ([1]). +/// +/// This structure helps building and accessing a create table with more ease, without needing to: +/// - Match the enum itself a lot of times; or +/// - Moving a lot of variables around the code. +/// +/// # Example +/// ```rust +/// use sqlparser::ast::helpers::stmt_create_table::CreateTableBuilder; +/// use sqlparser::ast::{ColumnDef, DataType, Ident, ObjectName}; +/// let builder = CreateTableBuilder::new(ObjectName(vec![Ident::new("table_name")])) +/// .if_not_exists(true) +/// .columns(vec![ColumnDef { +/// name: Ident::new("c1"), +/// data_type: DataType::Int(None), +/// collation: None, +/// options: vec![], +/// }]); +/// // You can access internal elements with ease +/// assert!(builder.if_not_exists); +/// // Convert to a statement +/// assert_eq!( +/// builder.build().to_string(), +/// "CREATE TABLE IF NOT EXISTS table_name (c1 INT)" +/// ) +/// ``` +/// +/// [1]: crate::ast::Statement::CreateTable +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CreateTableBuilder { + pub or_replace: bool, + pub temporary: bool, + pub external: bool, + pub global: Option, + pub if_not_exists: bool, + pub name: ObjectName, + pub columns: Vec, + 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 default_charset: Option, + pub collation: Option, + pub on_commit: Option, + pub on_cluster: Option, +} + +impl CreateTableBuilder { + pub fn new(name: ObjectName) -> Self { + Self { + or_replace: false, + temporary: false, + external: false, + global: None, + if_not_exists: false, + name, + columns: vec![], + 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, + default_charset: None, + collation: None, + on_commit: None, + on_cluster: None, + } + } + pub fn or_replace(mut self, or_replace: bool) -> Self { + self.or_replace = or_replace; + self + } + + pub fn temporary(mut self, temporary: bool) -> Self { + self.temporary = temporary; + self + } + + pub fn external(mut self, external: bool) -> Self { + self.external = external; + self + } + + pub fn global(mut self, global: Option) -> Self { + self.global = global; + self + } + + pub fn if_not_exists(mut self, if_not_exists: bool) -> Self { + self.if_not_exists = if_not_exists; + self + } + + pub fn columns(mut self, columns: Vec) -> Self { + self.columns = columns; + self + } + + pub fn constraints(mut self, constraints: Vec) -> Self { + self.constraints = constraints; + self + } + + pub fn hive_distribution(mut self, hive_distribution: HiveDistributionStyle) -> Self { + self.hive_distribution = hive_distribution; + self + } + + pub fn hive_formats(mut self, hive_formats: Option) -> Self { + self.hive_formats = hive_formats; + 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 + } + pub fn location(mut self, location: Option) -> Self { + self.location = location; + self + } + + pub fn query(mut self, query: Option>) -> Self { + self.query = query; + self + } + pub fn without_rowid(mut self, without_rowid: bool) -> Self { + self.without_rowid = without_rowid; + self + } + + pub fn like(mut self, like: Option) -> Self { + self.like = like; + self + } + + // Different name to allow the object to be cloned + pub fn clone_clause(mut self, clone: Option) -> Self { + self.clone = clone; + self + } + + pub fn engine(mut self, engine: Option) -> Self { + self.engine = engine; + 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 + } + + pub fn on_cluster(mut self, on_cluster: Option) -> Self { + self.on_cluster = on_cluster; + self + } + + pub fn build(self) -> Statement { + Statement::CreateTable { + or_replace: self.or_replace, + temporary: self.temporary, + external: self.external, + global: self.global, + if_not_exists: self.if_not_exists, + name: self.name, + columns: self.columns, + 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, + default_charset: self.default_charset, + collation: self.collation, + on_commit: self.on_commit, + on_cluster: self.on_cluster, + } + } +} + +impl TryFrom for CreateTableBuilder { + type Error = ParserError; + + // As the builder can be transformed back to a statement, it shouldn't be a problem to take the + // ownership. + fn try_from(stmt: Statement) -> Result { + match stmt { + Statement::CreateTable { + or_replace, + temporary, + external, + global, + if_not_exists, + name, + columns, + constraints, + hive_distribution, + hive_formats, + table_properties, + with_options, + file_format, + location, + query, + without_rowid, + like, + clone, + engine, + default_charset, + collation, + on_commit, + on_cluster, + } => Ok(Self { + or_replace, + temporary, + external, + global, + if_not_exists, + name, + columns, + constraints, + hive_distribution, + hive_formats, + table_properties, + with_options, + file_format, + location, + query, + without_rowid, + like, + clone, + engine, + default_charset, + collation, + on_commit, + on_cluster, + }), + _ => Err(ParserError::ParserError(format!( + "Expected create table statement, but received: {stmt}" + ))), + } + } +} + +#[cfg(test)] +mod tests { + use crate::ast::helpers::stmt_create_table::CreateTableBuilder; + use crate::ast::{Ident, ObjectName, Statement}; + use crate::parser::ParserError; + + #[test] + pub fn test_from_valid_statement() { + let builder = CreateTableBuilder::new(ObjectName(vec![Ident::new("table_name")])); + + let stmt = builder.clone().build(); + + assert_eq!(builder, CreateTableBuilder::try_from(stmt).unwrap()); + } + + #[test] + pub fn test_from_invalid_statement() { + let stmt = Statement::Commit { chain: false }; + + assert_eq!( + CreateTableBuilder::try_from(stmt).unwrap_err(), + ParserError::ParserError( + "Expected create table statement, but received: COMMIT".to_owned() + ) + ); + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4bc6ad1e9..342bd28cf 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -13,6 +13,7 @@ //! SQL Abstract Syntax Tree (AST) types mod data_type; mod ddl; +pub mod helpers; mod operator; mod query; mod value; diff --git a/src/ast/value.rs b/src/ast/value.rs index 2d273d031..3861ab008 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -10,8 +10,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(not(feature = "std"))] -use alloc::boxed::Box; #[cfg(not(feature = "std"))] use alloc::string::String; use core::fmt; diff --git a/src/parser.rs b/src/parser.rs index 36c577b05..fb473b74f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -27,6 +27,7 @@ use log::debug; use IsLateral::*; use IsOptional::*; +use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::*; use crate::dialect::*; use crate::keywords::{self, Keyword}; @@ -2032,31 +2033,18 @@ impl<'a> Parser<'a> { }; let location = hive_formats.location.clone(); let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; - Ok(Statement::CreateTable { - name: table_name, - columns, - constraints, - hive_distribution, - hive_formats: Some(hive_formats), - with_options: vec![], - table_properties, - or_replace, - if_not_exists, - external: true, - global: None, - temporary: false, - file_format, - location, - query: None, - without_rowid: false, - like: None, - clone: None, - default_charset: None, - engine: None, - collation: None, - on_commit: None, - on_cluster: None, - }) + Ok(CreateTableBuilder::new(table_name) + .columns(columns) + .constraints(constraints) + .hive_distribution(hive_distribution) + .hive_formats(Some(hive_formats)) + .table_properties(table_properties) + .or_replace(or_replace) + .if_not_exists(if_not_exists) + .external(true) + .file_format(file_format) + .location(location) + .build()) } pub fn parse_file_format(&mut self) -> Result { @@ -2667,31 +2655,27 @@ impl<'a> Parser<'a> { None }; - Ok(Statement::CreateTable { - name: table_name, - temporary, - columns, - constraints, - with_options, - table_properties, - or_replace, - if_not_exists, - hive_distribution, - hive_formats: Some(hive_formats), - external: false, - global, - file_format: None, - location: None, - query, - without_rowid, - like, - clone, - engine, - default_charset, - collation, - on_commit, - on_cluster, - }) + Ok(CreateTableBuilder::new(table_name) + .temporary(temporary) + .columns(columns) + .constraints(constraints) + .with_options(with_options) + .table_properties(table_properties) + .or_replace(or_replace) + .if_not_exists(if_not_exists) + .hive_distribution(hive_distribution) + .hive_formats(Some(hive_formats)) + .global(global) + .query(query) + .without_rowid(without_rowid) + .like(like) + .clone_clause(clone) + .engine(engine) + .default_charset(default_charset) + .collation(collation) + .on_commit(on_commit) + .on_cluster(on_cluster) + .build()) } pub fn parse_columns(&mut self) -> Result<(Vec, Vec), ParserError> { diff --git a/src/test_utils.rs b/src/test_utils.rs index c3d60ee62..d51aec1ae 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -15,7 +15,7 @@ /// on this module, as it will change without notice. // // Integration tests (i.e. everything under `tests/`) import this -// via `tests/test_utils/mod.rs`. +// via `tests/test_utils/helpers`. #[cfg(not(feature = "std"))] use alloc::{ From a9939b0a4f36634a3fa20883a85c0eef7ee3d696 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Sat, 8 Oct 2022 06:59:11 -0300 Subject: [PATCH 060/806] Enum to handle exact number precisios (#654) --- src/ast/data_type.rs | 41 ++++++++++++++++++++++++++++------ src/ast/mod.rs | 1 + src/parser.rs | 53 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 97123d212..0e3d4552d 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -60,7 +60,7 @@ pub enum DataType { /// [Oracle]: https://docs.oracle.com/javadb/10.8.3.0/ref/rrefblob.html Blob(Option), /// Decimal type with optional precision and scale e.g. DECIMAL(10,2) - Decimal(Option, Option), + Decimal(ExactNumberInfo), /// Floating point with optional precision e.g. FLOAT(8) Float(Option), /// Tiny integer with optional display width e.g. TINYINT or TINYINT(3) @@ -154,12 +154,8 @@ impl fmt::Display for DataType { format_type_with_optional_length(f, "VARBINARY", size, false) } DataType::Blob(size) => format_type_with_optional_length(f, "BLOB", size, false), - DataType::Decimal(precision, scale) => { - if let Some(scale) = scale { - write!(f, "NUMERIC({},{})", precision.unwrap(), scale) - } else { - format_type_with_optional_length(f, "NUMERIC", precision, false) - } + DataType::Decimal(info) => { + write!(f, "NUMERIC{}", info) } DataType::Float(size) => format_type_with_optional_length(f, "FLOAT", size, false), DataType::TinyInt(zerofill) => { @@ -297,3 +293,34 @@ impl fmt::Display for TimezoneInfo { } } } + +/// Additional information for `NUMERIC`, `DECIMAL`, and `DEC` data types +/// following the 2016 [standard]. +/// +/// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ExactNumberInfo { + /// No additional information e.g. `DECIMAL` + None, + /// Only precision information e.g. `DECIMAL(10)` + Precision(u64), + /// Precision and scale information e.g. `DECIMAL(10,2)` + PrecisionAndScale(u64, u64), +} + +impl fmt::Display for ExactNumberInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExactNumberInfo::None => { + write!(f, "") + } + ExactNumberInfo::Precision(p) => { + write!(f, "({p})") + } + ExactNumberInfo::PrecisionAndScale(p, s) => { + write!(f, "({p},{s})") + } + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 342bd28cf..7f9d42f05 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -30,6 +30,7 @@ use core::fmt; use serde::{Deserialize, Serialize}; pub use self::data_type::DataType; +pub use self::data_type::ExactNumberInfo; pub use self::data_type::TimezoneInfo; pub use self::ddl::{ AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, diff --git a/src/parser.rs b/src/parser.rs index fb473b74f..cb261e183 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3481,10 +3481,9 @@ impl<'a> Parser<'a> { Keyword::STRING => Ok(DataType::String), Keyword::TEXT => Ok(DataType::Text), Keyword::BYTEA => Ok(DataType::Bytea), - Keyword::NUMERIC | Keyword::DECIMAL | Keyword::DEC => { - let (precision, scale) = self.parse_optional_precision_scale()?; - Ok(DataType::Decimal(precision, scale)) - } + Keyword::NUMERIC | Keyword::DECIMAL | Keyword::DEC => Ok(DataType::Decimal( + self.parse_exact_number_optional_precision_scale()?, + )), Keyword::ENUM => Ok(DataType::Enum(self.parse_string_values()?)), Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)), Keyword::ARRAY => { @@ -3698,6 +3697,28 @@ impl<'a> Parser<'a> { } } + pub fn parse_exact_number_optional_precision_scale( + &mut self, + ) -> Result { + if self.consume_token(&Token::LParen) { + let precision = self.parse_literal_uint()?; + let scale = if self.consume_token(&Token::Comma) { + Some(self.parse_literal_uint()?) + } else { + None + }; + + self.expect_token(&Token::RParen)?; + + match scale { + None => Ok(ExactNumberInfo::Precision(precision)), + Some(scale) => Ok(ExactNumberInfo::PrecisionAndScale(precision, scale)), + } + } else { + Ok(ExactNumberInfo::None) + } + } + pub fn parse_delete(&mut self) -> Result { self.expect_keyword(Keyword::FROM)?; let table_name = self.parse_table_factor()?; @@ -5311,7 +5332,7 @@ mod tests { #[cfg(test)] mod test_parse_data_type { - use crate::ast::{DataType, TimezoneInfo}; + use crate::ast::{DataType, ExactNumberInfo, TimezoneInfo}; use crate::dialect::{AnsiDialect, GenericDialect}; use crate::test_utils::TestedDialects; @@ -5351,6 +5372,28 @@ mod tests { test_parse_data_type!(dialect, "VARCHAR(20)", DataType::Varchar(Some(20))); } + #[test] + fn test_ansii_exact_numeric_types() { + // Exact numeric types: + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + }; + + test_parse_data_type!(dialect, "NUMERIC", DataType::Decimal(ExactNumberInfo::None)); + + test_parse_data_type!( + dialect, + "NUMERIC(2)", + DataType::Decimal(ExactNumberInfo::Precision(2)) + ); + + test_parse_data_type!( + dialect, + "NUMERIC(2,10)", + DataType::Decimal(ExactNumberInfo::PrecisionAndScale(2, 10)) + ); + } + #[test] fn test_ansii_datetime_types() { // Datetime types: From 777672625ff7ee805fb7ac6e71a17053fc49b222 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Mon, 10 Oct 2022 18:14:37 -0300 Subject: [PATCH 061/806] Replacing the two booleans from 'SET ROLE' to a single enum. (#664) This way, we don't rely on the parser to have valid structures for that statement, as the structure itself is valid. --- src/ast/mod.rs | 75 +++++++++++++++++++++++-------------- src/parser.rs | 9 ++++- tests/sqlparser_postgres.rs | 21 ++++++----- 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7f9d42f05..5efa3a57c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -11,13 +11,6 @@ // limitations under the License. //! SQL Abstract Syntax Tree (AST) types -mod data_type; -mod ddl; -pub mod helpers; -mod operator; -mod query; -mod value; - #[cfg(not(feature = "std"))] use alloc::{ boxed::Box, @@ -44,6 +37,13 @@ pub use self::query::{ }; pub use self::value::{DateTimeField, TrimWhereField, Value}; +mod data_type; +mod ddl; +pub mod helpers; +mod operator; +mod query; +mod value; + struct DisplaySeparated<'a, T> where T: fmt::Display, @@ -1227,14 +1227,16 @@ pub enum Statement { /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. Discard { object_type: DiscardObject }, - /// SET [ SESSION | LOCAL ] ROLE role_name + /// SET `[ SESSION | LOCAL ]` ROLE role_name. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4]. /// - /// Note: this is a PostgreSQL-specific statement, - /// but may also compatible with other SQL. + /// [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 { - local: bool, - // SESSION is the default if neither SESSION nor LOCAL appears. - session: bool, + /// 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, }, /// SET @@ -2136,23 +2138,12 @@ impl fmt::Display for Statement { write!(f, "DISCARD {object_type}", object_type = object_type)?; Ok(()) } - Statement::SetRole { - local, - session, + Self::SetRole { + context_modifier, role_name, } => { - write!( - f, - "SET {local}{session}ROLE", - local = if *local { "LOCAL " } else { "" }, - session = if *session { "SESSION " } else { "" }, - )?; - if let Some(role_name) = role_name { - write!(f, " {}", role_name)?; - } else { - f.write_str(" NONE")?; - } - Ok(()) + let role_name = role_name.clone().unwrap_or_else(|| Ident::new("NONE")); + write!(f, "SET{context_modifier} ROLE {role_name}") } Statement::SetVariable { local, @@ -3283,6 +3274,34 @@ impl fmt::Display for DiscardObject { } } +/// Optional context modifier for statements that can be or `LOCAL`, or `SESSION`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +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 + Session, +} + +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") + } + Self::Session => { + write!(f, " SESSION") + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CreateFunctionUsing { diff --git a/src/parser.rs b/src/parser.rs index cb261e183..2db0185d4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4113,14 +4113,19 @@ impl<'a> Parser<'a> { if let Some(Keyword::HIVEVAR) = modifier { self.expect_token(&Token::Colon)?; } else if self.parse_keyword(Keyword::ROLE) { + let context_modifier = match modifier { + Some(keyword) if keyword == Keyword::LOCAL => ContextModifier::Local, + Some(keyword) if keyword == Keyword::SESSION => ContextModifier::Session, + _ => ContextModifier::None, + }; + let role_name = if self.parse_keyword(Keyword::NONE) { None } else { Some(self.parse_identifier()?) }; return Ok(Statement::SetRole { - local: modifier == Some(Keyword::LOCAL), - session: modifier == Some(Keyword::SESSION), + context_modifier, role_name, }); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c589feec5..0ce605ece 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -902,41 +902,44 @@ fn parse_set() { #[test] fn parse_set_role() { - let stmt = pg_and_generic().verified_stmt("SET SESSION ROLE NONE"); + let query = "SET SESSION ROLE NONE"; + let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, Statement::SetRole { - local: false, - session: true, + context_modifier: ContextModifier::Session, role_name: None, } ); + assert_eq!(query, stmt.to_string()); - let stmt = pg_and_generic().verified_stmt("SET LOCAL ROLE \"rolename\""); + let query = "SET LOCAL ROLE \"rolename\""; + let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, Statement::SetRole { - local: true, - session: false, + context_modifier: ContextModifier::Local, role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\"'), }), } ); + assert_eq!(query, stmt.to_string()); - let stmt = pg_and_generic().verified_stmt("SET ROLE 'rolename'"); + let query = "SET ROLE 'rolename'"; + let stmt = pg_and_generic().verified_stmt(query); assert_eq!( stmt, Statement::SetRole { - local: false, - session: false, + context_modifier: ContextModifier::None, role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\''), }), } ); + assert_eq!(query, stmt.to_string()); } #[test] From cacdf3305f33319bed4e870227cdd541b4c9f947 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Tue, 11 Oct 2022 09:54:15 -0300 Subject: [PATCH 062/806] Add support for unit on char length units for small character string types. (#663) This results in complete support for ANSI CHARACTER, CHAR, CHARACTER VARYING, CHAR VARYING, and VARCHAR. --- src/ast/data_type.rs | 84 +++++++++++++++---- src/ast/mod.rs | 6 +- src/keywords.rs | 2 + src/parser.rs | 156 +++++++++++++++++++++++++++++++++--- tests/sqlparser_common.rs | 15 +++- tests/sqlparser_postgres.rs | 15 +++- 6 files changed, 244 insertions(+), 34 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 0e3d4552d..baa23acf2 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -26,15 +26,15 @@ use super::value::escape_single_quote_string; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum DataType { /// Fixed-length character type e.g. CHARACTER(10) - Character(Option), + Character(Option), /// Fixed-length char type e.g. CHAR(10) - Char(Option), + Char(Option), /// Character varying type e.g. CHARACTER VARYING(10) - CharacterVarying(Option), + CharacterVarying(Option), /// Char varying type e.g. CHAR VARYING(10) - CharVarying(Option), + CharVarying(Option), /// Variable-length character type e.g. VARCHAR(10) - Varchar(Option), + Varchar(Option), /// Variable-length character type e.g. NVARCHAR(10) Nvarchar(Option), /// Uuid type @@ -133,17 +133,14 @@ pub enum DataType { impl fmt::Display for DataType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - DataType::Character(size) => { - format_type_with_optional_length(f, "CHARACTER", size, false) - } - DataType::Char(size) => format_type_with_optional_length(f, "CHAR", size, false), + DataType::Character(size) => format_character_string_type(f, "CHARACTER", size), + DataType::Char(size) => format_character_string_type(f, "CHAR", size), DataType::CharacterVarying(size) => { - format_type_with_optional_length(f, "CHARACTER VARYING", size, false) - } - DataType::CharVarying(size) => { - format_type_with_optional_length(f, "CHAR VARYING", size, false) + format_character_string_type(f, "CHARACTER VARYING", size) } - DataType::Varchar(size) => format_type_with_optional_length(f, "VARCHAR", size, false), + + DataType::CharVarying(size) => format_character_string_type(f, "CHAR VARYING", size), + DataType::Varchar(size) => format_character_string_type(f, "VARCHAR", size), DataType::Nvarchar(size) => { format_type_with_optional_length(f, "NVARCHAR", size, false) } @@ -247,6 +244,18 @@ fn format_type_with_optional_length( Ok(()) } +fn format_character_string_type( + f: &mut fmt::Formatter, + sql_type: &str, + size: &Option, +) -> fmt::Result { + write!(f, "{}", sql_type)?; + if let Some(size) = size { + write!(f, "({})", size)?; + } + Ok(()) +} + /// Timestamp and Time data types information about TimeZone formatting. /// /// This is more related to a display information than real differences between each variant. To @@ -324,3 +333,50 @@ impl fmt::Display for ExactNumberInfo { } } } + +/// Information about [character length][1], including length and possibly unit. +/// +/// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-length +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CharacterLength { + /// Default (if VARYING) or maximum (if not VARYING) length + pub length: u64, + /// Optional unit. If not informed, the ANSI handles it as CHARACTERS implicitly + pub unit: Option, +} + +impl fmt::Display for CharacterLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.length)?; + if let Some(unit) = &self.unit { + write!(f, " {}", unit)?; + } + Ok(()) + } +} + +/// Possible units for characters, initially based on 2016 ANSI [standard][1]. +/// +/// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#char-length-units +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CharLengthUnits { + /// CHARACTERS unit + Characters, + /// OCTETS unit + Octets, +} + +impl fmt::Display for CharLengthUnits { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Characters => { + write!(f, "CHARACTERS") + } + Self::Octets => { + write!(f, "OCTETS") + } + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5efa3a57c..019622433 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -22,9 +22,9 @@ use core::fmt; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -pub use self::data_type::DataType; -pub use self::data_type::ExactNumberInfo; -pub use self::data_type::TimezoneInfo; +pub use self::data_type::{ + CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, +}; pub use self::ddl::{ AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ReferentialAction, TableConstraint, diff --git a/src/keywords.rs b/src/keywords.rs index b84b4cf9d..0b6b06b33 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -123,6 +123,7 @@ define_keywords!( CHANGE, CHAR, CHARACTER, + CHARACTERS, CHARACTER_LENGTH, CHARSET, CHAR_LENGTH, @@ -372,6 +373,7 @@ define_keywords!( NVARCHAR, OBJECT, OCCURRENCES_REGEX, + OCTETS, OCTET_LENGTH, OF, OFFSET, diff --git a/src/parser.rs b/src/parser.rs index 2db0185d4..0c0bd9c5a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3426,20 +3426,24 @@ impl<'a> Parser<'a> { Ok(DataType::BigInt(optional_precision?)) } } - Keyword::VARCHAR => Ok(DataType::Varchar(self.parse_optional_precision()?)), + Keyword::VARCHAR => Ok(DataType::Varchar(self.parse_optional_character_length()?)), Keyword::NVARCHAR => Ok(DataType::Nvarchar(self.parse_optional_precision()?)), Keyword::CHARACTER => { if self.parse_keyword(Keyword::VARYING) { - Ok(DataType::CharacterVarying(self.parse_optional_precision()?)) + Ok(DataType::CharacterVarying( + self.parse_optional_character_length()?, + )) } else { - Ok(DataType::Character(self.parse_optional_precision()?)) + Ok(DataType::Character(self.parse_optional_character_length()?)) } } Keyword::CHAR => { if self.parse_keyword(Keyword::VARYING) { - Ok(DataType::CharVarying(self.parse_optional_precision()?)) + Ok(DataType::CharVarying( + self.parse_optional_character_length()?, + )) } else { - Ok(DataType::Char(self.parse_optional_precision()?)) + Ok(DataType::Char(self.parse_optional_character_length()?)) } } Keyword::CLOB => Ok(DataType::Clob(self.parse_optional_precision()?)), @@ -3680,6 +3684,31 @@ impl<'a> Parser<'a> { } } + pub fn parse_optional_character_length( + &mut self, + ) -> Result, ParserError> { + if self.consume_token(&Token::LParen) { + let character_length = self.parse_character_length()?; + self.expect_token(&Token::RParen)?; + Ok(Some(character_length)) + } else { + Ok(None) + } + } + + pub fn parse_character_length(&mut self) -> Result { + let length = self.parse_literal_uint()?; + let unit = if self.parse_keyword(Keyword::CHARACTERS) { + Some(CharLengthUnits::Characters) + } else if self.parse_keyword(Keyword::OCTETS) { + Some(CharLengthUnits::Octets) + } else { + None + }; + + Ok(CharacterLength { length, unit }) + } + pub fn parse_optional_precision_scale( &mut self, ) -> Result<(Option, Option), ParserError> { @@ -5337,7 +5366,9 @@ mod tests { #[cfg(test)] mod test_parse_data_type { - use crate::ast::{DataType, ExactNumberInfo, TimezoneInfo}; + use crate::ast::{ + CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, + }; use crate::dialect::{AnsiDialect, GenericDialect}; use crate::test_utils::TestedDialects; @@ -5360,21 +5391,124 @@ mod tests { test_parse_data_type!(dialect, "CHARACTER", DataType::Character(None)); - test_parse_data_type!(dialect, "CHARACTER(20)", DataType::Character(Some(20))); + test_parse_data_type!( + dialect, + "CHARACTER(20)", + DataType::Character(Some(CharacterLength { + length: 20, + unit: None + })) + ); + + test_parse_data_type!( + dialect, + "CHARACTER(20 CHARACTERS)", + DataType::Character(Some(CharacterLength { + length: 20, + unit: Some(CharLengthUnits::Characters) + })) + ); + + test_parse_data_type!( + dialect, + "CHARACTER(20 OCTETS)", + DataType::Character(Some(CharacterLength { + length: 20, + unit: Some(CharLengthUnits::Octets) + })) + ); test_parse_data_type!(dialect, "CHAR", DataType::Char(None)); - test_parse_data_type!(dialect, "CHAR(20)", DataType::Char(Some(20))); + test_parse_data_type!( + dialect, + "CHAR(20)", + DataType::Char(Some(CharacterLength { + length: 20, + unit: None + })) + ); + + test_parse_data_type!( + dialect, + "CHAR(20 CHARACTERS)", + DataType::Char(Some(CharacterLength { + length: 20, + unit: Some(CharLengthUnits::Characters) + })) + ); + + test_parse_data_type!( + dialect, + "CHAR(20 OCTETS)", + DataType::Char(Some(CharacterLength { + length: 20, + unit: Some(CharLengthUnits::Octets) + })) + ); test_parse_data_type!( dialect, "CHARACTER VARYING(20)", - DataType::CharacterVarying(Some(20)) + DataType::CharacterVarying(Some(CharacterLength { + length: 20, + unit: None + })) + ); + + test_parse_data_type!( + dialect, + "CHARACTER VARYING(20 CHARACTERS)", + DataType::CharacterVarying(Some(CharacterLength { + length: 20, + unit: Some(CharLengthUnits::Characters) + })) + ); + + test_parse_data_type!( + dialect, + "CHARACTER VARYING(20 OCTETS)", + DataType::CharacterVarying(Some(CharacterLength { + length: 20, + unit: Some(CharLengthUnits::Octets) + })) ); - test_parse_data_type!(dialect, "CHAR VARYING(20)", DataType::CharVarying(Some(20))); + test_parse_data_type!( + dialect, + "CHAR VARYING(20)", + DataType::CharVarying(Some(CharacterLength { + length: 20, + unit: None + })) + ); + + test_parse_data_type!( + dialect, + "CHAR VARYING(20 CHARACTERS)", + DataType::CharVarying(Some(CharacterLength { + length: 20, + unit: Some(CharLengthUnits::Characters) + })) + ); - test_parse_data_type!(dialect, "VARCHAR(20)", DataType::Varchar(Some(20))); + test_parse_data_type!( + dialect, + "CHAR VARYING(20 OCTETS)", + DataType::CharVarying(Some(CharacterLength { + length: 20, + unit: Some(CharLengthUnits::Octets) + })) + ); + + test_parse_data_type!( + dialect, + "VARCHAR(20)", + DataType::Varchar(Some(CharacterLength { + length: 20, + unit: None + })) + ); } #[test] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7654d677e..aa4013394 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1945,7 +1945,10 @@ fn parse_create_table() { vec![ ColumnDef { name: "name".into(), - data_type: DataType::Varchar(Some(100)), + data_type: DataType::Varchar(Some(CharacterLength { + length: 100, + unit: None + })), collation: None, options: vec![ColumnOptionDef { name: None, @@ -2401,7 +2404,10 @@ fn parse_create_external_table() { vec![ ColumnDef { name: "name".into(), - data_type: DataType::Varchar(Some(100)), + data_type: DataType::Varchar(Some(CharacterLength { + length: 100, + unit: None + })), collation: None, options: vec![ColumnOptionDef { name: None, @@ -2469,7 +2475,10 @@ fn parse_create_or_replace_external_table() { columns, vec![ColumnDef { name: "name".into(), - data_type: DataType::Varchar(Some(100)), + data_type: DataType::Varchar(Some(CharacterLength { + length: 100, + unit: None + })), collation: None, options: vec![ColumnOptionDef { name: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0ce605ece..93be8e4ad 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -74,7 +74,10 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "first_name".into(), - data_type: DataType::CharacterVarying(Some(45)), + data_type: DataType::CharacterVarying(Some(CharacterLength { + length: 45, + unit: None + })), collation: None, options: vec![ColumnOptionDef { name: None, @@ -83,7 +86,10 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "last_name".into(), - data_type: DataType::CharacterVarying(Some(45)), + data_type: DataType::CharacterVarying(Some(CharacterLength { + length: 45, + unit: None + })), collation: Some(ObjectName(vec![Ident::with_quote('"', "es_ES")])), options: vec![ColumnOptionDef { name: None, @@ -92,7 +98,10 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "email".into(), - data_type: DataType::CharacterVarying(Some(50)), + data_type: DataType::CharacterVarying(Some(CharacterLength { + length: 50, + unit: None + })), collation: None, options: vec![], }, From 427bec4cccd80a644c9687f57e73504a7be75f5e Mon Sep 17 00:00:00 2001 From: Mustafa akur <106137913+mustafasrepo@users.noreply.github.com> Date: Sat, 15 Oct 2022 13:34:52 +0300 Subject: [PATCH 063/806] Support for INTERVAL inside window frames (#655) * Add support to for INTERVAL inside window queries * Remove the unnecessary ancillary struct Interval * Convert Window Frame Bound to Expr * remove unnecessary changes * remove unnecessary changes Co-authored-by: Mehmet Ozan Kabak --- src/ast/mod.rs | 4 ++-- src/parser.rs | 6 ++++-- tests/sqlparser_common.rs | 6 +++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 019622433..d883bf3b3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -906,9 +906,9 @@ pub enum WindowFrameBound { /// `CURRENT ROW` CurrentRow, /// ` PRECEDING` or `UNBOUNDED PRECEDING` - Preceding(Option), + Preceding(Option>), /// ` FOLLOWING` or `UNBOUNDED FOLLOWING`. - Following(Option), + Following(Option>), } impl fmt::Display for WindowFrameBound { diff --git a/src/parser.rs b/src/parser.rs index 0c0bd9c5a..00b78f72c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -623,7 +623,6 @@ impl<'a> Parser<'a> { } else { None }; - Ok(Expr::Function(Function { name, args, @@ -685,7 +684,10 @@ impl<'a> Parser<'a> { let rows = if self.parse_keyword(Keyword::UNBOUNDED) { None } else { - Some(self.parse_literal_uint()?) + Some(Box::new(match self.peek_token() { + Token::SingleQuotedString(_) => self.parse_interval()?, + _ => self.parse_expr()?, + })) }; if self.parse_keyword(Keyword::PRECEDING) { Ok(WindowFrameBound::Preceding(rows)) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index aa4013394..b60f5f6a9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2940,13 +2940,17 @@ fn parse_window_functions() { ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), \ avg(bar) OVER (ORDER BY a \ RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING), \ + sum(bar) OVER (ORDER BY a \ + RANGE BETWEEN INTERVAL '1' DAY PRECEDING AND INTERVAL '1 MONTH' FOLLOWING), \ + COUNT(*) OVER (ORDER BY a \ + RANGE BETWEEN INTERVAL '1 DAY' PRECEDING AND INTERVAL '1 DAY' FOLLOWING), \ max(baz) OVER (ORDER BY a \ ROWS UNBOUNDED PRECEDING), \ sum(qux) OVER (ORDER BY a \ GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) \ FROM foo"; let select = verified_only_select(sql); - assert_eq!(5, select.projection.len()); + assert_eq!(7, select.projection.len()); assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), From a59874136d1fe22c45bd88c49aa106f420fab10a Mon Sep 17 00:00:00 2001 From: Francis Du Date: Sat, 15 Oct 2022 19:53:43 +0800 Subject: [PATCH 064/806] Support cache/uncache table syntax (#670) * feat: support cache/uncache table syntax * fix: support the full cache table syntax --- src/ast/mod.rs | 66 +++++ src/keywords.rs | 2 + src/parser.rs | 111 ++++++++ tests/sqlparser_common.rs | 559 +++++++++++++++++++++++++++----------- 4 files changed, 583 insertions(+), 155 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d883bf3b3..c39a77c64 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1430,6 +1430,25 @@ pub enum Statement { // Specifies the actions to perform when values match or do not match. clauses: Vec, }, + /// CACHE [ FLAG ] TABLE [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ ] + /// Based on Spark SQL,see + Cache { + // Table flag + table_flag: Option, + // Table name + table_name: ObjectName, + has_as: bool, + // Table confs + options: Vec, + // Cache table as a Query + query: Option, + }, + /// UNCACHE TABLE [ IF EXISTS ] + UNCache { + // Table name + table_name: ObjectName, + if_exists: bool, + }, } impl fmt::Display for Statement { @@ -2397,6 +2416,53 @@ impl fmt::Display for Statement { write!(f, "ON {} ", on)?; write!(f, "{}", display_separated(clauses, " ")) } + Statement::Cache { + table_name, + table_flag, + has_as, + options, + query, + } => { + if table_flag.is_some() { + write!( + f, + "CACHE {table_flag} TABLE {table_name}", + table_flag = table_flag.clone().unwrap(), + table_name = table_name, + )?; + } else { + write!(f, "CACHE TABLE {table_name}", table_name = table_name,)?; + } + + if !options.is_empty() { + write!(f, " OPTIONS({})", display_comma_separated(options))?; + } + + let has_query = query.is_some(); + if *has_as && has_query { + write!(f, " AS {query}", query = query.clone().unwrap()) + } else if !has_as && has_query { + write!(f, " {query}", query = query.clone().unwrap()) + } else if *has_as && !has_query { + write!(f, " AS") + } else { + Ok(()) + } + } + Statement::UNCache { + table_name, + if_exists, + } => { + if *if_exists { + write!( + f, + "UNCACHE TABLE IF EXISTS {table_name}", + table_name = table_name + ) + } else { + write!(f, "UNCACHE TABLE {table_name}", table_name = table_name) + } + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index 0b6b06b33..1c2c43854 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -383,6 +383,7 @@ define_keywords!( OPEN, OPERATOR, OPTION, + OPTIONS, OR, ORC, ORDER, @@ -554,6 +555,7 @@ define_keywords!( TYPE, UESCAPE, UNBOUNDED, + UNCACHE, UNCOMMITTED, UNION, UNIQUE, diff --git a/src/parser.rs b/src/parser.rs index 00b78f72c..8de7eec98 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -170,12 +170,14 @@ impl<'a> Parser<'a> { Keyword::TRUNCATE => Ok(self.parse_truncate()?), Keyword::MSCK => Ok(self.parse_msck()?), Keyword::CREATE => Ok(self.parse_create()?), + Keyword::CACHE => Ok(self.parse_cache_table()?), Keyword::DROP => Ok(self.parse_drop()?), Keyword::DISCARD => Ok(self.parse_discard()?), Keyword::DECLARE => Ok(self.parse_declare()?), Keyword::FETCH => Ok(self.parse_fetch_statement()?), Keyword::DELETE => Ok(self.parse_delete()?), Keyword::INSERT => Ok(self.parse_insert()?), + Keyword::UNCACHE => Ok(self.parse_uncache_table()?), Keyword::UPDATE => Ok(self.parse_update()?), Keyword::ALTER => Ok(self.parse_alter()?), Keyword::COPY => Ok(self.parse_copy()?), @@ -1907,6 +1909,115 @@ impl<'a> Parser<'a> { } } + /// Parse a CACHE TABLE statement + pub fn parse_cache_table(&mut self) -> Result { + let (mut table_flag, mut options, mut has_as, mut query) = (None, vec![], false, None); + if self.parse_keyword(Keyword::TABLE) { + let table_name = self.parse_object_name()?; + if self.peek_token() != Token::EOF { + if let Token::Word(word) = self.peek_token() { + if word.keyword == Keyword::OPTIONS { + options = self.parse_options(Keyword::OPTIONS)? + } + }; + + if self.peek_token() != Token::EOF { + let (a, q) = self.parse_as_query()?; + has_as = a; + query = Some(q); + } + + Ok(Statement::Cache { + table_flag, + table_name, + has_as, + options, + query, + }) + } else { + Ok(Statement::Cache { + table_flag, + table_name, + has_as, + options, + query, + }) + } + } else { + table_flag = Some(self.parse_object_name()?); + if self.parse_keyword(Keyword::TABLE) { + let table_name = self.parse_object_name()?; + if self.peek_token() != Token::EOF { + if let Token::Word(word) = self.peek_token() { + if word.keyword == Keyword::OPTIONS { + options = self.parse_options(Keyword::OPTIONS)? + } + }; + + if self.peek_token() != Token::EOF { + let (a, q) = self.parse_as_query()?; + has_as = a; + query = Some(q); + } + + Ok(Statement::Cache { + table_flag, + table_name, + has_as, + options, + query, + }) + } else { + Ok(Statement::Cache { + table_flag, + table_name, + has_as, + options, + query, + }) + } + } else { + if self.peek_token() == Token::EOF { + self.prev_token(); + } + self.expected("a `TABLE` keyword", self.peek_token()) + } + } + } + + /// Parse 'AS' before as query,such as `WITH XXX AS SELECT XXX` oer `CACHE TABLE AS SELECT XXX` + pub fn parse_as_query(&mut self) -> Result<(bool, Query), ParserError> { + match self.peek_token() { + Token::Word(word) => match word.keyword { + Keyword::AS => { + self.next_token(); + Ok((true, self.parse_query()?)) + } + _ => Ok((false, self.parse_query()?)), + }, + _ => self.expected("a QUERY statement", self.peek_token()), + } + } + + /// Parse a UNCACHE TABLE statement + pub fn parse_uncache_table(&mut self) -> Result { + let has_table = self.parse_keyword(Keyword::TABLE); + if has_table { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let table_name = self.parse_object_name()?; + if self.peek_token() == Token::EOF { + Ok(Statement::UNCache { + table_name, + if_exists, + }) + } else { + self.expected("an `EOF`", self.peek_token()) + } + } else { + self.expected("a `TABLE` keyword", self.peek_token()) + } + } + /// SQLite-specific `CREATE VIRTUAL TABLE` pub fn parse_create_virtual_table(&mut self) -> Result { self.expect_keyword(Keyword::TABLE)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b60f5f6a9..e34daed65 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -202,19 +202,19 @@ fn parse_update_with_table_alias() { name: ObjectName(vec![Ident::new("users")]), alias: Some(TableAlias { name: Ident::new("u"), - columns: vec![] + columns: vec![], }), args: None, with_hints: vec![], }, - joins: vec![] + joins: vec![], }, table ); assert_eq!( vec![Assignment { id: vec![Ident::new("u"), Ident::new("username")], - value: Expr::Value(Value::SingleQuotedString("new_user".to_string())) + value: Expr::Value(Value::SingleQuotedString("new_user".to_string())), }], assignments ); @@ -222,12 +222,12 @@ fn parse_update_with_table_alias() { Some(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("u"), - Ident::new("username") + Ident::new("username"), ])), op: BinaryOperator::Eq, right: Box::new(Expr::Value(Value::SingleQuotedString( "old_user".to_string() - ))) + ))), }), selection ); @@ -259,7 +259,7 @@ fn parse_delete_statement() { name: ObjectName(vec![Ident::with_quote('"', "table")]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], }, table_name ); @@ -284,7 +284,7 @@ fn parse_where_delete_statement() { name: ObjectName(vec![Ident::new("foo")]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], }, table_name, ); @@ -319,10 +319,10 @@ fn parse_where_delete_with_alias_statement() { name: ObjectName(vec![Ident::new("basket")]), alias: Some(TableAlias { name: Ident::new("a"), - columns: vec![] + columns: vec![], }), args: None, - with_hints: vec![] + with_hints: vec![], }, table_name, ); @@ -332,10 +332,10 @@ fn parse_where_delete_with_alias_statement() { name: ObjectName(vec![Ident::new("basket")]), alias: Some(TableAlias { name: Ident::new("b"), - columns: vec![] + columns: vec![], }), args: None, - with_hints: vec![] + with_hints: vec![], }), using ); @@ -343,12 +343,12 @@ fn parse_where_delete_with_alias_statement() { Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("a"), - Ident::new("id") + Ident::new("id"), ])), op: Lt, right: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("b"), - Ident::new("id") + Ident::new("id"), ])), }, selection.unwrap(), @@ -458,7 +458,7 @@ fn parse_select_into() { temporary: false, unlogged: false, table: false, - name: ObjectName(vec![Ident::new("table0")]) + name: ObjectName(vec![Ident::new("table0")]), }, only(&select.into) ); @@ -677,7 +677,7 @@ fn parse_select_with_date_column_name() { assert_eq!( &Expr::Identifier(Ident { value: "date".into(), - quote_style: None + quote_style: None, }), expr_from_projection(only(&select.projection)), ); @@ -695,7 +695,7 @@ fn parse_escaped_single_quote_string_predicate() { op: NotEq, right: Box::new(Expr::Value(Value::SingleQuotedString( "Jim's salary".to_string() - ))) + ))), }), ast.selection, ); @@ -727,8 +727,8 @@ fn parse_compound_expr_1() { right: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("b"))), op: Multiply, - right: Box::new(Identifier(Ident::new("c"))) - }) + right: Box::new(Identifier(Ident::new("c"))), + }), }, verified_expr(sql) ); @@ -744,10 +744,10 @@ fn parse_compound_expr_2() { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("a"))), op: Multiply, - right: Box::new(Identifier(Ident::new("b"))) + right: Box::new(Identifier(Ident::new("b"))), }), op: Plus, - right: Box::new(Identifier(Ident::new("c"))) + right: Box::new(Identifier(Ident::new("c"))), }, verified_expr(sql) ); @@ -800,11 +800,12 @@ fn parse_is_distinct_from() { assert_eq!( IsDistinctFrom( Box::new(Identifier(Ident::new("a"))), - Box::new(Identifier(Ident::new("b"))) + Box::new(Identifier(Ident::new("b"))), ), verified_expr(sql) ); } + #[test] fn parse_is_not_distinct_from() { use self::Expr::*; @@ -812,7 +813,7 @@ fn parse_is_not_distinct_from() { assert_eq!( IsNotDistinctFrom( Box::new(Identifier(Ident::new("a"))), - Box::new(Identifier(Ident::new("b"))) + Box::new(Identifier(Ident::new("b"))), ), verified_expr(sql) ); @@ -865,7 +866,7 @@ fn parse_not_precedence() { expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), negated: true, pattern: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), - escape_char: None + escape_char: None, }), }, ); @@ -898,7 +899,7 @@ fn parse_like() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, }, select.selection.unwrap() ); @@ -914,7 +915,7 @@ fn parse_like() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\') + escape_char: Some('\\'), }, select.selection.unwrap() ); @@ -931,7 +932,7 @@ fn parse_like() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, })), select.selection.unwrap() ); @@ -953,12 +954,12 @@ fn parse_null_like() { expr: Box::new(Expr::Identifier(Ident::new("column1"))), negated: false, pattern: Box::new(Expr::Value(Value::Null)), - escape_char: None + escape_char: None, }, alias: Ident { value: "col_null".to_owned(), - quote_style: None - } + quote_style: None, + }, }, select.projection[0] ); @@ -968,12 +969,12 @@ fn parse_null_like() { expr: Box::new(Expr::Value(Value::Null)), negated: false, pattern: Box::new(Expr::Identifier(Ident::new("column1"))), - escape_char: None + escape_char: None, }, alias: Ident { value: "null_col".to_owned(), - quote_style: None - } + quote_style: None, + }, }, select.projection[1] ); @@ -992,7 +993,7 @@ fn parse_ilike() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, }, select.selection.unwrap() ); @@ -1008,7 +1009,7 @@ fn parse_ilike() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('^') + escape_char: Some('^'), }, select.selection.unwrap() ); @@ -1025,7 +1026,7 @@ fn parse_ilike() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, })), select.selection.unwrap() ); @@ -1047,7 +1048,7 @@ fn parse_similar_to() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, }, select.selection.unwrap() ); @@ -1063,7 +1064,7 @@ fn parse_similar_to() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\') + escape_char: Some('\\'), }, select.selection.unwrap() ); @@ -1079,7 +1080,7 @@ fn parse_similar_to() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\') + escape_char: Some('\\'), })), select.selection.unwrap() ); @@ -1336,14 +1337,14 @@ fn parse_tuples() { vec![ SelectItem::UnnamedExpr(Expr::Tuple(vec![ Expr::Value(number("1")), - Expr::Value(number("2")) + Expr::Value(number("2")), ])), SelectItem::UnnamedExpr(Expr::Nested(Box::new(Expr::Value(number("1"))))), SelectItem::UnnamedExpr(Expr::Tuple(vec![ Expr::Value(Value::SingleQuotedString("foo".into())), Expr::Value(number("3")), - Expr::Identifier(Ident::new("baz")) - ])) + Expr::Identifier(Ident::new("baz")), + ])), ], select.projection ); @@ -1477,7 +1478,7 @@ fn parse_select_group_by_grouping_sets() { vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], vec![], - ]) + ]), ], select.group_by ); @@ -1496,7 +1497,7 @@ fn parse_select_group_by_rollup() { Expr::Rollup(vec![ vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], - ]) + ]), ], select.group_by ); @@ -1515,7 +1516,7 @@ fn parse_select_group_by_cube() { Expr::Cube(vec![ vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], - ]) + ]), ], select.group_by ); @@ -1535,7 +1536,7 @@ fn parse_select_having() { special: false, })), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(number("1"))) + right: Box::new(Expr::Value(number("1"))), }), select.having ); @@ -1560,15 +1561,15 @@ fn parse_select_qualify() { order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), asc: None, - nulls_first: None + nulls_first: None, }], - window_frame: None + window_frame: None, }), distinct: false, - special: false + special: false, })), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))) + right: Box::new(Expr::Value(number("1"))), }), select.qualify ); @@ -1579,7 +1580,7 @@ fn parse_select_qualify() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("row_num"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))) + right: Box::new(Expr::Value(number("1"))), }), select.qualify ); @@ -1600,7 +1601,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::BigInt(None) + data_type: DataType::BigInt(None), }, expr_from_projection(only(&select.projection)) ); @@ -1610,7 +1611,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::TinyInt(None) + data_type: DataType::TinyInt(None), }, expr_from_projection(only(&select.projection)) ); @@ -1642,7 +1643,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Nvarchar(Some(50)) + data_type: DataType::Nvarchar(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1652,7 +1653,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Clob(None) + data_type: DataType::Clob(None), }, expr_from_projection(only(&select.projection)) ); @@ -1662,7 +1663,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Clob(Some(50)) + data_type: DataType::Clob(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1672,7 +1673,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Binary(Some(50)) + data_type: DataType::Binary(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1682,7 +1683,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Varbinary(Some(50)) + data_type: DataType::Varbinary(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1692,7 +1693,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Blob(None) + data_type: DataType::Blob(None), }, expr_from_projection(only(&select.projection)) ); @@ -1702,7 +1703,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Blob(Some(50)) + data_type: DataType::Blob(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1715,7 +1716,7 @@ fn parse_try_cast() { assert_eq!( &Expr::TryCast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::BigInt(None) + data_type: DataType::BigInt(None), }, expr_from_projection(only(&select.projection)) ); @@ -1893,7 +1894,7 @@ fn parse_listagg() { ", ".to_string() )))), on_overflow, - within_group + within_group, }), expr_from_projection(only(&select.projection)) ); @@ -1947,12 +1948,12 @@ fn parse_create_table() { name: "name".into(), data_type: DataType::Varchar(Some(CharacterLength { length: 100, - unit: None + unit: None, })), collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::NotNull + option: ColumnOption::NotNull, }], }, ColumnDef { @@ -1961,7 +1962,7 @@ fn parse_create_table() { collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Null + option: ColumnOption::Null, }], }, ColumnDef { @@ -1977,15 +1978,15 @@ fn parse_create_table() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Null + option: ColumnOption::Null, }, ColumnOptionDef { name: Some("pkey".into()), - option: ColumnOption::Unique { is_primary: true } + option: ColumnOption::Unique { is_primary: true }, }, ColumnOptionDef { name: None, - option: ColumnOption::NotNull + option: ColumnOption::NotNull, }, ColumnOptionDef { name: None, @@ -1994,7 +1995,7 @@ fn parse_create_table() { ColumnOptionDef { name: None, option: ColumnOption::Check(verified_expr("constrained > 0")), - } + }, ], }, ColumnDef { @@ -2005,11 +2006,11 @@ fn parse_create_table() { name: None, option: ColumnOption::ForeignKey { foreign_table: ObjectName(vec!["othertable".into()]), - referred_columns: vec!["a".into(), "b".into(),], + referred_columns: vec!["a".into(), "b".into()], on_delete: None, on_update: None, - } - }] + }, + }], }, ColumnDef { name: "ref2".into(), @@ -2022,9 +2023,9 @@ fn parse_create_table() { referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::NoAction), - } - },] - } + }, + },], + }, ] ); assert_eq!( @@ -2036,7 +2037,7 @@ fn parse_create_table() { foreign_table: ObjectName(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), - on_update: None + on_update: None, }, TableConstraint::ForeignKey { name: Some("fkey2".into()), @@ -2044,7 +2045,7 @@ fn parse_create_table() { foreign_table: ObjectName(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), - on_update: Some(ReferentialAction::Restrict) + on_update: Some(ReferentialAction::Restrict), }, TableConstraint::ForeignKey { name: None, @@ -2052,7 +2053,7 @@ fn parse_create_table() { foreign_table: ObjectName(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), - on_update: Some(ReferentialAction::SetDefault) + on_update: Some(ReferentialAction::SetDefault), }, TableConstraint::ForeignKey { name: None, @@ -2060,7 +2061,7 @@ fn parse_create_table() { foreign_table: ObjectName(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], on_delete: None, - on_update: Some(ReferentialAction::SetNull) + on_update: Some(ReferentialAction::SetNull), }, ] ); @@ -2339,11 +2340,11 @@ fn parse_create_table_with_options() { vec![ SqlOption { name: "foo".into(), - value: Value::SingleQuotedString("bar".into()) + value: Value::SingleQuotedString("bar".into()), }, SqlOption { name: "a".into(), - value: number("123") + value: number("123"), }, ], with_options @@ -2406,12 +2407,12 @@ fn parse_create_external_table() { name: "name".into(), data_type: DataType::Varchar(Some(CharacterLength { length: 100, - unit: None + unit: None, })), collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::NotNull + option: ColumnOption::NotNull, }], }, ColumnDef { @@ -2420,7 +2421,7 @@ fn parse_create_external_table() { collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Null + option: ColumnOption::Null, }], }, ColumnDef { @@ -2477,12 +2478,12 @@ fn parse_create_or_replace_external_table() { name: "name".into(), data_type: DataType::Varchar(Some(CharacterLength { length: 100, - unit: None + unit: None, })), collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::NotNull + option: ColumnOption::NotNull, }], },] ); @@ -3024,7 +3025,7 @@ fn parse_literal_date() { assert_eq!( &Expr::TypedString { data_type: DataType::Date, - value: "1999-01-01".into() + value: "1999-01-01".into(), }, expr_from_projection(only(&select.projection)), ); @@ -3037,7 +3038,7 @@ fn parse_literal_time() { assert_eq!( &Expr::TypedString { data_type: DataType::Time(TimezoneInfo::None), - value: "01:23:34".into() + value: "01:23:34".into(), }, expr_from_projection(only(&select.projection)), ); @@ -3050,7 +3051,7 @@ fn parse_literal_datetime() { assert_eq!( &Expr::TypedString { data_type: DataType::Datetime, - value: "1999-01-01 01:23:34.45".into() + value: "1999-01-01 01:23:34.45".into(), }, expr_from_projection(only(&select.projection)), ); @@ -3063,7 +3064,7 @@ fn parse_literal_timestamp_without_time_zone() { assert_eq!( &Expr::TypedString { data_type: DataType::Timestamp(TimezoneInfo::None), - value: "1999-01-01 01:23:34".into() + value: "1999-01-01 01:23:34".into(), }, expr_from_projection(only(&select.projection)), ); @@ -3078,7 +3079,7 @@ fn parse_literal_timestamp_with_time_zone() { assert_eq!( &Expr::TypedString { data_type: DataType::Timestamp(TimezoneInfo::Tz), - value: "1999-01-01 01:23:34Z".into() + value: "1999-01-01 01:23:34Z".into(), }, expr_from_projection(only(&select.projection)), ); @@ -3162,7 +3163,7 @@ fn parse_interval() { value: Box::new(Expr::BinaryOp { left: Box::new(Expr::Value(number("1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))) + right: Box::new(Expr::Value(number("1"))), }), leading_field: Some(DateTimeField::Day), leading_precision: None, @@ -3243,14 +3244,14 @@ fn parse_at_timezone() { timestamp: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident { value: "FROM_UNIXTIME".to_string(), - quote_style: None + quote_style: None, }]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero.clone()))], over: None, distinct: false, special: false, })), - time_zone: "UTC-06:00".to_string() + time_zone: "UTC-06:00".to_string(), }, expr_from_projection(only(&select.projection)), ); @@ -3271,24 +3272,24 @@ fn parse_at_timezone() { value: "FROM_UNIXTIME".to_string(), quote_style: None, },],), - args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero,),),], + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero))], over: None, distinct: false, - special: false + special: false, },)), time_zone: "UTC-06:00".to_string(), },),), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("%Y-%m-%dT%H".to_string(),), + Value::SingleQuotedString("%Y-%m-%dT%H".to_string()), ),),), ], over: None, distinct: false, - special: false + special: false, },), alias: Ident { value: "hour".to_string(), - quote_style: Some('"',), + quote_style: Some('"'), }, }, only(&select.projection), @@ -3483,7 +3484,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::CompoundIdentifier(vec![ Ident::with_quote('"', "alias"), - Ident::with_quote('"', "bar baz") + Ident::with_quote('"', "bar baz"), ]), expr_from_projection(&select.projection[0]), ); @@ -3520,14 +3521,14 @@ fn parse_parens() { left: Box::new(Nested(Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("a"))), op: Plus, - right: Box::new(Identifier(Ident::new("b"))) + right: Box::new(Identifier(Ident::new("b"))), }))), op: Minus, right: Box::new(Nested(Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("c"))), op: Plus, - right: Box::new(Identifier(Ident::new("d"))) - }))) + right: Box::new(Identifier(Ident::new("d"))), + }))), }, verified_expr(sql) ); @@ -3547,22 +3548,22 @@ fn parse_searched_case_expr() { BinaryOp { left: Box::new(Identifier(Ident::new("bar"))), op: Eq, - right: Box::new(Expr::Value(number("0"))) + right: Box::new(Expr::Value(number("0"))), }, BinaryOp { left: Box::new(Identifier(Ident::new("bar"))), op: GtEq, - right: Box::new(Expr::Value(number("0"))) - } + right: Box::new(Expr::Value(number("0"))), + }, ], results: vec![ Expr::Value(Value::SingleQuotedString("null".to_string())), Expr::Value(Value::SingleQuotedString("=0".to_string())), - Expr::Value(Value::SingleQuotedString(">=0".to_string())) + Expr::Value(Value::SingleQuotedString(">=0".to_string())), ], else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( "<0".to_string() - )))) + )))), }, expr_from_projection(only(&select.projection)), ); @@ -3578,10 +3579,10 @@ fn parse_simple_case_expr() { &Case { operand: Some(Box::new(Identifier(Ident::new("foo")))), conditions: vec![Expr::Value(number("1"))], - results: vec![Expr::Value(Value::SingleQuotedString("Y".to_string())),], + results: vec![Expr::Value(Value::SingleQuotedString("Y".to_string()))], else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( "N".to_string() - )))) + )))), }, expr_from_projection(only(&select.projection)), ); @@ -3622,7 +3623,7 @@ fn parse_implicit_join() { with_hints: vec![], }, joins: vec![], - } + }, ], select.from, ); @@ -3646,7 +3647,7 @@ fn parse_implicit_join() { with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), - }] + }], }, TableWithJoins { relation: TableFactor::Table { @@ -3663,8 +3664,8 @@ fn parse_implicit_join() { with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), - }] - } + }], + }, ], select.from, ); @@ -3682,7 +3683,7 @@ fn parse_cross_join() { args: None, with_hints: vec![], }, - join_operator: JoinOperator::CrossJoin + join_operator: JoinOperator::CrossJoin, }, only(only(select.from).joins), ); @@ -3715,7 +3716,7 @@ fn parse_joins_on() { vec![join_with_constraint( "t2", table_alias("foo"), - JoinOperator::Inner + JoinOperator::Inner, )] ); one_statement_parses_to( @@ -3764,7 +3765,7 @@ fn parse_joins_using() { vec![join_with_constraint( "t2", table_alias("foo"), - JoinOperator::Inner + JoinOperator::Inner, )] ); one_statement_parses_to( @@ -3854,7 +3855,7 @@ fn parse_join_nesting() { only(&verified_only_select(sql).from).joins, vec![ join(nest!(table("b"), nest!(table("c"), table("d"), table("e")))), - join(nest!(table("f"), nest!(table("g"), table("h")))) + join(nest!(table("f"), nest!(table("g"), table("h")))), ], ); @@ -3889,7 +3890,7 @@ fn parse_join_nesting() { relation: table("a"), joins: vec![join(table("b"))], }), - alias: table_alias("c") + alias: table_alias("c"), } ); assert_eq!(from.joins, vec![]); @@ -4058,7 +4059,7 @@ fn parse_derived_tables() { alias: Some(TableAlias { name: "t1".into(), columns: vec![], - }) + }), }, joins: vec![Join { relation: TableFactor::Table { @@ -4070,7 +4071,7 @@ fn parse_derived_tables() { join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], }), - alias: None + alias: None, } ); } @@ -4196,7 +4197,7 @@ fn parse_overlay() { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Plus, right: Box::new(Expr::Value(number("1"))), - })) + })), }, expr_from_projection(only(&select.projection)) ); @@ -4243,7 +4244,7 @@ fn parse_exists_subquery() { assert_eq!( Expr::Exists { negated: false, - subquery: Box::new(expected_inner.clone()) + subquery: Box::new(expected_inner.clone()), }, select.selection.unwrap(), ); @@ -4253,7 +4254,7 @@ fn parse_exists_subquery() { assert_eq!( Expr::Exists { negated: true, - subquery: Box::new(expected_inner) + subquery: Box::new(expected_inner), }, select.selection.unwrap(), ); @@ -4348,11 +4349,11 @@ fn parse_create_view_with_options() { vec![ SqlOption { name: "foo".into(), - value: Value::SingleQuotedString("bar".into()) + value: Value::SingleQuotedString("bar".into()), }, SqlOption { name: "a".into(), - value: number("123") + value: number("123"), }, ], with_options @@ -4384,6 +4385,7 @@ fn parse_create_view_with_columns() { _ => unreachable!(), } } + #[test] fn parse_create_or_replace_view() { let sql = "CREATE OR REPLACE VIEW v AS SELECT 1"; @@ -4989,6 +4991,7 @@ fn parse_create_index() { _ => unreachable!(), } } + #[test] fn parse_drop_index() { let sql = "DROP INDEX idx_a"; @@ -5084,12 +5087,12 @@ fn parse_grant() { columns: Some(vec![ Ident { value: "shape".into(), - quote_style: None + quote_style: None, }, Ident { value: "size".into(), - quote_style: None - } + quote_style: None, + }, ]) }, Action::Usage, @@ -5263,10 +5266,10 @@ fn parse_merge() { name: ObjectName(vec![Ident::new("s"), Ident::new("bar")]), alias: Some(TableAlias { name: Ident::new("dest"), - columns: vec![] + columns: vec![], }), args: None, - with_hints: vec![] + with_hints: vec![], } ); assert_eq!(table, table_no_into); @@ -5287,9 +5290,9 @@ fn parse_merge() { name: ObjectName(vec![Ident::new("s"), Ident::new("foo")]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], }, - joins: vec![] + joins: vec![], }], lateral_views: vec![], selection: None, @@ -5304,15 +5307,15 @@ fn parse_merge() { limit: None, offset: None, fetch: None, - lock: None + lock: None, }), alias: Some(TableAlias { name: Ident { value: "stg".to_string(), - quote_style: None + quote_style: None, }, - columns: vec![] - }) + columns: vec![], + }), } ); assert_eq!(source, source_no_into); @@ -5323,26 +5326,26 @@ fn parse_merge() { left: Box::new(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("dest"), - Ident::new("D") + Ident::new("D"), ])), op: BinaryOperator::Eq, right: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("stg"), - Ident::new("D") - ])) + Ident::new("D"), + ])), }), op: BinaryOperator::And, right: Box::new(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("dest"), - Ident::new("E") + Ident::new("E"), ])), op: BinaryOperator::Eq, right: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("stg"), - Ident::new("E") - ])) - }) + Ident::new("E"), + ])), + }), }) ); assert_eq!(on, on_no_into); @@ -5357,37 +5360,37 @@ fn parse_merge() { Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("A")]), Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("B")]), Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("C")]), - ]]) + ]]), }, MergeClause::MatchedUpdate { predicate: Some(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("dest"), - Ident::new("A") + Ident::new("A"), ])), op: BinaryOperator::Eq, right: Box::new(Expr::Value(Value::SingleQuotedString( "a".to_string() - ))) + ))), }), assignments: vec![ Assignment { id: vec![Ident::new("dest"), Ident::new("F")], value: Expr::CompoundIdentifier(vec![ Ident::new("stg"), - Ident::new("F") - ]) + Ident::new("F"), + ]), }, Assignment { id: vec![Ident::new("dest"), Ident::new("G")], value: Expr::CompoundIdentifier(vec![ Ident::new("stg"), - Ident::new("G") - ]) - } - ] + Ident::new("G"), + ]), + }, + ], }, - MergeClause::MatchedDelete(None) + MergeClause::MatchedDelete(None), ] ); assert_eq!(clauses, clauses_no_into); @@ -5443,7 +5446,7 @@ fn test_placeholder() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Placeholder("?".into()))) + right: Box::new(Expr::Value(Value::Placeholder("?".into()))), }) ); @@ -5466,7 +5469,7 @@ fn test_placeholder() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Placeholder("$Id1".into()))) + right: Box::new(Expr::Value(Value::Placeholder("$Id1".into()))), }) ); @@ -5814,3 +5817,249 @@ fn parse_show_functions() { } ); } + +#[test] +fn parse_cache_table() { + let sql = "SELECT a, b, c FROM foo"; + let cache_table_name = "cache_table_name"; + let table_flag = "flag"; + let query = all_dialects().verified_query(sql); + + assert_eq!( + verified_stmt( + format!("CACHE TABLE '{table_name}'", table_name = cache_table_name).as_str() + ), + Statement::Cache { + table_flag: None, + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![], + query: None, + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}'", + flag = table_flag, + table_name = cache_table_name + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![], + query: None, + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88)", + flag = table_flag, + table_name = cache_table_name, + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![ + SqlOption { + name: Ident::with_quote('\'', "K1"), + value: Value::SingleQuotedString("V1".into()), + }, + SqlOption { + name: Ident::with_quote('\'', "K2"), + value: number("0.88"), + }, + ], + query: None, + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) {sql}", + flag = table_flag, + table_name = cache_table_name, + sql = sql, + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![ + SqlOption { + name: Ident::with_quote('\'', "K1"), + value: Value::SingleQuotedString("V1".into()), + }, + SqlOption { + name: Ident::with_quote('\'', "K2"), + value: number("0.88"), + }, + ], + query: Some(query.clone()), + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) AS {sql}", + flag = table_flag, + table_name = cache_table_name, + sql = sql, + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: true, + options: vec![ + SqlOption { + name: Ident::with_quote('\'', "K1"), + value: Value::SingleQuotedString("V1".into()), + }, + SqlOption { + name: Ident::with_quote('\'', "K2"), + value: number("0.88"), + }, + ], + query: Some(query.clone()), + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' {sql}", + flag = table_flag, + table_name = cache_table_name, + sql = sql + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![], + query: Some(query.clone()), + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' AS {sql}", + flag = table_flag, + table_name = cache_table_name + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: true, + options: vec![], + query: Some(query), + } + ); + + let res = parse_sql_statements("CACHE TABLE 'table_name' foo"); + assert_eq!( + ParserError::ParserError( + "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + ), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE flag TABLE 'table_name' OPTIONS('K1'='V1') foo"); + assert_eq!( + ParserError::ParserError( + "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + ), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE TABLE 'table_name' AS foo"); + assert_eq!( + ParserError::ParserError( + "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + ), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE flag TABLE 'table_name' OPTIONS('K1'='V1') AS foo"); + assert_eq!( + ParserError::ParserError( + "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + ), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE 'table_name'"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE 'table_name' OPTIONS('K1'='V1')"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: OPTIONS".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE flag 'table_name' OPTIONS('K1'='V1')"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + res.unwrap_err() + ); +} + +#[test] +fn parse_uncache_table() { + assert_eq!( + verified_stmt("UNCACHE TABLE 'table_name'"), + Statement::UNCache { + table_name: ObjectName(vec![Ident::with_quote('\'', "table_name")]), + if_exists: false, + } + ); + + assert_eq!( + verified_stmt("UNCACHE TABLE IF EXISTS 'table_name'"), + Statement::UNCache { + table_name: ObjectName(vec![Ident::with_quote('\'', "table_name")]), + if_exists: true, + } + ); + + let res = parse_sql_statements("UNCACHE TABLE 'table_name' foo"); + assert_eq!( + ParserError::ParserError("Expected an `EOF`, found: foo".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("UNCACHE 'table_name' foo"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("UNCACHE IF EXISTS 'table_name' foo"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: IF".to_string()), + res.unwrap_err() + ); +} From b42632fa0d0d79abd287db24d303c7b1782d06dc Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Sat, 15 Oct 2022 08:55:08 -0300 Subject: [PATCH 065/806] Support for ANSI `CHARACTER LARGE OBJECT[(p)]` and `CHAR LARGE OBJECT[(p)]`. (#671) Add tests for both and `CLOB[(p)]`. --- src/ast/data_type.rs | 16 +++++++++++++++- src/parser.rs | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index baa23acf2..58c1305ac 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -39,7 +39,15 @@ pub enum DataType { Nvarchar(Option), /// Uuid type Uuid, - /// Large character object with optional length e.g. CLOB, CLOB(1000), [standard], [Oracle] + /// Large character object with optional length e.g. CHARACTER LARGE OBJECT, CHARACTER LARGE OBJECT(1000), [standard] + /// + /// [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] + /// + /// [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] /// /// [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 @@ -145,6 +153,12 @@ impl fmt::Display for DataType { format_type_with_optional_length(f, "NVARCHAR", size, false) } DataType::Uuid => write!(f, "UUID"), + DataType::CharacterLargeObject(size) => { + format_type_with_optional_length(f, "CHARACTER LARGE OBJECT", size, false) + } + DataType::CharLargeObject(size) => { + format_type_with_optional_length(f, "CHAR LARGE OBJECT", size, false) + } DataType::Clob(size) => format_type_with_optional_length(f, "CLOB", size, false), DataType::Binary(size) => format_type_with_optional_length(f, "BINARY", size, false), DataType::Varbinary(size) => { diff --git a/src/parser.rs b/src/parser.rs index 8de7eec98..787a0c638 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3546,6 +3546,10 @@ impl<'a> Parser<'a> { Ok(DataType::CharacterVarying( self.parse_optional_character_length()?, )) + } else if self.parse_keywords(&[Keyword::LARGE, Keyword::OBJECT]) { + Ok(DataType::CharacterLargeObject( + self.parse_optional_precision()?, + )) } else { Ok(DataType::Character(self.parse_optional_character_length()?)) } @@ -3555,6 +3559,8 @@ impl<'a> Parser<'a> { Ok(DataType::CharVarying( self.parse_optional_character_length()?, )) + } else if self.parse_keywords(&[Keyword::LARGE, Keyword::OBJECT]) { + Ok(DataType::CharLargeObject(self.parse_optional_precision()?)) } else { Ok(DataType::Char(self.parse_optional_character_length()?)) } @@ -5624,6 +5630,39 @@ mod tests { ); } + #[test] + fn test_ansii_character_large_object_types() { + // Character large object types: + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + }; + + test_parse_data_type!( + dialect, + "CHARACTER LARGE OBJECT", + DataType::CharacterLargeObject(None) + ); + test_parse_data_type!( + dialect, + "CHARACTER LARGE OBJECT(20)", + DataType::CharacterLargeObject(Some(20)) + ); + + test_parse_data_type!( + dialect, + "CHAR LARGE OBJECT", + DataType::CharLargeObject(None) + ); + test_parse_data_type!( + dialect, + "CHAR LARGE OBJECT(20)", + DataType::CharLargeObject(Some(20)) + ); + + test_parse_data_type!(dialect, "CLOB", DataType::Clob(None)); + test_parse_data_type!(dialect, "CLOB(20)", DataType::Clob(Some(20))); + } + #[test] fn test_ansii_exact_numeric_types() { // Exact numeric types: From b32cbbd855ab446d1503ddd436ddf2df1cf43ae9 Mon Sep 17 00:00:00 2001 From: sam <2740878+sam-mmm@users.noreply.github.com> Date: Sat, 15 Oct 2022 17:34:19 +0530 Subject: [PATCH 066/806] Support drop sequence statement (#673) * Add ObjectType Sequence * Drop sequence test cases added. * Parser and Drop statement Display updated. * Parser and Drop statement Display updated. * Fix compile errors * add new test case --- src/ast/mod.rs | 9 ++++++++- src/parser.rs | 5 ++++- tests/sqlparser_common.rs | 2 ++ tests/sqlparser_postgres.rs | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c39a77c64..ea6925535 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1184,6 +1184,9 @@ pub enum Statement { /// Whether `CASCADE` was specified. This will be `false` when /// `RESTRICT` or no drop behavior at all was specified. cascade: bool, + /// Whether `RESTRICT` was specified. This will be `false` when + /// `CASCADE` or no drop behavior at all was specified. + restrict: bool, /// Hive allows you specify whether the table's stored data will be /// deleted along with the dropped table purge: bool, @@ -2143,14 +2146,16 @@ impl fmt::Display for Statement { if_exists, names, cascade, + restrict, purge, } => write!( f, - "DROP {}{} {}{}{}", + "DROP {}{} {}{}{}{}", object_type, if *if_exists { " IF EXISTS" } else { "" }, display_comma_separated(names), if *cascade { " CASCADE" } else { "" }, + if *restrict { " RESTRICT" } else { "" }, if *purge { " PURGE" } else { "" } ), Statement::Discard { object_type } => { @@ -2910,6 +2915,7 @@ pub enum ObjectType { Index, Schema, Role, + Sequence, } impl fmt::Display for ObjectType { @@ -2920,6 +2926,7 @@ impl fmt::Display for ObjectType { ObjectType::Index => "INDEX", ObjectType::Schema => "SCHEMA", ObjectType::Role => "ROLE", + ObjectType::Sequence => "SEQUENCE", }) } } diff --git a/src/parser.rs b/src/parser.rs index 787a0c638..045f7fa23 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2441,9 +2441,11 @@ impl<'a> Parser<'a> { ObjectType::Role } else if self.parse_keyword(Keyword::SCHEMA) { ObjectType::Schema + } else if self.parse_keyword(Keyword::SEQUENCE) { + ObjectType::Sequence } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, or SCHEMA after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, or SEQUENCE after DROP", self.peek_token(), ); }; @@ -2465,6 +2467,7 @@ impl<'a> Parser<'a> { if_exists, names, cascade, + restrict, purge, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e34daed65..9e5a5cdd1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4469,6 +4469,7 @@ fn parse_drop_table() { names, cascade, purge: _, + .. } => { assert!(!if_exists); assert_eq!(ObjectType::Table, object_type); @@ -4489,6 +4490,7 @@ fn parse_drop_table() { names, cascade, purge: _, + .. } => { assert!(if_exists); assert_eq!(ObjectType::Table, object_type); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 93be8e4ad..7b6828ae4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -22,6 +22,25 @@ use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; +#[test] +fn parse_drop_sequence() { + // SimpleLogger::new().init().unwrap(); + let sql1 = "DROP SEQUENCE IF EXISTS name0 CASCADE"; + pg().one_statement_parses_to(sql1, "DROP SEQUENCE IF EXISTS name0 CASCADE"); + let sql2 = "DROP SEQUENCE IF EXISTS name1 RESTRICT"; + pg().one_statement_parses_to(sql2, "DROP SEQUENCE IF EXISTS name1 RESTRICT"); + let sql3 = "DROP SEQUENCE name2 CASCADE"; + pg().one_statement_parses_to(sql3, "DROP SEQUENCE name2 CASCADE"); + let sql4 = "DROP SEQUENCE name2"; + pg().one_statement_parses_to(sql4, "DROP SEQUENCE name2"); + let sql5 = "DROP SEQUENCE name0 CASCADE"; + pg().one_statement_parses_to(sql5, "DROP SEQUENCE name0 CASCADE"); + let sql6 = "DROP SEQUENCE name1 RESTRICT"; + pg().one_statement_parses_to(sql6, "DROP SEQUENCE name1 RESTRICT"); + let sql7 = "DROP SEQUENCE name1, name2, name3"; + pg().one_statement_parses_to(sql7, "DROP SEQUENCE name1, name2, name3"); +} + #[test] fn parse_create_table_with_defaults() { let sql = "CREATE TABLE public.customer ( From e3c936a6ceca038fd9f21cefc5bdf982ed2a0494 Mon Sep 17 00:00:00 2001 From: sam <2740878+sam-mmm@users.noreply.github.com> Date: Thu, 20 Oct 2022 02:51:17 +0530 Subject: [PATCH 067/806] mod, parser and test cases for CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] (#678) --- src/ast/mod.rs | 20 ++++++++++++++++++++ src/parser.rs | 16 ++++++++++++++++ tests/sqlparser_postgres.rs | 17 +++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ea6925535..49e6fb01e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1452,6 +1452,13 @@ pub enum Statement { table_name: ObjectName, if_exists: bool, }, + ///CreateSequence -- define a new sequence + /// CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] + CreateSequence { + temporary: bool, + if_not_exists: bool, + name: ObjectName, + }, } impl fmt::Display for Statement { @@ -2468,6 +2475,19 @@ impl fmt::Display for Statement { write!(f, "UNCACHE TABLE {table_name}", table_name = table_name) } } + Statement::CreateSequence { + temporary, + if_not_exists, + name, + } => { + write!( + f, + "CREATE {temporary}SEQUENCE {if_not_exists}{name}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + temporary = if *temporary { "TEMPORARY " } else { "" }, + name = name + ) + } } } } diff --git a/src/parser.rs b/src/parser.rs index 045f7fa23..5dfb9f10c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1904,6 +1904,8 @@ impl<'a> Parser<'a> { self.parse_create_function(temporary) } else if self.parse_keyword(Keyword::ROLE) { self.parse_create_role() + } else if self.parse_keyword(Keyword::SEQUENCE) { + self.parse_create_sequence(temporary) } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -5414,6 +5416,20 @@ impl<'a> Parser<'a> { clauses, }) } + + /// https://www.postgresql.org/docs/current/sql-createsequence.html + /// CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] + pub fn parse_create_sequence(&mut self, temporary: bool) -> Result { + //[ IF NOT EXISTS ] + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + //name + let name = self.parse_object_name()?; + Ok(Statement::CreateSequence { + temporary, + if_not_exists, + name, + }) + } } impl Word { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7b6828ae4..c5d4bd0fc 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -22,6 +22,23 @@ use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; +#[test] +fn parse_create_sequence() { + // SimpleLogger::new().init().unwrap(); + + let sql1 = "CREATE SEQUENCE name0"; + pg().one_statement_parses_to(sql1, "CREATE SEQUENCE name0"); + + let sql2 = "CREATE SEQUENCE IF NOT EXISTS name0"; + pg().one_statement_parses_to(sql2, "CREATE SEQUENCE IF NOT EXISTS name0"); + + let sql3 = "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name0"; + pg().one_statement_parses_to(sql3, "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name0"); + + let sql4 = "CREATE TEMPORARY SEQUENCE name0"; + pg().one_statement_parses_to(sql4, "CREATE TEMPORARY SEQUENCE name0"); +} + #[test] fn parse_drop_sequence() { // SimpleLogger::new().init().unwrap(); From 2aba3f8c91287cb5505e00a550a361c585ad1b13 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:24:38 -0300 Subject: [PATCH 068/806] Adding MySQL table option {INDEX | KEY} to the CREATE TABLE definiton (partial). (#665) Theoretically the behavior should be the same as CREATE INDEX, but we cannot make that assumption, so the parse is (almost) identical as the input. Breaking changes: - Now HASH and BTREE are KEYWORDS, and using them as names can result in errors. - Now 'KEY' and 'INDEX' column names start the parsing of a table constraint if unquoted for the Generic dialect. This results in possible conficts if canonical results are compared for all dialects if a column is named 'key' without quotes. --- src/ast/ddl.rs | 60 +++++++++++++++++ src/ast/mod.rs | 2 +- src/keywords.rs | 2 + src/parser.rs | 131 ++++++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 9 +-- tests/sqlparser_mysql.rs | 43 +++++++++++++ 6 files changed, 242 insertions(+), 5 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1847f2518..cae0f597b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -247,6 +247,24 @@ pub enum TableConstraint { name: Option, expr: Box, }, + /// 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. + /// + /// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html + Index { + /// Whether this index starts with KEY (true) or INDEX (false), to maintain the same syntax. + display_as_key: bool, + /// Index name. + name: Option, + /// Optional [index type][1]. + /// + /// [1]: IndexType + index_type: Option, + /// Referred column identifier list. + columns: Vec, + }, } impl fmt::Display for TableConstraint { @@ -290,6 +308,48 @@ impl fmt::Display for TableConstraint { TableConstraint::Check { name, expr } => { write!(f, "{}CHECK ({})", display_constraint_name(name), expr) } + TableConstraint::Index { + display_as_key, + name, + index_type, + columns, + } => { + write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" })?; + if let Some(name) = name { + write!(f, " {}", name)?; + } + if let Some(index_type) = index_type { + write!(f, " USING {}", index_type)?; + } + write!(f, " ({})", display_comma_separated(columns))?; + + Ok(()) + } + } + } +} + +/// Indexing method used by that index. +/// +/// This structure isn't present on ANSI, but is found at least in [MySQL CREATE TABLE][1], +/// [MySQL CREATE INDEX][2], and [Postgresql CREATE INDEX][3] statements. +/// +/// [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, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum IndexType { + BTree, + Hash, + // TODO add Postgresql's possible indexes +} + +impl fmt::Display for IndexType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::BTree => write!(f, "BTREE"), + Self::Hash => write!(f, "HASH"), } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 49e6fb01e..c83ead544 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -26,7 +26,7 @@ pub use self::data_type::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, }; pub use self::ddl::{ - AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, + AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, IndexType, ReferentialAction, TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/keywords.rs b/src/keywords.rs index 1c2c43854..e29ebdfdb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -105,6 +105,7 @@ define_keywords!( BLOB, BOOLEAN, BOTH, + BTREE, BY, BYPASSRLS, BYTEA, @@ -265,6 +266,7 @@ define_keywords!( GROUP, GROUPING, GROUPS, + HASH, HAVING, HEADER, HIVEVAR, diff --git a/src/parser.rs b/src/parser.rs index 5dfb9f10c..0254a2f9a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3003,6 +3003,31 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; Ok(Some(TableConstraint::Check { name, expr })) } + Token::Word(w) + if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) + && dialect_of!(self is GenericDialect | MySqlDialect) => + { + let display_as_key = w.keyword == Keyword::KEY; + + let name = match self.peek_token() { + Token::Word(word) if word.keyword == Keyword::USING => None, + _ => self.maybe_parse(|parser| parser.parse_identifier()), + }; + + let index_type = if self.parse_keyword(Keyword::USING) { + Some(self.parse_index_type()?) + } else { + None + }; + let columns = self.parse_parenthesized_column_list(Mandatory)?; + + Ok(Some(TableConstraint::Index { + display_as_key, + name, + index_type, + columns, + })) + } unexpected => { if name.is_some() { self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", unexpected) @@ -3025,6 +3050,16 @@ impl<'a> Parser<'a> { } } + pub fn parse_index_type(&mut self) -> Result { + if self.parse_keyword(Keyword::BTREE) { + Ok(IndexType::BTree) + } else if self.parse_keyword(Keyword::HASH) { + Ok(IndexType::Hash) + } else { + self.expected("index type {BTREE | HASH}", self.peek_token()) + } + } + pub fn parse_sql_option(&mut self) -> Result { let name = self.parse_identifier()?; self.expect_token(&Token::Eq)?; @@ -5779,4 +5814,100 @@ mod tests { SchemaName::NamedAuthorization(dummy_name.clone(), dummy_authorization.clone()), ); } + + #[test] + fn mysql_parse_index_table_constraint() { + macro_rules! test_parse_table_constraint { + ($dialect:expr, $input:expr, $expected:expr $(,)?) => {{ + $dialect.run_parser_method(&*$input, |parser| { + let constraint = parser.parse_optional_table_constraint().unwrap().unwrap(); + // Validate that the structure is the same as expected + assert_eq!(constraint, $expected); + // Validate that the input and the expected structure serialization are the same + assert_eq!(constraint.to_string(), $input.to_string()); + }); + }}; + } + + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})], + }; + + test_parse_table_constraint!( + dialect, + "INDEX (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: None, + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "KEY (c1)", + TableConstraint::Index { + display_as_key: true, + name: None, + index_type: None, + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX 'index' (c1, c2)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::with_quote('\'', "index")), + index_type: None, + columns: vec![Ident::new("c1"), Ident::new("c2")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX USING BTREE (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: Some(IndexType::BTree), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX USING HASH (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: Some(IndexType::Hash), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX idx_name USING BTREE (c1)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::new("idx_name")), + index_type: Some(IndexType::BTree), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX idx_name USING HASH (c1)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::new("idx_name")), + index_type: Some(IndexType::Hash), + columns: vec![Ident::new("c1")], + } + ); + } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9e5a5cdd1..bae310ef0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2089,10 +2089,10 @@ fn parse_create_table_hive_array() { let dialects = TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(HiveDialect {})], }; - let sql = "CREATE TABLE IF NOT EXISTS something (key int, val array)"; + let sql = "CREATE TABLE IF NOT EXISTS something (name int, val array)"; match dialects.one_statement_parses_to( sql, - "CREATE TABLE IF NOT EXISTS something (key INT, val INT[])", + "CREATE TABLE IF NOT EXISTS something (name INT, val INT[])", ) { Statement::CreateTable { if_not_exists, @@ -2106,7 +2106,7 @@ fn parse_create_table_hive_array() { columns, vec![ ColumnDef { - name: Ident::new("key"), + name: Ident::new("name"), data_type: DataType::Int(None), collation: None, options: vec![], @@ -2123,7 +2123,8 @@ fn parse_create_table_hive_array() { _ => unreachable!(), } - let res = parse_sql_statements("CREATE TABLE IF NOT EXISTS something (key int, val array TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})], From b525c9f2f75cd9cb3b5fa4d81c1bfd048175db25 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 19 Oct 2022 17:35:37 -0400 Subject: [PATCH 069/806] Add changelog for 0.26.0 (#679) --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f63a963cb..6ae31adf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,24 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. + +## [0.26.0] 2022-10-19 + +### Added +* Support MySQL table option `{INDEX | KEY}` in CREATE TABLE definiton (#665) - Thanks @AugustoFKL +* Support `CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] ` (#678) - Thanks @sam-mmm +* Support `DROP SEQUENCE` statement (#673) - Thanks @sam-mmm +* Support for ANSI types `CHARACTER LARGE OBJECT[(p)]` and `CHAR LARGE OBJECT[(p)]` (#671) - Thanks @AugustoFKL +* Support `[CACHE|UNCACHE] TABLE` (#670) - Thanks @francis-du +* Support `CEIL(expr TO DateTimeField)` and `FLOOR(expr TO DateTimeField)` - Thanks @sarahyurick +* Support all ansii character string types, (#648) - Thanks @AugustoFKL + +### Changed +* Support expressions inside window frames (#655) - Thanks @mustafasrepo and @ozankabak +* Support unit on char length units for small character strings (#663) - Thanks @AugustoFKL +* Replace booleans on `SET ROLE` with a single enum. (#664) - Thanks @AugustoFKL +* Replace `Option`s with enum for `DECIMAL` precision (#654) - Thanks @AugustoFKL + ## [0.25.0] 2022-10-03 ### Added From 2c266a437cb8a59317b351bdb70fa1912748b985 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 19 Oct 2022 17:39:37 -0400 Subject: [PATCH 070/806] (cargo-release) version 0.26.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 60fe478c8..b5b587a6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.25.0" +version = "0.26.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 914810d36617581c2380ae9c0f501116a7d810b6 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Fri, 21 Oct 2022 03:27:15 +0800 Subject: [PATCH 071/806] Modifier support for Custom Datatype (#680) * feat: add support for custom types with argument * refactor: add support for number and string as type arguments * fix: ignore CustomWithArgs when parsing TypedString * refactor: merge CustomWithArgs into Custom * refactor: rename arguments to modifiers --- src/ast/data_type.rs | 10 +++++-- src/parser.rs | 63 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 58c1305ac..a66761167 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -129,7 +129,7 @@ pub enum DataType { /// Bytea Bytea, /// Custom type such as enums - Custom(ObjectName), + Custom(ObjectName, Vec), /// Arrays Array(Box), /// Enums @@ -217,7 +217,13 @@ impl fmt::Display for DataType { DataType::String => write!(f, "STRING"), DataType::Bytea => write!(f, "BYTEA"), DataType::Array(ty) => write!(f, "{}[]", ty), - DataType::Custom(ty) => write!(f, "{}", ty), + DataType::Custom(ty, modifiers) => { + if modifiers.is_empty() { + write!(f, "{}", ty) + } else { + write!(f, "{}({})", ty, modifiers.join(", ")) + } + } DataType::Enum(vals) => { write!(f, "ENUM(")?; for (i, v) in vals.iter().enumerate() { diff --git a/src/parser.rs b/src/parser.rs index 0254a2f9a..108427889 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3661,7 +3661,11 @@ impl<'a> Parser<'a> { _ => { self.prev_token(); let type_name = self.parse_object_name()?; - Ok(DataType::Custom(type_name)) + if let Some(modifiers) = self.parse_optional_type_modifiers()? { + Ok(DataType::Custom(type_name, modifiers)) + } else { + Ok(DataType::Custom(type_name, vec![])) + } } }, unexpected => self.expected("a data type name", unexpected), @@ -3907,6 +3911,31 @@ impl<'a> Parser<'a> { } } + pub fn parse_optional_type_modifiers(&mut self) -> Result>, ParserError> { + if self.consume_token(&Token::LParen) { + let mut modifiers = Vec::new(); + loop { + match self.next_token() { + Token::Word(w) => modifiers.push(w.to_string()), + Token::Number(n, _) => modifiers.push(n), + Token::SingleQuotedString(s) => modifiers.push(s), + + Token::Comma => { + continue; + } + Token::RParen => { + break; + } + unexpected => self.expected("type modifiers", unexpected)?, + } + } + + Ok(Some(modifiers)) + } else { + Ok(None) + } + } + pub fn parse_delete(&mut self) -> Result { self.expect_keyword(Keyword::FROM)?; let table_name = self.parse_table_factor()?; @@ -5540,7 +5569,7 @@ mod tests { #[cfg(test)] mod test_parse_data_type { use crate::ast::{ - CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, + CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, ObjectName, TimezoneInfo, }; use crate::dialect::{AnsiDialect, GenericDialect}; use crate::test_utils::TestedDialects; @@ -5717,6 +5746,36 @@ mod tests { test_parse_data_type!(dialect, "CLOB(20)", DataType::Clob(Some(20))); } + #[test] + fn test_parse_custom_types() { + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + }; + test_parse_data_type!( + dialect, + "GEOMETRY", + DataType::Custom(ObjectName(vec!["GEOMETRY".into()]), vec![]) + ); + + test_parse_data_type!( + dialect, + "GEOMETRY(POINT)", + DataType::Custom( + ObjectName(vec!["GEOMETRY".into()]), + vec!["POINT".to_string()] + ) + ); + + test_parse_data_type!( + dialect, + "GEOMETRY(POINT, 4326)", + DataType::Custom( + ObjectName(vec!["GEOMETRY".into()]), + vec!["POINT".to_string(), "4326".to_string()] + ) + ); + } + #[test] fn test_ansii_exact_numeric_types() { // Exact numeric types: From b671dc62d383123a143ff3d4d494b25256b6794f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Oct 2022 07:28:37 -0400 Subject: [PATCH 072/806] Update simple_logger requirement from 2.1 to 4.0 (#692) Updates the requirements on [simple_logger](https://github.com/borntyping/rust-simple_logger) to permit the latest version. - [Release notes](https://github.com/borntyping/rust-simple_logger/releases) - [Commits](https://github.com/borntyping/rust-simple_logger/compare/v2.1.0...v4.0.0) --- updated-dependencies: - dependency-name: simple_logger dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b5b587a6c..fbcd9ce9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } [dev-dependencies] -simple_logger = "2.1" +simple_logger = "4.0" matches = "0.1" pretty_assertions = "1" From f0646c8c1a5993ff14f9df54dda6613a7f3641fb Mon Sep 17 00:00:00 2001 From: Sarah Yurick <53962159+sarahyurick@users.noreply.github.com> Date: Mon, 31 Oct 2022 12:20:57 -0700 Subject: [PATCH 073/806] add Date keyword (#691) --- src/ast/value.rs | 2 ++ src/parser.rs | 1 + tests/sqlparser_common.rs | 1 + 3 files changed, 4 insertions(+) diff --git a/src/ast/value.rs b/src/ast/value.rs index 3861ab008..6412aa308 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -70,6 +70,7 @@ pub enum DateTimeField { Month, Week, Day, + Date, Hour, Minute, Second, @@ -101,6 +102,7 @@ impl fmt::Display for DateTimeField { DateTimeField::Month => "MONTH", DateTimeField::Week => "WEEK", DateTimeField::Day => "DAY", + DateTimeField::Date => "DATE", DateTimeField::Hour => "HOUR", DateTimeField::Minute => "MINUTE", DateTimeField::Second => "SECOND", diff --git a/src/parser.rs b/src/parser.rs index 108427889..bd74e5773 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1082,6 +1082,7 @@ impl<'a> Parser<'a> { Keyword::MONTH => Ok(DateTimeField::Month), Keyword::WEEK => Ok(DateTimeField::Week), Keyword::DAY => Ok(DateTimeField::Day), + Keyword::DATE => Ok(DateTimeField::Date), Keyword::HOUR => Ok(DateTimeField::Hour), Keyword::MINUTE => Ok(DateTimeField::Minute), Keyword::SECOND => Ok(DateTimeField::Second), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bae310ef0..7e9ecf9bb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1755,6 +1755,7 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(MONTH FROM d)"); verified_stmt("SELECT EXTRACT(WEEK FROM d)"); verified_stmt("SELECT EXTRACT(DAY FROM d)"); + verified_stmt("SELECT EXTRACT(DATE FROM d)"); verified_stmt("SELECT EXTRACT(HOUR FROM d)"); verified_stmt("SELECT EXTRACT(MINUTE FROM d)"); verified_stmt("SELECT EXTRACT(SECOND FROM d)"); From 1b3778e2d59d349d61ed22335102bdae34a4777f Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Mon, 31 Oct 2022 16:22:34 -0300 Subject: [PATCH 074/806] feature!: added NUMERIC and DEC ANSI data types, and now the DECIMAL (#695) type prints DECIMAL instead of NUMERIC. BREAKING CHANGE: enum DATA TYPE variants changed, changing any API that uses it. --- src/ast/data_type.rs | 20 ++++++++++++++++++-- src/parser.rs | 40 ++++++++++++++++++++++++++++++++++++--- tests/sqlparser_common.rs | 33 +++++++++----------------------- tests/sqlparser_hive.rs | 3 +-- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index a66761167..e906383fc 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -67,8 +67,18 @@ pub enum DataType { /// [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), - /// Decimal type with optional precision and scale e.g. DECIMAL(10,2) + /// Numeric type with optional precision and scale e.g. NUMERIC(10,2), [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] + /// + /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type Decimal(ExactNumberInfo), + /// Dec type with optional precision and scale e.g. DEC(10,2), [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) Float(Option), /// Tiny integer with optional display width e.g. TINYINT or TINYINT(3) @@ -165,9 +175,15 @@ impl fmt::Display for DataType { format_type_with_optional_length(f, "VARBINARY", size, false) } DataType::Blob(size) => format_type_with_optional_length(f, "BLOB", size, false), - DataType::Decimal(info) => { + DataType::Numeric(info) => { write!(f, "NUMERIC{}", info) } + DataType::Decimal(info) => { + write!(f, "DECIMAL{}", info) + } + DataType::Dec(info) => { + write!(f, "DEC{}", info) + } DataType::Float(size) => format_type_with_optional_length(f, "FLOAT", size, false), DataType::TinyInt(zerofill) => { format_type_with_optional_length(f, "TINYINT", zerofill, false) diff --git a/src/parser.rs b/src/parser.rs index bd74e5773..0f3f639ce 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3645,7 +3645,13 @@ impl<'a> Parser<'a> { Keyword::STRING => Ok(DataType::String), Keyword::TEXT => Ok(DataType::Text), Keyword::BYTEA => Ok(DataType::Bytea), - Keyword::NUMERIC | Keyword::DECIMAL | Keyword::DEC => Ok(DataType::Decimal( + Keyword::NUMERIC => Ok(DataType::Numeric( + self.parse_exact_number_optional_precision_scale()?, + )), + Keyword::DECIMAL => Ok(DataType::Decimal( + self.parse_exact_number_optional_precision_scale()?, + )), + Keyword::DEC => Ok(DataType::Dec( self.parse_exact_number_optional_precision_scale()?, )), Keyword::ENUM => Ok(DataType::Enum(self.parse_string_values()?)), @@ -5784,19 +5790,47 @@ mod tests { dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], }; - test_parse_data_type!(dialect, "NUMERIC", DataType::Decimal(ExactNumberInfo::None)); + test_parse_data_type!(dialect, "NUMERIC", DataType::Numeric(ExactNumberInfo::None)); test_parse_data_type!( dialect, "NUMERIC(2)", - DataType::Decimal(ExactNumberInfo::Precision(2)) + DataType::Numeric(ExactNumberInfo::Precision(2)) ); test_parse_data_type!( dialect, "NUMERIC(2,10)", + DataType::Numeric(ExactNumberInfo::PrecisionAndScale(2, 10)) + ); + + test_parse_data_type!(dialect, "DECIMAL", DataType::Decimal(ExactNumberInfo::None)); + + test_parse_data_type!( + dialect, + "DECIMAL(2)", + DataType::Decimal(ExactNumberInfo::Precision(2)) + ); + + test_parse_data_type!( + dialect, + "DECIMAL(2,10)", DataType::Decimal(ExactNumberInfo::PrecisionAndScale(2, 10)) ); + + test_parse_data_type!(dialect, "DEC", DataType::Dec(ExactNumberInfo::None)); + + test_parse_data_type!( + dialect, + "DEC(2)", + DataType::Dec(ExactNumberInfo::Precision(2)) + ); + + test_parse_data_type!( + dialect, + "DEC(2,10)", + DataType::Dec(ExactNumberInfo::PrecisionAndScale(2, 10)) + ); } #[test] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7e9ecf9bb..f6dfe1590 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18,10 +18,8 @@ //! sqlparser regardless of the chosen dialect (i.e. it doesn't conflict with //! dialect-specific parsing rules). -#[macro_use] -mod test_utils; - use matches::assert_matches; + use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::*; use sqlparser::dialect::{ @@ -30,12 +28,14 @@ use sqlparser::dialect::{ }; use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError}; - use test_utils::{ all_dialects, assert_eq_vec, expr_from_projection, join, number, only, table, table_alias, TestedDialects, }; +#[macro_use] +mod test_utils; + #[test] fn parse_insert_values() { let row = vec![ @@ -1628,15 +1628,9 @@ fn parse_cast() { verified_stmt("SELECT CAST(id AS NUMERIC) FROM customer"); - one_statement_parses_to( - "SELECT CAST(id AS DEC) FROM customer", - "SELECT CAST(id AS NUMERIC) FROM customer", - ); + verified_stmt("SELECT CAST(id AS DEC) FROM customer"); - one_statement_parses_to( - "SELECT CAST(id AS DECIMAL) FROM customer", - "SELECT CAST(id AS NUMERIC) FROM customer", - ); + verified_stmt("SELECT CAST(id AS DECIMAL) FROM customer"); let sql = "SELECT CAST(id AS NVARCHAR(50)) FROM customer"; let select = verified_only_select(sql); @@ -1720,22 +1714,13 @@ fn parse_try_cast() { }, expr_from_projection(only(&select.projection)) ); - one_statement_parses_to( - "SELECT TRY_CAST(id AS BIGINT) FROM customer", - "SELECT TRY_CAST(id AS BIGINT) FROM customer", - ); + verified_stmt("SELECT TRY_CAST(id AS BIGINT) FROM customer"); verified_stmt("SELECT TRY_CAST(id AS NUMERIC) FROM customer"); - one_statement_parses_to( - "SELECT TRY_CAST(id AS DEC) FROM customer", - "SELECT TRY_CAST(id AS NUMERIC) FROM customer", - ); + verified_stmt("SELECT TRY_CAST(id AS DEC) FROM customer"); - one_statement_parses_to( - "SELECT TRY_CAST(id AS DECIMAL) FROM customer", - "SELECT TRY_CAST(id AS NUMERIC) FROM customer", - ); + verified_stmt("SELECT TRY_CAST(id AS DECIMAL) FROM customer"); } #[test] diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 8839cea2b..c4df6b5dd 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -156,8 +156,7 @@ fn long_numerics() { #[test] fn decimal_precision() { let query = "SELECT CAST(a AS DECIMAL(18,2)) FROM db.table"; - let expected = "SELECT CAST(a AS NUMERIC(18,2)) FROM db.table"; - hive().one_statement_parses_to(query, expected); + hive().verified_stmt(query); } #[test] From 27c3ec87dbdccdb2660118b49006d00fb6aa47f6 Mon Sep 17 00:00:00 2001 From: ding-young Date: Wed, 2 Nov 2022 23:15:33 +0900 Subject: [PATCH 075/806] Support `ALTER TABLE DROP PRIMARY KEY` (#682) * parse alter table drop primary key * cargo nightly fmt * add Dialect validation --- src/ast/ddl.rs | 5 +++++ src/parser.rs | 4 ++++ tests/sqlparser_mysql.rs | 13 +++++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index cae0f597b..c758b4f55 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -44,6 +44,10 @@ pub enum AlterTableOperation { if_exists: bool, cascade: bool, }, + /// `DROP PRIMARY KEY` + /// + /// Note: this is a MySQL-specific operation. + DropPrimaryKey, /// `RENAME TO PARTITION (partition=val)` RenamePartitions { old_partitions: Vec, @@ -124,6 +128,7 @@ impl fmt::Display for AlterTableOperation { if *cascade { " CASCADE" } else { "" }, ) } + AlterTableOperation::DropPrimaryKey => write!(f, "DROP PRIMARY KEY"), AlterTableOperation::DropColumn { column_name, if_exists, diff --git a/src/parser.rs b/src/parser.rs index 0f3f639ce..8ff89c0ad 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3137,6 +3137,10 @@ impl<'a> Parser<'a> { name, cascade, } + } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) + && dialect_of!(self is MySqlDialect | GenericDialect) + { + AlterTableOperation::DropPrimaryKey } else { let _ = self.parse_keyword(Keyword::COLUMN); let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8c42c715b..52c0205d6 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -875,6 +875,19 @@ fn parse_update_with_joins() { } } +#[test] +fn parse_alter_table_drop_primary_key() { + match mysql_and_generic().verified_stmt("ALTER TABLE tab DROP PRIMARY KEY") { + Statement::AlterTable { + name, + operation: AlterTableOperation::DropPrimaryKey, + } => { + assert_eq!("tab", name.to_string()); + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table_change_column() { let expected_name = ObjectName(vec![Ident::new("orders")]); From 93a050e5f0fe0c5693b18f9a8e86d09d46123d5a Mon Sep 17 00:00:00 2001 From: yuval-illumex <85674443+yuval-illumex@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:05:35 +0200 Subject: [PATCH 076/806] Snowflake: Support semi-structured data (#693) * Support parse json in snowflake * MR Review * Lint * Try to fix right as value * Fix tests * Fix lint * Add generic dialect * Add support in key location --- src/ast/mod.rs | 11 ++++++++++- src/ast/value.rs | 3 +++ src/parser.rs | 11 +++++++++++ tests/sqlparser_snowflake.rs | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c83ead544..cda0d4e29 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -187,6 +187,8 @@ pub enum JsonOperator { HashArrow, /// #>> Extracts JSON sub-object at the specified path as text HashLongArrow, + /// : Colon is used by Snowflake (Which is similar to LongArrow) + Colon, } impl fmt::Display for JsonOperator { @@ -204,6 +206,9 @@ impl fmt::Display for JsonOperator { JsonOperator::HashLongArrow => { write!(f, "#>>") } + JsonOperator::Colon => { + write!(f, ":") + } } } } @@ -757,7 +762,11 @@ impl fmt::Display for Expr { operator, right, } => { - write!(f, "{} {} {}", left, operator, right) + if operator == &JsonOperator::Colon { + write!(f, "{}{}{}", left, operator, right) + } else { + write!(f, "{} {} {}", left, operator, right) + } } Expr::CompositeAccess { expr, key } => { write!(f, "{}.{}", expr, key) diff --git a/src/ast/value.rs b/src/ast/value.rs index 6412aa308..33660a628 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -45,6 +45,8 @@ pub enum Value { Null, /// `?` or `$` Prepared statement arg placeholder Placeholder(String), + /// Add support of snowflake field:key - key should be a value + UnQuotedString(String), } impl fmt::Display for Value { @@ -59,6 +61,7 @@ impl fmt::Display for Value { Value::Boolean(v) => write!(f, "{}", v), Value::Null => write!(f, "NULL"), Value::Placeholder(v) => write!(f, "{}", v), + Value::UnQuotedString(v) => write!(f, "{}", v), } } } diff --git a/src/parser.rs b/src/parser.rs index 8ff89c0ad..161062458 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1427,6 +1427,12 @@ impl<'a> Parser<'a> { return self.parse_array_index(expr); } self.parse_map_access(expr) + } else if Token::Colon == tok { + Ok(Expr::JsonAccess { + left: Box::new(expr), + operator: JsonOperator::Colon, + right: Box::new(Expr::Value(self.parse_value()?)), + }) } else if Token::Arrow == tok || Token::LongArrow == tok || Token::HashArrow == tok @@ -1628,6 +1634,7 @@ impl<'a> Parser<'a> { Token::Plus | Token::Minus => Ok(Self::PLUS_MINUS_PREC), Token::Mul | Token::Div | Token::Mod | Token::StringConcat => Ok(40), Token::DoubleColon => Ok(50), + Token::Colon => Ok(50), Token::ExclamationMark => Ok(50), Token::LBracket | Token::LongArrow @@ -3446,6 +3453,10 @@ impl<'a> Parser<'a> { Some('\'') => Ok(Value::SingleQuotedString(w.value)), _ => self.expected("A value?", Token::Word(w))?, }, + // Case when Snowflake Semi-structured data like key:value + Keyword::NoKeyword | Keyword::LOCATION if dialect_of!(self is SnowflakeDialect | GenericDialect) => { + Ok(Value::UnQuotedString(w.value)) + } _ => self.expected("a concrete value", Token::Word(w)), }, // The call to n.parse() returns a bigdecimal when the diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7c089a935..64fff62f9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -143,6 +143,22 @@ fn test_single_table_in_parenthesis_with_alias() { ); } +#[test] +fn parse_json_using_colon() { + let sql = "SELECT a:b FROM t"; + let select = snowflake().verified_only_select(sql); + assert_eq!( + SelectItem::UnnamedExpr(Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::new("a"))), + operator: JsonOperator::Colon, + right: Box::new(Expr::Value(Value::UnQuotedString("b".to_string()))), + }), + select.projection[0] + ); + + snowflake().one_statement_parses_to("SELECT a:b::int FROM t", "SELECT CAST(a:b AS INT) FROM t"); +} + fn snowflake() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SnowflakeDialect {})], From bbf32a9e81e8965e77b5b4035b10c5ccab3ad070 Mon Sep 17 00:00:00 2001 From: sam <2740878+sam-mmm@users.noreply.github.com> Date: Thu, 3 Nov 2022 18:46:43 +0530 Subject: [PATCH 077/806] Support create sequence with options INCREMENT, MINVALUE, MAXVALUE, START etc. (#681) * Creat sequence options model [ INCREMENT [ BY ] increment ] [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] [ START [ WITH ] start ] [ CACHE cache ] [ [ NO ] CYCLE ] [ OWNED BY { table_name.column_name | NONE } ] * Fix for format! not avalable in --target thumbv6m-none-eabi * Fix for format! not avalable in --target thumbv6m-none-eabi * Fix for format! not avalable in --target thumbv6m-none-eabi * Fix for format! not avalable in --target thumbv6m-none-eabi * Updated parser for sequence options * Updated parser for sequence options * Update src/ast/mod.rs Co-authored-by: Andrew Lamb Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 104 +++++++++++++++++++++++++++++++++++- src/keywords.rs | 4 ++ src/parser.rs | 86 +++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 43 +++++++++++++++ 4 files changed, 235 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cda0d4e29..4cbd881c3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1467,6 +1467,9 @@ pub enum Statement { temporary: bool, if_not_exists: bool, name: ObjectName, + data_type: Option, + sequence_options: Vec, + owned_by: Option, }, } @@ -2488,19 +2491,116 @@ impl fmt::Display for Statement { temporary, if_not_exists, name, + data_type, + sequence_options, + owned_by, } => { + let as_type: String = if let Some(dt) = data_type.as_ref() { + //Cannot use format!(" AS {}", dt), due to format! is not available in --target thumbv6m-none-eabi + // " AS ".to_owned() + &dt.to_string() + [" AS ", &dt.to_string()].concat() + } else { + "".to_string() + }; write!( f, - "CREATE {temporary}SEQUENCE {if_not_exists}{name}", + "CREATE {temporary}SEQUENCE {if_not_exists}{name}{as_type}", if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, temporary = if *temporary { "TEMPORARY " } else { "" }, - name = name + name = name, + as_type = as_type + )?; + for sequence_option in sequence_options { + write!(f, "{}", sequence_option)?; + } + if let Some(ob) = owned_by.as_ref() { + write!(f, " OWNED BY {}", ob)?; + } + write!(f, "") + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Can use to describe options in create sequence or table column type identity +/// [ INCREMENT [ BY ] increment ] +/// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] +/// [ START [ WITH ] start ] [ CACHE cache ] [ [ NO ] CYCLE ] +pub enum SequenceOptions { + IncrementBy(Expr, bool), + MinValue(MinMaxValue), + MaxValue(MinMaxValue), + StartWith(Expr, bool), + Cache(Expr), + Cycle(bool), +} + +impl fmt::Display for SequenceOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SequenceOptions::IncrementBy(increment, by) => { + write!( + f, + " INCREMENT{by} {increment}", + by = if *by { " BY" } else { "" }, + increment = increment ) } + SequenceOptions::MinValue(value) => match value { + MinMaxValue::Empty => { + write!(f, "") + } + MinMaxValue::None => { + write!(f, " NO MINVALUE") + } + MinMaxValue::Some(minvalue) => { + write!(f, " MINVALUE {minvalue}", minvalue = minvalue) + } + }, + SequenceOptions::MaxValue(value) => match value { + MinMaxValue::Empty => { + write!(f, "") + } + MinMaxValue::None => { + write!(f, " NO MAXVALUE") + } + MinMaxValue::Some(maxvalue) => { + write!(f, " MAXVALUE {maxvalue}", maxvalue = maxvalue) + } + }, + SequenceOptions::StartWith(start, with) => { + write!( + f, + " START{with} {start}", + with = if *with { " WITH" } else { "" }, + start = start + ) + } + SequenceOptions::Cache(cache) => { + write!(f, " CACHE {}", *cache) + } + SequenceOptions::Cycle(no) => { + write!(f, " {}CYCLE", if *no { "NO " } else { "" }) + } } } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Can use to describe options in create sequence or table column type identity +/// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] +pub enum MinMaxValue { + // clause is not specified + Empty, + // NO MINVALUE/NO MAXVALUE + None, + // MINVALUE / MAXVALUE + Some(Expr), +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[non_exhaustive] diff --git a/src/keywords.rs b/src/keywords.rs index e29ebdfdb..f8d125067 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -277,6 +277,7 @@ define_keywords!( IGNORE, ILIKE, IN, + INCREMENT, INDEX, INDICATOR, INHERIT, @@ -328,6 +329,7 @@ define_keywords!( MATCHED, MATERIALIZED, MAX, + MAXVALUE, MEDIUMINT, MEMBER, MERGE, @@ -341,6 +343,7 @@ define_keywords!( MILLISECONDS, MIN, MINUTE, + MINVALUE, MOD, MODIFIES, MODULE, @@ -397,6 +400,7 @@ define_keywords!( OVERLAPS, OVERLAY, OVERWRITE, + OWNED, PARAMETER, PARQUET, PARTITION, diff --git a/src/parser.rs b/src/parser.rs index 161062458..1f514b75d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5510,12 +5510,98 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); //name let name = self.parse_object_name()?; + //[ AS data_type ] + let mut data_type: Option = None; + if self.parse_keywords(&[Keyword::AS]) { + data_type = Some(self.parse_data_type()?) + } + let sequence_options = self.parse_create_sequence_options()?; + // [ OWNED BY { table_name.column_name | NONE } ] + let owned_by = if self.parse_keywords(&[Keyword::OWNED, Keyword::BY]) { + if self.parse_keywords(&[Keyword::NONE]) { + Some(ObjectName(vec![Ident::new("NONE")])) + } else { + Some(self.parse_object_name()?) + } + } else { + None + }; Ok(Statement::CreateSequence { temporary, if_not_exists, name, + data_type, + sequence_options, + owned_by, }) } + + fn parse_create_sequence_options(&mut self) -> Result, ParserError> { + let mut sequence_options = vec![]; + //[ INCREMENT [ BY ] increment ] + if self.parse_keywords(&[Keyword::INCREMENT]) { + if self.parse_keywords(&[Keyword::BY]) { + sequence_options.push(SequenceOptions::IncrementBy( + Expr::Value(self.parse_number_value()?), + true, + )); + } else { + sequence_options.push(SequenceOptions::IncrementBy( + Expr::Value(self.parse_number_value()?), + false, + )); + } + } + //[ MINVALUE minvalue | NO MINVALUE ] + if self.parse_keyword(Keyword::MINVALUE) { + sequence_options.push(SequenceOptions::MinValue(MinMaxValue::Some(Expr::Value( + self.parse_number_value()?, + )))); + } else if self.parse_keywords(&[Keyword::NO, Keyword::MINVALUE]) { + sequence_options.push(SequenceOptions::MinValue(MinMaxValue::None)); + } else { + sequence_options.push(SequenceOptions::MinValue(MinMaxValue::Empty)); + } + //[ MAXVALUE maxvalue | NO MAXVALUE ] + if self.parse_keywords(&[Keyword::MAXVALUE]) { + sequence_options.push(SequenceOptions::MaxValue(MinMaxValue::Some(Expr::Value( + self.parse_number_value()?, + )))); + } else if self.parse_keywords(&[Keyword::NO, Keyword::MAXVALUE]) { + sequence_options.push(SequenceOptions::MaxValue(MinMaxValue::None)); + } else { + sequence_options.push(SequenceOptions::MaxValue(MinMaxValue::Empty)); + } + //[ START [ WITH ] start ] + if self.parse_keywords(&[Keyword::START]) { + if self.parse_keywords(&[Keyword::WITH]) { + sequence_options.push(SequenceOptions::StartWith( + Expr::Value(self.parse_number_value()?), + true, + )); + } else { + sequence_options.push(SequenceOptions::StartWith( + Expr::Value(self.parse_number_value()?), + false, + )); + } + } + //[ CACHE cache ] + if self.parse_keywords(&[Keyword::CACHE]) { + sequence_options.push(SequenceOptions::Cache(Expr::Value( + self.parse_number_value()?, + ))); + } + // [ [ NO ] CYCLE ] + if self.parse_keywords(&[Keyword::NO]) { + if self.parse_keywords(&[Keyword::CYCLE]) { + sequence_options.push(SequenceOptions::Cycle(true)); + } + } else if self.parse_keywords(&[Keyword::CYCLE]) { + sequence_options.push(SequenceOptions::Cycle(false)); + } + Ok(sequence_options) + } } impl Word { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c5d4bd0fc..67302e840 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -37,6 +37,49 @@ fn parse_create_sequence() { let sql4 = "CREATE TEMPORARY SEQUENCE name0"; pg().one_statement_parses_to(sql4, "CREATE TEMPORARY SEQUENCE name0"); + + let sql2 = "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name1 + AS BIGINT + INCREMENT BY 1 + MINVALUE 1 MAXVALUE 20 + START WITH 10"; + pg().one_statement_parses_to( + sql2, + "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name1 AS BIGINT INCREMENT BY 1 MINVALUE 1 MAXVALUE 20 START WITH 10", ); + + let sql3 = "CREATE SEQUENCE IF NOT EXISTS name2 + AS BIGINT + INCREMENT 1 + MINVALUE 1 MAXVALUE 20 + START WITH 10 CACHE 2 NO CYCLE"; + pg().one_statement_parses_to( + sql3, + "CREATE SEQUENCE IF NOT EXISTS name2 AS BIGINT INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 NO CYCLE", + ); + + let sql4 = "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name3 + INCREMENT 1 + NO MINVALUE MAXVALUE 20 CACHE 2 CYCLE"; + pg().one_statement_parses_to( + sql4, + "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name3 INCREMENT 1 NO MINVALUE MAXVALUE 20 CACHE 2 CYCLE", + ); + + let sql5 = "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name3 + INCREMENT 1 + NO MINVALUE MAXVALUE 20 OWNED BY public.table01"; + pg().one_statement_parses_to( + sql5, + "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name3 INCREMENT 1 NO MINVALUE MAXVALUE 20 OWNED BY public.table01", + ); + + let sql6 = "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name3 + INCREMENT 1 + NO MINVALUE MAXVALUE 20 OWNED BY NONE"; + pg().one_statement_parses_to( + sql6, + "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name3 INCREMENT 1 NO MINVALUE MAXVALUE 20 OWNED BY NONE", + ); } #[test] From 0f7e144890db1e2f1b9da37787376449d26e6c13 Mon Sep 17 00:00:00 2001 From: yuval-illumex <85674443+yuval-illumex@users.noreply.github.com> Date: Fri, 4 Nov 2022 16:25:25 +0200 Subject: [PATCH 078/806] Support the ARRAY type of Snowflake (#699) * Snowflake Array * Use the array data type * Try to fix build * Try to fix build * Change Array to option * Remove unused import --- src/ast/data_type.rs | 10 ++++++++-- src/parser.rs | 20 ++++++++++++-------- tests/sqlparser_common.rs | 24 ++++++++++++++++-------- tests/sqlparser_postgres.rs | 6 +++--- tests/sqlparser_snowflake.rs | 13 +++++++++++++ 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index e906383fc..c0b2f977b 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -141,7 +141,7 @@ pub enum DataType { /// Custom type such as enums Custom(ObjectName, Vec), /// Arrays - Array(Box), + Array(Option>), /// Enums Enum(Vec), /// Set @@ -232,7 +232,13 @@ impl fmt::Display for DataType { DataType::Text => write!(f, "TEXT"), DataType::String => write!(f, "STRING"), DataType::Bytea => write!(f, "BYTEA"), - DataType::Array(ty) => write!(f, "{}[]", ty), + DataType::Array(ty) => { + if let Some(t) = &ty { + write!(f, "{}[]", t) + } else { + write!(f, "ARRAY") + } + } DataType::Custom(ty, modifiers) => { if modifiers.is_empty() { write!(f, "{}", ty) diff --git a/src/parser.rs b/src/parser.rs index 1f514b75d..76d845074 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3672,13 +3672,17 @@ impl<'a> Parser<'a> { Keyword::ENUM => Ok(DataType::Enum(self.parse_string_values()?)), Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)), Keyword::ARRAY => { - // Hive array syntax. Note that nesting arrays - or other Hive syntax - // that ends with > will fail due to "C++" problem - >> is parsed as - // Token::ShiftRight - self.expect_token(&Token::Lt)?; - let inside_type = self.parse_data_type()?; - self.expect_token(&Token::Gt)?; - Ok(DataType::Array(Box::new(inside_type))) + if dialect_of!(self is SnowflakeDialect) { + Ok(DataType::Array(None)) + } else { + // Hive array syntax. Note that nesting arrays - or other Hive syntax + // that ends with > will fail due to "C++" problem - >> is parsed as + // Token::ShiftRight + self.expect_token(&Token::Lt)?; + let inside_type = self.parse_data_type()?; + self.expect_token(&Token::Gt)?; + Ok(DataType::Array(Some(Box::new(inside_type)))) + } } _ => { self.prev_token(); @@ -3697,7 +3701,7 @@ impl<'a> Parser<'a> { // Keyword::ARRAY syntax from above while self.consume_token(&Token::LBracket) { self.expect_token(&Token::RBracket)?; - data = DataType::Array(Box::new(data)) + data = DataType::Array(Some(Box::new(data))) } Ok(data) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f6dfe1590..a3d6ea766 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -24,7 +24,7 @@ use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::*; use sqlparser::dialect::{ AnsiDialect, BigQueryDialect, ClickHouseDialect, GenericDialect, HiveDialect, MsSqlDialect, - PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, + MySqlDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, }; use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError}; @@ -2099,7 +2099,7 @@ fn parse_create_table_hive_array() { }, ColumnDef { name: Ident::new("val"), - data_type: DataType::Array(Box::new(DataType::Int(None))), + data_type: DataType::Array(Some(Box::new(DataType::Int(None)))), collation: None, options: vec![], }, @@ -2109,12 +2109,20 @@ fn parse_create_table_hive_array() { _ => unreachable!(), } - let res = - parse_sql_statements("CREATE TABLE IF NOT EXISTS something (name int, val array, found: )")); + // SnowflakeDialect using array diffrent + let dialects = TestedDialects { + dialects: vec![ + Box::new(PostgreSqlDialect {}), + Box::new(HiveDialect {}), + Box::new(MySqlDialect {}), + ], + }; + let sql = "CREATE TABLE IF NOT EXISTS something (name int, val array, found: )".to_string()) + ); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 67302e840..fbdb0a24e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1324,9 +1324,9 @@ fn parse_array_index_expr() { })], named: true, })), - data_type: DataType::Array(Box::new(DataType::Array(Box::new(DataType::Int( - None - ))))) + data_type: DataType::Array(Some(Box::new(DataType::Array(Some(Box::new( + DataType::Int(None) + )))))) }))), indexes: vec![num[1].clone(), num[2].clone()], }, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 64fff62f9..b182becb0 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -143,6 +143,19 @@ fn test_single_table_in_parenthesis_with_alias() { ); } +#[test] +fn parse_array() { + let sql = "SELECT CAST(a AS ARRAY) FROM customer"; + let select = snowflake().verified_only_select(sql); + assert_eq!( + &Expr::Cast { + expr: Box::new(Expr::Identifier(Ident::new("a"))), + data_type: DataType::Array(None), + }, + expr_from_projection(only(&select.projection)) + ); +} + #[test] fn parse_json_using_colon() { let sql = "SELECT a:b FROM t"; From f7817bc7c257b6db76bb19b83033a7f2f001313b Mon Sep 17 00:00:00 2001 From: unvalley <38400669+unvalley@users.noreply.github.com> Date: Mon, 7 Nov 2022 21:05:59 +0900 Subject: [PATCH 079/806] Support `DISTINCT` for SetOperator (#689) * Update SetOperation field all to op_option * Implement parse_set_operator_option cargo fmt Fix parse_set_operator_option after next_token * Add test for parsing union distinct * Rename to SetQualifier and fix fmt space * Add None to SetQualifier * Update parse method * Rename to SetQuantifier * Rename parse_set_operator_option parse_set_operator * Fix test to parse union, except, intersect * Add some comments to SetQuantifier * Fix comment --- src/ast/mod.rs | 4 ++-- src/ast/query.rs | 35 +++++++++++++++++++++++++++++++---- src/parser.rs | 27 ++++++++++++++++++++++++++- tests/sqlparser_common.rs | 6 +++++- tests/sqlparser_postgres.rs | 2 +- 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4cbd881c3..4ee4bab05 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -32,8 +32,8 @@ pub use self::ddl::{ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ Cte, Fetch, Join, JoinConstraint, JoinOperator, LateralView, LockType, Offset, OffsetRows, - OrderByExpr, Query, Select, SelectInto, SelectItem, SetExpr, SetOperator, TableAlias, - TableFactor, TableWithJoins, Top, Values, With, + OrderByExpr, Query, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, + TableAlias, TableFactor, TableWithJoins, Top, Values, With, }; pub use self::value::{DateTimeField, TrimWhereField, Value}; diff --git a/src/ast/query.rs b/src/ast/query.rs index 011d4658b..4f3d79cdf 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -78,7 +78,7 @@ pub enum SetExpr { /// UNION/EXCEPT/INTERSECT of two queries SetOperation { op: SetOperator, - all: bool, + set_quantifier: SetQuantifier, left: Box, right: Box, }, @@ -98,10 +98,17 @@ impl fmt::Display for SetExpr { left, right, op, - all, + set_quantifier, } => { - let all_str = if *all { " ALL" } else { "" }; - write!(f, "{} {}{} {}", left, op, all_str, right) + write!(f, "{} {}", left, op)?; + match set_quantifier { + SetQuantifier::All | SetQuantifier::Distinct => { + write!(f, " {}", set_quantifier)? + } + SetQuantifier::None => write!(f, "{}", set_quantifier)?, + } + write!(f, " {}", right)?; + Ok(()) } } } @@ -125,6 +132,26 @@ impl fmt::Display for SetOperator { } } +/// A quantifier for [SetOperator]. +// TODO: Restrict parsing specific SetQuantifier in some specific dialects. +// For example, BigQuery does not support `DISTINCT` for `EXCEPT` and `INTERSECT` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum SetQuantifier { + All, + Distinct, + None, +} + +impl fmt::Display for SetQuantifier { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SetQuantifier::All => write!(f, "ALL"), + SetQuantifier::Distinct => write!(f, "DISTINCT"), + SetQuantifier::None => write!(f, ""), + } + } +} /// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may /// appear either as the only body item of a `Query`, or as an operand /// to a set operation like `UNION`. diff --git a/src/parser.rs b/src/parser.rs index 76d845074..73af099d2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4199,10 +4199,11 @@ impl<'a> Parser<'a> { break; } self.next_token(); // skip past the set operator + let set_quantifier = self.parse_set_quantifier(&op); expr = SetExpr::SetOperation { left: Box::new(expr), op: op.unwrap(), - all: self.parse_keyword(Keyword::ALL), + set_quantifier, right: Box::new(self.parse_query_body(next_precedence)?), }; } @@ -4219,6 +4220,30 @@ impl<'a> Parser<'a> { } } + pub fn parse_set_quantifier(&mut self, op: &Option) -> SetQuantifier { + match op { + Some(SetOperator::Union) => { + if self.parse_keyword(Keyword::ALL) { + SetQuantifier::All + } else if self.parse_keyword(Keyword::DISTINCT) { + SetQuantifier::Distinct + } else { + SetQuantifier::None + } + } + Some(SetOperator::Except) | Some(SetOperator::Intersect) => { + if self.parse_keyword(Keyword::ALL) { + SetQuantifier::All + } else if self.parse_keyword(Keyword::DISTINCT) { + SetQuantifier::Distinct + } else { + SetQuantifier::None + } + } + _ => SetQuantifier::None, + } + } + /// Parse a restricted `SELECT` statement (no CTEs / `UNION` / `ORDER BY`), /// assuming the initial `SELECT` was already consumed pub fn parse_select(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a3d6ea766..52c6a56aa 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4072,14 +4072,17 @@ fn parse_derived_tables() { } #[test] -fn parse_union() { +fn parse_union_except_intersect() { // TODO: add assertions verified_stmt("SELECT 1 UNION SELECT 2"); verified_stmt("SELECT 1 UNION ALL SELECT 2"); + verified_stmt("SELECT 1 UNION DISTINCT SELECT 1"); verified_stmt("SELECT 1 EXCEPT SELECT 2"); verified_stmt("SELECT 1 EXCEPT ALL SELECT 2"); + verified_stmt("SELECT 1 EXCEPT DISTINCT SELECT 1"); verified_stmt("SELECT 1 INTERSECT SELECT 2"); verified_stmt("SELECT 1 INTERSECT ALL SELECT 2"); + verified_stmt("SELECT 1 INTERSECT DISTINCT SELECT 1"); verified_stmt("SELECT 1 UNION SELECT 2 UNION SELECT 3"); verified_stmt("SELECT 1 EXCEPT SELECT 2 UNION SELECT 3"); // Union[Except[1,2], 3] verified_stmt("SELECT 1 INTERSECT (SELECT 2 EXCEPT SELECT 3)"); @@ -4088,6 +4091,7 @@ fn parse_union() { verified_stmt("SELECT 1 UNION SELECT 2 INTERSECT SELECT 3"); // Union[1, Intersect[2,3]] verified_stmt("SELECT foo FROM tab UNION SELECT bar FROM TAB"); verified_stmt("(SELECT * FROM new EXCEPT SELECT * FROM old) UNION ALL (SELECT * FROM old EXCEPT SELECT * FROM new) ORDER BY 1"); + verified_stmt("(SELECT * FROM new EXCEPT DISTINCT SELECT * FROM old) UNION DISTINCT (SELECT * FROM old EXCEPT DISTINCT SELECT * FROM new) ORDER BY 1"); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fbdb0a24e..c19b264a2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1353,7 +1353,7 @@ fn parse_array_subquery_expr() { with: None, body: Box::new(SetExpr::SetOperation { op: SetOperator::Union, - all: false, + set_quantifier: SetQuantifier::None, left: Box::new(SetExpr::Select(Box::new(Select { distinct: false, top: None, From 0428ac742b204edd159be9fa0b2c3fb3ec542f14 Mon Sep 17 00:00:00 2001 From: omer-shtivi <85064354+omer-shtivi@users.noreply.github.com> Date: Mon, 7 Nov 2022 22:32:47 +0200 Subject: [PATCH 080/806] Add MySql, BigQuery to all dialects (#697) * Add MySql, BigQuery to all dialects * move unsupported on mysql from common --- src/ast/mod.rs | 2 +- src/parser.rs | 3 + src/test_utils.rs | 2 + tests/sqlparser_bigquery.rs | 109 ++++++++++++++++++++++ tests/sqlparser_clickhouse.rs | 162 +++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 162 --------------------------------- tests/sqlparser_hive.rs | 167 +++++++++++++++++++++++++++++++++- tests/sqlparser_mssql.rs | 162 +++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 162 +++++++++++++++++++++++++++++++++ tests/sqlparser_redshift.rs | 162 +++++++++++++++++++++++++++++++++ tests/sqlparser_snowflake.rs | 162 +++++++++++++++++++++++++++++++++ tests/sqlparser_sqlite.rs | 109 ++++++++++++++++++++++ 12 files changed, 1200 insertions(+), 164 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4ee4bab05..2b81f0b39 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -35,7 +35,7 @@ pub use self::query::{ OrderByExpr, Query, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, TableAlias, TableFactor, TableWithJoins, Top, Values, With, }; -pub use self::value::{DateTimeField, TrimWhereField, Value}; +pub use self::value::{escape_quoted_string, DateTimeField, TrimWhereField, Value}; mod data_type; mod ddl; diff --git a/src/parser.rs b/src/parser.rs index 73af099d2..0753d263e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3753,6 +3753,8 @@ impl<'a> Parser<'a> { // ignore the and treat the multiple strings as // a single ." Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))), + // Support for MySql dialect double qouted string, `AS "HOUR"` for example + Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))), not_an_ident => { if after_as { return self.expected("an identifier after AS", not_an_ident); @@ -3836,6 +3838,7 @@ impl<'a> Parser<'a> { match self.next_token() { Token::Word(w) => Ok(w.to_ident()), Token::SingleQuotedString(s) => Ok(Ident::with_quote('\'', s)), + Token::DoubleQuotedString(s) => Ok(Ident::with_quote('\"', s)), unexpected => self.expected("identifier", unexpected), } } diff --git a/src/test_utils.rs b/src/test_utils.rs index d51aec1ae..cbb929285 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -142,6 +142,8 @@ pub fn all_dialects() -> TestedDialects { Box::new(SnowflakeDialect {}), Box::new(HiveDialect {}), Box::new(RedshiftSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), ], } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 0a606c3ec..86b47ddad 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -115,6 +115,115 @@ fn parse_cast_type() { bigquery().verified_only_select(sql); } +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = bigquery().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = bigquery().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = bigquery().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = bigquery().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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = bigquery().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()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = bigquery().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + fn bigquery() -> TestedDialects { TestedDialects { dialects: vec![Box::new(BigQueryDialect {})], diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index a61df73cc..3e974d56d 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -152,6 +152,168 @@ fn parse_kill() { ); } +#[test] +fn parse_delimited_identifiers() { + // check that quoted identifiers in any position remain quoted after serialization + let select = clickhouse().verified_only_select( + r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#, + ); + // check FROM + match only(select.from).relation { + TableFactor::Table { + name, + alias, + args, + with_hints, + } => { + assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); + assert!(args.is_none()); + assert!(with_hints.is_empty()); + } + _ => panic!("Expecting TableFactor::Table"), + } + // check SELECT + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::CompoundIdentifier(vec![ + Ident::with_quote('"', "alias"), + Ident::with_quote('"', "bar baz"), + ]), + expr_from_projection(&select.projection[0]), + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + args: vec![], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(&select.projection[1]), + ); + match &select.projection[2] { + SelectItem::ExprWithAlias { expr, alias } => { + assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); + assert_eq!(&Ident::with_quote('"', "column alias"), alias); + } + _ => panic!("Expected ExprWithAlias"), + } + + clickhouse().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); + clickhouse().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); + //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); +} + +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = clickhouse().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = clickhouse().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = clickhouse().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = clickhouse().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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = clickhouse().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()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = clickhouse().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 52c6a56aa..4efb8cc7c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -886,61 +886,6 @@ fn parse_not_precedence() { ); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_null_like() { let sql = "SELECT \ @@ -1035,60 +980,6 @@ fn parse_ilike() { chk(true); } -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - 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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - 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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_in_list() { fn chk(negated: bool) { @@ -3453,59 +3344,6 @@ fn parse_unnest() { ); } -#[test] -fn parse_delimited_identifiers() { - // check that quoted identifiers in any position remain quoted after serialization - let select = verified_only_select( - r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#, - ); - // check FROM - match only(select.from).relation { - TableFactor::Table { - name, - alias, - args, - with_hints, - } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); - assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); - assert!(args.is_none()); - assert!(with_hints.is_empty()); - } - _ => panic!("Expecting TableFactor::Table"), - } - // check SELECT - assert_eq!(3, select.projection.len()); - assert_eq!( - &Expr::CompoundIdentifier(vec![ - Ident::with_quote('"', "alias"), - Ident::with_quote('"', "bar baz"), - ]), - expr_from_projection(&select.projection[0]), - ); - assert_eq!( - &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), - args: vec![], - over: None, - distinct: false, - special: false, - }), - expr_from_projection(&select.projection[1]), - ); - match &select.projection[2] { - SelectItem::ExprWithAlias { expr, alias } => { - assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); - assert_eq!(&Ident::with_quote('"', "column alias"), alias); - } - _ => panic!("Expected ExprWithAlias"), - } - - verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); - verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); - //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); -} - #[test] fn parse_parens() { use self::BinaryOperator::*; diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index c4df6b5dd..695f63b54 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -15,7 +15,10 @@ //! Test SQL syntax specific to Hive. The parser based on the generic dialect //! is also tested (on the inputs it can handle). -use sqlparser::ast::{CreateFunctionUsing, Expr, Ident, ObjectName, Statement, UnaryOperator}; +use sqlparser::ast::{ + CreateFunctionUsing, Expr, Function, Ident, ObjectName, SelectItem, Statement, TableFactor, + UnaryOperator, Value, +}; use sqlparser::dialect::{GenericDialect, HiveDialect}; use sqlparser::parser::ParserError; use sqlparser::test_utils::*; @@ -300,6 +303,168 @@ fn filter_as_alias() { println!("{}", hive().one_statement_parses_to(sql, expected)); } +#[test] +fn parse_delimited_identifiers() { + // check that quoted identifiers in any position remain quoted after serialization + let select = hive().verified_only_select( + r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#, + ); + // check FROM + match only(select.from).relation { + TableFactor::Table { + name, + alias, + args, + with_hints, + } => { + assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); + assert!(args.is_none()); + assert!(with_hints.is_empty()); + } + _ => panic!("Expecting TableFactor::Table"), + } + // check SELECT + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::CompoundIdentifier(vec![ + Ident::with_quote('"', "alias"), + Ident::with_quote('"', "bar baz"), + ]), + expr_from_projection(&select.projection[0]), + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + args: vec![], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(&select.projection[1]), + ); + match &select.projection[2] { + SelectItem::ExprWithAlias { expr, alias } => { + assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); + assert_eq!(&Ident::with_quote('"', "column alias"), alias); + } + _ => panic!("Expected ExprWithAlias"), + } + + hive().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); + hive().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); + //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); +} + +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = hive().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = hive().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = hive().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = hive().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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = hive().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()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = hive().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + fn hive() -> TestedDialects { TestedDialects { dialects: vec![Box::new(HiveDialect {})], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index de2376f92..41b0803e4 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -140,6 +140,168 @@ fn parse_mssql_create_role() { } } +#[test] +fn parse_delimited_identifiers() { + // check that quoted identifiers in any position remain quoted after serialization + let select = ms_and_generic().verified_only_select( + r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#, + ); + // check FROM + match only(select.from).relation { + TableFactor::Table { + name, + alias, + args, + with_hints, + } => { + assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); + assert!(args.is_none()); + assert!(with_hints.is_empty()); + } + _ => panic!("Expecting TableFactor::Table"), + } + // check SELECT + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::CompoundIdentifier(vec![ + Ident::with_quote('"', "alias"), + Ident::with_quote('"', "bar baz"), + ]), + expr_from_projection(&select.projection[0]), + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + args: vec![], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(&select.projection[1]), + ); + match &select.projection[2] { + SelectItem::ExprWithAlias { expr, alias } => { + assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); + assert_eq!(&Ident::with_quote('"', "column alias"), alias); + } + _ => panic!("Expected ExprWithAlias"), + } + + ms_and_generic().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); + ms_and_generic().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); + //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); +} + +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = ms_and_generic().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = ms_and_generic().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = ms_and_generic().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = ms_and_generic().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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = ms_and_generic().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()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = ms_and_generic().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c19b264a2..d53e1f575 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1966,3 +1966,165 @@ fn parse_create_role() { } } } + +#[test] +fn parse_delimited_identifiers() { + // check that quoted identifiers in any position remain quoted after serialization + let select = pg().verified_only_select( + r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#, + ); + // check FROM + match only(select.from).relation { + TableFactor::Table { + name, + alias, + args, + with_hints, + } => { + assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); + assert!(args.is_none()); + assert!(with_hints.is_empty()); + } + _ => panic!("Expecting TableFactor::Table"), + } + // check SELECT + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::CompoundIdentifier(vec![ + Ident::with_quote('"', "alias"), + Ident::with_quote('"', "bar baz"), + ]), + expr_from_projection(&select.projection[0]), + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + args: vec![], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(&select.projection[1]), + ); + match &select.projection[2] { + SelectItem::ExprWithAlias { expr, alias } => { + assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); + assert_eq!(&Ident::with_quote('"', "column alias"), alias); + } + _ => panic!("Expected ExprWithAlias"), + } + + pg().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); + pg().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); + //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); +} + +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = pg().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = pg().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = pg().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = pg().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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = pg().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()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = pg().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 6f77cf335..7597ee981 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -95,6 +95,168 @@ fn test_double_quotes_over_db_schema_table_name() { ); } +#[test] +fn parse_delimited_identifiers() { + // check that quoted identifiers in any position remain quoted after serialization + let select = redshift().verified_only_select( + r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#, + ); + // check FROM + match only(select.from).relation { + TableFactor::Table { + name, + alias, + args, + with_hints, + } => { + assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); + assert!(args.is_none()); + assert!(with_hints.is_empty()); + } + _ => panic!("Expecting TableFactor::Table"), + } + // check SELECT + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::CompoundIdentifier(vec![ + Ident::with_quote('"', "alias"), + Ident::with_quote('"', "bar baz"), + ]), + expr_from_projection(&select.projection[0]), + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + args: vec![], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(&select.projection[1]), + ); + match &select.projection[2] { + SelectItem::ExprWithAlias { expr, alias } => { + assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); + assert_eq!(&Ident::with_quote('"', "column alias"), alias); + } + _ => panic!("Expected ExprWithAlias"), + } + + redshift().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); + redshift().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); + //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); +} + +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = redshift().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = redshift().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = redshift().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = redshift().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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = redshift().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()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = redshift().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + fn redshift() -> TestedDialects { TestedDialects { dialects: vec![Box::new(RedshiftSqlDialect {})], diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b182becb0..2a53b0840 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -172,6 +172,168 @@ fn parse_json_using_colon() { snowflake().one_statement_parses_to("SELECT a:b::int FROM t", "SELECT CAST(a:b AS INT) FROM t"); } +#[test] +fn parse_delimited_identifiers() { + // check that quoted identifiers in any position remain quoted after serialization + let select = snowflake().verified_only_select( + r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#, + ); + // check FROM + match only(select.from).relation { + TableFactor::Table { + name, + alias, + args, + with_hints, + } => { + assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); + assert!(args.is_none()); + assert!(with_hints.is_empty()); + } + _ => panic!("Expecting TableFactor::Table"), + } + // check SELECT + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::CompoundIdentifier(vec![ + Ident::with_quote('"', "alias"), + Ident::with_quote('"', "bar baz"), + ]), + expr_from_projection(&select.projection[0]), + ); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + args: vec![], + over: None, + distinct: false, + special: false, + }), + expr_from_projection(&select.projection[1]), + ); + match &select.projection[2] { + SelectItem::ExprWithAlias { expr, alias } => { + assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); + assert_eq!(&Ident::with_quote('"', "column alias"), alias); + } + _ => panic!("Expected ExprWithAlias"), + } + + snowflake().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); + snowflake().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); + //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); +} + +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = snowflake().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = snowflake().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = snowflake().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = snowflake().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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = snowflake().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()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = snowflake().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + fn snowflake() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SnowflakeDialect {})], diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 3f60e16d1..8fc3a8d63 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -133,6 +133,115 @@ fn test_placeholder() { ); } +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = sqlite().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = sqlite().verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = sqlite().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = sqlite().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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = sqlite().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()))), + escape_char: Some('\\'), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = sqlite().verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('\\'), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + fn sqlite() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SQLiteDialect {})], From 87b4a168cbcd83439c4e8729dd992d150eebd15c Mon Sep 17 00:00:00 2001 From: SuperBo Date: Sat, 12 Nov 2022 03:25:07 +0700 Subject: [PATCH 081/806] Parse ARRAY_AGG for Bigquery and Snowflake (#662) --- src/ast/mod.rs | 42 +++++++++++++++++++++++++++++++ src/dialect/mod.rs | 6 +++++ src/dialect/snowflake.rs | 4 +++ src/parser.rs | 49 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_bigquery.rs | 11 ++++++++ tests/sqlparser_common.rs | 21 ++++++++++++++++ tests/sqlparser_hive.rs | 8 +++--- tests/sqlparser_snowflake.rs | 19 ++++++++++++++ 8 files changed, 156 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2b81f0b39..63c4c739f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -416,6 +416,8 @@ pub enum Expr { ArraySubquery(Box), /// The `LISTAGG` function `SELECT LISTAGG(...) WITHIN GROUP (ORDER BY ...)` ListAgg(ListAgg), + /// The `ARRAY_AGG` function `SELECT ARRAY_AGG(... ORDER BY ...)` + ArrayAgg(ArrayAgg), /// The `GROUPING SETS` expr. GroupingSets(Vec>), /// The `CUBE` expr. @@ -655,6 +657,7 @@ impl fmt::Display for Expr { Expr::Subquery(s) => write!(f, "({})", s), Expr::ArraySubquery(s) => write!(f, "ARRAY({})", s), Expr::ListAgg(listagg) => write!(f, "{}", listagg), + Expr::ArrayAgg(arrayagg) => write!(f, "{}", arrayagg), Expr::GroupingSets(sets) => { write!(f, "GROUPING SETS (")?; let mut sep = ""; @@ -3036,6 +3039,45 @@ impl fmt::Display for ListAggOnOverflow { } } +/// An `ARRAY_AGG` invocation `ARRAY_AGG( [ DISTINCT ] [ORDER BY ] [LIMIT ] )` +/// Or `ARRAY_AGG( [ DISTINCT ] ) [ WITHIN GROUP ( ORDER BY ) ]` +/// ORDER BY position is defined differently for BigQuery, Postgres and Snowflake. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ArrayAgg { + pub distinct: bool, + pub expr: Box, + pub order_by: Option>, + pub limit: Option>, + pub within_group: bool, // order by is used inside a within group or not +} + +impl fmt::Display for ArrayAgg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ARRAY_AGG({}{}", + if self.distinct { "DISTINCT " } else { "" }, + self.expr + )?; + if !self.within_group { + if let Some(order_by) = &self.order_by { + write!(f, " ORDER BY {}", order_by)?; + } + if let Some(limit) = &self.limit { + write!(f, " LIMIT {}", limit)?; + } + } + write!(f, ")")?; + if self.within_group { + if let Some(order_by) = &self.order_by { + write!(f, " WITHIN GROUP (ORDER BY {})", order_by)?; + } + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ObjectType { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 1d3c9cf5f..1eaa41aa7 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -71,6 +71,12 @@ pub trait Dialect: Debug + Any { fn supports_filter_during_aggregation(&self) -> bool { false } + /// Returns true if the dialect supports ARRAY_AGG() [WITHIN GROUP (ORDER BY)] expressions. + /// Otherwise, the dialect should expect an `ORDER BY` without the `WITHIN GROUP` clause, e.g. `ANSI` [(1)]. + /// [(1)]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#array-aggregate-function + fn supports_within_after_array_aggregation(&self) -> bool { + false + } /// Dialect-specific prefix parser override fn parse_prefix(&self, _parser: &mut Parser) -> Option> { // return None to fall back to the default behavior diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 93db95692..11108e973 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -28,4 +28,8 @@ impl Dialect for SnowflakeDialect { || ch == '$' || ch == '_' } + + fn supports_within_after_array_aggregation(&self) -> bool { + true + } } diff --git a/src/parser.rs b/src/parser.rs index 0753d263e..201b344de 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -473,6 +473,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; self.parse_array_subquery() } + Keyword::ARRAY_AGG => self.parse_array_agg_expr(), Keyword::NOT => self.parse_not(), // Here `w` is a word, check if it's a part of a multi-part // identifier, a function call, or a simple identifier: @@ -1071,6 +1072,54 @@ impl<'a> Parser<'a> { })) } + pub fn parse_array_agg_expr(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let distinct = self.parse_keyword(Keyword::DISTINCT); + let expr = Box::new(self.parse_expr()?); + // ANSI SQL and BigQuery define ORDER BY inside function. + if !self.dialect.supports_within_after_array_aggregation() { + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + let order_by_expr = self.parse_order_by_expr()?; + Some(Box::new(order_by_expr)) + } else { + None + }; + let limit = if self.parse_keyword(Keyword::LIMIT) { + self.parse_limit()?.map(Box::new) + } else { + None + }; + self.expect_token(&Token::RParen)?; + return Ok(Expr::ArrayAgg(ArrayAgg { + distinct, + expr, + order_by, + limit, + within_group: false, + })); + } + // Snowflake defines ORDERY BY in within group instead of inside the function like + // ANSI SQL. + self.expect_token(&Token::RParen)?; + let within_group = if self.parse_keywords(&[Keyword::WITHIN, Keyword::GROUP]) { + self.expect_token(&Token::LParen)?; + self.expect_keywords(&[Keyword::ORDER, Keyword::BY])?; + let order_by_expr = self.parse_order_by_expr()?; + self.expect_token(&Token::RParen)?; + Some(Box::new(order_by_expr)) + } else { + None + }; + + Ok(Expr::ArrayAgg(ArrayAgg { + distinct, + expr, + order_by: within_group, + limit: None, + within_group: true, + })) + } + // This function parses date/time fields for the EXTRACT function-like // operator, interval qualifiers, and the ceil/floor operations. // EXTRACT supports a wider set of date/time fields than interval qualifiers, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 86b47ddad..8ada172cf 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -224,6 +224,17 @@ fn parse_similar_to() { chk(true); } +#[test] +fn parse_array_agg_func() { + for sql in [ + "SELECT ARRAY_AGG(x ORDER BY x) AS a FROM T", + "SELECT ARRAY_AGG(x ORDER BY x LIMIT 2) FROM tbl", + "SELECT ARRAY_AGG(DISTINCT x ORDER BY x LIMIT 2) FROM tbl", + ] { + bigquery().verified_stmt(sql); + } +} + fn bigquery() -> TestedDialects { TestedDialects { dialects: vec![Box::new(BigQueryDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4efb8cc7c..e3390a479 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1777,6 +1777,27 @@ fn parse_listagg() { ); } +#[test] +fn parse_array_agg_func() { + let supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(HiveDialect {}), + ], + }; + + for sql in [ + "SELECT ARRAY_AGG(x ORDER BY x) AS a FROM T", + "SELECT ARRAY_AGG(x ORDER BY x LIMIT 2) FROM tbl", + "SELECT ARRAY_AGG(DISTINCT x ORDER BY x LIMIT 2) FROM tbl", + ] { + supported_dialects.verified_stmt(sql); + } +} + #[test] fn parse_create_table() { let sql = "CREATE TABLE uk_cities (\ diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 695f63b54..070f55089 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -281,8 +281,8 @@ fn parse_create_function() { #[test] fn filtering_during_aggregation() { let rename = "SELECT \ - array_agg(name) FILTER (WHERE name IS NOT NULL), \ - array_agg(name) FILTER (WHERE name LIKE 'a%') \ + ARRAY_AGG(name) FILTER (WHERE name IS NOT NULL), \ + ARRAY_AGG(name) FILTER (WHERE name LIKE 'a%') \ FROM region"; println!("{}", hive().verified_stmt(rename)); } @@ -290,8 +290,8 @@ fn filtering_during_aggregation() { #[test] fn filtering_during_aggregation_aliased() { let rename = "SELECT \ - array_agg(name) FILTER (WHERE name IS NOT NULL) AS agg1, \ - array_agg(name) FILTER (WHERE name LIKE 'a%') AS agg2 \ + ARRAY_AGG(name) FILTER (WHERE name IS NOT NULL) AS agg1, \ + ARRAY_AGG(name) FILTER (WHERE name LIKE 'a%') AS agg2 \ FROM region"; println!("{}", hive().verified_stmt(rename)); } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2a53b0840..a201c5db7 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -334,6 +334,25 @@ fn parse_similar_to() { chk(true); } +#[test] +fn test_array_agg_func() { + for sql in [ + "SELECT ARRAY_AGG(x) WITHIN GROUP (ORDER BY x) AS a FROM T", + "SELECT ARRAY_AGG(DISTINCT x) WITHIN GROUP (ORDER BY x ASC) FROM tbl", + ] { + snowflake().verified_stmt(sql); + } + + let sql = "select array_agg(x order by x) as a from T"; + let result = snowflake().parse_sql_statements(sql); + assert_eq!( + result, + Err(ParserError::ParserError(String::from( + "Expected ), found: order" + ))) + ) +} + fn snowflake() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SnowflakeDialect {})], From cdf444706573714cc71f3f4e8fe93a43d740c6bc Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Fri, 11 Nov 2022 18:03:39 -0300 Subject: [PATCH 082/806] feat: add FULLTEXT option on create table for MySQL and Generic dialects (#702) --- src/ast/ddl.rs | 80 ++++++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 2 +- src/keywords.rs | 2 + src/parser.rs | 32 ++++++++++++++++ tests/sqlparser_mysql.rs | 51 ++++++++++++++++++++++--- 5 files changed, 161 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c758b4f55..3203f0e20 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -270,6 +270,28 @@ pub enum TableConstraint { /// Referred column identifier list. 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. + /// + /// Supported syntax: + /// + /// ```markdown + /// {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...) + /// + /// key_part: col_name + /// ``` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html + FulltextOrSpatial { + /// Whether this is a `FULLTEXT` (true) or `SPATIAL` (false) definition. + fulltext: bool, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + index_type_display: KeyOrIndexDisplay, + /// Optional index name. + opt_index_name: Option, + /// Referred column identifier list. + columns: Vec, + }, } impl fmt::Display for TableConstraint { @@ -330,6 +352,64 @@ impl fmt::Display for TableConstraint { Ok(()) } + Self::FulltextOrSpatial { + fulltext, + index_type_display, + opt_index_name, + columns, + } => { + if *fulltext { + write!(f, "FULLTEXT")?; + } else { + write!(f, "SPATIAL")?; + } + + if !matches!(index_type_display, KeyOrIndexDisplay::None) { + write!(f, " {}", index_type_display)?; + } + + if let Some(name) = opt_index_name { + write!(f, " {}", name)?; + } + + write!(f, " ({})", display_comma_separated(columns))?; + + Ok(()) + } + } + } +} + +/// Representation whether a definition can can contains the KEY or INDEX keywords with the same +/// meaning. +/// +/// This enum initially is directed to `FULLTEXT`,`SPATIAL`, and `UNIQUE` indexes on create table +/// statements of `MySQL` [(1)]. +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyOrIndexDisplay { + /// Nothing to display + None, + /// Display the KEY keyword + Key, + /// Display the INDEX keyword + Index, +} + +impl fmt::Display for KeyOrIndexDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeyOrIndexDisplay::None => { + write!(f, "") + } + KeyOrIndexDisplay::Key => { + write!(f, "KEY") + } + KeyOrIndexDisplay::Index => { + write!(f, "INDEX") + } } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 63c4c739f..931bff729 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -27,7 +27,7 @@ pub use self::data_type::{ }; pub use self::ddl::{ AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, IndexType, - ReferentialAction, TableConstraint, + KeyOrIndexDisplay, ReferentialAction, TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ diff --git a/src/keywords.rs b/src/keywords.rs index f8d125067..bda144d02 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -255,6 +255,7 @@ define_keywords!( FREEZE, FROM, FULL, + FULLTEXT, FUNCTION, FUNCTIONS, FUSION, @@ -498,6 +499,7 @@ define_keywords!( SNAPSHOT, SOME, SORT, + SPATIAL, SPECIFIC, SPECIFICTYPE, SQL, diff --git a/src/parser.rs b/src/parser.rs index 201b344de..924318c0d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3085,6 +3085,38 @@ impl<'a> Parser<'a> { columns, })) } + Token::Word(w) + if (w.keyword == Keyword::FULLTEXT || w.keyword == Keyword::SPATIAL) + && dialect_of!(self is GenericDialect | MySqlDialect) => + { + if let Some(name) = name { + return self.expected( + "FULLTEXT or SPATIAL option without constraint name", + Token::make_keyword(&name.to_string()), + ); + } + + let fulltext = w.keyword == Keyword::FULLTEXT; + + let index_type_display = if self.parse_keyword(Keyword::KEY) { + KeyOrIndexDisplay::Key + } else if self.parse_keyword(Keyword::INDEX) { + KeyOrIndexDisplay::Index + } else { + KeyOrIndexDisplay::None + }; + + let opt_index_name = self.maybe_parse(|parser| parser.parse_identifier()); + + let columns = self.parse_parenthesized_column_list(Mandatory)?; + + Ok(Some(TableConstraint::FulltextOrSpatial { + fulltext, + index_type_display, + opt_index_name, + columns, + })) + } unexpected => { if name.is_some() { self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", unexpected) diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 52c0205d6..fb205e29a 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -14,16 +14,15 @@ //! Test SQL syntax specific to MySQL. The parser based on the generic dialect //! is also tested (on the inputs it can handle). -#[macro_use] -mod test_utils; - -use test_utils::*; - use sqlparser::ast::Expr; use sqlparser::ast::Value; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; use sqlparser::tokenizer::Token; +use test_utils::*; + +#[macro_use] +mod test_utils; #[test] fn parse_identifiers() { @@ -1129,6 +1128,48 @@ fn parse_create_table_with_index_definition() { ); } +#[test] +fn parse_create_table_with_fulltext_definition() { + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT INDEX (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT KEY (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT potato (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT INDEX potato (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT KEY potato (id))"); + + mysql_and_generic() + .verified_stmt("CREATE TABLE tb (c1 INT, c2 INT, FULLTEXT KEY potato (c1, c2))"); +} + +#[test] +fn parse_create_table_with_spatial_definition() { + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL INDEX (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL KEY (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL potato (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL INDEX potato (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL KEY potato (id))"); + + mysql_and_generic() + .verified_stmt("CREATE TABLE tb (c1 INT, c2 INT, SPATIAL KEY potato (c1, c2))"); +} + +#[test] +#[should_panic = "Expected FULLTEXT or SPATIAL option without constraint name, found: cons"] +fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name() { + mysql_and_generic().verified_stmt("CREATE TABLE tb (c1 INT, CONSTRAINT cons FULLTEXT (c1))"); +} + fn mysql() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})], From 65c5a37bcebb5ff53b46dfc28bca6aee65efd020 Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Fri, 11 Nov 2022 18:06:57 -0300 Subject: [PATCH 083/806] feat: add precision for TIME, DATETIME, and TIMESTAMP data types (#701) Now all those statements are both parsed and displayed with precision and timezone info. Tests were added to the ones presented in the ANSI standard. --- src/ast/data_type.rs | 53 ++++++++++++++++++----- src/parser.rs | 83 ++++++++++++++++++++++++++----------- tests/sqlparser_common.rs | 8 ++-- tests/sqlparser_mysql.rs | 2 +- tests/sqlparser_postgres.rs | 2 +- 5 files changed, 108 insertions(+), 40 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index c0b2f977b..12a95a4fc 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -11,7 +11,7 @@ // limitations under the License. #[cfg(not(feature = "std"))] -use alloc::{boxed::Box, string::String, vec::Vec}; +use alloc::{boxed::Box, format, string::String, vec::Vec}; use core::fmt; #[cfg(feature = "serde")] @@ -122,12 +122,18 @@ pub enum DataType { Boolean, /// Date Date, - /// Time - Time(TimezoneInfo), - /// Datetime - Datetime, - /// Timestamp - Timestamp(TimezoneInfo), + /// Time with optional time precision and time zone information e.g. [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]. + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/datetime.html + Datetime(Option), + /// Timestamp with optional time precision and time zone information e.g. [standard][1]. + /// + /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type + Timestamp(Option, TimezoneInfo), /// Interval Interval, /// Regclass used in postgresql serial @@ -224,9 +230,15 @@ impl fmt::Display for DataType { DataType::DoublePrecision => write!(f, "DOUBLE PRECISION"), DataType::Boolean => write!(f, "BOOLEAN"), DataType::Date => write!(f, "DATE"), - DataType::Time(timezone_info) => write!(f, "TIME{}", timezone_info), - DataType::Datetime => write!(f, "DATETIME"), - DataType::Timestamp(timezone_info) => write!(f, "TIMESTAMP{}", timezone_info), + DataType::Time(precision, timezone_info) => { + format_datetime_precision_and_tz(f, "TIME", precision, timezone_info) + } + DataType::Datetime(precision) => { + format_type_with_optional_length(f, "DATETIME", precision, false) + } + DataType::Timestamp(precision, timezone_info) => { + format_datetime_precision_and_tz(f, "TIMESTAMP", precision, timezone_info) + } DataType::Interval => write!(f, "INTERVAL"), DataType::Regclass => write!(f, "REGCLASS"), DataType::Text => write!(f, "TEXT"), @@ -298,6 +310,27 @@ fn format_character_string_type( Ok(()) } +fn format_datetime_precision_and_tz( + f: &mut fmt::Formatter, + sql_type: &'static str, + len: &Option, + time_zone: &TimezoneInfo, +) -> fmt::Result { + write!(f, "{}", sql_type)?; + let len_fmt = len.as_ref().map(|l| format!("({l})")).unwrap_or_default(); + + match time_zone { + TimezoneInfo::Tz => { + write!(f, "{time_zone}{len_fmt}")?; + } + _ => { + write!(f, "{len_fmt}{time_zone}")?; + } + } + + Ok(()) +} + /// Timestamp and Time data types information about TimeZone formatting. /// /// This is more related to a display information than real differences between each variant. To diff --git a/src/parser.rs b/src/parser.rs index 924318c0d..3cac793dd 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3708,31 +3708,41 @@ impl<'a> Parser<'a> { Keyword::BLOB => Ok(DataType::Blob(self.parse_optional_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), - Keyword::DATETIME => Ok(DataType::Datetime), + Keyword::DATETIME => Ok(DataType::Datetime(self.parse_optional_precision()?)), Keyword::TIMESTAMP => { - if self.parse_keyword(Keyword::WITH) { + let precision = self.parse_optional_precision()?; + let tz = if self.parse_keyword(Keyword::WITH) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; - Ok(DataType::Timestamp(TimezoneInfo::WithTimeZone)) + TimezoneInfo::WithTimeZone } else if self.parse_keyword(Keyword::WITHOUT) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; - Ok(DataType::Timestamp(TimezoneInfo::WithoutTimeZone)) + TimezoneInfo::WithoutTimeZone } else { - Ok(DataType::Timestamp(TimezoneInfo::None)) - } + TimezoneInfo::None + }; + Ok(DataType::Timestamp(precision, tz)) } - Keyword::TIMESTAMPTZ => Ok(DataType::Timestamp(TimezoneInfo::Tz)), + Keyword::TIMESTAMPTZ => Ok(DataType::Timestamp( + self.parse_optional_precision()?, + TimezoneInfo::Tz, + )), Keyword::TIME => { - if self.parse_keyword(Keyword::WITH) { + let precision = self.parse_optional_precision()?; + let tz = if self.parse_keyword(Keyword::WITH) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; - Ok(DataType::Time(TimezoneInfo::WithTimeZone)) + TimezoneInfo::WithTimeZone } else if self.parse_keyword(Keyword::WITHOUT) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; - Ok(DataType::Time(TimezoneInfo::WithoutTimeZone)) + TimezoneInfo::WithoutTimeZone } else { - Ok(DataType::Time(TimezoneInfo::None)) - } + TimezoneInfo::None + }; + Ok(DataType::Time(precision, tz)) } - Keyword::TIMETZ => Ok(DataType::Time(TimezoneInfo::Tz)), + Keyword::TIMETZ => Ok(DataType::Time( + self.parse_optional_precision()?, + TimezoneInfo::Tz, + )), // Interval types can be followed by a complicated interval // qualifier that we don't currently support. See // parse_interval for a taste. @@ -5789,6 +5799,7 @@ mod tests { #[cfg(test)] mod test_parse_data_type { + use crate::ast::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, ObjectName, TimezoneInfo, }; @@ -5799,8 +5810,8 @@ mod tests { ($dialect:expr, $input:expr, $expected_type:expr $(,)?) => {{ $dialect.run_parser_method(&*$input, |parser| { let data_type = parser.parse_data_type().unwrap(); - assert_eq!(data_type, $expected_type); - assert_eq!(data_type.to_string(), $input.to_string()); + assert_eq!($expected_type, data_type); + assert_eq!($input.to_string(), data_type.to_string()); }); }}; } @@ -6048,7 +6059,7 @@ mod tests { } #[test] - fn test_ansii_datetime_types() { + fn test_ansii_date_type() { // Datetime types: let dialect = TestedDialects { dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], @@ -6056,36 +6067,60 @@ mod tests { test_parse_data_type!(dialect, "DATE", DataType::Date); - test_parse_data_type!(dialect, "TIME", DataType::Time(TimezoneInfo::None)); + test_parse_data_type!(dialect, "TIME", DataType::Time(None, TimezoneInfo::None)); + + test_parse_data_type!( + dialect, + "TIME(6)", + DataType::Time(Some(6), TimezoneInfo::None) + ); test_parse_data_type!( dialect, "TIME WITH TIME ZONE", - DataType::Time(TimezoneInfo::WithTimeZone) + DataType::Time(None, TimezoneInfo::WithTimeZone) + ); + + test_parse_data_type!( + dialect, + "TIME(6) WITH TIME ZONE", + DataType::Time(Some(6), TimezoneInfo::WithTimeZone) ); test_parse_data_type!( dialect, "TIME WITHOUT TIME ZONE", - DataType::Time(TimezoneInfo::WithoutTimeZone) + DataType::Time(None, TimezoneInfo::WithoutTimeZone) + ); + + test_parse_data_type!( + dialect, + "TIME(6) WITHOUT TIME ZONE", + DataType::Time(Some(6), TimezoneInfo::WithoutTimeZone) ); test_parse_data_type!( dialect, "TIMESTAMP", - DataType::Timestamp(TimezoneInfo::None) + DataType::Timestamp(None, TimezoneInfo::None) + ); + + test_parse_data_type!( + dialect, + "TIMESTAMP(22)", + DataType::Timestamp(Some(22), TimezoneInfo::None) ); test_parse_data_type!( dialect, - "TIMESTAMP WITH TIME ZONE", - DataType::Timestamp(TimezoneInfo::WithTimeZone) + "TIMESTAMP(22) WITH TIME ZONE", + DataType::Timestamp(Some(22), TimezoneInfo::WithTimeZone) ); test_parse_data_type!( dialect, - "TIMESTAMP WITHOUT TIME ZONE", - DataType::Timestamp(TimezoneInfo::WithoutTimeZone) + "TIMESTAMP(33) WITHOUT TIME ZONE", + DataType::Timestamp(Some(33), TimezoneInfo::WithoutTimeZone) ); } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e3390a479..d093ae3be 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2944,7 +2944,7 @@ fn parse_literal_time() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::Time(TimezoneInfo::None), + data_type: DataType::Time(None, TimezoneInfo::None), value: "01:23:34".into(), }, expr_from_projection(only(&select.projection)), @@ -2957,7 +2957,7 @@ fn parse_literal_datetime() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::Datetime, + data_type: DataType::Datetime(None), value: "1999-01-01 01:23:34.45".into(), }, expr_from_projection(only(&select.projection)), @@ -2970,7 +2970,7 @@ fn parse_literal_timestamp_without_time_zone() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::Timestamp(TimezoneInfo::None), + data_type: DataType::Timestamp(None, TimezoneInfo::None), value: "1999-01-01 01:23:34".into(), }, expr_from_projection(only(&select.projection)), @@ -2985,7 +2985,7 @@ fn parse_literal_timestamp_with_time_zone() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::Timestamp(TimezoneInfo::Tz), + data_type: DataType::Timestamp(None, TimezoneInfo::Tz), value: "1999-01-01 01:23:34Z".into(), }, expr_from_projection(only(&select.projection)), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index fb205e29a..e0b34e728 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1026,7 +1026,7 @@ fn parse_table_colum_option_on_update() { assert_eq!( vec![ColumnDef { name: Ident::with_quote('`', "modification_time"), - data_type: DataType::Datetime, + data_type: DataType::Datetime(None), collation: None, options: vec![ColumnOptionDef { name: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d53e1f575..6a0672b20 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -227,7 +227,7 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "last_update".into(), - data_type: DataType::Timestamp(TimezoneInfo::WithoutTimeZone), + data_type: DataType::Timestamp(None, TimezoneInfo::WithoutTimeZone), collation: None, options: vec![ ColumnOptionDef { From ae1c69034e1a6924bceb54d4eb6eb8b14919d84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= Date: Fri, 11 Nov 2022 21:15:05 +0000 Subject: [PATCH 084/806] Fix broken DataFusion link (#703) --- docs/custom_sql_parser.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/custom_sql_parser.md b/docs/custom_sql_parser.md index 225fc8629..f7c9635f2 100644 --- a/docs/custom_sql_parser.md +++ b/docs/custom_sql_parser.md @@ -6,5 +6,5 @@ The concept is simply to write a new parser that delegates to the ANSI parser so I also plan on building in specific support for custom data types, where a lambda function can be parsed to the parser to parse data types. -For an example of this, see the [DataFusion](https://github.com/datafusion-rs/datafusion) project. +For an example of this, see the [DataFusion](https://github.com/apache/arrow-datafusion) project and its [query planner] (https://github.com/apache/arrow-datafusion/tree/master/datafusion/sql). From 814367a6abe319bb8c9e030a4a804cd6c9cf2231 Mon Sep 17 00:00:00 2001 From: "main()" Date: Fri, 11 Nov 2022 22:15:31 +0100 Subject: [PATCH 085/806] Implement ON CONFLICT and RETURNING (#666) * Implement RETURNING on INSERT/UPDATE/DELETE * Implement INSERT ... ON CONFLICT * Fix tests * cargo fmt * tests: on conflict and returning Co-authored-by: gamife --- src/ast/mod.rs | 58 ++++++++++++++- src/keywords.rs | 4 ++ src/parser.rs | 50 +++++++++++-- tests/sqlparser_common.rs | 6 ++ tests/sqlparser_mysql.rs | 2 + tests/sqlparser_postgres.rs | 138 ++++++++++++++++++++++++++++++++++++ 6 files changed, 250 insertions(+), 8 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 931bff729..f568c3960 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1049,6 +1049,8 @@ pub enum Statement { /// whether the insert has the table keyword (Hive) table: bool, on: Option, + /// RETURNING + returning: Option>, }, // TODO: Support ROW FORMAT Directory { @@ -1089,6 +1091,8 @@ pub enum Statement { from: Option, /// WHERE selection: Option, + /// RETURNING + returning: Option>, }, /// DELETE Delete { @@ -1098,6 +1102,8 @@ pub enum Statement { using: Option, /// WHERE selection: Option, + /// RETURNING + returning: Option>, }, /// CREATE VIEW CreateView { @@ -1679,6 +1685,7 @@ impl fmt::Display for Statement { source, table, on, + returning, } => { if let Some(action) = or { write!(f, "INSERT OR {} INTO {} ", action, table_name)?; @@ -1706,10 +1713,14 @@ impl fmt::Display for Statement { write!(f, "{}", source)?; if let Some(on) = on { - write!(f, "{}", on) - } else { - Ok(()) + write!(f, "{}", on)?; + } + + if let Some(returning) = returning { + write!(f, " RETURNING {}", display_comma_separated(returning))?; } + + Ok(()) } Statement::Copy { @@ -1753,6 +1764,7 @@ impl fmt::Display for Statement { assignments, from, selection, + returning, } => { write!(f, "UPDATE {}", table)?; if !assignments.is_empty() { @@ -1764,12 +1776,16 @@ impl fmt::Display for Statement { if let Some(selection) = selection { write!(f, " WHERE {}", selection)?; } + if let Some(returning) = returning { + write!(f, " RETURNING {}", display_comma_separated(returning))?; + } Ok(()) } Statement::Delete { table_name, using, selection, + returning, } => { write!(f, "DELETE FROM {}", table_name)?; if let Some(using) = using { @@ -1778,6 +1794,9 @@ impl fmt::Display for Statement { if let Some(selection) = selection { write!(f, " WHERE {}", selection)?; } + if let Some(returning) = returning { + write!(f, " RETURNING {}", display_comma_separated(returning))?; + } Ok(()) } Statement::Close { cursor } => { @@ -2610,6 +2629,21 @@ pub enum MinMaxValue { pub enum OnInsert { /// ON DUPLICATE KEY UPDATE (MySQL when the key already exists, then execute an update instead) DuplicateKeyUpdate(Vec), + /// ON CONFLICT is a PostgreSQL and Sqlite extension + OnConflict(OnConflict), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct OnConflict { + pub conflict_target: Vec, + pub action: OnConflictAction, +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum OnConflictAction { + DoNothing, + DoUpdate(Vec), } impl fmt::Display for OnInsert { @@ -2620,6 +2654,24 @@ impl fmt::Display for OnInsert { " ON DUPLICATE KEY UPDATE {}", display_comma_separated(expr) ), + Self::OnConflict(o) => write!(f, " {o}"), + } + } +} +impl fmt::Display for OnConflict { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, " ON CONFLICT")?; + if !self.conflict_target.is_empty() { + write!(f, "({})", display_comma_separated(&self.conflict_target))?; + } + write!(f, " {}", self.action) + } +} +impl fmt::Display for OnConflictAction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::DoNothing => write!(f, "DO NOTHING"), + Self::DoUpdate(a) => write!(f, "DO UPDATE SET {}", display_comma_separated(a)), } } } diff --git a/src/keywords.rs b/src/keywords.rs index bda144d02..47dd21e21 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -144,6 +144,7 @@ define_keywords!( COMMITTED, COMPUTE, CONDITION, + CONFLICT, CONNECT, CONNECTION, CONSTRAINT, @@ -200,6 +201,7 @@ define_keywords!( DISCONNECT, DISTINCT, DISTRIBUTE, + DO, DOUBLE, DOW, DOY, @@ -370,6 +372,7 @@ define_keywords!( NOSCAN, NOSUPERUSER, NOT, + NOTHING, NTH_VALUE, NTILE, NULL, @@ -464,6 +467,7 @@ define_keywords!( RESTRICT, RESULT, RETURN, + RETURNING, RETURNS, REVOKE, RIGHT, diff --git a/src/parser.rs b/src/parser.rs index 3cac793dd..0dbc4aa36 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4070,10 +4070,17 @@ impl<'a> Parser<'a> { None }; + let returning = if self.parse_keyword(Keyword::RETURNING) { + Some(self.parse_comma_separated(Parser::parse_select_item)?) + } else { + None + }; + Ok(Statement::Delete { table_name, using, selection, + returning, }) } @@ -5191,12 +5198,38 @@ impl<'a> Parser<'a> { let source = Box::new(self.parse_query()?); let on = if self.parse_keyword(Keyword::ON) { - self.expect_keyword(Keyword::DUPLICATE)?; - self.expect_keyword(Keyword::KEY)?; - self.expect_keyword(Keyword::UPDATE)?; - let l = self.parse_comma_separated(Parser::parse_assignment)?; + if self.parse_keyword(Keyword::CONFLICT) { + let conflict_target = + self.parse_parenthesized_column_list(IsOptional::Optional)?; - Some(OnInsert::DuplicateKeyUpdate(l)) + self.expect_keyword(Keyword::DO)?; + let action = if self.parse_keyword(Keyword::NOTHING) { + OnConflictAction::DoNothing + } else { + self.expect_keyword(Keyword::UPDATE)?; + self.expect_keyword(Keyword::SET)?; + let l = self.parse_comma_separated(Parser::parse_assignment)?; + OnConflictAction::DoUpdate(l) + }; + + Some(OnInsert::OnConflict(OnConflict { + conflict_target, + action, + })) + } else { + self.expect_keyword(Keyword::DUPLICATE)?; + self.expect_keyword(Keyword::KEY)?; + self.expect_keyword(Keyword::UPDATE)?; + let l = self.parse_comma_separated(Parser::parse_assignment)?; + + Some(OnInsert::DuplicateKeyUpdate(l)) + } + } else { + None + }; + + let returning = if self.parse_keyword(Keyword::RETURNING) { + Some(self.parse_comma_separated(Parser::parse_select_item)?) } else { None }; @@ -5212,6 +5245,7 @@ impl<'a> Parser<'a> { source, table, on, + returning, }) } } @@ -5230,11 +5264,17 @@ impl<'a> Parser<'a> { } else { None }; + let returning = if self.parse_keyword(Keyword::RETURNING) { + Some(self.parse_comma_separated(Parser::parse_select_item)?) + } else { + None + }; Ok(Statement::Update { table, assignments, from, selection, + returning, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d093ae3be..93439cafe 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -195,6 +195,7 @@ fn parse_update_with_table_alias() { assignments, from: _from, selection, + returning, } => { assert_eq!( TableWithJoins { @@ -231,6 +232,7 @@ fn parse_update_with_table_alias() { }), selection ); + assert_eq!(None, returning); } _ => unreachable!(), } @@ -278,6 +280,7 @@ fn parse_where_delete_statement() { table_name, using, selection, + returning, } => { assert_eq!( TableFactor::Table { @@ -298,6 +301,7 @@ fn parse_where_delete_statement() { }, selection.unwrap(), ); + assert_eq!(None, returning); } _ => unreachable!(), } @@ -313,6 +317,7 @@ fn parse_where_delete_with_alias_statement() { table_name, using, selection, + returning, } => { assert_eq!( TableFactor::Table { @@ -353,6 +358,7 @@ fn parse_where_delete_with_alias_statement() { }, selection.unwrap(), ); + assert_eq!(None, returning); } _ => unreachable!(), } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e0b34e728..e91cea0ef 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -814,6 +814,7 @@ fn parse_update_with_joins() { assignments, from: _from, selection, + returning, } => { assert_eq!( TableWithJoins { @@ -869,6 +870,7 @@ fn parse_update_with_joins() { }), selection ); + assert_eq!(None, returning); } _ => unreachable!(), } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6a0672b20..6cce4fdb9 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -564,6 +564,7 @@ fn parse_update_set_from() { Ident::new("id") ])), }), + returning: None, } ); } @@ -1177,6 +1178,143 @@ fn parse_prepare() { ); } +#[test] +fn parse_pg_on_conflict() { + let stmt = pg_and_generic().verified_stmt( + "INSERT INTO distributors (did, dname) \ + VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') \ + ON CONFLICT(did) \ + DO UPDATE SET dname = EXCLUDED.dname", + ); + match stmt { + Statement::Insert { + on: + Some(OnInsert::OnConflict(OnConflict { + conflict_target, + action, + })), + .. + } => { + assert_eq!(vec![Ident::from("did")], conflict_target); + assert_eq!( + OnConflictAction::DoUpdate(vec![Assignment { + id: vec!["dname".into()], + value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "dname".into()]) + },]), + action + ); + } + _ => unreachable!(), + }; + + let stmt = pg_and_generic().verified_stmt( + "INSERT INTO distributors (did, dname, area) \ + VALUES (5, 'Gizmo Transglobal', 'Mars'), (6, 'Associated Computing, Inc', 'Venus') \ + ON CONFLICT(did, area) \ + DO UPDATE SET dname = EXCLUDED.dname, area = EXCLUDED.area", + ); + match stmt { + Statement::Insert { + on: + Some(OnInsert::OnConflict(OnConflict { + conflict_target, + action, + })), + .. + } => { + assert_eq!( + vec![Ident::from("did"), Ident::from("area"),], + conflict_target + ); + assert_eq!( + OnConflictAction::DoUpdate(vec![ + Assignment { + id: vec!["dname".into()], + value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "dname".into()]) + }, + Assignment { + id: vec!["area".into()], + value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "area".into()]) + }, + ]), + action + ); + } + _ => unreachable!(), + }; + + let stmt = pg_and_generic().verified_stmt( + "INSERT INTO distributors (did, dname) \ + VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') \ + ON CONFLICT DO NOTHING", + ); + match stmt { + Statement::Insert { + on: + Some(OnInsert::OnConflict(OnConflict { + conflict_target, + action, + })), + .. + } => { + assert_eq!(Vec::::new(), conflict_target); + assert_eq!(OnConflictAction::DoNothing, action); + } + _ => unreachable!(), + }; +} + +#[test] +fn parse_pg_returning() { + let stmt = pg_and_generic().verified_stmt( + "INSERT INTO distributors (did, dname) VALUES (DEFAULT, 'XYZ Widgets') RETURNING did", + ); + match stmt { + Statement::Insert { returning, .. } => { + assert_eq!( + Some(vec![SelectItem::UnnamedExpr(Expr::Identifier( + "did".into() + )),]), + returning + ); + } + _ => unreachable!(), + }; + + let stmt = pg_and_generic().verified_stmt( + "UPDATE weather SET temp_lo = temp_lo + 1, temp_hi = temp_lo + 15, prcp = DEFAULT \ + WHERE city = 'San Francisco' AND date = '2003-07-03' \ + RETURNING temp_lo AS lo, temp_hi AS hi, prcp", + ); + match stmt { + Statement::Update { returning, .. } => { + assert_eq!( + Some(vec![ + SelectItem::ExprWithAlias { + expr: Expr::Identifier("temp_lo".into()), + alias: "lo".into() + }, + SelectItem::ExprWithAlias { + expr: Expr::Identifier("temp_hi".into()), + alias: "hi".into() + }, + SelectItem::UnnamedExpr(Expr::Identifier("prcp".into())), + ]), + returning + ); + } + _ => unreachable!(), + }; + let stmt = + pg_and_generic().verified_stmt("DELETE FROM tasks WHERE status = 'DONE' RETURNING *"); + match stmt { + Statement::Delete { returning, .. } => { + assert_eq!(Some(vec![SelectItem::Wildcard,]), returning); + } + _ => unreachable!(), + }; +} + #[test] fn parse_pg_bitwise_binary_ops() { let bitwise_ops = &[ From 6a847107cebf45e1efc9eebc4966327ea7d82bbd Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 11 Nov 2022 16:34:40 -0500 Subject: [PATCH 086/806] Update CHANGELOG for 0.27.0 release (#706) --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae31adf8..2b988de78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,30 @@ Given that the parser produces a typed AST, any changes to the AST will technica Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.27.0] 2022-11-11 + +### Added +* Support `ON CONFLICT` and `RETURNING` in `UPDATE` statement (#666) - Thanks @main and @gamife +* Support `FULLTEXT` option on create table for MySQL and Generic dialects (#702) - Thanks @AugustoFKL +* Support `ARRAY_AGG` for Bigquery and Snowflake (#662) - Thanks @SuperBo +* Support DISTINCT for SetOperator (#689) - Thanks @unvalley +* Support the ARRAY type of Snowflake (#699) - Thanks @yuval-illumex +* Support create sequence with options INCREMENT, MINVALUE, MAXVALUE, START etc. (#681) - Thanks @sam-mmm +* Support `:` operator for semi-structured data in Snowflake(#693) - Thanks @yuval-illumex +* Support ALTER TABLE DROP PRIMARY KEY (#682) - Thanks @ding-young +* Support `NUMERIC` and `DEC` ANSI data types (#695) - Thanks @AugustoFKL +* Support modifiers for Custom Datatype (#680) - Thanks @sunng87 + +### Changed +* Add precision for TIME, DATETIME, and TIMESTAMP data types (#701) - Thanks @AugustoFKL +* add Date keyword (#691) - Thanks @sarahyurick +* Update simple_logger requirement from 2.1 to 4.0 - Thanks @dependabot + +### Fixed +* Fix broken DataFusion link (#703) - Thanks @jmg-duarte +* Add MySql, BigQuery to all dialects tests, fixed bugs (#697) - Thanks @omer-shtivi + + ## [0.26.0] 2022-10-19 ### Added From 0d91662c88c55c7d33a9a0b7e44a5f29f60d9fba Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 11 Nov 2022 16:35:41 -0500 Subject: [PATCH 087/806] (cargo-release) version 0.27.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fbcd9ce9a..f7e08b494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.26.0" +version = "0.27.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 4b1dc1abf7138cb1576707c194ec89ad89049d27 Mon Sep 17 00:00:00 2001 From: unvalley <38400669+unvalley@users.noreply.github.com> Date: Sat, 12 Nov 2022 06:37:09 +0900 Subject: [PATCH 088/806] Support `UPDATE ... FROM ( subquery )` in some dialects (#694) * Apply UPDATE SET FROM statement for some dialects * Add GenericDialect to support * Test SnowflakeDialect Co-authored-by: Andrew Lamb --- src/parser.rs | 4 +- tests/sqlparser_common.rs | 92 ++++++++++++++++++++++++++++++++++++- tests/sqlparser_postgres.rs | 80 -------------------------------- 3 files changed, 94 insertions(+), 82 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 0dbc4aa36..f91223c2f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5254,7 +5254,9 @@ impl<'a> Parser<'a> { let table = self.parse_table_and_joins()?; self.expect_keyword(Keyword::SET)?; let assignments = self.parse_comma_separated(Parser::parse_assignment)?; - let from = if self.parse_keyword(Keyword::FROM) && dialect_of!(self is PostgreSqlDialect) { + let from = if self.parse_keyword(Keyword::FROM) + && dialect_of!(self is GenericDialect | PostgreSqlDialect | BigQueryDialect | SnowflakeDialect | RedshiftSqlDialect | MsSqlDialect) + { Some(self.parse_table_and_joins()?) } else { None diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 93439cafe..83a7a4ca6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -24,7 +24,7 @@ use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::*; use sqlparser::dialect::{ AnsiDialect, BigQueryDialect, ClickHouseDialect, GenericDialect, HiveDialect, MsSqlDialect, - MySqlDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect, + MySqlDialect, PostgreSqlDialect, RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, }; use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError}; @@ -186,6 +186,96 @@ fn parse_update() { ); } +#[test] +fn parse_update_set_from() { + let sql = "UPDATE t1 SET name = t2.name FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 WHERE t1.id = t2.id"; + let dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(MsSqlDialect {}), + ], + }; + let stmt = dialects.verified_stmt(sql); + assert_eq!( + stmt, + Statement::Update { + table: TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("t1")]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }, + assignments: vec![Assignment { + id: vec![Ident::new("name")], + value: Expr::CompoundIdentifier(vec![Ident::new("t2"), Ident::new("name")]) + }], + from: Some(TableWithJoins { + relation: TableFactor::Derived { + lateral: false, + subquery: Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![ + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))), + ], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("t1")]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: vec![Expr::Identifier(Ident::new("id"))], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + qualify: None + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + lock: None, + }), + alias: Some(TableAlias { + name: Ident::new("t2"), + columns: vec![], + }) + }, + joins: vec![], + }), + selection: Some(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("t1"), + Ident::new("id") + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("t2"), + Ident::new("id") + ])), + }), + returning: None, + } + ); +} + #[test] fn parse_update_with_table_alias() { let sql = "UPDATE users AS u SET u.username = 'new_user' WHERE u.username = 'old_user'"; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6cce4fdb9..5cc333935 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -489,86 +489,6 @@ PHP ₱ USD $ //assert_eq!(sql, ast.to_string()); } -#[test] -fn parse_update_set_from() { - let sql = "UPDATE t1 SET name = t2.name FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 WHERE t1.id = t2.id"; - let stmt = pg().verified_stmt(sql); - assert_eq!( - stmt, - Statement::Update { - table: TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t1")]), - alias: None, - args: None, - with_hints: vec![], - }, - joins: vec![], - }, - assignments: vec![Assignment { - id: vec![Ident::new("name")], - value: Expr::CompoundIdentifier(vec![Ident::new("t2"), Ident::new("name")]) - }], - from: Some(TableWithJoins { - relation: TableFactor::Derived { - lateral: false, - subquery: Box::new(Query { - with: None, - body: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, - top: None, - projection: vec![ - SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), - SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))), - ], - into: None, - from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t1")]), - alias: None, - args: None, - with_hints: vec![], - }, - joins: vec![], - }], - lateral_views: vec![], - selection: None, - group_by: vec![Expr::Identifier(Ident::new("id"))], - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - qualify: None - }))), - order_by: vec![], - limit: None, - offset: None, - fetch: None, - lock: None, - }), - alias: Some(TableAlias { - name: Ident::new("t2"), - columns: vec![], - }) - }, - joins: vec![], - }), - selection: Some(Expr::BinaryOp { - left: Box::new(Expr::CompoundIdentifier(vec![ - Ident::new("t1"), - Ident::new("id") - ])), - op: BinaryOperator::Eq, - right: Box::new(Expr::CompoundIdentifier(vec![ - Ident::new("t2"), - Ident::new("id") - ])), - }), - returning: None, - } - ); -} - #[test] fn test_copy_from() { let stmt = pg().verified_stmt("COPY users FROM 'data.csv'"); From 57083a0df19fb759cdff77e569875abaa7f55bd7 Mon Sep 17 00:00:00 2001 From: Sarah Yurick <53962159+sarahyurick@users.noreply.github.com> Date: Tue, 22 Nov 2022 04:45:47 -0800 Subject: [PATCH 089/806] Fix interval parsing logic and precedence (#705) * initial fix * add comma * add test * style * add more tests * codestyle fix --- src/parser.rs | 32 ++++++++++- tests/sqlparser_common.rs | 115 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index f91223c2f..a0b141bc1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -363,6 +363,36 @@ impl<'a> Parser<'a> { Ok(expr) } + pub fn parse_interval_expr(&mut self) -> Result { + let precedence = 0; + let mut expr = self.parse_prefix()?; + + loop { + let next_precedence = self.get_next_interval_precedence()?; + + if precedence >= next_precedence { + break; + } + + expr = self.parse_infix(expr, next_precedence)?; + } + + Ok(expr) + } + + /// Get the precedence of the next token + /// With AND, OR, and XOR + pub fn get_next_interval_precedence(&self) -> Result { + let token = self.peek_token(); + + match token { + Token::Word(w) if w.keyword == Keyword::AND => Ok(0), + Token::Word(w) if w.keyword == Keyword::OR => Ok(0), + Token::Word(w) if w.keyword == Keyword::XOR => Ok(0), + _ => self.get_next_precedence(), + } + } + pub fn parse_assert(&mut self) -> Result { let condition = self.parse_expr()?; let message = if self.parse_keyword(Keyword::AS) { @@ -1200,7 +1230,7 @@ impl<'a> Parser<'a> { // The first token in an interval is a string literal which specifies // the duration of the interval. - let value = self.parse_expr()?; + let value = self.parse_interval_expr()?; // Following the string literal is a qualifier which indicates the units // of the duration specified in the string literal. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 83a7a4ca6..ec4c2f257 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3237,6 +3237,121 @@ fn parse_interval() { ); } +#[test] +fn parse_interval_and_or_xor() { + let sql = "SELECT col FROM test \ + WHERE d3_date > d1_date + INTERVAL '5 days' \ + AND d2_date > d1_date + INTERVAL '3 days'"; + + let actual_ast = Parser::parse_sql(&GenericDialect {}, sql).unwrap(); + + let expected_ast = vec![Statement::Query(Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![UnnamedExpr(Expr::Identifier(Ident { + value: "col".to_string(), + quote_style: None, + }))], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "test".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: Some(Expr::BinaryOp { + left: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "d3_date".to_string(), + quote_style: None, + })), + op: BinaryOperator::Gt, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "d1_date".to_string(), + quote_style: None, + })), + op: BinaryOperator::Plus, + right: Box::new(Expr::Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString( + "5 days".to_string(), + ))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + }), + }), + }), + op: BinaryOperator::And, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "d2_date".to_string(), + quote_style: None, + })), + op: BinaryOperator::Gt, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "d1_date".to_string(), + quote_style: None, + })), + op: BinaryOperator::Plus, + right: Box::new(Expr::Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString( + "3 days".to_string(), + ))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + }), + }), + }), + }), + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + qualify: None, + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + lock: None, + }))]; + + assert_eq!(actual_ast, expected_ast); + + verified_stmt( + "SELECT col FROM test \ + WHERE d3_date > d1_date + INTERVAL '5 days' \ + AND d2_date > d1_date + INTERVAL '3 days'", + ); + + verified_stmt( + "SELECT col FROM test \ + WHERE d3_date > d1_date + INTERVAL '5 days' \ + OR d2_date > d1_date + INTERVAL '3 days'", + ); + + verified_stmt( + "SELECT col FROM test \ + WHERE d3_date > d1_date + INTERVAL '5 days' \ + XOR d2_date > d1_date + INTERVAL '3 days'", + ); +} + #[test] fn parse_at_timezone() { let zero = Expr::Value(number("0")); From 10652b61b4d2092884b7cf49dca7620f73d35b03 Mon Sep 17 00:00:00 2001 From: mao <50707849+step-baby@users.noreply.github.com> Date: Sun, 27 Nov 2022 20:29:09 +0800 Subject: [PATCH 090/806] =?UTF-8?q?fix:=20Supports=20updating=20valid=20co?= =?UTF-8?q?lumn=20names=20whose=20names=20are=20the=20same=20as=E2=80=A6?= =?UTF-8?q?=20(#725)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Supports updating valid column names whose names are the same as keywords * fix: warning --- src/parser.rs | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index a0b141bc1..8361e02ad 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3916,41 +3916,19 @@ impl<'a> Parser<'a> { Ok(ObjectName(idents)) } - /// Parse identifiers strictly i.e. don't parse keywords - pub fn parse_identifiers_non_keywords(&mut self) -> Result, ParserError> { + /// Parse identifiers + pub fn parse_identifiers(&mut self) -> Result, ParserError> { let mut idents = vec![]; loop { match self.peek_token() { Token::Word(w) => { - if w.keyword != Keyword::NoKeyword { - break; - } - idents.push(w.to_ident()); } Token::EOF | Token::Eq => break, _ => {} } - self.next_token(); } - - Ok(idents) - } - - /// Parse identifiers - pub fn parse_identifiers(&mut self) -> Result, ParserError> { - let mut idents = vec![]; - loop { - match self.next_token() { - Token::Word(w) => { - idents.push(w.to_ident()); - } - Token::EOF => break, - _ => {} - } - } - Ok(idents) } @@ -5312,7 +5290,7 @@ impl<'a> Parser<'a> { /// Parse a `var = expr` assignment, used in an UPDATE statement pub fn parse_assignment(&mut self) -> Result { - let id = self.parse_identifiers_non_keywords()?; + let id = self.parse_identifiers()?; self.expect_token(&Token::Eq)?; let value = self.parse_expr()?; Ok(Assignment { id, value }) @@ -6325,4 +6303,22 @@ mod tests { } ); } + + #[test] + fn test_update_has_keyword() { + let sql = r#"UPDATE test SET name=$1, + value=$2, + where=$3, + create=$4, + is_default=$5, + classification=$6, + sort=$7 + WHERE id=$8"#; + let pg_dialect = PostgreSqlDialect {}; + let ast = Parser::parse_sql(&pg_dialect, sql).unwrap(); + assert_eq!( + ast[0].to_string(), + r#"UPDATE test SET name = $1, value = $2, where = $3, create = $4, is_default = $5, classification = $6, sort = $7 WHERE id = $8"# + ); + } } From bae682255d14912c982681a1be597433caba009d Mon Sep 17 00:00:00 2001 From: Wei-Ting Kuo Date: Tue, 29 Nov 2022 05:16:08 +0800 Subject: [PATCH 091/806] add set time zone sometimezone as a exception while parsing keyword::set (#727) * add set time zone sometimezone as a exception while parsing keyword::set * remove redundant parentheses * add Statement::SetTimeZone * delete useless comments --- src/ast/mod.rs | 12 ++++++++++++ src/parser.rs | 9 +++++++++ tests/sqlparser_common.rs | 11 +++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f568c3960..16210f863 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1271,6 +1271,11 @@ pub enum Statement { variable: ObjectName, value: Vec, }, + /// 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 }, /// SET NAMES 'charset_name' [COLLATE 'collation_name'] /// /// Note: this is a MySQL-specific statement. @@ -2228,6 +2233,13 @@ impl fmt::Display for Statement { value = display_comma_separated(value) ) } + 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, diff --git a/src/parser.rs b/src/parser.rs index 8361e02ad..237371472 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4551,6 +4551,15 @@ impl<'a> Parser<'a> { value: values, }); } + } else 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" { self.expect_keywords(&[Keyword::AS, Keyword::TRANSACTION])?; Ok(Statement::SetTransaction { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ec4c2f257..2af06d15e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4979,6 +4979,17 @@ 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()))); + } + _ => unreachable!(), + } +} + #[test] fn parse_commit() { match verified_stmt("COMMIT") { From 886875f3bf22f6d034f83522d36afe17cc780ac4 Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:37:14 -0300 Subject: [PATCH 092/806] Support for `IF NOT EXISTS` in ALTER TABLE ADD COLUMN (#707) --- src/ast/ddl.rs | 34 +++++++++++++++---- src/parser.rs | 18 ++++++++-- tests/sqlparser_common.rs | 69 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3203f0e20..3c2bc3427 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,8 +30,15 @@ use crate::tokenizer::Token; pub enum AlterTableOperation { /// `ADD ` AddConstraint(TableConstraint), - /// `ADD [ COLUMN ] ` - AddColumn { column_def: ColumnDef }, + /// `ADD [COLUMN] [IF NOT EXISTS] ` + AddColumn { + /// `[COLUMN]`. + column_keyword: bool, + /// `[IF NOT EXISTS]` + if_not_exists: bool, + /// . + column_def: ColumnDef, + }, /// `DROP CONSTRAINT [ IF EXISTS ] ` DropConstraint { if_exists: bool, @@ -100,8 +107,21 @@ impl fmt::Display for AlterTableOperation { ine = if *if_not_exists { " IF NOT EXISTS" } else { "" } ), AlterTableOperation::AddConstraint(c) => write!(f, "ADD {}", c), - AlterTableOperation::AddColumn { column_def } => { - write!(f, "ADD COLUMN {}", column_def) + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + } => { + write!(f, "ADD")?; + if *column_keyword { + write!(f, " COLUMN")?; + } + if *if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " {column_def}")?; + + Ok(()) } AlterTableOperation::AlterColumn { column_name, op } => { write!(f, "ALTER COLUMN {} {}", column_name, op) @@ -416,8 +436,8 @@ impl fmt::Display for KeyOrIndexDisplay { /// Indexing method used by that index. /// -/// This structure isn't present on ANSI, but is found at least in [MySQL CREATE TABLE][1], -/// [MySQL CREATE INDEX][2], and [Postgresql CREATE INDEX][3] statements. +/// This structure isn't present on ANSI, but is found at least in [`MySQL` CREATE TABLE][1], +/// [`MySQL` CREATE INDEX][2], and [Postgresql CREATE INDEX][3] statements. /// /// [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 @@ -466,7 +486,7 @@ impl fmt::Display for ColumnDef { /// they are allowed to be named. The specification distinguishes between /// constraints (NOT NULL, UNIQUE, PRIMARY KEY, and CHECK), which can be named /// and can appear in any order, and other options (DEFAULT, GENERATED), which -/// cannot be named and must appear in a fixed order. PostgreSQL, however, +/// cannot be named and must appear in a fixed order. `PostgreSQL`, however, /// allows preceding any option with `CONSTRAINT `, even those that are /// not really constraints, like NULL and DEFAULT. MSSQL is less permissive, /// allowing DEFAULT, UNIQUE, PRIMARY KEY and CHECK to be named, but not NULL or diff --git a/src/parser.rs b/src/parser.rs index 237371472..e537eefae 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3205,9 +3205,22 @@ impl<'a> Parser<'a> { new_partitions: partitions, } } else { - let _ = self.parse_keyword(Keyword::COLUMN); + let column_keyword = self.parse_keyword(Keyword::COLUMN); + + let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | GenericDialect) + { + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) + || if_not_exists + } else { + false + }; + let column_def = self.parse_column_def()?; - AlterTableOperation::AddColumn { column_def } + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + } } } } else if self.parse_keyword(Keyword::RENAME) { @@ -5858,7 +5871,6 @@ mod tests { #[cfg(test)] mod test_parse_data_type { - use crate::ast::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, ObjectName, TimezoneInfo, }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2af06d15e..6790bbff6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2528,8 +2528,15 @@ fn parse_alter_table() { match one_statement_parses_to(add_column, "ALTER TABLE tab ADD COLUMN foo TEXT") { Statement::AlterTable { name, - operation: AlterTableOperation::AddColumn { column_def }, + operation: + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + }, } => { + assert!(column_keyword); + assert!(!if_not_exists); assert_eq!("tab", name.to_string()); assert_eq!("foo", column_def.name.to_string()); assert_eq!("TEXT", column_def.data_type.to_string()); @@ -2567,6 +2574,66 @@ fn parse_alter_table() { } } +#[test] +fn parse_alter_table_add_column() { + match verified_stmt("ALTER TABLE tab ADD foo TEXT") { + Statement::AlterTable { + operation: AlterTableOperation::AddColumn { column_keyword, .. }, + .. + } => { + assert!(!column_keyword); + } + _ => unreachable!(), + }; + + match verified_stmt("ALTER TABLE tab ADD COLUMN foo TEXT") { + Statement::AlterTable { + operation: AlterTableOperation::AddColumn { column_keyword, .. }, + .. + } => { + assert!(column_keyword); + } + _ => unreachable!(), + }; +} + +#[test] +fn parse_alter_table_add_column_if_not_exists() { + let dialects = TestedDialects { + dialects: vec![ + Box::new(PostgreSqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(GenericDialect {}), + ], + }; + + match dialects.verified_stmt("ALTER TABLE tab ADD IF NOT EXISTS foo TEXT") { + Statement::AlterTable { + operation: AlterTableOperation::AddColumn { if_not_exists, .. }, + .. + } => { + assert!(if_not_exists); + } + _ => unreachable!(), + }; + + match dialects.verified_stmt("ALTER TABLE tab ADD COLUMN IF NOT EXISTS foo TEXT") { + Statement::AlterTable { + operation: + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + .. + }, + .. + } => { + assert!(column_keyword); + assert!(if_not_exists); + } + _ => unreachable!(), + }; +} + #[test] fn parse_alter_table_constraints() { check_one("CONSTRAINT address_pkey PRIMARY KEY (address_id)"); From 09d53623bc8e682a5fe1c968aa44ed71d4523248 Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:16:50 -0300 Subject: [PATCH 093/806] Support `MATCH AGAINST` (#708) Support added for both MySQL and Generic dialects. --- src/ast/mod.rs | 72 ++++++++++++++++++++++++++++++++++++++++ src/keywords.rs | 3 ++ src/parser.rs | 54 ++++++++++++++++++++++++++++++ tests/sqlparser_mysql.rs | 26 +++++++++++++++ 4 files changed, 155 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 16210f863..7fa320d3f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -449,6 +449,26 @@ pub enum Expr { /// or as `__ TO SECOND(x)`. fractional_seconds_precision: Option, }, + /// `MySQL` specific text search function [(1)]. + /// + /// Syntax: + /// ```text + /// MARCH (, , ...) AGAINST ( []) + /// + /// = CompoundIdentifier + /// = String literal + /// ``` + /// + /// + /// [(1)]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match + MatchAgainst { + /// `(, , ...)`. + columns: Vec, + /// ``. + match_value: Value, + /// `` + opt_search_modifier: Option, + }, } impl fmt::Display for Expr { @@ -818,6 +838,21 @@ impl fmt::Display for Expr { } Ok(()) } + Expr::MatchAgainst { + columns, + match_value: match_expr, + opt_search_modifier, + } => { + write!(f, "MATCH ({}) AGAINST ", display_comma_separated(columns),)?; + + if let Some(search_modifier) = opt_search_modifier { + write!(f, "({match_expr} {search_modifier})")?; + } else { + write!(f, "({match_expr})")?; + } + + Ok(()) + } } } } @@ -3659,6 +3694,43 @@ impl fmt::Display for SchemaName { } } +/// Fulltext search modifiers ([1]). +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum SearchModifier { + /// `IN NATURAL LANGUAGE MODE`. + InNaturalLanguageMode, + /// `IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION`. + InNaturalLanguageModeWithQueryExpansion, + ///`IN BOOLEAN MODE`. + InBooleanMode, + ///`WITH QUERY EXPANSION`. + WithQueryExpansion, +} + +impl fmt::Display for SearchModifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InNaturalLanguageMode => { + write!(f, "IN NATURAL LANGUAGE MODE")?; + } + Self::InNaturalLanguageModeWithQueryExpansion => { + write!(f, "IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION")?; + } + Self::InBooleanMode => { + write!(f, "IN BOOLEAN MODE")?; + } + Self::WithQueryExpansion => { + write!(f, "WITH QUERY EXPANSION")?; + } + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/keywords.rs b/src/keywords.rs index 47dd21e21..955bee771 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -71,6 +71,7 @@ define_keywords!( ACTION, ADD, ADMIN, + AGAINST, ALL, ALLOCATE, ALTER, @@ -229,6 +230,7 @@ define_keywords!( EXECUTE, EXISTS, EXP, + EXPANSION, EXPLAIN, EXTENDED, EXTERNAL, @@ -348,6 +350,7 @@ define_keywords!( MINUTE, MINVALUE, MOD, + MODE, MODIFIES, MODULE, MONTH, diff --git a/src/parser.rs b/src/parser.rs index e537eefae..c875f653b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -505,6 +505,9 @@ impl<'a> Parser<'a> { } Keyword::ARRAY_AGG => self.parse_array_agg_expr(), Keyword::NOT => self.parse_not(), + Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => { + self.parse_match_against() + } // Here `w` is a word, check if it's a part of a multi-part // identifier, a function call, or a simple identifier: _ => match self.peek_token() { @@ -1209,6 +1212,57 @@ impl<'a> Parser<'a> { } } + /// Parses fulltext expressions [(1)] + /// + /// # Errors + /// 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. + /// + /// [(1)]: Expr::MatchAgainst + pub fn parse_match_against(&mut self) -> Result { + let columns = self.parse_parenthesized_column_list(Mandatory)?; + + self.expect_keyword(Keyword::AGAINST)?; + + self.expect_token(&Token::LParen)?; + + // MySQL is too permissive about the value, IMO we can't validate it perfectly on syntax level. + let match_value = self.parse_value()?; + + let in_natural_language_mode_keywords = &[ + Keyword::IN, + Keyword::NATURAL, + Keyword::LANGUAGE, + Keyword::MODE, + ]; + + let with_query_expansion_keywords = &[Keyword::WITH, Keyword::QUERY, Keyword::EXPANSION]; + + let in_boolean_mode_keywords = &[Keyword::IN, Keyword::BOOLEAN, Keyword::MODE]; + + let opt_search_modifier = if self.parse_keywords(in_natural_language_mode_keywords) { + if self.parse_keywords(with_query_expansion_keywords) { + Some(SearchModifier::InNaturalLanguageModeWithQueryExpansion) + } else { + Some(SearchModifier::InNaturalLanguageMode) + } + } else if self.parse_keywords(in_boolean_mode_keywords) { + Some(SearchModifier::InBooleanMode) + } else if self.parse_keywords(with_query_expansion_keywords) { + Some(SearchModifier::WithQueryExpansion) + } else { + None + }; + + self.expect_token(&Token::RParen)?; + + Ok(Expr::MatchAgainst { + columns, + match_value, + opt_search_modifier, + }) + } + /// Parse an INTERVAL expression. /// /// Some syntactically valid intervals: diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e91cea0ef..960dbf5ae 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1166,6 +1166,32 @@ fn parse_create_table_with_spatial_definition() { .verified_stmt("CREATE TABLE tb (c1 INT, c2 INT, SPATIAL KEY potato (c1, c2))"); } +#[test] +fn parse_fulltext_expression() { + mysql_and_generic().verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string')"); + + mysql_and_generic().verified_stmt( + "SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string' IN NATURAL LANGUAGE MODE)", + ); + + mysql_and_generic().verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION)"); + + mysql_and_generic() + .verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string' IN BOOLEAN MODE)"); + + mysql_and_generic() + .verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string' WITH QUERY EXPANSION)"); + + mysql_and_generic() + .verified_stmt("SELECT * FROM tb WHERE MATCH (c1, c2, c3) AGAINST ('string')"); + + mysql_and_generic().verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST (123)"); + + mysql_and_generic().verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST (NULL)"); + + mysql_and_generic().verified_stmt("SELECT COUNT(IF(MATCH (title, body) AGAINST ('database' IN NATURAL LANGUAGE MODE), 1, NULL)) AS count FROM articles"); +} + #[test] #[should_panic = "Expected FULLTEXT or SPATIAL option without constraint name, found: cons"] fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name() { From c4291973405feb4d2f5c4c1d961ff8af177d9ed0 Mon Sep 17 00:00:00 2001 From: Paul McGee Date: Thu, 1 Dec 2022 01:24:10 +0800 Subject: [PATCH 094/806] tiny typo (#709) --- docs/custom_sql_parser.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/custom_sql_parser.md b/docs/custom_sql_parser.md index f7c9635f2..c13041af4 100644 --- a/docs/custom_sql_parser.md +++ b/docs/custom_sql_parser.md @@ -4,7 +4,7 @@ I have explored many different ways of building this library to make it easy to The concept is simply to write a new parser that delegates to the ANSI parser so that as much as possible of the core functionality can be re-used. -I also plan on building in specific support for custom data types, where a lambda function can be parsed to the parser to parse data types. +I also plan on building in specific support for custom data types, where a lambda function can be passed to the parser to parse data types. For an example of this, see the [DataFusion](https://github.com/apache/arrow-datafusion) project and its [query planner] (https://github.com/apache/arrow-datafusion/tree/master/datafusion/sql). From 5e1d9f8d6e9e3904425f03806ac80df8e61ae276 Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:25:59 -0300 Subject: [PATCH 095/806] Derive `PartialOrd`, `Ord`, and `Copy` whenever possible (#717) This allow other projects to use our structures inside others that need those. --- src/ast/data_type.rs | 10 ++-- src/ast/ddl.rs | 18 +++--- src/ast/mod.rs | 114 ++++++++++++++++++------------------ src/ast/operator.rs | 4 +- src/ast/query.rs | 46 +++++++-------- src/ast/value.rs | 6 +- src/parser.rs | 2 +- src/tokenizer.rs | 6 +- tests/sqlparser_mysql.rs | 2 +- tests/sqlparser_postgres.rs | 4 +- 10 files changed, 106 insertions(+), 106 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 12a95a4fc..1353eca90 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -22,7 +22,7 @@ use crate::ast::ObjectName; use super::value::escape_single_quote_string; /// SQL data types -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum DataType { /// Fixed-length character type e.g. CHARACTER(10) @@ -335,7 +335,7 @@ fn format_datetime_precision_and_tz( /// /// This is more related to a display information than real differences between each variant. To /// guarantee compatibility with the input query we must maintain its exact information. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TimezoneInfo { /// No information about time zone. E.g., TIMESTAMP @@ -382,7 +382,7 @@ impl fmt::Display for TimezoneInfo { /// following the 2016 [standard]. /// /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ExactNumberInfo { /// No additional information e.g. `DECIMAL` @@ -412,7 +412,7 @@ impl fmt::Display for ExactNumberInfo { /// Information about [character length][1], including length and possibly unit. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-length -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct CharacterLength { /// Default (if VARYING) or maximum (if not VARYING) length @@ -434,7 +434,7 @@ impl fmt::Display for CharacterLength { /// Possible units for characters, initially based on 2016 ANSI [standard][1]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#char-length-units -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CharLengthUnits { /// CHARACTERS unit diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3c2bc3427..00bf83aba 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -25,7 +25,7 @@ use crate::ast::{display_comma_separated, display_separated, DataType, Expr, Ide use crate::tokenizer::Token; /// An `ALTER TABLE` (`Statement::AlterTable`) operation -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum AlterTableOperation { /// `ADD ` @@ -201,7 +201,7 @@ impl fmt::Display for AlterTableOperation { } /// An `ALTER COLUMN` (`Statement::AlterTable`) operation -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum AlterColumnOperation { /// `SET NOT NULL` @@ -244,7 +244,7 @@ impl fmt::Display for AlterColumnOperation { /// A table-level constraint, specified in a `CREATE TABLE` or an /// `ALTER TABLE ADD ` statement. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TableConstraint { /// `[ CONSTRAINT ] { PRIMARY KEY | UNIQUE } ()` @@ -407,7 +407,7 @@ impl fmt::Display for TableConstraint { /// statements of `MySQL` [(1)]. /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum KeyOrIndexDisplay { /// Nothing to display @@ -442,7 +442,7 @@ 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, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum IndexType { BTree, @@ -460,7 +460,7 @@ impl fmt::Display for IndexType { } /// SQL column definition -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ColumnDef { pub name: Ident, @@ -495,7 +495,7 @@ impl fmt::Display for ColumnDef { /// For maximum flexibility, we don't distinguish between constraint and /// non-constraint options, lumping them all together under the umbrella of /// "column options," and we allow any column option to be named. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ColumnOptionDef { pub name: Option, @@ -510,7 +510,7 @@ impl fmt::Display for ColumnOptionDef { /// `ColumnOption`s are modifiers that follow a column definition in a `CREATE /// TABLE` statement. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ColumnOption { /// `NULL` @@ -597,7 +597,7 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { /// { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` /// /// Used in foreign key constraints in `ON UPDATE` and `ON DELETE` options. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ReferentialAction { Restrict, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7fa320d3f..226dc8b43 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -82,7 +82,7 @@ where } /// An identifier, decomposed into its value or character data and the quote style. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Ident { /// The value of the identifier without quotes. @@ -142,7 +142,7 @@ impl fmt::Display for Ident { } /// A name of a table, view, custom type, etc., possibly multi-part, i.e. db.schema.obj -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ObjectName(pub Vec); @@ -152,7 +152,7 @@ impl fmt::Display for ObjectName { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] /// Represents an Array Expression, either /// `ARRAY[..]`, or `[..]` @@ -176,7 +176,7 @@ impl fmt::Display for Array { } /// JsonOperator -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum JsonOperator { /// -> keeps the value as json @@ -218,7 +218,7 @@ impl fmt::Display for JsonOperator { /// The parser does not distinguish between expressions of different types /// (e.g. boolean vs string), so the caller must handle expressions of /// inappropriate type, like `WHERE 1` or `SELECT 1=1`, as necessary. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Expr { /// Identifier e.g. table name or column name @@ -858,7 +858,7 @@ impl fmt::Display for Expr { } /// A window specification (i.e. `OVER (PARTITION BY .. ORDER BY .. etc.)`) -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct WindowSpec { pub partition_by: Vec, @@ -903,7 +903,7 @@ impl fmt::Display for WindowSpec { /// /// Note: The parser does not validate the specified bounds; the caller should /// reject invalid bounds like `ROWS UNBOUNDED FOLLOWING` before execution. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct WindowFrame { pub units: WindowFrameUnits, @@ -928,7 +928,7 @@ impl Default for WindowFrame { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum WindowFrameUnits { Rows, @@ -947,7 +947,7 @@ impl fmt::Display for WindowFrameUnits { } /// Specifies [WindowFrame]'s `start_bound` and `end_bound` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum WindowFrameBound { /// `CURRENT ROW` @@ -970,7 +970,7 @@ impl fmt::Display for WindowFrameBound { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum AddDropSync { ADD, @@ -988,7 +988,7 @@ impl fmt::Display for AddDropSync { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ShowCreateObject { Event, @@ -1012,7 +1012,7 @@ impl fmt::Display for ShowCreateObject { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CommentObject { Column, @@ -1028,7 +1028,7 @@ impl fmt::Display for CommentObject { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Password { Password(Expr), @@ -1037,7 +1037,7 @@ pub enum Password { /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Statement { /// Analyze (Hive) @@ -2591,12 +2591,12 @@ impl fmt::Display for Statement { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] /// Can use to describe options in create sequence or table column type identity /// [ INCREMENT [ BY ] increment ] /// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] /// [ START [ WITH ] start ] [ CACHE cache ] [ [ NO ] CYCLE ] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SequenceOptions { IncrementBy(Expr, bool), MinValue(MinMaxValue), @@ -2657,10 +2657,10 @@ impl fmt::Display for SequenceOptions { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] /// Can use to describe options in create sequence or table column type identity /// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MinMaxValue { // clause is not specified Empty, @@ -2670,7 +2670,7 @@ pub enum MinMaxValue { Some(Expr), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[non_exhaustive] pub enum OnInsert { @@ -2680,13 +2680,13 @@ pub enum OnInsert { OnConflict(OnConflict), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct OnConflict { pub conflict_target: Vec, pub action: OnConflictAction, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum OnConflictAction { DoNothing, @@ -2724,7 +2724,7 @@ impl fmt::Display for OnConflictAction { } /// Privileges granted in a GRANT statement or revoked in a REVOKE statement. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Privileges { /// All privileges applicable to the object type @@ -2760,7 +2760,7 @@ impl fmt::Display for Privileges { } /// Specific direction for FETCH statement -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum FetchDirection { Count { limit: Value }, @@ -2823,7 +2823,7 @@ impl fmt::Display for FetchDirection { } /// A privilege on a database object (table, sequence, etc.). -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Action { Connect, @@ -2872,7 +2872,7 @@ impl fmt::Display for Action { } /// Objects on which privileges are granted in a GRANT statement. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum GrantObjects { /// Grant privileges on `ALL SEQUENCES IN SCHEMA [, ...]` @@ -2918,7 +2918,7 @@ impl fmt::Display for GrantObjects { } /// SQL assignment `foo = expr` as used in SQLUpdate -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Assignment { pub id: Vec, @@ -2931,7 +2931,7 @@ impl fmt::Display for Assignment { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum FunctionArgExpr { Expr(Expr), @@ -2951,7 +2951,7 @@ impl fmt::Display for FunctionArgExpr { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum FunctionArg { Named { name: Ident, arg: FunctionArgExpr }, @@ -2967,7 +2967,7 @@ impl fmt::Display for FunctionArg { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CloseCursor { All, @@ -2984,7 +2984,7 @@ impl fmt::Display for CloseCursor { } /// A function call -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Function { pub name: ObjectName, @@ -2997,7 +2997,7 @@ pub struct Function { pub special: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum AnalyzeFormat { TEXT, @@ -3038,7 +3038,7 @@ impl fmt::Display for Function { } /// External table's available file format -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum FileFormat { TEXTFILE, @@ -3067,7 +3067,7 @@ impl fmt::Display for FileFormat { /// A `LISTAGG` invocation `LISTAGG( [ DISTINCT ] [, ] [ON OVERFLOW ] ) ) /// [ WITHIN GROUP (ORDER BY [, ...] ) ]` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ListAgg { pub distinct: bool, @@ -3104,7 +3104,7 @@ impl fmt::Display for ListAgg { } /// The `ON OVERFLOW` clause of a LISTAGG invocation -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ListAggOnOverflow { /// `ON OVERFLOW ERROR` @@ -3141,7 +3141,7 @@ impl fmt::Display for ListAggOnOverflow { /// An `ARRAY_AGG` invocation `ARRAY_AGG( [ DISTINCT ] [ORDER BY ] [LIMIT ] )` /// Or `ARRAY_AGG( [ DISTINCT ] ) [ WITHIN GROUP ( ORDER BY ) ]` /// ORDER BY position is defined differently for BigQuery, Postgres and Snowflake. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ArrayAgg { pub distinct: bool, @@ -3177,7 +3177,7 @@ impl fmt::Display for ArrayAgg { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ObjectType { Table, @@ -3201,7 +3201,7 @@ impl fmt::Display for ObjectType { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum KillType { Connection, @@ -3221,7 +3221,7 @@ impl fmt::Display for KillType { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum HiveDistributionStyle { PARTITIONED { @@ -3240,7 +3240,7 @@ pub enum HiveDistributionStyle { NONE, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum HiveRowFormat { SERDE { class: String }, @@ -3248,7 +3248,7 @@ pub enum HiveRowFormat { } #[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[allow(clippy::large_enum_variant)] pub enum HiveIOFormat { @@ -3261,7 +3261,7 @@ pub enum HiveIOFormat { }, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HiveFormat { pub row_format: Option, @@ -3269,7 +3269,7 @@ pub struct HiveFormat { pub location: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SqlOption { pub name: Ident, @@ -3282,7 +3282,7 @@ impl fmt::Display for SqlOption { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TransactionMode { AccessMode(TransactionAccessMode), @@ -3299,7 +3299,7 @@ impl fmt::Display for TransactionMode { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TransactionAccessMode { ReadOnly, @@ -3316,7 +3316,7 @@ impl fmt::Display for TransactionAccessMode { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TransactionIsolationLevel { ReadUncommitted, @@ -3337,7 +3337,7 @@ impl fmt::Display for TransactionIsolationLevel { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ShowStatementFilter { Like(String), @@ -3359,7 +3359,7 @@ impl fmt::Display for ShowStatementFilter { /// Sqlite specific syntax /// /// https://sqlite.org/lang_conflict.html -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SqliteOnConflict { Rollback, @@ -3382,7 +3382,7 @@ impl fmt::Display for SqliteOnConflict { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CopyTarget { Stdin, @@ -3413,7 +3413,7 @@ impl fmt::Display for CopyTarget { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum OnCommit { DeleteRows, @@ -3424,7 +3424,7 @@ pub enum OnCommit { /// An option in `COPY` statement. /// /// -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CopyOption { /// FORMAT format_name @@ -3477,7 +3477,7 @@ impl fmt::Display for CopyOption { /// An option in `COPY` statement before PostgreSQL version 9.0. /// /// -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CopyLegacyOption { /// BINARY @@ -3505,7 +3505,7 @@ impl fmt::Display for CopyLegacyOption { /// A `CSV` option in `COPY` statement before PostgreSQL version 9.0. /// /// -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CopyLegacyCsvOption { /// HEADER @@ -3536,7 +3536,7 @@ impl fmt::Display for CopyLegacyCsvOption { } /// -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MergeClause { MatchedUpdate { @@ -3597,7 +3597,7 @@ impl fmt::Display for MergeClause { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum DiscardObject { ALL, @@ -3618,7 +3618,7 @@ impl fmt::Display for DiscardObject { } /// Optional context modifier for statements that can be or `LOCAL`, or `SESSION`. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ContextModifier { /// No context defined. Each dialect defines the default in this scenario. @@ -3645,7 +3645,7 @@ impl fmt::Display for ContextModifier { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CreateFunctionUsing { Jar(String), @@ -3667,7 +3667,7 @@ impl fmt::Display for CreateFunctionUsing { /// Schema possible naming variants ([1]). /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#schema-definition -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SchemaName { /// Only schema name specified: ``. diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 1c96ebbcb..f22839474 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize}; use super::display_separated; /// Unary operators -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum UnaryOperator { Plus, @@ -57,7 +57,7 @@ impl fmt::Display for UnaryOperator { } /// Binary operators -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum BinaryOperator { Plus, diff --git a/src/ast/query.rs b/src/ast/query.rs index 4f3d79cdf..34b4ed7f5 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -20,7 +20,7 @@ use crate::ast::*; /// The most complete variant of a `SELECT` query expression, optionally /// including `WITH`, `UNION` / other set operations, and `ORDER BY`. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Query { /// WITH (common table expressions, or CTEs) @@ -67,7 +67,7 @@ impl fmt::Display for Query { /// A node in a tree, representing a "query body" expression, roughly: /// `SELECT ... [ {UNION|EXCEPT|INTERSECT} SELECT ...]` #[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SetExpr { /// Restricted SELECT .. FROM .. HAVING (no ORDER BY or set operations) @@ -114,7 +114,7 @@ impl fmt::Display for SetExpr { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SetOperator { Union, @@ -135,7 +135,7 @@ impl fmt::Display for SetOperator { /// A quantifier for [SetOperator]. // TODO: Restrict parsing specific SetQuantifier in some specific dialects. // For example, BigQuery does not support `DISTINCT` for `EXCEPT` and `INTERSECT` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SetQuantifier { All, @@ -155,7 +155,7 @@ impl fmt::Display for SetQuantifier { /// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may /// appear either as the only body item of a `Query`, or as an operand /// to a set operation like `UNION`. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Select { pub distinct: bool, @@ -239,7 +239,7 @@ impl fmt::Display for Select { } /// A hive LATERAL VIEW with potential column aliases -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct LateralView { /// LATERAL VIEW @@ -272,7 +272,7 @@ impl fmt::Display for LateralView { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct With { pub recursive: bool, @@ -294,7 +294,7 @@ impl fmt::Display for With { /// The names in the column list before `AS`, when specified, replace the names /// of the columns returned by the query. The parser does not validate that the /// number of columns in the query matches the number of columns in the query. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Cte { pub alias: TableAlias, @@ -313,7 +313,7 @@ impl fmt::Display for Cte { } /// One item of the comma-separated list following `SELECT` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SelectItem { /// Any expression, not followed by `[ AS ] alias` @@ -337,7 +337,7 @@ impl fmt::Display for SelectItem { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TableWithJoins { pub relation: TableFactor, @@ -355,7 +355,7 @@ impl fmt::Display for TableWithJoins { } /// A table name or a parenthesized subquery with an optional alias -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TableFactor { Table { @@ -482,7 +482,7 @@ impl fmt::Display for TableFactor { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TableAlias { pub name: Ident, @@ -499,7 +499,7 @@ impl fmt::Display for TableAlias { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Join { pub relation: TableFactor, @@ -565,7 +565,7 @@ impl fmt::Display for Join { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum JoinOperator { Inner(JoinConstraint), @@ -579,7 +579,7 @@ pub enum JoinOperator { OuterApply, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum JoinConstraint { On(Expr), @@ -589,7 +589,7 @@ pub enum JoinConstraint { } /// An `ORDER BY` expression -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct OrderByExpr { pub expr: Expr, @@ -616,7 +616,7 @@ impl fmt::Display for OrderByExpr { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Offset { pub value: Expr, @@ -630,7 +630,7 @@ impl fmt::Display for Offset { } /// Stores the keyword after `OFFSET ` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum OffsetRows { /// Omitting ROW/ROWS is non-standard MySQL quirk. @@ -649,7 +649,7 @@ impl fmt::Display for OffsetRows { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Fetch { pub with_ties: bool, @@ -669,7 +669,7 @@ impl fmt::Display for Fetch { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum LockType { Share, @@ -686,7 +686,7 @@ impl fmt::Display for LockType { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Top { /// SQL semantic equivalent of LIMIT but with same structure as FETCH. @@ -707,7 +707,7 @@ impl fmt::Display for Top { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Values(pub Vec>); @@ -724,7 +724,7 @@ impl fmt::Display for Values { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SelectInto { pub temporary: bool, diff --git a/src/ast/value.rs b/src/ast/value.rs index 33660a628..49c0b5d7a 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -20,7 +20,7 @@ use bigdecimal::BigDecimal; use serde::{Deserialize, Serialize}; /// Primitive SQL values such as number and string -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Value { /// Numeric literal @@ -66,7 +66,7 @@ impl fmt::Display for Value { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum DateTimeField { Year, @@ -192,7 +192,7 @@ pub fn escape_escaped_string(s: &str) -> EscapeEscapedStringLiteral<'_> { EscapeEscapedStringLiteral(s) } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TrimWhereField { Both, diff --git a/src/parser.rs b/src/parser.rs index c875f653b..deb6d6bf9 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2281,7 +2281,7 @@ impl<'a> Parser<'a> { let file_format = if let Some(ff) = &hive_formats.storage { match ff { - HiveIOFormat::FileFormat { format } => Some(format.clone()), + HiveIOFormat::FileFormat { format } => Some(*format), _ => None, } } else { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 5a9afdbef..e34ccf1f9 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -36,7 +36,7 @@ use crate::dialect::{Dialect, MySqlDialect}; use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX}; /// SQL Token enumeration -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Token { /// An end-of-file marker, not a real token @@ -240,7 +240,7 @@ impl Token { } /// A keyword (like SELECT) or an optionally quoted SQL identifier -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Word { /// The value of the token, without the enclosing quotes, and with the @@ -278,7 +278,7 @@ impl Word { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Whitespace { Space, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 960dbf5ae..1d8c6a05e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -211,7 +211,7 @@ fn parse_show_create() { assert_eq!( mysql_and_generic().verified_stmt(format!("SHOW CREATE {} myident", obj_type).as_str()), Statement::ShowCreate { - obj_type: obj_type.clone(), + obj_type: *obj_type, obj_name: obj_name.clone(), } ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5cc333935..d7e07a762 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1271,7 +1271,7 @@ fn parse_pg_unary_ops() { let select = pg().verified_only_select(&format!("SELECT {}a", &str_op)); assert_eq!( SelectItem::UnnamedExpr(Expr::UnaryOp { - op: op.clone(), + op: *op, expr: Box::new(Expr::Identifier(Ident::new("a"))), }), select.projection[0] @@ -1287,7 +1287,7 @@ fn parse_pg_postfix_factorial() { let select = pg().verified_only_select(&format!("SELECT a{}", &str_op)); assert_eq!( SelectItem::UnnamedExpr(Expr::UnaryOp { - op: op.clone(), + op: *op, expr: Box::new(Expr::Identifier(Ident::new("a"))), }), select.projection[0] From 3df0e444c860e5fc08f4992f15588c3e5337ba78 Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:26:35 -0300 Subject: [PATCH 096/806] Add 'compression' as keyword (#720) --- src/keywords.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keywords.rs b/src/keywords.rs index 955bee771..79d456f15 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -143,6 +143,7 @@ define_keywords!( COMMENT, COMMIT, COMMITTED, + COMPRESSION, COMPUTE, CONDITION, CONFLICT, From fa6bd01b19fabc9087d3b306e7c92f12cb8d368d Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:29:43 -0300 Subject: [PATCH 097/806] Support EXCLUDE support for snowflake and generic dialect (#721) The exclude clause can be used after a possibly qualified on SELECT --- src/ast/mod.rs | 6 ++-- src/ast/query.rs | 60 +++++++++++++++++++++++++++++--- src/keywords.rs | 1 + src/parser.rs | 42 +++++++++++++++++++++-- tests/sqlparser_common.rs | 14 ++++---- tests/sqlparser_postgres.rs | 2 +- tests/sqlparser_snowflake.rs | 66 +++++++++++++++++++++++++++++++++--- 7 files changed, 170 insertions(+), 21 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 226dc8b43..fbf318921 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -31,9 +31,9 @@ pub use self::ddl::{ }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, Fetch, Join, JoinConstraint, JoinOperator, LateralView, LockType, Offset, OffsetRows, - OrderByExpr, Query, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, - TableAlias, TableFactor, TableWithJoins, Top, Values, With, + Cte, ExcludeSelectItem, Fetch, Join, JoinConstraint, JoinOperator, LateralView, LockType, + Offset, OffsetRows, OrderByExpr, Query, Select, SelectInto, SelectItem, SetExpr, SetOperator, + SetQuantifier, TableAlias, TableFactor, TableWithJoins, Top, Values, With, }; pub use self::value::{escape_quoted_string, DateTimeField, TrimWhereField, Value}; diff --git a/src/ast/query.rs b/src/ast/query.rs index 34b4ed7f5..0c07ce640 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -321,9 +321,49 @@ pub enum SelectItem { /// An expression, followed by `[ AS ] alias` ExprWithAlias { expr: Expr, alias: Ident }, /// `alias.*` or even `schema.table.*` - QualifiedWildcard(ObjectName), + QualifiedWildcard(ObjectName, Option), /// An unqualified `*` - Wildcard, + Wildcard(Option), +} + +/// Snowflake `EXCLUDE` information. +/// +/// # Syntax +/// ```plaintext +/// +/// | (, , ...) +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ExcludeSelectItem { + /// Single column name without parenthesis. + /// + /// # Syntax + /// ```plaintext + /// + /// ``` + Single(Ident), + /// Multiple column names inside parenthesis. + /// # Syntax + /// ```plaintext + /// (, , ...) + /// ``` + Multiple(Vec), +} + +impl fmt::Display for ExcludeSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "EXCLUDE")?; + match self { + Self::Single(column) => { + write!(f, " {column}")?; + } + Self::Multiple(columns) => { + write!(f, " ({})", display_comma_separated(columns))?; + } + } + Ok(()) + } } impl fmt::Display for SelectItem { @@ -331,8 +371,20 @@ impl fmt::Display for SelectItem { match &self { SelectItem::UnnamedExpr(expr) => write!(f, "{}", expr), SelectItem::ExprWithAlias { expr, alias } => write!(f, "{} AS {}", expr, alias), - SelectItem::QualifiedWildcard(prefix) => write!(f, "{}.*", prefix), - SelectItem::Wildcard => write!(f, "*"), + SelectItem::QualifiedWildcard(prefix, opt_exclude) => { + write!(f, "{}.*", prefix)?; + if let Some(exclude) = opt_exclude { + write!(f, " {exclude}")?; + } + Ok(()) + } + SelectItem::Wildcard(opt_exclude) => { + write!(f, "*")?; + if let Some(exclude) = opt_exclude { + write!(f, " {exclude}")?; + } + Ok(()) + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index 79d456f15..1cbe9f289 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -227,6 +227,7 @@ define_keywords!( EVENT, EVERY, EXCEPT, + EXCLUDE, EXEC, EXECUTE, EXISTS, diff --git a/src/parser.rs b/src/parser.rs index deb6d6bf9..741135a6e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5423,11 +5423,49 @@ impl<'a> Parser<'a> { None => SelectItem::UnnamedExpr(expr), }) } - WildcardExpr::QualifiedWildcard(prefix) => Ok(SelectItem::QualifiedWildcard(prefix)), - WildcardExpr::Wildcard => Ok(SelectItem::Wildcard), + WildcardExpr::QualifiedWildcard(prefix) => { + let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_exclude()? + } else { + None + }; + + Ok(SelectItem::QualifiedWildcard(prefix, opt_exclude)) + } + WildcardExpr::Wildcard => { + let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_exclude()? + } else { + None + }; + + Ok(SelectItem::Wildcard(opt_exclude)) + } } } + /// Parse an [`Exclude`](ExcludeSelectItem) information for wildcard select items. + /// + /// If it is not possible to parse it, will return an option. + pub fn parse_optional_select_item_exclude( + &mut self, + ) -> Result, ParserError> { + let opt_exclude = if self.parse_keyword(Keyword::EXCLUDE) { + if self.consume_token(&Token::LParen) { + let columns = self.parse_comma_separated(|parser| parser.parse_identifier())?; + self.expect_token(&Token::RParen)?; + Some(ExcludeSelectItem::Multiple(columns)) + } else { + let column = self.parse_identifier()?; + Some(ExcludeSelectItem::Single(column)) + } + } else { + None + }; + + Ok(opt_exclude) + } + /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parse_expr()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6790bbff6..7041a0609 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -578,22 +578,22 @@ fn parse_select_into() { fn parse_select_wildcard() { let sql = "SELECT * FROM foo"; let select = verified_only_select(sql); - assert_eq!(&SelectItem::Wildcard, only(&select.projection)); + assert_eq!(&SelectItem::Wildcard(None), only(&select.projection)); let sql = "SELECT foo.* FROM foo"; let select = verified_only_select(sql); assert_eq!( - &SelectItem::QualifiedWildcard(ObjectName(vec![Ident::new("foo")])), + &SelectItem::QualifiedWildcard(ObjectName(vec![Ident::new("foo")]), None), only(&select.projection) ); let sql = "SELECT myschema.mytable.* FROM myschema.mytable"; let select = verified_only_select(sql); assert_eq!( - &SelectItem::QualifiedWildcard(ObjectName(vec![ - Ident::new("myschema"), - Ident::new("mytable"), - ])), + &SelectItem::QualifiedWildcard( + ObjectName(vec![Ident::new("myschema"), Ident::new("mytable"),]), + None + ), only(&select.projection) ); @@ -5432,7 +5432,7 @@ fn parse_merge() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: false, top: None, - projection: vec![SelectItem::Wildcard], + projection: vec![SelectItem::Wildcard(None)], into: None, from: vec![TableWithJoins { relation: TableFactor::Table { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d7e07a762..2a871a320 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1229,7 +1229,7 @@ fn parse_pg_returning() { pg_and_generic().verified_stmt("DELETE FROM tasks WHERE status = 'DONE' RETURNING *"); match stmt { Statement::Delete { returning, .. } => { - assert_eq!(Some(vec![SelectItem::Wildcard,]), returning); + assert_eq!(Some(vec![SelectItem::Wildcard(None),]), returning); } _ => unreachable!(), }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a201c5db7..127528b82 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -14,14 +14,14 @@ //! Test SQL syntax specific to Snowflake. The parser based on the //! generic dialect is also tested (on the inputs it can handle). -#[macro_use] -mod test_utils; -use test_utils::*; - use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SnowflakeDialect}; use sqlparser::parser::ParserError; use sqlparser::tokenizer::*; +use test_utils::*; + +#[macro_use] +mod test_utils; #[test] fn test_snowflake_create_table() { @@ -364,3 +364,61 @@ fn snowflake_and_generic() -> TestedDialects { dialects: vec![Box::new(SnowflakeDialect {}), Box::new(GenericDialect {})], } } + +#[test] +fn test_select_wildcard_with_exclude() { + match snowflake_and_generic().verified_stmt("SELECT * EXCLUDE (col_a) FROM data") { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::Wildcard(Some(exclude)) => { + assert_eq!( + *exclude, + ExcludeSelectItem::Multiple(vec![Ident::new("col_a")]) + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; + + match snowflake_and_generic() + .verified_stmt("SELECT name.* EXCLUDE department_id FROM employee_table") + { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::QualifiedWildcard(_, Some(exclude)) => { + assert_eq!( + *exclude, + ExcludeSelectItem::Single(Ident::new("department_id")) + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; + + match snowflake_and_generic() + .verified_stmt("SELECT * EXCLUDE (department_id, employee_id) FROM employee_table") + { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::Wildcard(Some(exclude)) => { + assert_eq!( + *exclude, + ExcludeSelectItem::Multiple(vec![ + Ident::new("department_id"), + Ident::new("employee_id") + ]) + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; +} From 1f22ea74aee80a9547c405ec4e8ba9b8f138be5d Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 30 Nov 2022 12:55:55 -0500 Subject: [PATCH 098/806] fix: logical conflicts --- src/ast/mod.rs | 2 +- src/ast/query.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index fbf318921..ac429ee32 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3697,7 +3697,7 @@ impl fmt::Display for SchemaName { /// Fulltext search modifiers ([1]). /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SearchModifier { /// `IN NATURAL LANGUAGE MODE`. diff --git a/src/ast/query.rs b/src/ast/query.rs index 0c07ce640..b367484d7 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -333,7 +333,7 @@ pub enum SelectItem { /// /// | (, , ...) /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ExcludeSelectItem { /// Single column name without parenthesis. From 96bca38fae6f1fa81c544446e28dadd64c9ffae8 Mon Sep 17 00:00:00 2001 From: mingmwang Date: Thu, 1 Dec 2022 01:57:45 +0800 Subject: [PATCH 099/806] Support SEMI/ANTI JOIN syntax (#723) --- src/ast/query.rs | 36 +++++++++++++++++++++++++ src/keywords.rs | 2 ++ src/parser.rs | 55 ++++++++++++++++++++++++++++++++++----- tests/sqlparser_common.rs | 32 +++++++++++++++++++++++ 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index b367484d7..172ba0f91 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -611,6 +611,34 @@ impl fmt::Display for Join { suffix(constraint) ), JoinOperator::CrossJoin => write!(f, " CROSS JOIN {}", self.relation), + JoinOperator::LeftSemi(constraint) => write!( + f, + " {}LEFT SEMI JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + ), + JoinOperator::RightSemi(constraint) => write!( + f, + " {}RIGHT SEMI JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + ), + JoinOperator::LeftAnti(constraint) => write!( + f, + " {}LEFT ANTI JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + ), + JoinOperator::RightAnti(constraint) => write!( + f, + " {}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), } @@ -625,6 +653,14 @@ pub enum JoinOperator { RightOuter(JoinConstraint), FullOuter(JoinConstraint), CrossJoin, + /// LEFT SEMI (non-standard) + LeftSemi(JoinConstraint), + /// RIGHT SEMI (non-standard) + RightSemi(JoinConstraint), + /// LEFT ANTI (non-standard) + LeftAnti(JoinConstraint), + /// RIGHT ANTI (non-standard) + RightAnti(JoinConstraint), /// CROSS APPLY (non-standard) CrossApply, /// OUTER APPLY (non-standard) diff --git a/src/keywords.rs b/src/keywords.rs index 1cbe9f289..9246411aa 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -77,6 +77,7 @@ define_keywords!( ALTER, ANALYZE, AND, + ANTI, ANY, APPLY, ARCHIVE, @@ -491,6 +492,7 @@ define_keywords!( SEARCH, SECOND, SELECT, + SEMI, SENSITIVE, SEQUENCE, SEQUENCEFILE, diff --git a/src/parser.rs b/src/parser.rs index 741135a6e..16e728943 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4830,16 +4830,57 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::JOIN)?; JoinOperator::Inner } - kw @ Keyword::LEFT | kw @ Keyword::RIGHT | kw @ Keyword::FULL => { + kw @ Keyword::LEFT | kw @ Keyword::RIGHT => { + let _ = self.next_token(); + let join_type = self.parse_one_of_keywords(&[ + Keyword::OUTER, + Keyword::SEMI, + Keyword::ANTI, + Keyword::JOIN, + ]); + match join_type { + Some(Keyword::OUTER) => { + self.expect_keyword(Keyword::JOIN)?; + match kw { + Keyword::LEFT => JoinOperator::LeftOuter, + Keyword::RIGHT => JoinOperator::RightOuter, + _ => unreachable!(), + } + } + Some(Keyword::SEMI) => { + self.expect_keyword(Keyword::JOIN)?; + match kw { + Keyword::LEFT => JoinOperator::LeftSemi, + Keyword::RIGHT => JoinOperator::RightSemi, + _ => unreachable!(), + } + } + Some(Keyword::ANTI) => { + self.expect_keyword(Keyword::JOIN)?; + match kw { + Keyword::LEFT => JoinOperator::LeftAnti, + Keyword::RIGHT => JoinOperator::RightAnti, + _ => unreachable!(), + } + } + Some(Keyword::JOIN) => match kw { + Keyword::LEFT => JoinOperator::LeftOuter, + Keyword::RIGHT => JoinOperator::RightOuter, + _ => unreachable!(), + }, + _ => { + return Err(ParserError::ParserError(format!( + "expected OUTER, SEMI, ANTI or JOIN after {:?}", + kw + ))) + } + } + } + Keyword::FULL => { let _ = self.next_token(); let _ = self.parse_keyword(Keyword::OUTER); self.expect_keyword(Keyword::JOIN)?; - match kw { - Keyword::LEFT => JoinOperator::LeftOuter, - Keyword::RIGHT => JoinOperator::RightOuter, - Keyword::FULL => JoinOperator::FullOuter, - _ => unreachable!(), - } + JoinOperator::FullOuter } Keyword::OUTER => { return self.expected("LEFT, RIGHT, or FULL", self.peek_token()); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7041a0609..f65c3813e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3868,6 +3868,22 @@ fn parse_joins_on() { only(&verified_only_select("SELECT * FROM t1 RIGHT JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint("t2", None, JoinOperator::RightOuter)] ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 LEFT SEMI JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::LeftSemi)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 RIGHT SEMI JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::RightSemi)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 LEFT ANTI JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::LeftAnti)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 RIGHT ANTI JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::RightAnti)] + ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 FULL JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint("t2", None, JoinOperator::FullOuter)] @@ -3917,6 +3933,22 @@ fn parse_joins_using() { only(&verified_only_select("SELECT * FROM t1 RIGHT JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::RightOuter)] ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 LEFT SEMI JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::LeftSemi)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 RIGHT SEMI JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::RightSemi)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 LEFT ANTI JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::LeftAnti)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 RIGHT ANTI JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::RightAnti)] + ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 FULL JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::FullOuter)] From 6f48fc9acb9b9e8e9f2f75c862251324942049c7 Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 30 Nov 2022 15:03:10 -0300 Subject: [PATCH 100/806] docs: add information about parting semantic logic to README.md (#724) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6e9f6290d..5da375870 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ foundation for vendor-specific parsers. This parser is currently being used by the [DataFusion] query engine, [LocustDB], [Ballista] and [GlueSQL]. +This parser is used as a syntax analyzer. We don't intend to have more semantic logic because it varies drastically +between dialects, the same is true for projects like compilers. If you want to do semantic analysis, feel free to use +this project as a base + ## Example To parse a simple `SELECT` statement: From 7101e0021f6d3d35e87a480c1e1f4cf5ba02803a Mon Sep 17 00:00:00 2001 From: Tao Wu Date: Thu, 1 Dec 2022 02:14:47 +0800 Subject: [PATCH 101/806] feat: add method to get current parsing index (#728) --- src/parser.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/parser.rs b/src/parser.rs index 16e728943..393c17a14 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5930,6 +5930,11 @@ impl<'a> Parser<'a> { } Ok(sequence_options) } + + /// The index of the first unprocessed token. + pub fn index(&self) -> usize { + self.index + } } impl Word { From 316359760d330b9940517c6e35e53cdb64c7a13b Mon Sep 17 00:00:00 2001 From: mao <50707849+step-baby@users.noreply.github.com> Date: Thu, 1 Dec 2022 02:17:38 +0800 Subject: [PATCH 102/806] Support USING method when creating indexes. (#731) * fix: create index using function * fix: code style Co-authored-by: yangjiaxin --- src/ast/mod.rs | 25 ++++++++++++++++--------- src/parser.rs | 6 ++++++ tests/sqlparser_common.rs | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ac429ee32..0810a41f2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1192,6 +1192,7 @@ pub enum Statement { /// index name name: ObjectName, table_name: ObjectName, + using: Option, columns: Vec, unique: bool, if_not_exists: bool, @@ -2115,18 +2116,24 @@ impl fmt::Display for Statement { Statement::CreateIndex { name, table_name, + using, columns, unique, if_not_exists, - } => write!( - f, - "CREATE {unique}INDEX {if_not_exists}{name} ON {table_name}({columns})", - unique = if *unique { "UNIQUE " } else { "" }, - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - name = name, - table_name = table_name, - columns = display_separated(columns, ",") - ), + } => { + write!( + f, + "CREATE {unique}INDEX {if_not_exists}{name} ON {table_name}", + unique = if *unique { "UNIQUE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = name, + table_name = table_name + )?; + if let Some(value) = using { + write!(f, " USING {} ", value)?; + } + write!(f, "({})", display_separated(columns, ",")) + } Statement::CreateRole { names, if_not_exists, diff --git a/src/parser.rs b/src/parser.rs index 393c17a14..1c71dadf0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2749,12 +2749,18 @@ impl<'a> Parser<'a> { let index_name = self.parse_object_name()?; self.expect_keyword(Keyword::ON)?; let table_name = self.parse_object_name()?; + let using = if self.expect_keyword(Keyword::USING).is_ok() { + Some(self.parse_identifier()?) + } else { + None + }; self.expect_token(&Token::LParen)?; let columns = self.parse_comma_separated(Parser::parse_order_by_expr)?; self.expect_token(&Token::RParen)?; Ok(Statement::CreateIndex { name: index_name, table_name, + using, columns, unique, if_not_exists, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f65c3813e..1bf261041 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5162,9 +5162,45 @@ fn parse_create_index() { columns, unique, if_not_exists, + .. + } => { + assert_eq!("idx_name", name.to_string()); + assert_eq!("test", table_name.to_string()); + assert_eq!(indexed_columns, columns); + assert!(unique); + assert!(if_not_exists) + } + _ => unreachable!(), + } +} + +#[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")), + asc: None, + nulls_first: None, + }, + OrderByExpr { + expr: Expr::Identifier(Ident::new("age")), + asc: Some(false), + nulls_first: None, + }, + ]; + match verified_stmt(sql) { + Statement::CreateIndex { + name, + table_name, + using, + columns, + unique, + if_not_exists, } => { assert_eq!("idx_name", name.to_string()); assert_eq!("test", table_name.to_string()); + assert_eq!("btree", using.unwrap().to_string()); assert_eq!(indexed_columns, columns); assert!(unique); assert!(if_not_exists) From e22aa783150979e402ec4c177c14d36b51baf3f7 Mon Sep 17 00:00:00 2001 From: Han YANG Date: Thu, 1 Dec 2022 02:22:29 +0800 Subject: [PATCH 103/806] fix: handle nested comments (#726) --- src/tokenizer.rs | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index e34ccf1f9..5dfac47f0 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -832,22 +832,23 @@ impl<'a> Tokenizer<'a> { chars: &mut Peekable>, ) -> Result, TokenizerError> { let mut s = String::new(); - let mut maybe_closing_comment = false; - // TODO: deal with nested comments + let mut nested = 1; + let mut last_ch = ' '; + loop { match chars.next() { Some(ch) => { - if maybe_closing_comment { - if ch == '/' { + if last_ch == '/' && ch == '*' { + nested += 1; + } else if last_ch == '*' && ch == '/' { + nested -= 1; + if nested == 0 { + s.pop(); break Ok(Some(Token::Whitespace(Whitespace::MultiLineComment(s)))); - } else { - s.push('*'); } } - maybe_closing_comment = ch == '*'; - if !maybe_closing_comment { - s.push(ch); - } + s.push(ch); + last_ch = ch; } None => break self.tokenizer_error("Unexpected EOF while in a multi-line comment"), } @@ -1355,6 +1356,23 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_nested_multiline_comment() { + let sql = String::from("0/*multi-line\n* \n/* comment \n /*comment*/*/ */ /comment*/1"); + + let dialect = GenericDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, &sql); + let tokens = tokenizer.tokenize().unwrap(); + let expected = vec![ + Token::Number("0".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment( + "multi-line\n* \n/* comment \n /*comment*/*/ */ /comment".to_string(), + )), + Token::Number("1".to_string(), false), + ]; + compare(expected, tokens); + } + #[test] fn tokenize_multiline_comment_with_even_asterisks() { let sql = String::from("\n/** Comment **/\n"); From b17c44a64aa2b7ce0cdae84220f5db937c4fd558 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 30 Nov 2022 13:24:13 -0500 Subject: [PATCH 104/806] Docs: Add an example to clarify semantic analysis (#739) --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5da375870..e8a14e131 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,16 @@ foundation for vendor-specific parsers. This parser is currently being used by the [DataFusion] query engine, [LocustDB], [Ballista] and [GlueSQL]. -This parser is used as a syntax analyzer. We don't intend to have more semantic logic because it varies drastically -between dialects, the same is true for projects like compilers. If you want to do semantic analysis, feel free to use -this project as a base +This crate provides only a syntax parser, and tries to avoid applying +any SQL semantics, and accepts queries that specific databases would +reject, even when using that Database's specific `Dialect`. For +example, `CREATE TABLE(x int, x int)` is accepted by this crate, even +though most SQL engines will reject this statement due to the repeated +column name `x`. + +This crate avoids semantic analysis because it varies drastically +between dialects and implementations. If you want to do semantic +analysis, feel free to use this project as a base ## Example From bd35273789b2403cdc533ce6263de224c1cadd28 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 30 Nov 2022 13:25:02 -0500 Subject: [PATCH 105/806] Cleanup: avoid using `unreachable!` when parsing semi/anti join (#738) --- src/parser.rs | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 1c71dadf0..e72ad8343 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4838,6 +4838,7 @@ impl<'a> Parser<'a> { } kw @ Keyword::LEFT | kw @ Keyword::RIGHT => { let _ = self.next_token(); + let is_left = kw == Keyword::LEFT; let join_type = self.parse_one_of_keywords(&[ Keyword::OUTER, Keyword::SEMI, @@ -4847,33 +4848,35 @@ impl<'a> Parser<'a> { match join_type { Some(Keyword::OUTER) => { self.expect_keyword(Keyword::JOIN)?; - match kw { - Keyword::LEFT => JoinOperator::LeftOuter, - Keyword::RIGHT => JoinOperator::RightOuter, - _ => unreachable!(), + if is_left { + JoinOperator::LeftOuter + } else { + JoinOperator::RightOuter } } Some(Keyword::SEMI) => { self.expect_keyword(Keyword::JOIN)?; - match kw { - Keyword::LEFT => JoinOperator::LeftSemi, - Keyword::RIGHT => JoinOperator::RightSemi, - _ => unreachable!(), + if is_left { + JoinOperator::LeftSemi + } else { + JoinOperator::RightSemi } } Some(Keyword::ANTI) => { self.expect_keyword(Keyword::JOIN)?; - match kw { - Keyword::LEFT => JoinOperator::LeftAnti, - Keyword::RIGHT => JoinOperator::RightAnti, - _ => unreachable!(), + if is_left { + JoinOperator::LeftAnti + } else { + JoinOperator::RightAnti + } + } + Some(Keyword::JOIN) => { + if is_left { + JoinOperator::LeftOuter + } else { + JoinOperator::RightOuter } } - Some(Keyword::JOIN) => match kw { - Keyword::LEFT => JoinOperator::LeftOuter, - Keyword::RIGHT => JoinOperator::RightOuter, - _ => unreachable!(), - }, _ => { return Err(ParserError::ParserError(format!( "expected OUTER, SEMI, ANTI or JOIN after {:?}", From 77eddfcc8d4eb2c9bbd917bf445552cb5a26525b Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 30 Nov 2022 13:25:17 -0500 Subject: [PATCH 106/806] minor cleanup to avoid `is_ok()` (#740) --- src/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index e72ad8343..ff822b6a3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2749,7 +2749,7 @@ impl<'a> Parser<'a> { let index_name = self.parse_object_name()?; self.expect_keyword(Keyword::ON)?; let table_name = self.parse_object_name()?; - let using = if self.expect_keyword(Keyword::USING).is_ok() { + let using = if self.parse_keyword(Keyword::USING) { Some(self.parse_identifier()?) } else { None From a422116b8945d00855d7b8864652073265781385 Mon Sep 17 00:00:00 2001 From: yuval-illumex <85674443+yuval-illumex@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:33:15 +0200 Subject: [PATCH 107/806] inital commit (#736) --- src/ast/mod.rs | 5 +++++ src/parser.rs | 9 +++++++++ tests/sqlparser_common.rs | 41 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0810a41f2..159249c17 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1149,6 +1149,7 @@ pub enum Statement { columns: Vec, query: Box, with_options: Vec, + cluster_by: Vec, }, /// CREATE TABLE CreateTable { @@ -1887,6 +1888,7 @@ impl fmt::Display for Statement { query, materialized, with_options, + cluster_by, } => { write!( f, @@ -1901,6 +1903,9 @@ impl fmt::Display for Statement { if !columns.is_empty() { write!(f, " ({})", display_comma_separated(columns))?; } + if !cluster_by.is_empty() { + write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; + } write!(f, " AS {}", query) } Statement::CreateTable { diff --git a/src/parser.rs b/src/parser.rs index ff822b6a3..cb78cf5a3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2339,6 +2339,14 @@ impl<'a> Parser<'a> { let name = self.parse_object_name()?; let columns = self.parse_parenthesized_column_list(Optional)?; let with_options = self.parse_options(Keyword::WITH)?; + + let cluster_by = if self.parse_keyword(Keyword::CLUSTER) { + self.expect_keyword(Keyword::BY)?; + self.parse_parenthesized_column_list(Optional)? + } else { + vec![] + }; + self.expect_keyword(Keyword::AS)?; let query = Box::new(self.parse_query()?); // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. @@ -2349,6 +2357,7 @@ impl<'a> Parser<'a> { materialized, or_replace, with_options, + cluster_by, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1bf261041..e9b73b93d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4496,6 +4496,7 @@ fn parse_create_view() { or_replace, materialized, with_options, + cluster_by, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -4503,6 +4504,7 @@ fn parse_create_view() { assert!(!materialized); assert!(!or_replace); assert_eq!(with_options, vec![]); + assert_eq!(cluster_by, vec![]); } _ => unreachable!(), } @@ -4542,13 +4544,15 @@ fn parse_create_view_with_columns() { with_options, query, materialized, + cluster_by, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![Ident::new("has"), Ident::new("cols")]); assert_eq!(with_options, vec![]); assert_eq!("SELECT 1, 2", query.to_string()); assert!(!materialized); - assert!(!or_replace) + assert!(!or_replace); + assert_eq!(cluster_by, vec![]); } _ => unreachable!(), } @@ -4565,13 +4569,15 @@ fn parse_create_or_replace_view() { with_options, query, materialized, + cluster_by, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(with_options, vec![]); assert_eq!("SELECT 1", query.to_string()); assert!(!materialized); - assert!(or_replace) + assert!(or_replace); + assert_eq!(cluster_by, vec![]); } _ => unreachable!(), } @@ -4592,13 +4598,15 @@ fn parse_create_or_replace_materialized_view() { with_options, query, materialized, + cluster_by, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(with_options, vec![]); assert_eq!("SELECT 1", query.to_string()); assert!(materialized); - assert!(or_replace) + assert!(or_replace); + assert_eq!(cluster_by, vec![]); } _ => unreachable!(), } @@ -4615,6 +4623,32 @@ fn parse_create_materialized_view() { query, materialized, with_options, + cluster_by, + } => { + assert_eq!("myschema.myview", name.to_string()); + assert_eq!(Vec::::new(), columns); + assert_eq!("SELECT foo FROM bar", query.to_string()); + assert!(materialized); + assert_eq!(with_options, vec![]); + assert!(!or_replace); + assert_eq!(cluster_by, vec![]); + } + _ => unreachable!(), + } +} + +#[test] +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 { + name, + or_replace, + columns, + query, + materialized, + with_options, + cluster_by, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -4622,6 +4656,7 @@ fn parse_create_materialized_view() { assert!(materialized); assert_eq!(with_options, vec![]); assert!(!or_replace); + assert_eq!(cluster_by, vec![Ident::new("foo")]); } _ => unreachable!(), } From 042effdf1406ae0a81eb72e67c107396b49539fb Mon Sep 17 00:00:00 2001 From: zidaye <44500963+zidaye@users.noreply.github.com> Date: Thu, 1 Dec 2022 02:33:33 +0800 Subject: [PATCH 108/806] update on conflict method (#735) --- src/ast/mod.rs | 26 ++++++++++++- src/parser.rs | 12 +++++- tests/sqlparser_postgres.rs | 74 ++++++++++++++++++++++++++++++------- 3 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 159249c17..f727de27e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2702,7 +2702,16 @@ pub struct OnConflict { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum OnConflictAction { DoNothing, - DoUpdate(Vec), + DoUpdate(DoUpdate), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DoUpdate { + /// Column assignments + pub assignments: Vec, + /// WHERE + pub selection: Option, } impl fmt::Display for OnInsert { @@ -2730,7 +2739,20 @@ impl fmt::Display for OnConflictAction { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::DoNothing => write!(f, "DO NOTHING"), - Self::DoUpdate(a) => write!(f, "DO UPDATE SET {}", display_comma_separated(a)), + Self::DoUpdate(do_update) => { + write!(f, "DO UPDATE")?; + if !do_update.assignments.is_empty() { + write!( + f, + " SET {}", + display_comma_separated(&do_update.assignments) + )?; + } + if let Some(selection) = &do_update.selection { + write!(f, " WHERE {}", selection)?; + } + Ok(()) + } } } } diff --git a/src/parser.rs b/src/parser.rs index cb78cf5a3..260dbfbeb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5351,8 +5351,16 @@ impl<'a> Parser<'a> { } else { self.expect_keyword(Keyword::UPDATE)?; self.expect_keyword(Keyword::SET)?; - let l = self.parse_comma_separated(Parser::parse_assignment)?; - OnConflictAction::DoUpdate(l) + let assignments = self.parse_comma_separated(Parser::parse_assignment)?; + let selection = if self.parse_keyword(Keyword::WHERE) { + Some(self.parse_expr()?) + } else { + None + }; + OnConflictAction::DoUpdate(DoUpdate { + assignments, + selection, + }) }; Some(OnInsert::OnConflict(OnConflict { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2a871a320..e32a7b027 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1117,10 +1117,13 @@ fn parse_pg_on_conflict() { } => { assert_eq!(vec![Ident::from("did")], conflict_target); assert_eq!( - OnConflictAction::DoUpdate(vec![Assignment { - id: vec!["dname".into()], - value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "dname".into()]) - },]), + OnConflictAction::DoUpdate(DoUpdate { + assignments: vec![Assignment { + id: vec!["dname".into()], + value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "dname".into()]) + },], + selection: None + }), action ); } @@ -1147,16 +1150,22 @@ fn parse_pg_on_conflict() { conflict_target ); assert_eq!( - OnConflictAction::DoUpdate(vec![ - Assignment { - id: vec!["dname".into()], - value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "dname".into()]) - }, - Assignment { - id: vec!["area".into()], - value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "area".into()]) - }, - ]), + OnConflictAction::DoUpdate(DoUpdate { + assignments: vec![ + Assignment { + id: vec!["dname".into()], + value: Expr::CompoundIdentifier(vec![ + "EXCLUDED".into(), + "dname".into() + ]) + }, + Assignment { + id: vec!["area".into()], + value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "area".into()]) + }, + ], + selection: None + }), action ); } @@ -1182,6 +1191,43 @@ fn parse_pg_on_conflict() { } _ => unreachable!(), }; + + let stmt = pg_and_generic().verified_stmt( + "INSERT INTO distributors (did, dname, dsize) \ + VALUES (5, 'Gizmo Transglobal', 1000), (6, 'Associated Computing, Inc', 1010) \ + ON CONFLICT(did) \ + DO UPDATE SET dname = $1 WHERE dsize > $2", + ); + match stmt { + Statement::Insert { + on: + Some(OnInsert::OnConflict(OnConflict { + conflict_target, + action, + })), + .. + } => { + assert_eq!(vec![Ident::from("did")], conflict_target); + assert_eq!( + OnConflictAction::DoUpdate(DoUpdate { + assignments: vec![Assignment { + id: vec!["dname".into()], + value: Expr::Value(Value::Placeholder("$1".to_string())) + },], + selection: Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "dsize".to_string(), + quote_style: None + })), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(Value::Placeholder("$2".to_string()))) + }) + }), + action + ); + } + _ => unreachable!(), + }; } #[test] From faf75b71613431e66ec22c82e3c9ad6157474458 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 30 Nov 2022 13:34:31 -0500 Subject: [PATCH 109/806] fix logical conflict --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f727de27e..276189f95 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2705,7 +2705,7 @@ pub enum OnConflictAction { DoUpdate(DoUpdate), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct DoUpdate { /// Column assignments From 8e1c90c0d804c32a4b55d49ffae96bbd07cea058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alja=C5=BE=20Mur=20Er=C5=BEen?= Date: Thu, 1 Dec 2022 15:46:55 +0100 Subject: [PATCH 110/806] Support MySQL `ROWS` syntax for `VALUES` (#737) * Adapt VALUES to MySQL dialect * Update src/ast/query.rs Co-authored-by: Andrew Lamb * remove *requirement* for ROW Co-authored-by: Andrew Lamb --- src/ast/query.rs | 12 +++++-- src/parser.rs | 10 ++++-- tests/sqlparser_common.rs | 19 ++++++++---- tests/sqlparser_mysql.rs | 62 +++++++++++++++++++++++-------------- tests/sqlparser_postgres.rs | 4 ++- 5 files changed, 72 insertions(+), 35 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 172ba0f91..d2b5e6b3b 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -797,16 +797,22 @@ impl fmt::Display for Top { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Values(pub Vec>); +pub struct Values { + /// Was there an explict ROWs keyword (MySQL)? + /// + pub explicit_row: bool, + pub rows: Vec>, +} impl fmt::Display for Values { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "VALUES ")?; + let prefix = if self.explicit_row { "ROW" } else { "" }; let mut delim = ""; - for row in &self.0 { + for row in &self.rows { write!(f, "{}", delim)?; delim = ", "; - write!(f, "({})", display_comma_separated(row))?; + write!(f, "{prefix}({})", display_comma_separated(row))?; } Ok(()) } diff --git a/src/parser.rs b/src/parser.rs index 260dbfbeb..faadedcc8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5642,13 +5642,19 @@ impl<'a> Parser<'a> { } pub fn parse_values(&mut self) -> Result { - let values = self.parse_comma_separated(|parser| { + let mut explicit_row = false; + + let rows = self.parse_comma_separated(|parser| { + if parser.parse_keyword(Keyword::ROW) { + explicit_row = true; + } + parser.expect_token(&Token::LParen)?; let exprs = parser.parse_comma_separated(Parser::parse_expr)?; parser.expect_token(&Token::RParen)?; Ok(exprs) })?; - Ok(Values(values)) + Ok(Values { explicit_row, rows }) } pub fn parse_start_transaction(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e9b73b93d..2ef92f458 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -88,7 +88,9 @@ fn parse_insert_values() { assert_eq!(column, &Ident::new(expected_columns[index].clone())); } match &*source.body { - SetExpr::Values(Values(values)) => assert_eq!(values.as_slice(), expected_rows), + SetExpr::Values(Values { rows, .. }) => { + assert_eq!(rows.as_slice(), expected_rows) + } _ => unreachable!(), } } @@ -460,6 +462,7 @@ fn parse_top_level() { verified_stmt("(SELECT 1)"); verified_stmt("((SELECT 1))"); verified_stmt("VALUES (1)"); + verified_stmt("VALUES ROW(1, true, 'a'), ROW(2, false, 'b')"); } #[test] @@ -4268,6 +4271,7 @@ fn parse_values() { verified_stmt("SELECT * FROM (VALUES (1), (2), (3))"); verified_stmt("SELECT * FROM (VALUES (1), (2), (3)), (VALUES (1, 2, 3))"); verified_stmt("SELECT * FROM (VALUES (1)) UNION VALUES (1)"); + verified_stmt("SELECT * FROM (VALUES ROW(1, true, 'a'), ROW(2, false, 'b')) AS t (a, b, c)"); } #[test] @@ -5608,11 +5612,14 @@ fn parse_merge() { MergeClause::NotMatched { predicate: None, columns: vec![Ident::new("A"), Ident::new("B"), Ident::new("C")], - values: Values(vec![vec![ - Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("A")]), - Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("B")]), - Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("C")]), - ]]), + values: Values { + explicit_row: false, + rows: vec![vec![ + Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("A")]), + Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("B")]), + Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("C")]), + ]] + }, }, MergeClause::MatchedUpdate { predicate: Some(Expr::BinaryOp { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 1d8c6a05e..18f554f9e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -660,20 +660,25 @@ fn parse_simple_insert() { assert_eq!( Box::new(Query { with: None, - body: Box::new(SetExpr::Values(Values(vec![ - vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(Value::Number("1".to_string(), false)) - ], - vec![ - Expr::Value(Value::SingleQuotedString("Test Entry 2".to_string())), - Expr::Value(Value::Number("2".to_string(), false)) - ], - vec![ - Expr::Value(Value::SingleQuotedString("Test Entry 3".to_string())), - Expr::Value(Value::Number("3".to_string(), false)) + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![ + vec![ + Expr::Value(Value::SingleQuotedString( + "Test Some Inserts".to_string() + )), + Expr::Value(Value::Number("1".to_string(), false)) + ], + vec![ + Expr::Value(Value::SingleQuotedString("Test Entry 2".to_string())), + Expr::Value(Value::Number("2".to_string(), false)) + ], + vec![ + Expr::Value(Value::SingleQuotedString("Test Entry 3".to_string())), + Expr::Value(Value::Number("3".to_string(), false)) + ] ] - ]))), + })), order_by: vec![], limit: None, offset: None, @@ -717,16 +722,21 @@ fn parse_insert_with_on_duplicate_update() { assert_eq!( Box::new(Query { with: None, - body: Box::new(SetExpr::Values(Values(vec![vec![ - Expr::Value(Value::SingleQuotedString("accounting_manager".to_string())), - Expr::Value(Value::SingleQuotedString( - "Some description about the group".to_string() - )), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), - ]]))), + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![vec![ + Expr::Value(Value::SingleQuotedString( + "accounting_manager".to_string() + )), + Expr::Value(Value::SingleQuotedString( + "Some description about the group".to_string() + )), + Expr::Value(Value::Boolean(true)), + Expr::Value(Value::Boolean(true)), + Expr::Value(Value::Boolean(true)), + Expr::Value(Value::Boolean(true)), + ]] + })), order_by: vec![], limit: None, offset: None, @@ -1209,3 +1219,9 @@ fn mysql_and_generic() -> TestedDialects { dialects: vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})], } } + +#[test] +fn parse_values() { + mysql().verified_stmt("VALUES ROW(1, true, 'a')"); + mysql().verified_stmt("SELECT a, c FROM (VALUES ROW(1, true, 'a'), ROW(2, false, 'b'), ROW(3, false, 'c')) AS t (a, b, c)"); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e32a7b027..7f6af7b26 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1067,7 +1067,9 @@ fn parse_prepare() { Expr::Identifier("a3".into()), ]]; match &*source.body { - SetExpr::Values(Values(values)) => assert_eq!(values.as_slice(), &expected_values), + SetExpr::Values(Values { rows, .. }) => { + assert_eq!(rows.as_slice(), &expected_values) + } _ => unreachable!(), } } From 528b3f2234c5f200baeae11fe5f646c3565ed9c4 Mon Sep 17 00:00:00 2001 From: Sarah Yurick <53962159+sarahyurick@users.noreply.github.com> Date: Thu, 1 Dec 2022 06:56:14 -0800 Subject: [PATCH 111/806] Support `CREATE TABLE x AS TABLE y` (#704) * first commit * fix style and edit test * fix test? * remove unnecessary logic * refactor implementation * codestyle * add schema support * codestyle and lint * Apply suggestions from code review Co-authored-by: Andrew Lamb * PartialOrd and Ord * clean up parser logic * codestyle and lint Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 2 +- src/ast/query.rs | 28 +++++++++++++++++++- src/parser.rs | 54 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 49 +++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 276189f95..49c82857f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -33,7 +33,7 @@ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ Cte, ExcludeSelectItem, Fetch, Join, JoinConstraint, JoinOperator, LateralView, LockType, Offset, OffsetRows, OrderByExpr, Query, Select, SelectInto, SelectItem, SetExpr, SetOperator, - SetQuantifier, TableAlias, TableFactor, TableWithJoins, Top, Values, With, + SetQuantifier, Table, TableAlias, TableFactor, TableWithJoins, Top, Values, With, }; pub use self::value::{escape_quoted_string, DateTimeField, TrimWhereField, Value}; diff --git a/src/ast/query.rs b/src/ast/query.rs index d2b5e6b3b..6cccb6e44 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -84,7 +84,7 @@ pub enum SetExpr { }, Values(Values), Insert(Statement), - // TODO: ANSI SQL supports `TABLE` here. + Table(Box
), } impl fmt::Display for SetExpr { @@ -94,6 +94,7 @@ impl fmt::Display for SetExpr { SetExpr::Query(q) => write!(f, "({})", q), SetExpr::Values(v) => write!(f, "{}", v), SetExpr::Insert(v) => write!(f, "{}", v), + SetExpr::Table(t) => write!(f, "{}", t), SetExpr::SetOperation { left, right, @@ -152,6 +153,31 @@ impl fmt::Display for SetQuantifier { } } } + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// A [`TABLE` command]( https://www.postgresql.org/docs/current/sql-select.html#SQL-TABLE) +pub struct Table { + pub table_name: Option, + pub schema_name: Option, +} + +impl fmt::Display for Table { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref schema_name) = self.schema_name { + write!( + f, + "TABLE {}.{}", + schema_name, + self.table_name.as_ref().unwrap(), + )?; + } else { + write!(f, "TABLE {}", self.table_name.as_ref().unwrap(),)?; + } + Ok(()) + } +} + /// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may /// appear either as the only body item of a `Query`, or as an operand /// to a set operation like `UNION`. diff --git a/src/parser.rs b/src/parser.rs index faadedcc8..654cbc4fd 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4368,6 +4368,14 @@ impl<'a> Parser<'a> { SetExpr::Query(Box::new(subquery)) } else if self.parse_keyword(Keyword::VALUES) { SetExpr::Values(self.parse_values()?) + } else if self.parse_keyword(Keyword::TABLE) { + let token1 = self.peek_token(); + let token2 = self.peek_nth_token(1); + let token3 = self.peek_nth_token(2); + self.next_token(); + self.next_token(); + self.next_token(); + SetExpr::Table(Box::new(self.parse_as_table(token1, token2, token3)?)) } else { return self.expected( "SELECT, VALUES, or a subquery in the query body", @@ -4566,6 +4574,52 @@ impl<'a> Parser<'a> { }) } + /// Parse `CREATE TABLE x AS TABLE y` + pub fn parse_as_table( + &self, + token1: Token, + token2: Token, + token3: Token, + ) -> Result { + let table_name; + let schema_name; + if token2 == Token::Period { + match token1 { + Token::Word(w) => { + schema_name = w.value; + } + _ => { + return self.expected("Schema name", token1); + } + } + match token3 { + Token::Word(w) => { + table_name = w.value; + } + _ => { + return self.expected("Table name", token3); + } + } + Ok(Table { + table_name: Some(table_name), + schema_name: Some(schema_name), + }) + } else { + match token1 { + Token::Word(w) => { + table_name = w.value; + } + _ => { + return self.expected("Table name", token1); + } + } + Ok(Table { + table_name: Some(table_name), + schema_name: None, + }) + } + } + pub fn parse_set(&mut self) -> Result { let modifier = self.parse_one_of_keywords(&[Keyword::SESSION, Keyword::LOCAL, Keyword::HIVEVAR]); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2ef92f458..24d101919 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2273,6 +2273,55 @@ fn parse_create_table_as() { } } +#[test] +fn parse_create_table_as_table() { + let sql1 = "CREATE TABLE new_table AS TABLE old_table"; + + let expected_query1 = Box::new(Query { + with: None, + body: Box::new(SetExpr::Table(Box::new(Table { + table_name: Some("old_table".to_string()), + schema_name: None, + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + lock: None, + }); + + match verified_stmt(sql1) { + Statement::CreateTable { query, name, .. } => { + assert_eq!(name, ObjectName(vec![Ident::new("new_table")])); + assert_eq!(query.unwrap(), expected_query1); + } + _ => unreachable!(), + } + + let sql2 = "CREATE TABLE new_table AS TABLE schema_name.old_table"; + + let expected_query2 = Box::new(Query { + with: None, + body: Box::new(SetExpr::Table(Box::new(Table { + table_name: Some("old_table".to_string()), + schema_name: Some("schema_name".to_string()), + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + lock: None, + }); + + match verified_stmt(sql2) { + Statement::CreateTable { query, name, .. } => { + assert_eq!(name, ObjectName(vec![Ident::new("new_table")])); + assert_eq!(query.unwrap(), expected_query2); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_on_cluster() { // Using single-quote literal to define current cluster From f621142f890316574fd5970945a8493a3861f206 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 1 Dec 2022 13:12:27 -0500 Subject: [PATCH 112/806] Clean up some redundant code in parser (#741) --- src/parser.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 654cbc4fd..298a66a7c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4369,13 +4369,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::VALUES) { SetExpr::Values(self.parse_values()?) } else if self.parse_keyword(Keyword::TABLE) { - let token1 = self.peek_token(); - let token2 = self.peek_nth_token(1); - let token3 = self.peek_nth_token(2); - self.next_token(); - self.next_token(); - self.next_token(); - SetExpr::Table(Box::new(self.parse_as_table(token1, token2, token3)?)) + SetExpr::Table(Box::new(self.parse_as_table()?)) } else { return self.expected( "SELECT, VALUES, or a subquery in the query body", @@ -4575,12 +4569,11 @@ impl<'a> Parser<'a> { } /// Parse `CREATE TABLE x AS TABLE y` - pub fn parse_as_table( - &self, - token1: Token, - token2: Token, - token3: Token, - ) -> Result { + pub fn parse_as_table(&mut self) -> Result { + let token1 = self.next_token(); + let token2 = self.next_token(); + let token3 = self.next_token(); + let table_name; let schema_name; if token2 == Token::Period { From 5b53df97c423c27b8a7d9dfef6faef74d62db991 Mon Sep 17 00:00:00 2001 From: Runji Wang Date: Fri, 2 Dec 2022 05:08:49 +0800 Subject: [PATCH 113/806] Support postgres `CREATE FUNCTION` (#722) * support basic pg CREATE FUNCTION Signed-off-by: Runji Wang * support function argument Signed-off-by: Runji Wang * fix display and use verify in test Signed-off-by: Runji Wang * support OR REPLACE Signed-off-by: Runji Wang * fix compile error in bigdecimal Signed-off-by: Runji Wang * unify all `CreateFunctionBody` to a structure Signed-off-by: Runji Wang Signed-off-by: Runji Wang --- src/ast/mod.rs | 150 ++++++++++++++++++++++++++++++++++-- src/keywords.rs | 3 + src/parser.rs | 130 ++++++++++++++++++++++++++++--- tests/sqlparser_hive.rs | 23 +++--- tests/sqlparser_postgres.rs | 54 +++++++++++++ 5 files changed, 331 insertions(+), 29 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 49c82857f..3fe548c81 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1405,11 +1405,15 @@ pub enum Statement { /// CREATE FUNCTION /// /// Hive: https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction + /// Postgres: https://www.postgresql.org/docs/15/sql-createfunction.html CreateFunction { + or_replace: bool, temporary: bool, name: ObjectName, - class_name: String, - using: Option, + args: Option>, + return_type: Option, + /// Optional parameters. + params: CreateFunctionBody, }, /// `ASSERT [AS ]` Assert { @@ -1866,19 +1870,26 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateFunction { + or_replace, temporary, name, - class_name, - using, + args, + return_type, + params, } => { write!( f, - "CREATE {temp}FUNCTION {name} AS '{class_name}'", + "CREATE {or_replace}{temp}FUNCTION {name}", temp = if *temporary { "TEMPORARY " } else { "" }, + or_replace = if *or_replace { "OR REPLACE " } else { "" }, )?; - if let Some(u) = using { - write!(f, " {}", u)?; + if let Some(args) = args { + write!(f, "({})", display_comma_separated(args))?; } + if let Some(return_type) = return_type { + write!(f, " RETURNS {}", return_type)?; + } + write!(f, "{params}")?; Ok(()) } Statement::CreateView { @@ -3679,6 +3690,131 @@ impl fmt::Display for ContextModifier { } } +/// Function argument in CREATE FUNCTION. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CreateFunctionArg { + pub mode: Option, + pub name: Option, + pub data_type: DataType, + pub default_expr: Option, +} + +impl CreateFunctionArg { + /// Returns an unnamed argument. + pub fn unnamed(data_type: DataType) -> Self { + Self { + mode: None, + name: None, + data_type, + default_expr: None, + } + } + + /// Returns an argument with name. + pub fn with_name(name: &str, data_type: DataType) -> Self { + Self { + mode: None, + name: Some(name.into()), + data_type, + default_expr: None, + } + } +} + +impl fmt::Display for CreateFunctionArg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(mode) = &self.mode { + write!(f, "{} ", mode)?; + } + if let Some(name) = &self.name { + write!(f, "{} ", name)?; + } + write!(f, "{}", self.data_type)?; + if let Some(default_expr) = &self.default_expr { + write!(f, " = {}", default_expr)?; + } + Ok(()) + } +} + +/// The mode of an argument in CREATE FUNCTION. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ArgMode { + In, + Out, + InOut, +} + +impl fmt::Display for ArgMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ArgMode::In => write!(f, "IN"), + ArgMode::Out => write!(f, "OUT"), + ArgMode::InOut => write!(f, "INOUT"), + } + } +} + +/// These attributes inform the query optimizer about the behavior of the function. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum FunctionBehavior { + Immutable, + Stable, + Volatile, +} + +impl fmt::Display for FunctionBehavior { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionBehavior::Immutable => write!(f, "IMMUTABLE"), + FunctionBehavior::Stable => write!(f, "STABLE"), + FunctionBehavior::Volatile => write!(f, "VOLATILE"), + } + } +} + +/// Postgres: https://www.postgresql.org/docs/15/sql-createfunction.html +#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CreateFunctionBody { + /// LANGUAGE lang_name + pub language: Option, + /// IMMUTABLE | STABLE | VOLATILE + pub behavior: Option, + /// AS 'definition' + /// + /// Note that Hive's `AS class_name` is also parsed here. + pub as_: Option, + /// RETURN expression + pub return_: Option, + /// USING ... (Hive only) + pub using: Option, +} + +impl fmt::Display for CreateFunctionBody { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(language) = &self.language { + write!(f, " LANGUAGE {language}")?; + } + if let Some(behavior) = &self.behavior { + write!(f, " {behavior}")?; + } + if let Some(definition) = &self.as_ { + write!(f, " AS '{definition}'")?; + } + if let Some(expr) = &self.return_ { + write!(f, " RETURN {expr}")?; + } + if let Some(using) = &self.using { + write!(f, " {using}")?; + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CreateFunctionUsing { diff --git a/src/keywords.rs b/src/keywords.rs index 9246411aa..1fc15fda0 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -284,6 +284,7 @@ define_keywords!( IF, IGNORE, ILIKE, + IMMUTABLE, IN, INCREMENT, INDEX, @@ -518,6 +519,7 @@ define_keywords!( SQLSTATE, SQLWARNING, SQRT, + STABLE, START, STATIC, STATISTICS, @@ -604,6 +606,7 @@ define_keywords!( VERSIONING, VIEW, VIRTUAL, + VOLATILE, WEEK, WHEN, WHENEVER, diff --git a/src/parser.rs b/src/parser.rs index 298a66a7c..29577f053 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2026,9 +2026,11 @@ impl<'a> Parser<'a> { self.parse_create_view(or_replace) } 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) } else if or_replace { self.expected( - "[EXTERNAL] TABLE or [MATERIALIZED] VIEW after CREATE OR REPLACE", + "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE", self.peek_token(), ) } else if self.parse_keyword(Keyword::INDEX) { @@ -2041,8 +2043,6 @@ impl<'a> Parser<'a> { self.parse_create_schema() } else if self.parse_keyword(Keyword::DATABASE) { self.parse_create_database() - } else if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::FUNCTION) { - self.parse_create_function(temporary) } else if self.parse_keyword(Keyword::ROLE) { self.parse_create_role() } else if self.parse_keyword(Keyword::SEQUENCE) { @@ -2253,20 +2253,126 @@ impl<'a> Parser<'a> { } } - pub fn parse_create_function(&mut self, temporary: bool) -> Result { - let name = self.parse_object_name()?; - self.expect_keyword(Keyword::AS)?; - let class_name = self.parse_literal_string()?; - let using = self.parse_optional_create_function_using()?; + pub fn parse_create_function( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { + if dialect_of!(self is HiveDialect) { + let name = self.parse_object_name()?; + self.expect_keyword(Keyword::AS)?; + let class_name = self.parse_literal_string()?; + let params = CreateFunctionBody { + as_: Some(class_name), + using: self.parse_optional_create_function_using()?, + ..Default::default() + }; - Ok(Statement::CreateFunction { - temporary, + Ok(Statement::CreateFunction { + or_replace, + temporary, + name, + args: None, + return_type: None, + params, + }) + } else if dialect_of!(self is PostgreSqlDialect) { + let name = self.parse_object_name()?; + self.expect_token(&Token::LParen)?; + let args = self.parse_comma_separated(Parser::parse_create_function_arg)?; + self.expect_token(&Token::RParen)?; + + let return_type = if self.parse_keyword(Keyword::RETURNS) { + Some(self.parse_data_type()?) + } else { + None + }; + + let params = self.parse_create_function_body()?; + + Ok(Statement::CreateFunction { + or_replace, + temporary, + name, + args: Some(args), + return_type, + params, + }) + } else { + self.prev_token(); + self.expected("an object type after CREATE", self.peek_token()) + } + } + + fn parse_create_function_arg(&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 + }; + + // 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 + name = Some(n.0[0].clone()); + data_type = self.parse_data_type()?; + } + + let default_expr = if self.parse_keyword(Keyword::DEFAULT) || self.consume_token(&Token::Eq) + { + Some(self.parse_expr()?) + } else { + None + }; + Ok(CreateFunctionArg { + mode, name, - class_name, - using, + data_type, + default_expr, }) } + fn parse_create_function_body(&mut self) -> Result { + let mut body = CreateFunctionBody::default(); + loop { + fn ensure_not_set(field: &Option, name: &str) -> Result<(), ParserError> { + if field.is_some() { + return Err(ParserError::ParserError(format!( + "{name} specified more than once", + ))); + } + Ok(()) + } + if self.parse_keyword(Keyword::AS) { + ensure_not_set(&body.as_, "AS")?; + body.as_ = Some(self.parse_literal_string()?); + } else if self.parse_keyword(Keyword::LANGUAGE) { + ensure_not_set(&body.language, "LANGUAGE")?; + body.language = Some(self.parse_identifier()?); + } else if self.parse_keyword(Keyword::IMMUTABLE) { + ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE")?; + body.behavior = Some(FunctionBehavior::Immutable); + } else if self.parse_keyword(Keyword::STABLE) { + ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE")?; + body.behavior = Some(FunctionBehavior::Stable); + } else if self.parse_keyword(Keyword::VOLATILE) { + ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE")?; + body.behavior = Some(FunctionBehavior::Volatile); + } else if self.parse_keyword(Keyword::RETURN) { + ensure_not_set(&body.return_, "RETURN")?; + body.return_ = Some(self.parse_expr()?); + } else { + return Ok(body); + } + } + } + pub fn parse_create_external_table( &mut self, or_replace: bool, diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 070f55089..99a81eff2 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -16,8 +16,8 @@ //! is also tested (on the inputs it can handle). use sqlparser::ast::{ - CreateFunctionUsing, Expr, Function, Ident, ObjectName, SelectItem, Statement, TableFactor, - UnaryOperator, Value, + CreateFunctionBody, CreateFunctionUsing, Expr, Function, Ident, ObjectName, SelectItem, + Statement, TableFactor, UnaryOperator, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect}; use sqlparser::parser::ParserError; @@ -244,17 +244,20 @@ fn parse_create_function() { Statement::CreateFunction { temporary, name, - class_name, - using, + params, + .. } => { assert!(temporary); - assert_eq!("mydb.myfunc", name.to_string()); - assert_eq!("org.random.class.Name", class_name); + assert_eq!(name.to_string(), "mydb.myfunc"); assert_eq!( - using, - Some(CreateFunctionUsing::Jar( - "hdfs://somewhere.com:8020/very/far".to_string() - )) + params, + CreateFunctionBody { + as_: Some("org.random.class.Name".to_string()), + using: Some(CreateFunctionUsing::Jar( + "hdfs://somewhere.com:8020/very/far".to_string() + )), + ..Default::default() + } ) } _ => unreachable!(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7f6af7b26..d1cd60ff3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2234,3 +2234,57 @@ fn parse_similar_to() { chk(false); chk(true); } + +#[test] +fn parse_create_function() { + let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE AS 'select $1 + $2;'"; + assert_eq!( + pg().verified_stmt(sql), + Statement::CreateFunction { + or_replace: false, + temporary: false, + name: ObjectName(vec![Ident::new("add")]), + args: Some(vec![ + CreateFunctionArg::unnamed(DataType::Integer(None)), + CreateFunctionArg::unnamed(DataType::Integer(None)), + ]), + return_type: Some(DataType::Integer(None)), + params: CreateFunctionBody { + language: Some("SQL".into()), + behavior: Some(FunctionBehavior::Immutable), + as_: Some("select $1 + $2;".into()), + ..Default::default() + }, + } + ); + + let sql = "CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL IMMUTABLE RETURN a + b"; + assert_eq!( + pg().verified_stmt(sql), + Statement::CreateFunction { + or_replace: true, + temporary: false, + name: ObjectName(vec![Ident::new("add")]), + args: Some(vec![ + CreateFunctionArg::with_name("a", DataType::Integer(None)), + CreateFunctionArg { + mode: Some(ArgMode::In), + name: Some("b".into()), + data_type: DataType::Integer(None), + default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), + } + ]), + return_type: Some(DataType::Integer(None)), + params: CreateFunctionBody { + language: Some("SQL".into()), + behavior: Some(FunctionBehavior::Immutable), + return_: Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier("a".into())), + op: BinaryOperator::Plus, + right: Box::new(Expr::Identifier("b".into())), + }), + ..Default::default() + }, + } + ); +} From b3688513ebc90fd4f516f303f1cc1345d8c9aef1 Mon Sep 17 00:00:00 2001 From: Augusto Fotino <75763288+AugustoFKL@users.noreply.github.com> Date: Mon, 5 Dec 2022 16:03:59 -0300 Subject: [PATCH 114/806] feat: add support for except clause on wildcards (#745) --- src/ast/mod.rs | 7 ++-- src/ast/query.rs | 70 +++++++++++++++++++++++++++----- src/parser.rs | 73 +++++++++++++++++++++++++-------- tests/sqlparser_bigquery.rs | 79 +++++++++++++++++++++++++++++++++++- tests/sqlparser_common.rs | 16 ++++++-- tests/sqlparser_postgres.rs | 7 +++- tests/sqlparser_snowflake.rs | 18 ++++++-- 7 files changed, 231 insertions(+), 39 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3fe548c81..7f3d15438 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -31,9 +31,10 @@ pub use self::ddl::{ }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, ExcludeSelectItem, Fetch, Join, JoinConstraint, JoinOperator, LateralView, LockType, - Offset, OffsetRows, OrderByExpr, Query, Select, SelectInto, SelectItem, SetExpr, SetOperator, - SetQuantifier, Table, TableAlias, TableFactor, TableWithJoins, Top, Values, With, + Cte, ExceptSelectItem, ExcludeSelectItem, Fetch, Join, JoinConstraint, JoinOperator, + LateralView, LockType, Offset, OffsetRows, OrderByExpr, Query, Select, SelectInto, SelectItem, + SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, TableWithJoins, Top, + Values, WildcardAdditionalOptions, With, }; pub use self::value::{escape_quoted_string, DateTimeField, TrimWhereField, Value}; diff --git a/src/ast/query.rs b/src/ast/query.rs index 6cccb6e44..f813f44dd 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -347,9 +347,31 @@ pub enum SelectItem { /// An expression, followed by `[ AS ] alias` ExprWithAlias { expr: Expr, alias: Ident }, /// `alias.*` or even `schema.table.*` - QualifiedWildcard(ObjectName, Option), + QualifiedWildcard(ObjectName, WildcardAdditionalOptions), /// An unqualified `*` - Wildcard(Option), + Wildcard(WildcardAdditionalOptions), +} + +/// Additional options for wildcards, e.g. Snowflake `EXCLUDE` and Bigquery `EXCEPT`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct WildcardAdditionalOptions { + /// `[EXCLUDE...]`. + pub opt_exclude: Option, + /// `[EXCEPT...]`. + pub opt_except: Option, +} + +impl fmt::Display for WildcardAdditionalOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(exclude) = &self.opt_exclude { + write!(f, " {exclude}")?; + } + if let Some(except) = &self.opt_except { + write!(f, " {except}")?; + } + Ok(()) + } } /// Snowflake `EXCLUDE` information. @@ -392,23 +414,51 @@ impl fmt::Display for ExcludeSelectItem { } } +/// Bigquery `EXCEPT` information, with at least one column. +/// +/// # Syntax +/// ```plaintext +/// EXCEPT ( [, ...]) +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ExceptSelectItem { + /// First guaranteed column. + pub fist_elemnt: Ident, + /// Additional columns. This list can be empty. + pub additional_elements: Vec, +} + +impl fmt::Display for ExceptSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "EXCEPT ")?; + if self.additional_elements.is_empty() { + write!(f, "({})", self.fist_elemnt)?; + } else { + write!( + f, + "({}, {})", + self.fist_elemnt, + display_comma_separated(&self.additional_elements) + )?; + } + Ok(()) + } +} + impl fmt::Display for SelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { SelectItem::UnnamedExpr(expr) => write!(f, "{}", expr), SelectItem::ExprWithAlias { expr, alias } => write!(f, "{} AS {}", expr, alias), - SelectItem::QualifiedWildcard(prefix, opt_exclude) => { + SelectItem::QualifiedWildcard(prefix, additional_options) => { write!(f, "{}.*", prefix)?; - if let Some(exclude) = opt_exclude { - write!(f, " {exclude}")?; - } + write!(f, "{additional_options}")?; Ok(()) } - SelectItem::Wildcard(opt_exclude) => { + SelectItem::Wildcard(additional_options) => { write!(f, "*")?; - if let Some(exclude) = opt_exclude { - write!(f, " {exclude}")?; - } + write!(f, "{additional_options}")?; Ok(()) } } diff --git a/src/parser.rs b/src/parser.rs index 29577f053..d1d26293d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5643,25 +5643,37 @@ impl<'a> Parser<'a> { None => SelectItem::UnnamedExpr(expr), }) } - WildcardExpr::QualifiedWildcard(prefix) => { - let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { - self.parse_optional_select_item_exclude()? - } else { - None - }; + WildcardExpr::QualifiedWildcard(prefix) => Ok(SelectItem::QualifiedWildcard( + prefix, + self.parse_wildcard_additional_options()?, + )), + WildcardExpr::Wildcard => Ok(SelectItem::Wildcard( + self.parse_wildcard_additional_options()?, + )), + } + } - Ok(SelectItem::QualifiedWildcard(prefix, opt_exclude)) - } - WildcardExpr::Wildcard => { - let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { - self.parse_optional_select_item_exclude()? - } else { - None - }; + /// Parse an [`WildcardAdditionalOptions`](WildcardAdditionalOptions) information for wildcard select items. + /// + /// If it is not possible to parse it, will return an option. + pub fn parse_wildcard_additional_options( + &mut self, + ) -> Result { + let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_exclude()? + } else { + None + }; + let opt_except = if dialect_of!(self is GenericDialect | BigQueryDialect) { + self.parse_optional_select_item_except()? + } else { + None + }; - Ok(SelectItem::Wildcard(opt_exclude)) - } - } + Ok(WildcardAdditionalOptions { + opt_exclude, + opt_except, + }) } /// Parse an [`Exclude`](ExcludeSelectItem) information for wildcard select items. @@ -5686,6 +5698,33 @@ impl<'a> Parser<'a> { Ok(opt_exclude) } + /// Parse an [`Except`](ExceptSelectItem) information for wildcard select items. + /// + /// If it is not possible to parse it, will return an option. + pub fn parse_optional_select_item_except( + &mut self, + ) -> Result, ParserError> { + let opt_except = if self.parse_keyword(Keyword::EXCEPT) { + let idents = self.parse_parenthesized_column_list(Mandatory)?; + match &idents[..] { + [] => { + return self.expected( + "at least one column should be parsed by the expect clause", + self.peek_token(), + )?; + } + [first, idents @ ..] => Some(ExceptSelectItem { + fist_elemnt: first.clone(), + additional_elements: idents.to_vec(), + }), + } + } else { + None + }; + + Ok(opt_except) + } + /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parse_expr()?; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 8ada172cf..90ca27a69 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -16,7 +16,7 @@ mod test_utils; use test_utils::*; use sqlparser::ast::*; -use sqlparser::dialect::BigQueryDialect; +use sqlparser::dialect::{BigQueryDialect, GenericDialect}; #[test] fn parse_literal_string() { @@ -235,8 +235,85 @@ fn parse_array_agg_func() { } } +#[test] +fn test_select_wildcard_with_except() { + match bigquery_and_generic().verified_stmt("SELECT * EXCEPT (col_a) FROM data") { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_except: Some(except), + .. + }) => { + assert_eq!( + *except, + ExceptSelectItem { + fist_elemnt: Ident::new("col_a"), + additional_elements: vec![] + } + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; + + match bigquery_and_generic() + .verified_stmt("SELECT * EXCEPT (department_id, employee_id) FROM employee_table") + { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_except: Some(except), + .. + }) => { + assert_eq!( + *except, + ExceptSelectItem { + fist_elemnt: Ident::new("department_id"), + additional_elements: vec![Ident::new("employee_id")] + } + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; + + match bigquery_and_generic().verified_stmt("SELECT * EXCEPT (col1, col2) FROM _table") { + Statement::Query(query) => match *query.body { + SetExpr::Select(select) => match &select.projection[0] { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_except: Some(except), + .. + }) => { + assert_eq!( + *except, + ExceptSelectItem { + fist_elemnt: Ident::new("col1"), + additional_elements: vec![Ident::new("col2")] + } + ) + } + _ => unreachable!(), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }; +} + fn bigquery() -> TestedDialects { TestedDialects { dialects: vec![Box::new(BigQueryDialect {})], } } + +fn bigquery_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(BigQueryDialect {}), Box::new(GenericDialect {})], + } +} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 24d101919..addc68e47 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -581,12 +581,18 @@ fn parse_select_into() { fn parse_select_wildcard() { let sql = "SELECT * FROM foo"; let select = verified_only_select(sql); - assert_eq!(&SelectItem::Wildcard(None), only(&select.projection)); + assert_eq!( + &SelectItem::Wildcard(WildcardAdditionalOptions::default()), + only(&select.projection) + ); let sql = "SELECT foo.* FROM foo"; let select = verified_only_select(sql); assert_eq!( - &SelectItem::QualifiedWildcard(ObjectName(vec![Ident::new("foo")]), None), + &SelectItem::QualifiedWildcard( + ObjectName(vec![Ident::new("foo")]), + WildcardAdditionalOptions::default() + ), only(&select.projection) ); @@ -595,7 +601,7 @@ fn parse_select_wildcard() { assert_eq!( &SelectItem::QualifiedWildcard( ObjectName(vec![Ident::new("myschema"), Ident::new("mytable"),]), - None + WildcardAdditionalOptions::default(), ), only(&select.projection) ); @@ -5588,7 +5594,9 @@ fn parse_merge() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: false, top: None, - projection: vec![SelectItem::Wildcard(None)], + projection: vec![SelectItem::Wildcard( + WildcardAdditionalOptions::default() + )], into: None, from: vec![TableWithJoins { relation: TableFactor::Table { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d1cd60ff3..fac98b8ef 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1277,7 +1277,12 @@ fn parse_pg_returning() { pg_and_generic().verified_stmt("DELETE FROM tasks WHERE status = 'DONE' RETURNING *"); match stmt { Statement::Delete { returning, .. } => { - assert_eq!(Some(vec![SelectItem::Wildcard(None),]), returning); + assert_eq!( + Some(vec![SelectItem::Wildcard( + WildcardAdditionalOptions::default() + ),]), + returning + ); } _ => unreachable!(), }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 127528b82..1ec57939c 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -370,7 +370,10 @@ fn test_select_wildcard_with_exclude() { match snowflake_and_generic().verified_stmt("SELECT * EXCLUDE (col_a) FROM data") { Statement::Query(query) => match *query.body { SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(Some(exclude)) => { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(exclude), + .. + }) => { assert_eq!( *exclude, ExcludeSelectItem::Multiple(vec![Ident::new("col_a")]) @@ -388,7 +391,13 @@ fn test_select_wildcard_with_exclude() { { Statement::Query(query) => match *query.body { SetExpr::Select(select) => match &select.projection[0] { - SelectItem::QualifiedWildcard(_, Some(exclude)) => { + SelectItem::QualifiedWildcard( + _, + WildcardAdditionalOptions { + opt_exclude: Some(exclude), + .. + }, + ) => { assert_eq!( *exclude, ExcludeSelectItem::Single(Ident::new("department_id")) @@ -406,7 +415,10 @@ fn test_select_wildcard_with_exclude() { { Statement::Query(query) => match *query.body { SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(Some(exclude)) => { + SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(exclude), + .. + }) => { assert_eq!( *exclude, ExcludeSelectItem::Multiple(vec![ From 84291088d962966256d5ab1834d18813568638af Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 5 Dec 2022 14:21:06 -0500 Subject: [PATCH 115/806] CHANGELOG.md for 0.28.0 (#747) --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b988de78..620f8023d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,41 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.28.0] 2022-12-05 + +### Added +* Support for `EXCEPT` clause on wildcards (#745) - Thanks @AugustoFKL +* Support `CREATE FUNCTION` Postgres options (#722) - Thanks @wangrunji0408 +* Support `CREATE TABLE x AS TABLE y` (#704) - Thanks @sarahyurick +* Support MySQL `ROWS` syntax for `VALUES` (#737) - Thanks @aljazerzen +* Support `WHERE` condition for `UPDATE ON CONFLICT` (#735) - Thanks @zidaye +* Support `CLUSTER BY` when creating Materialized View (#736) - Thanks @yuval-illumex +* Support nested comments (#726) - Thanks @yang-han +* Support `USING` method when creating indexes. (#731) - Thanks @step-baby and @yangjiaxin01 +* Support `SEMI`/`ANTI` `JOIN` syntax (#723) - Thanks @mingmwang +* Support `EXCLUDE` support for snowflake and generic dialect (#721) - Thanks @AugustoFKL +* Support `MATCH AGAINST` (#708) - Thanks @AugustoFKL +* Support `IF NOT EXISTS` in `ALTER TABLE ADD COLUMN` (#707) - Thanks @AugustoFKL +* Support `SET TIME ZONE ` (#727) - Thanks @waitingkuo +* Support `UPDATE ... FROM ( subquery )` (#694) - Thanks @unvalley + +### Changed +* Add `Parser::index()` method to get current parsing index (#728) - Thanks @neverchanje +* Add `COMPRESSION` as keyword (#720)- Thanks @AugustoFKL +* Derive `PartialOrd`, `Ord`, and `Copy` whenever possible (#717) - Thanks @AugustoFKL +* Fixed `INTERVAL` parsing logic and precedence (#705) - Thanks @sarahyurick +* Support updating multiple column names whose names are the same as(#725) - Thanks @step-baby + +### Fixed +* Clean up some redundant code in parser (#741) - Thanks @alamb +* Fix logical conflict - Thanks @alamb +* Cleanup to avoid is_ok() (#740) - Thanks @alamb +* Cleanup to avoid using unreachable! when parsing semi/anti join (#738) - Thanks @alamb +* Add an example to docs to clarify semantic analysis (#739) - Thanks @alamb +* Add information about parting semantic logic to README.md (#724) - Thanks @AugustoFKL +* Logical conflicts - Thanks @alamb +* Tiny typo in docs (#709) - Thanks @pmcgee69 + ## [0.27.0] 2022-11-11 From 512a159f08c9a17ddeb1ee8b2a23bf20dbfef265 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 5 Dec 2022 14:26:15 -0500 Subject: [PATCH 116/806] (cargo-release) version 0.28.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f7e08b494..2355f4646 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.27.0" +version = "0.28.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 813f4a2eff8f091b643058ac1a46a4fbffd96806 Mon Sep 17 00:00:00 2001 From: Ankur Goyal Date: Mon, 5 Dec 2022 11:47:42 -0800 Subject: [PATCH 117/806] Introduce location tracking in the tokenizer and parser (#710) * Add locations * Add PartialEq * Add PartialEq * Add some tests * Fix rebase conflicts * Fix clippy Co-authored-by: Andrew Lamb --- .gitignore | 2 + src/dialect/postgresql.rs | 2 +- src/parser.rs | 351 +++++++++++++++++++++++++------------- src/tokenizer.rs | 223 ++++++++++++++++++------ 4 files changed, 404 insertions(+), 174 deletions(-) diff --git a/.gitignore b/.gitignore index 6dfd81a68..baccda415 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ Cargo.lock # IDEs .idea .vscode + +*.swp diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index b1f261b2e..fe1953d2a 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -52,7 +52,7 @@ pub fn parse_comment(parser: &mut Parser) -> Result { parser.expect_keyword(Keyword::ON)?; let token = parser.next_token(); - let (object_type, object_name) = match token { + let (object_type, object_name) = match token.token { Token::Word(w) if w.keyword == Keyword::COLUMN => { let object_name = parser.parse_object_name()?; (CommentObject::Column, object_name) diff --git a/src/parser.rs b/src/parser.rs index d1d26293d..45c472acd 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -105,7 +105,7 @@ impl fmt::Display for ParserError { impl std::error::Error for ParserError {} pub struct Parser<'a> { - tokens: Vec, + tokens: Vec, /// The index of the first unprocessed token in `self.tokens` index: usize, dialect: &'a dyn Dialect, @@ -113,7 +113,26 @@ pub struct Parser<'a> { impl<'a> Parser<'a> { /// Parse the specified tokens + /// To avoid breaking backwards compatibility, this function accepts + /// bare tokens. pub fn new(tokens: Vec, dialect: &'a dyn Dialect) -> Self { + Parser::new_without_locations(tokens, dialect) + } + + pub fn new_without_locations(tokens: Vec, dialect: &'a dyn Dialect) -> Self { + // Put in dummy locations + let tokens_with_locations: Vec = tokens + .into_iter() + .map(|token| TokenWithLocation { + token, + location: Location { line: 0, column: 0 }, + }) + .collect(); + Parser::new_with_locations(tokens_with_locations, dialect) + } + + /// Parse the specified tokens + pub fn new_with_locations(tokens: Vec, dialect: &'a dyn Dialect) -> Self { Parser { tokens, index: 0, @@ -157,7 +176,8 @@ impl<'a> Parser<'a> { return statement; } - match self.next_token() { + let next_token = self.next_token(); + match &next_token.token { Token::Word(w) => match w.keyword { Keyword::KILL => Ok(self.parse_kill()?), Keyword::DESCRIBE => Ok(self.parse_explain(true)?), @@ -202,13 +222,13 @@ impl<'a> Parser<'a> { Keyword::EXECUTE => Ok(self.parse_execute()?), Keyword::PREPARE => Ok(self.parse_prepare()?), Keyword::MERGE => Ok(self.parse_merge()?), - _ => self.expected("an SQL statement", Token::Word(w)), + _ => self.expected("an SQL statement", next_token), }, Token::LParen => { self.prev_token(); Ok(Statement::Query(Box::new(self.parse_query()?))) } - unexpected => self.expected("an SQL statement", unexpected), + _ => self.expected("an SQL statement", next_token), } } @@ -314,18 +334,20 @@ impl<'a> Parser<'a> { pub fn parse_wildcard_expr(&mut self) -> Result { let index = self.index; - match self.next_token() { - Token::Word(w) if self.peek_token() == Token::Period => { + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) if self.peek_token().token == Token::Period => { let mut id_parts: Vec = vec![w.to_ident()]; while self.consume_token(&Token::Period) { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(w) => id_parts.push(w.to_ident()), Token::Mul => { return Ok(WildcardExpr::QualifiedWildcard(ObjectName(id_parts))); } - unexpected => { - return self.expected("an identifier or a '*' after '.'", unexpected); + _ => { + return self.expected("an identifier or a '*' after '.'", next_token); } } } @@ -385,7 +407,7 @@ impl<'a> Parser<'a> { pub fn get_next_interval_precedence(&self) -> Result { let token = self.peek_token(); - match token { + match token.token { Token::Word(w) if w.keyword == Keyword::AND => Ok(0), Token::Word(w) if w.keyword == Keyword::OR => Ok(0), Token::Word(w) if w.keyword == Keyword::XOR => Ok(0), @@ -450,7 +472,8 @@ impl<'a> Parser<'a> { } })); - let expr = match self.next_token() { + let next_token = self.next_token(); + let expr = match next_token.token { Token::Word(w) => match w.keyword { Keyword::TRUE | Keyword::FALSE | Keyword::NULL => { self.prev_token(); @@ -510,15 +533,16 @@ impl<'a> Parser<'a> { } // Here `w` is a word, check if it's a part of a multi-part // identifier, a function call, or a simple identifier: - _ => match self.peek_token() { + _ => match self.peek_token().token { Token::LParen | Token::Period => { let mut id_parts: Vec = vec![w.to_ident()]; while self.consume_token(&Token::Period) { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(w) => id_parts.push(w.to_ident()), - unexpected => { + _ => { return self - .expected("an identifier or a '*' after '.'", unexpected); + .expected("an identifier or a '*' after '.'", next_token); } } } @@ -598,7 +622,7 @@ impl<'a> Parser<'a> { Ok(expr) } else { let tok = self.next_token(); - let key = match tok { + let key = match tok.token { Token::Word(word) => word.to_ident(), _ => return parser_err!(format!("Expected identifier, found: {}", tok)), }; @@ -612,7 +636,7 @@ impl<'a> Parser<'a> { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } - unexpected => self.expected("an expression:", unexpected), + _ => self.expected("an expression:", next_token), }?; if self.parse_keyword(Keyword::COLLATE) { @@ -684,14 +708,15 @@ impl<'a> Parser<'a> { } pub fn parse_window_frame_units(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match &next_token.token { Token::Word(w) => match w.keyword { Keyword::ROWS => Ok(WindowFrameUnits::Rows), Keyword::RANGE => Ok(WindowFrameUnits::Range), Keyword::GROUPS => Ok(WindowFrameUnits::Groups), - _ => self.expected("ROWS, RANGE, GROUPS", Token::Word(w))?, + _ => self.expected("ROWS, RANGE, GROUPS", next_token)?, }, - unexpected => self.expected("ROWS, RANGE, GROUPS", unexpected), + _ => self.expected("ROWS, RANGE, GROUPS", next_token), } } @@ -720,7 +745,7 @@ impl<'a> Parser<'a> { let rows = if self.parse_keyword(Keyword::UNBOUNDED) { None } else { - Some(Box::new(match self.peek_token() { + Some(Box::new(match self.peek_token().token { Token::SingleQuotedString(_) => self.parse_interval()?, _ => self.parse_expr()?, })) @@ -980,7 +1005,7 @@ impl<'a> Parser<'a> { pub fn parse_trim_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; let mut trim_where = None; - if let Token::Word(word) = self.peek_token() { + if let Token::Word(word) = self.peek_token().token { if [Keyword::BOTH, Keyword::LEADING, Keyword::TRAILING] .iter() .any(|d| word.keyword == *d) @@ -1009,21 +1034,22 @@ impl<'a> Parser<'a> { } pub fn parse_trim_where(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match &next_token.token { Token::Word(w) => match w.keyword { Keyword::BOTH => Ok(TrimWhereField::Both), Keyword::LEADING => Ok(TrimWhereField::Leading), Keyword::TRAILING => Ok(TrimWhereField::Trailing), - _ => self.expected("trim_where field", Token::Word(w))?, + _ => self.expected("trim_where field", next_token)?, }, - unexpected => self.expected("trim_where field", unexpected), + _ => self.expected("trim_where field", next_token), } } /// Parses an array expression `[ex1, ex2, ..]` /// if `named` is `true`, came from an expression like `ARRAY[ex1, ex2]` pub fn parse_array_expr(&mut self, named: bool) -> Result { - if self.peek_token() == Token::RBracket { + if self.peek_token().token == Token::RBracket { let _ = self.next_token(); Ok(Expr::Array(Array { elem: vec![], @@ -1060,7 +1086,7 @@ impl<'a> Parser<'a> { Some(ListAggOnOverflow::Error) } else { self.expect_keyword(Keyword::TRUNCATE)?; - let filler = match self.peek_token() { + let filler = match self.peek_token().token { Token::Word(w) if w.keyword == Keyword::WITH || w.keyword == Keyword::WITHOUT => { @@ -1070,9 +1096,10 @@ impl<'a> Parser<'a> { | Token::EscapedStringLiteral(_) | Token::NationalStringLiteral(_) | Token::HexStringLiteral(_) => Some(Box::new(self.parse_expr()?)), - unexpected => { - self.expected("either filler, WITH, or WITHOUT in LISTAGG", unexpected)? - } + _ => self.expected( + "either filler, WITH, or WITHOUT in LISTAGG", + self.peek_token(), + )?, }; let with_count = self.parse_keyword(Keyword::WITH); if !with_count && !self.parse_keyword(Keyword::WITHOUT) { @@ -1158,7 +1185,8 @@ impl<'a> Parser<'a> { // EXTRACT supports a wider set of date/time fields than interval qualifiers, // so this function may need to be split in two. pub fn parse_date_time_field(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match &next_token.token { Token::Word(w) => match w.keyword { Keyword::YEAR => Ok(DateTimeField::Year), Keyword::MONTH => Ok(DateTimeField::Month), @@ -1186,14 +1214,14 @@ impl<'a> Parser<'a> { Keyword::TIMEZONE => Ok(DateTimeField::Timezone), Keyword::TIMEZONE_HOUR => Ok(DateTimeField::TimezoneHour), Keyword::TIMEZONE_MINUTE => Ok(DateTimeField::TimezoneMinute), - _ => self.expected("date/time field", Token::Word(w))?, + _ => self.expected("date/time field", next_token), }, - unexpected => self.expected("date/time field", unexpected), + _ => self.expected("date/time field", next_token), } } pub fn parse_not(&mut self) -> Result { - match self.peek_token() { + match self.peek_token().token { Token::Word(w) => match w.keyword { Keyword::EXISTS => { let negated = true; @@ -1291,7 +1319,7 @@ impl<'a> Parser<'a> { // // Note that PostgreSQL allows omitting the qualifier, so we provide // this more general implementation. - let leading_field = match self.peek_token() { + let leading_field = match self.peek_token().token { Token::Word(kw) if [ Keyword::YEAR, @@ -1371,7 +1399,7 @@ impl<'a> Parser<'a> { let tok = self.next_token(); - let regular_binary_operator = match &tok { + let regular_binary_operator = match &tok.token { Token::Spaceship => Some(BinaryOperator::Spaceship), Token::DoubleEq => Some(BinaryOperator::Eq), Token::Eq => Some(BinaryOperator::Eq), @@ -1451,7 +1479,7 @@ impl<'a> Parser<'a> { right: Box::new(self.parse_subexpr(precedence)?), }) } - } else if let Token::Word(w) = &tok { + } else if let Token::Word(w) = &tok.token { match w.keyword { Keyword::IS => { if self.parse_keyword(Keyword::NULL) { @@ -1489,7 +1517,7 @@ impl<'a> Parser<'a> { // self.expect_keyword(Keyword::ZONE)?; if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) { let time_zone = self.next_token(); - match time_zone { + match time_zone.token { Token::SingleQuotedString(time_zone) => { log::trace!("Peek token: {:?}", self.peek_token()); Ok(Expr::AtTimeZone { @@ -1497,9 +1525,9 @@ impl<'a> Parser<'a> { time_zone, }) } - tok => self.expected( + _ => self.expected( "Expected Token::SingleQuotedString after AT TIME ZONE", - tok, + time_zone, ), } } else { @@ -1544,7 +1572,7 @@ impl<'a> Parser<'a> { } } // Can only happen if `get_next_precedence` got out of sync with this function - _ => parser_err!(format!("No infix parser for token {:?}", tok)), + _ => parser_err!(format!("No infix parser for token {:?}", tok.token)), } } else if Token::DoubleColon == tok { self.parse_pg_cast(expr) @@ -1571,7 +1599,7 @@ impl<'a> Parser<'a> { || Token::HashArrow == tok || Token::HashLongArrow == tok { - let operator = match tok { + let operator = match tok.token { Token::Arrow => JsonOperator::Arrow, Token::LongArrow => JsonOperator::LongArrow, Token::HashArrow => JsonOperator::HashArrow, @@ -1585,7 +1613,7 @@ impl<'a> Parser<'a> { }) } else { // Can only happen if `get_next_precedence` got out of sync with this function - parser_err!(format!("No infix parser for token {:?}", tok)) + parser_err!(format!("No infix parser for token {:?}", tok.token)) } } @@ -1713,13 +1741,13 @@ impl<'a> Parser<'a> { let token_1 = self.peek_nth_token(1); let token_2 = self.peek_nth_token(2); debug!("0: {token_0} 1: {token_1} 2: {token_2}"); - match token { + match token.token { Token::Word(w) if w.keyword == Keyword::OR => Ok(Self::OR_PREC), Token::Word(w) if w.keyword == Keyword::AND => Ok(Self::AND_PREC), Token::Word(w) if w.keyword == Keyword::XOR => Ok(Self::XOR_PREC), Token::Word(w) if w.keyword == Keyword::AT => { - match (self.peek_nth_token(1), self.peek_nth_token(2)) { + match (self.peek_nth_token(1).token, self.peek_nth_token(2).token) { (Token::Word(w), Token::Word(w2)) if w.keyword == Keyword::TIME && w2.keyword == Keyword::ZONE => { @@ -1729,7 +1757,7 @@ impl<'a> Parser<'a> { } } - Token::Word(w) if w.keyword == Keyword::NOT => match self.peek_nth_token(1) { + Token::Word(w) if w.keyword == Keyword::NOT => match self.peek_nth_token(1).token { // The precedence of NOT varies depending on keyword that // follows it. If it is followed by IN, BETWEEN, or LIKE, // it takes on the precedence of those tokens. Otherwise it @@ -1780,20 +1808,26 @@ impl<'a> Parser<'a> { /// Return the first non-whitespace token that has not yet been processed /// (or None if reached end-of-file) - pub fn peek_token(&self) -> Token { + pub fn peek_token(&self) -> TokenWithLocation { self.peek_nth_token(0) } /// Return nth non-whitespace token that has not yet been processed - pub fn peek_nth_token(&self, mut n: usize) -> Token { + pub fn peek_nth_token(&self, mut n: usize) -> TokenWithLocation { let mut index = self.index; loop { index += 1; match self.tokens.get(index - 1) { - Some(Token::Whitespace(_)) => continue, + Some(TokenWithLocation { + token: Token::Whitespace(_), + location: _, + }) => continue, non_whitespace => { if n == 0 { - return non_whitespace.cloned().unwrap_or(Token::EOF); + return non_whitespace.cloned().unwrap_or(TokenWithLocation { + token: Token::EOF, + location: Location { line: 0, column: 0 }, + }); } n -= 1; } @@ -1804,18 +1838,25 @@ impl<'a> Parser<'a> { /// Return the first non-whitespace token that has not yet been processed /// (or None if reached end-of-file) and mark it as processed. OK to call /// repeatedly after reaching EOF. - pub fn next_token(&mut self) -> Token { + pub fn next_token(&mut self) -> TokenWithLocation { loop { self.index += 1; match self.tokens.get(self.index - 1) { - Some(Token::Whitespace(_)) => continue, - token => return token.cloned().unwrap_or(Token::EOF), + Some(TokenWithLocation { + token: Token::Whitespace(_), + location: _, + }) => continue, + token => { + return token + .cloned() + .unwrap_or_else(|| TokenWithLocation::wrap(Token::EOF)) + } } } } /// Return the first unprocessed token, possibly whitespace. - pub fn next_token_no_skip(&mut self) -> Option<&Token> { + pub fn next_token_no_skip(&mut self) -> Option<&TokenWithLocation> { self.index += 1; self.tokens.get(self.index - 1) } @@ -1827,7 +1868,11 @@ impl<'a> Parser<'a> { loop { assert!(self.index > 0); self.index -= 1; - if let Some(Token::Whitespace(_)) = self.tokens.get(self.index) { + if let Some(TokenWithLocation { + token: Token::Whitespace(_), + location: _, + }) = self.tokens.get(self.index) + { continue; } return; @@ -1835,14 +1880,14 @@ impl<'a> Parser<'a> { } /// Report unexpected token - pub fn expected(&self, expected: &str, found: Token) -> Result { + pub fn expected(&self, expected: &str, found: TokenWithLocation) -> Result { parser_err!(format!("Expected {}, found: {}", expected, found)) } /// Look for an expected keyword and consume it if it exists #[must_use] pub fn parse_keyword(&mut self, expected: Keyword) -> bool { - match self.peek_token() { + match self.peek_token().token { Token::Word(w) if expected == w.keyword => { self.next_token(); true @@ -1869,7 +1914,7 @@ impl<'a> Parser<'a> { /// Look for one of the given keywords and return the one that matches. #[must_use] pub fn parse_one_of_keywords(&mut self, keywords: &[Keyword]) -> Option { - match self.peek_token() { + match self.peek_token().token { Token::Word(w) => { keywords .iter() @@ -1945,7 +1990,7 @@ impl<'a> Parser<'a> { // BigQuery allows trailing commas. // e.g. `SELECT 1, 2, FROM t` // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#trailing_commas - match self.peek_token() { + match self.peek_token().token { Token::Word(kw) if keywords::RESERVED_FOR_COLUMN_ALIAS .iter() @@ -2057,14 +2102,14 @@ impl<'a> Parser<'a> { let (mut table_flag, mut options, mut has_as, mut query) = (None, vec![], false, None); if self.parse_keyword(Keyword::TABLE) { let table_name = self.parse_object_name()?; - if self.peek_token() != Token::EOF { - if let Token::Word(word) = self.peek_token() { + if self.peek_token().token != Token::EOF { + if let Token::Word(word) = self.peek_token().token { if word.keyword == Keyword::OPTIONS { options = self.parse_options(Keyword::OPTIONS)? } }; - if self.peek_token() != Token::EOF { + if self.peek_token().token != Token::EOF { let (a, q) = self.parse_as_query()?; has_as = a; query = Some(q); @@ -2091,7 +2136,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::TABLE) { let table_name = self.parse_object_name()?; if self.peek_token() != Token::EOF { - if let Token::Word(word) = self.peek_token() { + if let Token::Word(word) = self.peek_token().token { if word.keyword == Keyword::OPTIONS { options = self.parse_options(Keyword::OPTIONS)? } @@ -2130,7 +2175,7 @@ impl<'a> Parser<'a> { /// Parse 'AS' before as query,such as `WITH XXX AS SELECT XXX` oer `CACHE TABLE AS SELECT XXX` pub fn parse_as_query(&mut self) -> Result<(bool, Query), ParserError> { - match self.peek_token() { + match self.peek_token().token { Token::Word(word) => match word.keyword { Keyword::AS => { self.next_token(); @@ -2148,7 +2193,7 @@ impl<'a> Parser<'a> { if has_table { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let table_name = self.parse_object_name()?; - if self.peek_token() == Token::EOF { + if self.peek_token().token == Token::EOF { Ok(Statement::UNCache { table_name, if_exists, @@ -2248,7 +2293,7 @@ impl<'a> Parser<'a> { Keyword::ARCHIVE => Ok(Some(CreateFunctionUsing::Archive(uri))), _ => self.expected( "JAR, FILE or ARCHIVE, got {:?}", - Token::make_keyword(format!("{:?}", keyword).as_str()), + TokenWithLocation::wrap(Token::make_keyword(format!("{:?}", keyword).as_str())), ), } } @@ -2410,7 +2455,8 @@ impl<'a> Parser<'a> { } pub fn parse_file_format(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match &next_token.token { Token::Word(w) => match w.keyword { Keyword::AVRO => Ok(FileFormat::AVRO), Keyword::JSONFILE => Ok(FileFormat::JSONFILE), @@ -2419,21 +2465,22 @@ impl<'a> Parser<'a> { Keyword::RCFILE => Ok(FileFormat::RCFILE), Keyword::SEQUENCEFILE => Ok(FileFormat::SEQUENCEFILE), Keyword::TEXTFILE => Ok(FileFormat::TEXTFILE), - _ => self.expected("fileformat", Token::Word(w)), + _ => self.expected("fileformat", next_token), }, - unexpected => self.expected("fileformat", unexpected), + _ => self.expected("fileformat", next_token), } } pub fn parse_analyze_format(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match &next_token.token { Token::Word(w) => match w.keyword { Keyword::TEXT => Ok(AnalyzeFormat::TEXT), Keyword::GRAPHVIZ => Ok(AnalyzeFormat::GRAPHVIZ), Keyword::JSON => Ok(AnalyzeFormat::JSON), - _ => self.expected("fileformat", Token::Word(w)), + _ => self.expected("fileformat", next_token), }, - unexpected => self.expected("fileformat", unexpected), + _ => self.expected("fileformat", next_token), } } @@ -2949,10 +2996,11 @@ impl<'a> Parser<'a> { // Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs let on_cluster = if self.parse_keywords(&[Keyword::ON, Keyword::CLUSTER]) { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::SingleQuotedString(s) => Some(s), Token::Word(s) => Some(s.to_string()), - unexpected => self.expected("identifier or cluster literal", unexpected)?, + _ => self.expected("identifier or cluster literal", next_token)?, } } else { None @@ -2990,9 +3038,10 @@ impl<'a> Parser<'a> { let engine = if self.parse_keyword(Keyword::ENGINE) { self.expect_token(&Token::Eq)?; - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(w) => Some(w.value), - unexpected => self.expected("identifier", unexpected)?, + _ => self.expected("identifier", next_token)?, } } else { None @@ -3000,9 +3049,10 @@ impl<'a> Parser<'a> { let default_charset = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARSET]) { self.expect_token(&Token::Eq)?; - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(w) => Some(w.value), - unexpected => self.expected("identifier", unexpected)?, + _ => self.expected("identifier", next_token)?, } } else { None @@ -3010,9 +3060,10 @@ impl<'a> Parser<'a> { let collation = if self.parse_keywords(&[Keyword::COLLATE]) { self.expect_token(&Token::Eq)?; - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(w) => Some(w.value), - unexpected => self.expected("identifier", unexpected)?, + _ => self.expected("identifier", next_token)?, } } else { None @@ -3068,7 +3119,7 @@ impl<'a> Parser<'a> { loop { if let Some(constraint) = self.parse_optional_table_constraint()? { constraints.push(constraint); - } else if let Token::Word(_) = self.peek_token() { + } else if let Token::Word(_) = self.peek_token().token { columns.push(self.parse_column_def()?); } else { return self.expected("column name or constraint definition", self.peek_token()); @@ -3125,9 +3176,10 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { Ok(Some(ColumnOption::NotNull)) } else if self.parse_keywords(&[Keyword::COMMENT]) { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::SingleQuotedString(value, ..) => Ok(Some(ColumnOption::Comment(value))), - unexpected => self.expected("string", unexpected), + _ => self.expected("string", next_token), } } else if self.parse_keyword(Keyword::NULL) { Ok(Some(ColumnOption::Null)) @@ -3218,7 +3270,9 @@ impl<'a> Parser<'a> { } else { None }; - match self.next_token() { + + let next_token = self.next_token(); + match next_token.token { Token::Word(w) if w.keyword == Keyword::PRIMARY || w.keyword == Keyword::UNIQUE => { let is_primary = w.keyword == Keyword::PRIMARY; if is_primary { @@ -3271,7 +3325,7 @@ impl<'a> Parser<'a> { { let display_as_key = w.keyword == Keyword::KEY; - let name = match self.peek_token() { + let name = match self.peek_token().token { Token::Word(word) if word.keyword == Keyword::USING => None, _ => self.maybe_parse(|parser| parser.parse_identifier()), }; @@ -3297,7 +3351,10 @@ impl<'a> Parser<'a> { if let Some(name) = name { return self.expected( "FULLTEXT or SPATIAL option without constraint name", - Token::make_keyword(&name.to_string()), + TokenWithLocation { + token: Token::make_keyword(&name.to_string()), + location: next_token.location, + }, ); } @@ -3322,9 +3379,9 @@ impl<'a> Parser<'a> { columns, })) } - unexpected => { + _ => { if name.is_some() { - self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", unexpected) + self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", next_token) } else { self.prev_token(); Ok(None) @@ -3712,7 +3769,7 @@ impl<'a> Parser<'a> { pub fn parse_tab_value(&mut self) -> Vec> { let mut values = vec![]; let mut content = String::from(""); - while let Some(t) = self.next_token_no_skip() { + while let Some(t) = self.next_token_no_skip().map(|t| &t.token) { match t { Token::Whitespace(Whitespace::Tab) => { values.push(Some(content.to_string())); @@ -3726,7 +3783,7 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::Period) { return values; } - if let Token::Word(w) = self.next_token() { + if let Token::Word(w) = self.next_token().token { if w.value == "N" { values.push(None); } @@ -3742,7 +3799,9 @@ impl<'a> Parser<'a> { /// Parse a literal value (numbers, strings, date/time, booleans) pub fn parse_value(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + let location = next_token.location; + match next_token.token { Token::Word(w) => match w.keyword { Keyword::TRUE => Ok(Value::Boolean(true)), Keyword::FALSE => Ok(Value::Boolean(false)), @@ -3750,13 +3809,25 @@ impl<'a> Parser<'a> { Keyword::NoKeyword if w.quote_style.is_some() => match w.quote_style { Some('"') => Ok(Value::DoubleQuotedString(w.value)), Some('\'') => Ok(Value::SingleQuotedString(w.value)), - _ => self.expected("A value?", Token::Word(w))?, + _ => self.expected( + "A value?", + TokenWithLocation { + token: Token::Word(w), + location, + }, + )?, }, // Case when Snowflake Semi-structured data like key:value Keyword::NoKeyword | Keyword::LOCATION if dialect_of!(self is SnowflakeDialect | GenericDialect) => { Ok(Value::UnQuotedString(w.value)) } - _ => self.expected("a concrete value", Token::Word(w)), + _ => self.expected( + "a concrete value", + TokenWithLocation { + token: Token::Word(w), + location, + }, + ), }, // The call to n.parse() returns a bigdecimal when the // bigdecimal feature is enabled, and is otherwise a no-op @@ -3776,7 +3847,13 @@ impl<'a> Parser<'a> { let placeholder = tok.to_string() + &ident.value; Ok(Value::Placeholder(placeholder)) } - unexpected => self.expected("a value", unexpected), + unexpected => self.expected( + "a value", + TokenWithLocation { + token: unexpected, + location, + }, + ), } } @@ -3793,30 +3870,33 @@ impl<'a> Parser<'a> { /// Parse an unsigned literal integer/long pub fn parse_literal_uint(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Number(s, _) => s.parse::().map_err(|e| { ParserError::ParserError(format!("Could not parse '{}' as u64: {}", s, e)) }), - unexpected => self.expected("literal int", unexpected), + _ => self.expected("literal int", next_token), } } /// Parse a literal string pub fn parse_literal_string(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(Word { value, keyword, .. }) if keyword == Keyword::NoKeyword => Ok(value), Token::SingleQuotedString(s) => Ok(s), Token::DoubleQuotedString(s) => Ok(s), Token::EscapedStringLiteral(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { Ok(s) } - unexpected => self.expected("literal string", unexpected), + _ => self.expected("literal string", next_token), } } /// Parse a map key string pub fn parse_map_key(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(Word { value, keyword, .. }) if keyword == Keyword::NoKeyword => { if self.peek_token() == Token::LParen { return self.parse_function(ObjectName(vec![Ident::new(value)])); @@ -3828,13 +3908,14 @@ impl<'a> Parser<'a> { Token::Number(s, _) => Ok(Expr::Value(Value::Number(s, false))), #[cfg(feature = "bigdecimal")] Token::Number(s, _) => Ok(Expr::Value(Value::Number(s.parse().unwrap(), false))), - unexpected => self.expected("literal string, number or function", unexpected), + _ => self.expected("literal string, number or function", next_token), } } /// Parse a SQL datatype (in the context of a CREATE TABLE statement for example) pub fn parse_data_type(&mut self) -> Result { - let mut data = match self.next_token() { + let next_token = self.next_token(); + let mut data = match next_token.token { Token::Word(w) => match w.keyword { Keyword::BOOLEAN => Ok(DataType::Boolean), Keyword::FLOAT => Ok(DataType::Float(self.parse_optional_precision()?)), @@ -4003,7 +4084,7 @@ impl<'a> Parser<'a> { } } }, - unexpected => self.expected("a data type name", unexpected), + _ => self.expected("a data type name", next_token), }?; // Parse array data types. Note: this is postgresql-specific and different from @@ -4019,14 +4100,16 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let mut values = Vec::new(); loop { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::SingleQuotedString(value) => values.push(value), - unexpected => self.expected("a string", unexpected)?, + _ => self.expected("a string", next_token)?, } - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Comma => (), Token::RParen => break, - unexpected => self.expected(", or }", unexpected)?, + _ => self.expected(", or }", next_token)?, } } Ok(values) @@ -4040,7 +4123,8 @@ impl<'a> Parser<'a> { reserved_kwds: &[Keyword], ) -> Result, ParserError> { let after_as = self.parse_keyword(Keyword::AS); - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { // Accept any identifier after `AS` (though many dialects have restrictions on // keywords that may appear here). If there's no `AS`: don't parse keywords, // which may start a construct allowed in this position, to be parsed as aliases. @@ -4064,9 +4148,9 @@ impl<'a> Parser<'a> { Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))), // Support for MySql dialect double qouted string, `AS "HOUR"` for example Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))), - not_an_ident => { + _ => { if after_as { - return self.expected("an identifier after AS", not_an_ident); + return self.expected("an identifier after AS", next_token); } self.prev_token(); Ok(None) // no alias found @@ -4108,7 +4192,7 @@ impl<'a> Parser<'a> { pub fn parse_identifiers(&mut self) -> Result, ParserError> { let mut idents = vec![]; loop { - match self.peek_token() { + match self.peek_token().token { Token::Word(w) => { idents.push(w.to_ident()); } @@ -4122,11 +4206,12 @@ impl<'a> Parser<'a> { /// Parse a simple one-word identifier (possibly quoted, possibly a keyword) pub fn parse_identifier(&mut self) -> Result { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(w) => Ok(w.to_ident()), Token::SingleQuotedString(s) => Ok(Ident::with_quote('\'', s)), Token::DoubleQuotedString(s) => Ok(Ident::with_quote('\"', s)), - unexpected => self.expected("identifier", unexpected), + _ => self.expected("identifier", next_token), } } @@ -4231,7 +4316,8 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::LParen) { let mut modifiers = Vec::new(); loop { - match self.next_token() { + let next_token = self.next_token(); + match next_token.token { Token::Word(w) => modifiers.push(w.to_string()), Token::Number(n, _) => modifiers.push(n), Token::SingleQuotedString(s) => modifiers.push(s), @@ -4242,7 +4328,7 @@ impl<'a> Parser<'a> { Token::RParen => { break; } - unexpected => self.expected("type modifiers", unexpected)?, + _ => self.expected("type modifiers", next_token)?, } } @@ -4485,7 +4571,7 @@ impl<'a> Parser<'a> { loop { // The query can be optionally followed by a set operator: - let op = self.parse_set_operator(&self.peek_token()); + let op = self.parse_set_operator(&self.peek_token().token); let next_precedence = match op { // UNION and EXCEPT have the same binding power and evaluate left-to-right Some(SetOperator::Union) | Some(SetOperator::Except) => 10, @@ -4683,7 +4769,7 @@ impl<'a> Parser<'a> { let table_name; let schema_name; if token2 == Token::Period { - match token1 { + match token1.token { Token::Word(w) => { schema_name = w.value; } @@ -4691,7 +4777,7 @@ impl<'a> Parser<'a> { return self.expected("Schema name", token1); } } - match token3 { + match token3.token { Token::Word(w) => { table_name = w.value; } @@ -4704,7 +4790,7 @@ impl<'a> Parser<'a> { schema_name: Some(schema_name), }) } else { - match token1 { + match token1.token { Token::Word(w) => { table_name = w.value; } @@ -4986,7 +5072,7 @@ impl<'a> Parser<'a> { } } else { let natural = self.parse_keyword(Keyword::NATURAL); - let peek_keyword = if let Token::Word(w) = self.peek_token() { + let peek_keyword = if let Token::Word(w) = self.peek_token().token { w.keyword } else { Keyword::NoKeyword @@ -6704,4 +6790,31 @@ mod tests { r#"UPDATE test SET name = $1, value = $2, where = $3, create = $4, is_default = $5, classification = $6, sort = $7 WHERE id = $8"# ); } + + #[test] + fn test_tokenizer_error_loc() { + let sql = "foo '"; + let ast = Parser::parse_sql(&GenericDialect, sql); + assert_eq!( + ast, + Err(ParserError::TokenizerError( + "Unterminated string literal at Line: 1, Column 5".to_string() + )) + ); + } + + #[test] + fn test_parser_error_loc() { + // TODO: Once we thread token locations through the parser, we should update this + // test to assert the locations of the referenced token + let sql = "SELECT this is a syntax error"; + let ast = Parser::parse_sql(&GenericDialect, sql); + assert_eq!( + ast, + Err(ParserError::ParserError( + "Expected [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: a" + .to_string() + )) + ); + } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 5dfac47f0..68dd652d5 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -300,6 +300,53 @@ impl fmt::Display for Whitespace { } } +/// Location in input string +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Location { + /// Line number, starting from 1 + pub line: u64, + /// Line column, starting from 1 + pub column: u64, +} + +/// A [Token] with [Location] attached to it +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct TokenWithLocation { + pub token: Token, + pub location: Location, +} + +impl TokenWithLocation { + pub fn new(token: Token, line: u64, column: u64) -> TokenWithLocation { + TokenWithLocation { + token, + location: Location { line, column }, + } + } + + pub fn wrap(token: Token) -> TokenWithLocation { + TokenWithLocation::new(token, 0, 0) + } +} + +impl PartialEq for TokenWithLocation { + fn eq(&self, other: &Token) -> bool { + &self.token == other + } +} + +impl PartialEq for Token { + fn eq(&self, other: &TokenWithLocation) -> bool { + self == &other.token + } +} + +impl fmt::Display for TokenWithLocation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.token.fmt(f) + } +} + /// Tokenizer error #[derive(Debug, PartialEq, Eq)] pub struct TokenizerError { @@ -321,58 +368,88 @@ impl fmt::Display for TokenizerError { #[cfg(feature = "std")] impl std::error::Error for TokenizerError {} +struct State<'a> { + peekable: Peekable>, + pub line: u64, + pub col: u64, +} + +impl<'a> State<'a> { + pub fn next(&mut self) -> Option { + match self.peekable.next() { + None => None, + Some(s) => { + if s == '\n' { + self.line += 1; + self.col = 1; + } else { + self.col += 1; + } + Some(s) + } + } + } + + pub fn peek(&mut self) -> Option<&char> { + self.peekable.peek() + } + + pub fn location(&self) -> Location { + Location { + line: self.line, + column: self.col, + } + } +} + /// SQL Tokenizer pub struct Tokenizer<'a> { dialect: &'a dyn Dialect, query: &'a str, - line: u64, - col: u64, } impl<'a> Tokenizer<'a> { /// Create a new SQL tokenizer for the specified SQL statement pub fn new(dialect: &'a dyn Dialect, query: &'a str) -> Self { - Self { - dialect, - query, - line: 1, - col: 1, - } + Self { dialect, query } } /// Tokenize the statement and produce a vector of tokens pub fn tokenize(&mut self) -> Result, TokenizerError> { - let mut peekable = self.query.chars().peekable(); + let twl = self.tokenize_with_location()?; let mut tokens: Vec = vec![]; + tokens.reserve(twl.len()); + for token_with_location in twl { + tokens.push(token_with_location.token); + } + Ok(tokens) + } - while let Some(token) = self.next_token(&mut peekable)? { - match &token { - Token::Whitespace(Whitespace::Newline) => { - self.line += 1; - self.col = 1; - } + /// Tokenize the statement and produce a vector of tokens with location information + pub fn tokenize_with_location(&mut self) -> Result, TokenizerError> { + let mut state = State { + peekable: self.query.chars().peekable(), + line: 1, + col: 1, + }; - Token::Whitespace(Whitespace::Tab) => self.col += 4, - Token::Word(w) => { - self.col += w.value.chars().count() as u64; - if w.quote_style.is_some() { - self.col += 2 - } - } - Token::Number(s, _) => self.col += s.chars().count() as u64, - Token::SingleQuotedString(s) => self.col += s.chars().count() as u64, - Token::Placeholder(s) => self.col += s.chars().count() as u64, - _ => self.col += 1, - } + let mut tokens: Vec = vec![]; + + let mut location = state.location(); + while let Some(token) = self.next_token(&mut state)? { + tokens.push(TokenWithLocation { + token, + location: location.clone(), + }); - tokens.push(token); + location = state.location(); } Ok(tokens) } /// Get the next token or return None - fn next_token(&self, chars: &mut Peekable>) -> Result, TokenizerError> { + fn next_token(&self, chars: &mut State) -> Result, TokenizerError> { //println!("next_token: {:?}", chars.peek()); match chars.peek() { Some(&ch) => match ch { @@ -405,10 +482,12 @@ impl<'a> Tokenizer<'a> { } // PostgreSQL accepts "escape" string constants, which are an extension to the SQL standard. x @ 'e' | x @ 'E' => { + let starting_loc = chars.location(); chars.next(); // consume, to check the next char match chars.peek() { Some('\'') => { - let s = self.tokenize_escaped_single_quoted_string(chars)?; + let s = + self.tokenize_escaped_single_quoted_string(starting_loc, chars)?; Ok(Some(Token::EscapedStringLiteral(s))) } _ => { @@ -441,7 +520,12 @@ impl<'a> Tokenizer<'a> { let s = self.tokenize_word(ch, chars); if s.chars().all(|x| ('0'..='9').contains(&x) || x == '.') { - let mut s = peeking_take_while(&mut s.chars().peekable(), |ch| { + let mut inner_state = State { + peekable: s.chars().peekable(), + line: 0, + col: 0, + }; + let mut s = peeking_take_while(&mut inner_state, |ch| { matches!(ch, '0'..='9' | '.') }); let s2 = peeking_take_while(chars, |ch| matches!(ch, '0'..='9' | '.')); @@ -469,8 +553,9 @@ impl<'a> Tokenizer<'a> { if self.dialect.is_delimited_identifier_start(ch) && self .dialect - .is_proper_identifier_inside_quotes(chars.clone()) => + .is_proper_identifier_inside_quotes(chars.peekable.clone()) => { + let error_loc = chars.location(); chars.next(); // consume the opening quote let quote_end = Word::matching_end_quote(quote_start); let (s, last_char) = parse_quoted_ident(chars, quote_end); @@ -478,10 +563,10 @@ impl<'a> Tokenizer<'a> { if last_char == Some(quote_end) { Ok(Some(Token::make_word(&s, Some(quote_start)))) } else { - self.tokenizer_error(format!( - "Expected close delimiter '{}' before EOF.", - quote_end - )) + self.tokenizer_error( + error_loc, + format!("Expected close delimiter '{}' before EOF.", quote_end), + ) } } // numbers and period @@ -698,16 +783,20 @@ impl<'a> Tokenizer<'a> { } } - fn tokenizer_error(&self, message: impl Into) -> Result { + fn tokenizer_error( + &self, + loc: Location, + message: impl Into, + ) -> Result { Err(TokenizerError { message: message.into(), - col: self.col, - line: self.line, + col: loc.column, + line: loc.line, }) } // Consume characters until newline - fn tokenize_single_line_comment(&self, chars: &mut Peekable>) -> String { + fn tokenize_single_line_comment(&self, chars: &mut State) -> String { let mut comment = peeking_take_while(chars, |ch| ch != '\n'); if let Some(ch) = chars.next() { assert_eq!(ch, '\n'); @@ -717,7 +806,7 @@ impl<'a> Tokenizer<'a> { } /// Tokenize an identifier or keyword, after the first char is already consumed. - fn tokenize_word(&self, first_char: char, chars: &mut Peekable>) -> String { + fn tokenize_word(&self, first_char: char, chars: &mut State) -> String { let mut s = first_char.to_string(); s.push_str(&peeking_take_while(chars, |ch| { self.dialect.is_identifier_part(ch) @@ -728,9 +817,13 @@ impl<'a> Tokenizer<'a> { /// Read a single quoted string, starting with the opening quote. fn tokenize_escaped_single_quoted_string( &self, - chars: &mut Peekable>, + starting_loc: Location, + chars: &mut State, ) -> Result { let mut s = String::new(); + + // This case is a bit tricky + chars.next(); // consume the opening quote // slash escaping @@ -782,16 +875,18 @@ impl<'a> Tokenizer<'a> { } } } - self.tokenizer_error("Unterminated encoded string literal") + self.tokenizer_error(starting_loc, "Unterminated encoded string literal") } /// Read a single quoted string, starting with the opening quote. fn tokenize_quoted_string( &self, - chars: &mut Peekable>, + chars: &mut State, quote_style: char, ) -> Result { let mut s = String::new(); + let error_loc = chars.location(); + chars.next(); // consume the opening quote // slash escaping is specific to MySQL dialect @@ -824,12 +919,12 @@ impl<'a> Tokenizer<'a> { } } } - self.tokenizer_error("Unterminated string literal") + self.tokenizer_error(error_loc, "Unterminated string literal") } fn tokenize_multiline_comment( &self, - chars: &mut Peekable>, + chars: &mut State, ) -> Result, TokenizerError> { let mut s = String::new(); let mut nested = 1; @@ -850,7 +945,12 @@ impl<'a> Tokenizer<'a> { s.push(ch); last_ch = ch; } - None => break self.tokenizer_error("Unexpected EOF while in a multi-line comment"), + None => { + break self.tokenizer_error( + chars.location(), + "Unexpected EOF while in a multi-line comment", + ) + } } } } @@ -858,7 +958,7 @@ impl<'a> Tokenizer<'a> { #[allow(clippy::unnecessary_wraps)] fn consume_and_return( &self, - chars: &mut Peekable>, + chars: &mut State, t: Token, ) -> Result, TokenizerError> { chars.next(); @@ -869,10 +969,7 @@ impl<'a> Tokenizer<'a> { /// Read from `chars` until `predicate` returns `false` or EOF is hit. /// Return the characters read as String, and keep the first non-matching /// char available as `chars.next()`. -fn peeking_take_while( - chars: &mut Peekable>, - mut predicate: impl FnMut(char) -> bool, -) -> String { +fn peeking_take_while(chars: &mut State, mut predicate: impl FnMut(char) -> bool) -> String { let mut s = String::new(); while let Some(&ch) = chars.peek() { if predicate(ch) { @@ -885,7 +982,7 @@ fn peeking_take_while( s } -fn parse_quoted_ident(chars: &mut Peekable>, quote_end: char) -> (String, Option) { +fn parse_quoted_ident(chars: &mut State, quote_end: char) -> (String, Option) { let mut last_char = None; let mut s = String::new(); while let Some(ch) = chars.next() { @@ -1518,7 +1615,25 @@ mod tests { compare(expected, tokens); } - fn compare(expected: Vec, actual: Vec) { + #[test] + fn tokenize_with_location() { + let sql = "SELECT a,\n b"; + let dialect = GenericDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, sql); + let tokens = tokenizer.tokenize_with_location().unwrap(); + let expected = vec![ + TokenWithLocation::new(Token::make_keyword("SELECT"), 1, 1), + TokenWithLocation::new(Token::Whitespace(Whitespace::Space), 1, 7), + TokenWithLocation::new(Token::make_word("a", None), 1, 8), + TokenWithLocation::new(Token::Comma, 1, 9), + TokenWithLocation::new(Token::Whitespace(Whitespace::Newline), 1, 10), + TokenWithLocation::new(Token::Whitespace(Whitespace::Space), 2, 1), + TokenWithLocation::new(Token::make_word("b", None), 2, 2), + ]; + compare(expected, tokens); + } + + fn compare(expected: Vec, actual: Vec) { //println!("------------------------------"); //println!("tokens = {:?}", actual); //println!("expected = {:?}", expected); From bda8268e56e82367a7a226632b3a5bbe3ad4b7f0 Mon Sep 17 00:00:00 2001 From: Wei-Ting Kuo Date: Wed, 7 Dec 2022 04:29:41 +0800 Subject: [PATCH 118/806] add keyword NANOSECOND (#749) --- src/ast/value.rs | 4 ++++ src/keywords.rs | 2 ++ src/parser.rs | 4 ++++ tests/sqlparser_common.rs | 3 +++ 4 files changed, 13 insertions(+) diff --git a/src/ast/value.rs b/src/ast/value.rs index 49c0b5d7a..9a356e8bf 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -91,6 +91,8 @@ pub enum DateTimeField { Millennium, Millisecond, Milliseconds, + Nanosecond, + Nanoseconds, Quarter, Timezone, TimezoneHour, @@ -123,6 +125,8 @@ impl fmt::Display for DateTimeField { DateTimeField::Millennium => "MILLENNIUM", DateTimeField::Millisecond => "MILLISECOND", DateTimeField::Milliseconds => "MILLISECONDS", + DateTimeField::Nanosecond => "NANOSECOND", + DateTimeField::Nanoseconds => "NANOSECONDS", DateTimeField::Quarter => "QUARTER", DateTimeField::Timezone => "TIMEZONE", DateTimeField::TimezoneHour => "TIMEZONE_HOUR", diff --git a/src/keywords.rs b/src/keywords.rs index 1fc15fda0..e87062846 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -361,6 +361,8 @@ define_keywords!( MSCK, MULTISET, MUTATION, + NANOSECOND, + NANOSECONDS, NATIONAL, NATURAL, NCHAR, diff --git a/src/parser.rs b/src/parser.rs index 45c472acd..63d4e4d92 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1210,6 +1210,8 @@ impl<'a> Parser<'a> { Keyword::MILLENNIUM => Ok(DateTimeField::Millennium), Keyword::MILLISECOND => Ok(DateTimeField::Millisecond), Keyword::MILLISECONDS => Ok(DateTimeField::Milliseconds), + Keyword::NANOSECOND => Ok(DateTimeField::Nanosecond), + Keyword::NANOSECONDS => Ok(DateTimeField::Nanoseconds), Keyword::QUARTER => Ok(DateTimeField::Quarter), Keyword::TIMEZONE => Ok(DateTimeField::Timezone), Keyword::TIMEZONE_HOUR => Ok(DateTimeField::TimezoneHour), @@ -1343,6 +1345,8 @@ impl<'a> Parser<'a> { Keyword::MILLENNIUM, Keyword::MILLISECOND, Keyword::MILLISECONDS, + Keyword::NANOSECOND, + Keyword::NANOSECONDS, Keyword::QUARTER, Keyword::TIMEZONE, Keyword::TIMEZONE_HOUR, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index addc68e47..85cde27f5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1740,6 +1740,9 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(HOUR FROM d)"); verified_stmt("SELECT EXTRACT(MINUTE FROM d)"); verified_stmt("SELECT EXTRACT(SECOND FROM d)"); + verified_stmt("SELECT EXTRACT(MILLISECOND FROM d)"); + verified_stmt("SELECT EXTRACT(MICROSECOND FROM d)"); + verified_stmt("SELECT EXTRACT(NANOSECOND FROM d)"); verified_stmt("SELECT EXTRACT(CENTURY FROM d)"); verified_stmt("SELECT EXTRACT(DECADE FROM d)"); verified_stmt("SELECT EXTRACT(DOW FROM d)"); From 01fd20f0a31e6fc128bf458a0ac1675125975621 Mon Sep 17 00:00:00 2001 From: yuval-illumex <85674443+yuval-illumex@users.noreply.github.com> Date: Tue, 6 Dec 2022 23:07:53 +0200 Subject: [PATCH 119/806] Support the type key (#750) --- src/parser.rs | 2 +- tests/sqlparser_snowflake.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index 63d4e4d92..87fb674aa 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3822,7 +3822,7 @@ impl<'a> Parser<'a> { )?, }, // Case when Snowflake Semi-structured data like key:value - Keyword::NoKeyword | Keyword::LOCATION if dialect_of!(self is SnowflakeDialect | GenericDialect) => { + Keyword::NoKeyword | Keyword::LOCATION | Keyword::TYPE if dialect_of!(self is SnowflakeDialect | GenericDialect) => { Ok(Value::UnQuotedString(w.value)) } _ => self.expected( diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 1ec57939c..e10141545 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -169,6 +169,28 @@ fn parse_json_using_colon() { select.projection[0] ); + let sql = "SELECT a:type FROM t"; + let select = snowflake().verified_only_select(sql); + assert_eq!( + SelectItem::UnnamedExpr(Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::new("a"))), + operator: JsonOperator::Colon, + right: Box::new(Expr::Value(Value::UnQuotedString("type".to_string()))), + }), + select.projection[0] + ); + + let sql = "SELECT a:location FROM t"; + let select = snowflake().verified_only_select(sql); + assert_eq!( + SelectItem::UnnamedExpr(Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::new("a"))), + operator: JsonOperator::Colon, + right: Box::new(Expr::Value(Value::UnQuotedString("location".to_string()))), + }), + select.projection[0] + ); + snowflake().one_statement_parses_to("SELECT a:b::int FROM t", "SELECT CAST(a:b AS INT) FROM t"); } From 650c53dc77d2e4bae51e82e0b55819b0dc65a1e7 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 7 Dec 2022 12:19:43 -0500 Subject: [PATCH 120/806] Add negative test for except clause on wildcards (#746) --- tests/sqlparser_bigquery.rs | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 90ca27a69..8422245a6 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -283,27 +283,13 @@ fn test_select_wildcard_with_except() { _ => unreachable!(), }; - match bigquery_and_generic().verified_stmt("SELECT * EXCEPT (col1, col2) FROM _table") { - Statement::Query(query) => match *query.body { - SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(WildcardAdditionalOptions { - opt_except: Some(except), - .. - }) => { - assert_eq!( - *except, - ExceptSelectItem { - fist_elemnt: Ident::new("col1"), - additional_elements: vec![Ident::new("col2")] - } - ) - } - _ => unreachable!(), - }, - _ => unreachable!(), - }, - _ => unreachable!(), - }; + assert_eq!( + bigquery_and_generic() + .parse_sql_statements("SELECT * EXCEPT () FROM employee_table") + .unwrap_err() + .to_string(), + "sql parser error: Expected identifier, found: )" + ); } fn bigquery() -> TestedDialects { From d420001c37eeb38aac26a85dd5661755d6ef4228 Mon Sep 17 00:00:00 2001 From: Ziinc Date: Wed, 14 Dec 2022 05:44:45 +0800 Subject: [PATCH 121/806] fix: unnest join constraint with alias parsing for BigQuery dialect (#732) * fix: unnest join constraint with alias parsing for BigQuery dialect * chore: fix failing tests --- src/parser.rs | 7 +++++-- tests/sqlparser_bigquery.rs | 28 ++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 19 ------------------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 87fb674aa..8e64a6fea 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5291,12 +5291,15 @@ impl<'a> Parser<'a> { Err(_) => false, }; - let with_offset_alias = + let with_offset_alias = if with_offset { match self.parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) { Ok(Some(alias)) => Some(alias), Ok(None) => None, Err(e) => return Err(e), - }; + } + } else { + None + }; Ok(TableFactor::UNNEST { alias, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 8422245a6..4bf26ba81 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -94,6 +94,34 @@ fn parse_table_identifiers() { test_table_ident("abc5.GROUP", vec![Ident::new("abc5"), Ident::new("GROUP")]); } +#[test] +fn parse_join_constraint_unnest_alias() { + assert_eq!( + only( + bigquery() + .verified_only_select("SELECT * FROM t1 JOIN UNNEST(t1.a) AS f ON c1 = c2") + .from + ) + .joins, + vec![Join { + relation: TableFactor::UNNEST { + alias: table_alias("f"), + array_expr: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("t1"), + Ident::new("a") + ])), + with_offset: false, + with_offset_alias: None + }, + join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::Identifier("c1".into())), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier("c2".into())), + })), + }] + ); +} + #[test] fn parse_trailing_comma() { for (sql, canonical) in [ diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 85cde27f5..66557f6a6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3683,25 +3683,6 @@ fn parse_unnest() { joins: vec![], }], ); - // 5. WITH OFFSET and WITH OFFSET Alias - chk( - true, - false, - true, - &dialects, - vec![TableWithJoins { - relation: TableFactor::UNNEST { - alias: Some(TableAlias { - name: Ident::new("numbers"), - columns: vec![], - }), - array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), - with_offset: false, - with_offset_alias: Some(Ident::new("with_offset_alias")), - }, - joins: vec![], - }], - ); } #[test] From 6c545195e1cd9a8db56b0c4c0db199b40e1d9d01 Mon Sep 17 00:00:00 2001 From: zidaye <44500963+zidaye@users.noreply.github.com> Date: Wed, 14 Dec 2022 06:15:33 +0800 Subject: [PATCH 122/806] support create function definition with `$$` (#755) * support create function definition using '2700775' * fix warn --- src/ast/mod.rs | 21 +++++++++++++++++++-- src/parser.rs | 31 +++++++++++++++++++++++++++++-- src/tokenizer.rs | 13 +++++++++++-- tests/sqlparser_hive.rs | 8 +++++--- tests/sqlparser_postgres.rs | 28 +++++++++++++++++++++++++++- 5 files changed, 91 insertions(+), 10 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7f3d15438..29839b630 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3777,6 +3777,23 @@ impl fmt::Display for FunctionBehavior { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum FunctionDefinition { + SingleQuotedDef(String), + DoubleDollarDef(String), +} + +impl fmt::Display for FunctionDefinition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionDefinition::SingleQuotedDef(s) => write!(f, "'{s}'")?, + FunctionDefinition::DoubleDollarDef(s) => write!(f, "$${s}$$")?, + } + Ok(()) + } +} + /// Postgres: https://www.postgresql.org/docs/15/sql-createfunction.html #[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -3788,7 +3805,7 @@ pub struct CreateFunctionBody { /// AS 'definition' /// /// Note that Hive's `AS class_name` is also parsed here. - pub as_: Option, + pub as_: Option, /// RETURN expression pub return_: Option, /// USING ... (Hive only) @@ -3804,7 +3821,7 @@ impl fmt::Display for CreateFunctionBody { write!(f, " {behavior}")?; } if let Some(definition) = &self.as_ { - write!(f, " AS '{definition}'")?; + write!(f, " AS {definition}")?; } if let Some(expr) = &self.return_ { write!(f, " RETURN {expr}")?; diff --git a/src/parser.rs b/src/parser.rs index 8e64a6fea..551961b8f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2310,7 +2310,7 @@ impl<'a> Parser<'a> { if dialect_of!(self is HiveDialect) { let name = self.parse_object_name()?; self.expect_keyword(Keyword::AS)?; - let class_name = self.parse_literal_string()?; + let class_name = self.parse_function_definition()?; let params = CreateFunctionBody { as_: Some(class_name), using: self.parse_optional_create_function_using()?, @@ -2400,7 +2400,7 @@ impl<'a> Parser<'a> { } if self.parse_keyword(Keyword::AS) { ensure_not_set(&body.as_, "AS")?; - body.as_ = Some(self.parse_literal_string()?); + body.as_ = Some(self.parse_function_definition()?); } else if self.parse_keyword(Keyword::LANGUAGE) { ensure_not_set(&body.language, "LANGUAGE")?; body.language = Some(self.parse_identifier()?); @@ -3883,6 +3883,33 @@ impl<'a> Parser<'a> { } } + pub fn parse_function_definition(&mut self) -> Result { + let peek_token = self.peek_token(); + match peek_token.token { + Token::DoubleDollarQuoting if dialect_of!(self is PostgreSqlDialect) => { + self.next_token(); + let mut func_desc = String::new(); + loop { + if let Some(next_token) = self.next_token_no_skip() { + match &next_token.token { + Token::DoubleDollarQuoting => break, + Token::EOF => { + return self.expected( + "literal string", + TokenWithLocation::wrap(Token::EOF), + ); + } + token => func_desc.push_str(token.to_string().as_str()), + } + } + } + Ok(FunctionDefinition::DoubleDollarDef(func_desc)) + } + _ => Ok(FunctionDefinition::SingleQuotedDef( + self.parse_literal_string()?, + )), + } + } /// Parse a literal string pub fn parse_literal_string(&mut self) -> Result { let next_token = self.next_token(); diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 68dd652d5..99dccaa0a 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -145,6 +145,8 @@ pub enum Token { PGCubeRoot, /// `?` or `$` , a prepared statement arg placeholder Placeholder(String), + /// `$$`, used for PostgreSQL create function definition + DoubleDollarQuoting, /// ->, used as a operator to extract json field in PostgreSQL Arrow, /// ->>, used as a operator to extract json field as text in PostgreSQL @@ -215,6 +217,7 @@ impl fmt::Display for Token { Token::LongArrow => write!(f, "->>"), Token::HashArrow => write!(f, "#>"), Token::HashLongArrow => write!(f, "#>>"), + Token::DoubleDollarQuoting => write!(f, "$$"), } } } @@ -770,8 +773,14 @@ impl<'a> Tokenizer<'a> { } '$' => { chars.next(); - let s = peeking_take_while(chars, |ch| ch.is_alphanumeric() || ch == '_'); - Ok(Some(Token::Placeholder(String::from("$") + &s))) + match chars.peek() { + Some('$') => self.consume_and_return(chars, Token::DoubleDollarQuoting), + _ => { + let s = + peeking_take_while(chars, |ch| ch.is_alphanumeric() || ch == '_'); + Ok(Some(Token::Placeholder(String::from("$") + &s))) + } + } } //whitespace check (including unicode chars) should be last as it covers some of the chars above ch if ch.is_whitespace() => { diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 99a81eff2..064a090f7 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -16,8 +16,8 @@ //! is also tested (on the inputs it can handle). use sqlparser::ast::{ - CreateFunctionBody, CreateFunctionUsing, Expr, Function, Ident, ObjectName, SelectItem, - Statement, TableFactor, UnaryOperator, Value, + CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionDefinition, Ident, ObjectName, + SelectItem, Statement, TableFactor, UnaryOperator, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect}; use sqlparser::parser::ParserError; @@ -252,7 +252,9 @@ fn parse_create_function() { assert_eq!( params, CreateFunctionBody { - as_: Some("org.random.class.Name".to_string()), + as_: Some(FunctionDefinition::SingleQuotedDef( + "org.random.class.Name".to_string() + )), using: Some(CreateFunctionUsing::Jar( "hdfs://somewhere.com:8020/very/far".to_string() )), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fac98b8ef..53be5e465 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2257,7 +2257,9 @@ fn parse_create_function() { params: CreateFunctionBody { language: Some("SQL".into()), behavior: Some(FunctionBehavior::Immutable), - as_: Some("select $1 + $2;".into()), + as_: Some(FunctionDefinition::SingleQuotedDef( + "select $1 + $2;".into() + )), ..Default::default() }, } @@ -2292,4 +2294,28 @@ fn parse_create_function() { }, } ); + + let sql = r#"CREATE OR REPLACE FUNCTION increment(i INTEGER) RETURNS INTEGER LANGUAGE plpgsql AS $$ BEGIN RETURN i + 1; END; $$"#; + assert_eq!( + pg().verified_stmt(sql), + Statement::CreateFunction { + or_replace: true, + temporary: false, + name: ObjectName(vec![Ident::new("increment")]), + args: Some(vec![CreateFunctionArg::with_name( + "i", + DataType::Integer(None) + )]), + return_type: Some(DataType::Integer(None)), + params: CreateFunctionBody { + language: Some("plpgsql".into()), + behavior: None, + return_: None, + as_: Some(FunctionDefinition::DoubleDollarDef( + " BEGIN RETURN i + 1; END; ".into() + )), + using: None + }, + } + ); } From fb023441313a1646574ff6026c65f2e0a582e433 Mon Sep 17 00:00:00 2001 From: Audun Skaugen Date: Tue, 13 Dec 2022 23:19:07 +0100 Subject: [PATCH 123/806] Generalize locking clause (#759) Postgres supports more generalized locking clauses, for example: FOR UPDATE OF SKIP LOCKED also, multiple locking clauses. Generalize the parser to support these. Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 6 +- src/ast/query.rs | 50 ++++++++++++++-- src/keywords.rs | 3 + src/parser.rs | 40 +++++++++---- tests/sqlparser_common.rs | 112 +++++++++++++++++++++++++++++++++--- tests/sqlparser_mysql.rs | 10 ++-- tests/sqlparser_postgres.rs | 2 +- 7 files changed, 187 insertions(+), 36 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 29839b630..1bf8c9cd0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -32,9 +32,9 @@ pub use self::ddl::{ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ Cte, ExceptSelectItem, ExcludeSelectItem, Fetch, Join, JoinConstraint, JoinOperator, - LateralView, LockType, Offset, OffsetRows, OrderByExpr, Query, Select, SelectInto, SelectItem, - SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, TableWithJoins, Top, - Values, WildcardAdditionalOptions, With, + LateralView, LockClause, LockType, NonBlock, Offset, OffsetRows, OrderByExpr, Query, Select, + SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, + TableWithJoins, Top, Values, WildcardAdditionalOptions, With, }; pub use self::value::{escape_quoted_string, DateTimeField, TrimWhereField, Value}; diff --git a/src/ast/query.rs b/src/ast/query.rs index f813f44dd..9d23b1ae7 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -35,8 +35,8 @@ pub struct Query { pub offset: Option, /// `FETCH { FIRST | NEXT } [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES }` pub fetch: Option, - /// `FOR { UPDATE | SHARE }` - pub lock: Option, + /// `FOR { UPDATE | SHARE } [ OF table_name ] [ SKIP LOCKED | NOWAIT ]` + pub locks: Vec, } impl fmt::Display for Query { @@ -57,8 +57,8 @@ impl fmt::Display for Query { if let Some(ref fetch) = self.fetch { write!(f, " {}", fetch)?; } - if let Some(ref lock) = self.lock { - write!(f, " {}", lock)?; + if !self.locks.is_empty() { + write!(f, " {}", display_separated(&self.locks, " "))?; } Ok(()) } @@ -833,6 +833,27 @@ impl fmt::Display for Fetch { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LockClause { + pub lock_type: LockType, + pub of: Option, + pub nonblock: Option, +} + +impl fmt::Display for LockClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "FOR {}", &self.lock_type)?; + if let Some(ref of) = self.of { + write!(f, " OF {}", of)?; + } + if let Some(ref nb) = self.nonblock { + write!(f, " {}", nb)?; + } + Ok(()) + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum LockType { @@ -843,13 +864,30 @@ pub enum LockType { impl fmt::Display for LockType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let select_lock = match self { - LockType::Share => "FOR SHARE", - LockType::Update => "FOR UPDATE", + LockType::Share => "SHARE", + LockType::Update => "UPDATE", }; write!(f, "{}", select_lock) } } +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NonBlock { + Nowait, + SkipLocked, +} + +impl fmt::Display for NonBlock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let nonblock = match self { + NonBlock::Nowait => "NOWAIT", + NonBlock::SkipLocked => "SKIP LOCKED", + }; + write!(f, "{}", nonblock) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Top { diff --git a/src/keywords.rs b/src/keywords.rs index e87062846..d41463e9e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -331,6 +331,7 @@ define_keywords!( LOCALTIME, LOCALTIMESTAMP, LOCATION, + LOCKED, LOGIN, LOWER, MANAGEDLOCATION, @@ -382,6 +383,7 @@ define_keywords!( NOSUPERUSER, NOT, NOTHING, + NOWAIT, NTH_VALUE, NTILE, NULL, @@ -509,6 +511,7 @@ define_keywords!( SHARE, SHOW, SIMILAR, + SKIP, SMALLINT, SNAPSHOT, SOME, diff --git a/src/parser.rs b/src/parser.rs index 551961b8f..1e3d71b34 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4505,11 +4505,10 @@ impl<'a> Parser<'a> { None }; - let lock = if self.parse_keyword(Keyword::FOR) { - Some(self.parse_lock()?) - } else { - None - }; + let mut locks = Vec::new(); + while self.parse_keyword(Keyword::FOR) { + locks.push(self.parse_lock()?); + } Ok(Query { with, @@ -4518,7 +4517,7 @@ impl<'a> Parser<'a> { limit, offset, fetch, - lock, + locks, }) } else { let insert = self.parse_insert()?; @@ -4530,7 +4529,7 @@ impl<'a> Parser<'a> { order_by: vec![], offset: None, fetch: None, - lock: None, + locks: vec![], }) } } @@ -5945,12 +5944,29 @@ impl<'a> Parser<'a> { } /// Parse a FOR UPDATE/FOR SHARE clause - pub fn parse_lock(&mut self) -> Result { - match self.expect_one_of_keywords(&[Keyword::UPDATE, Keyword::SHARE])? { - Keyword::UPDATE => Ok(LockType::Update), - Keyword::SHARE => Ok(LockType::Share), + pub fn parse_lock(&mut self) -> Result { + let lock_type = match self.expect_one_of_keywords(&[Keyword::UPDATE, Keyword::SHARE])? { + Keyword::UPDATE => LockType::Update, + Keyword::SHARE => LockType::Share, _ => unreachable!(), - } + }; + let of = if self.parse_keyword(Keyword::OF) { + Some(self.parse_object_name()?) + } else { + None + }; + let nonblock = if self.parse_keyword(Keyword::NOWAIT) { + Some(NonBlock::Nowait) + } else if self.parse_keywords(&[Keyword::SKIP, Keyword::LOCKED]) { + Some(NonBlock::SkipLocked) + } else { + None + }; + Ok(LockClause { + lock_type, + of, + nonblock, + }) } pub fn parse_values(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 66557f6a6..e5ed0bb80 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -253,7 +253,7 @@ fn parse_update_set_from() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], }), alias: Some(TableAlias { name: Ident::new("t2"), @@ -2296,7 +2296,7 @@ fn parse_create_table_as_table() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], }); match verified_stmt(sql1) { @@ -2319,7 +2319,7 @@ fn parse_create_table_as_table() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], }); match verified_stmt(sql2) { @@ -3456,7 +3456,7 @@ fn parse_interval_and_or_xor() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], }))]; assert_eq!(actual_ast, expected_ast); @@ -5604,7 +5604,7 @@ fn parse_merge() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], }), alias: Some(TableAlias { name: Ident { @@ -5729,12 +5729,106 @@ fn test_merge_with_delimiter() { #[test] fn test_lock() { let sql = "SELECT * FROM student WHERE id = '1' FOR UPDATE"; - let ast = verified_query(sql); - assert_eq!(ast.lock.unwrap(), LockType::Update); + let mut ast = verified_query(sql); + assert_eq!(ast.locks.len(), 1); + let lock = ast.locks.pop().unwrap(); + assert_eq!(lock.lock_type, LockType::Update); + assert!(lock.of.is_none()); + assert!(lock.nonblock.is_none()); let sql = "SELECT * FROM student WHERE id = '1' FOR SHARE"; - let ast = verified_query(sql); - assert_eq!(ast.lock.unwrap(), LockType::Share); + let mut ast = verified_query(sql); + assert_eq!(ast.locks.len(), 1); + let lock = ast.locks.pop().unwrap(); + assert_eq!(lock.lock_type, LockType::Share); + assert!(lock.of.is_none()); + assert!(lock.nonblock.is_none()); +} + +#[test] +fn test_lock_table() { + let sql = "SELECT * FROM student WHERE id = '1' FOR UPDATE OF school"; + let mut ast = verified_query(sql); + assert_eq!(ast.locks.len(), 1); + let lock = ast.locks.pop().unwrap(); + assert_eq!(lock.lock_type, LockType::Update); + assert_eq!( + lock.of.unwrap().0, + vec![Ident { + value: "school".to_string(), + quote_style: None + }] + ); + assert!(lock.nonblock.is_none()); + + let sql = "SELECT * FROM student WHERE id = '1' FOR SHARE OF school"; + let mut ast = verified_query(sql); + assert_eq!(ast.locks.len(), 1); + let lock = ast.locks.pop().unwrap(); + assert_eq!(lock.lock_type, LockType::Share); + assert_eq!( + lock.of.unwrap().0, + vec![Ident { + value: "school".to_string(), + quote_style: None + }] + ); + assert!(lock.nonblock.is_none()); + + let sql = "SELECT * FROM student WHERE id = '1' FOR SHARE OF school FOR UPDATE OF student"; + let mut ast = verified_query(sql); + assert_eq!(ast.locks.len(), 2); + let lock = ast.locks.remove(0); + assert_eq!(lock.lock_type, LockType::Share); + assert_eq!( + lock.of.unwrap().0, + vec![Ident { + value: "school".to_string(), + quote_style: None + }] + ); + assert!(lock.nonblock.is_none()); + let lock = ast.locks.remove(0); + assert_eq!(lock.lock_type, LockType::Update); + assert_eq!( + lock.of.unwrap().0, + vec![Ident { + value: "student".to_string(), + quote_style: None + }] + ); + assert!(lock.nonblock.is_none()); +} + +#[test] +fn test_lock_nonblock() { + let sql = "SELECT * FROM student WHERE id = '1' FOR UPDATE OF school SKIP LOCKED"; + let mut ast = verified_query(sql); + assert_eq!(ast.locks.len(), 1); + let lock = ast.locks.pop().unwrap(); + assert_eq!(lock.lock_type, LockType::Update); + assert_eq!( + lock.of.unwrap().0, + vec![Ident { + value: "school".to_string(), + quote_style: None + }] + ); + assert_eq!(lock.nonblock.unwrap(), NonBlock::SkipLocked); + + let sql = "SELECT * FROM student WHERE id = '1' FOR SHARE OF school NOWAIT"; + let mut ast = verified_query(sql); + assert_eq!(ast.locks.len(), 1); + let lock = ast.locks.pop().unwrap(); + assert_eq!(lock.lock_type, LockType::Share); + assert_eq!( + lock.of.unwrap().0, + vec![Ident { + value: "school".to_string(), + quote_style: None + }] + ); + assert_eq!(lock.nonblock.unwrap(), NonBlock::Nowait); } #[test] diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 18f554f9e..67850b1b9 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -466,7 +466,7 @@ fn parse_quote_identifiers_2() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], })) ); } @@ -500,7 +500,7 @@ fn parse_quote_identifiers_3() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], })) ); } @@ -683,7 +683,7 @@ fn parse_simple_insert() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], }), source ); @@ -741,7 +741,7 @@ fn parse_insert_with_on_duplicate_update() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], }), source ); @@ -983,7 +983,7 @@ fn parse_substring_in_select() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], }), query ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 53be5e465..d8144a716 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1512,7 +1512,7 @@ fn parse_array_subquery_expr() { limit: None, offset: None, fetch: None, - lock: None, + locks: vec![], })), expr_from_projection(only(&select.projection)), ); From 6d6eb4bc9bd8fff4700401fdd51b1e245e2373d5 Mon Sep 17 00:00:00 2001 From: Audun Skaugen Date: Wed, 14 Dec 2022 20:54:56 +0100 Subject: [PATCH 124/806] Support json operators `@>` `<@` `#-` `@?` and `@@` postgres supports a bunch more json operators. See https://www.postgresql.org/docs/15/functions-json.html Skipping operators starting with a question mark for now, since those are hard to distinguish from placeholders without more context. --- src/ast/mod.rs | 21 ++++++++++++ src/parser.rs | 17 +++++++++- src/tokenizer.rs | 31 +++++++++++++++++- tests/sqlparser_postgres.rs | 65 +++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1bf8c9cd0..6a718702d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -190,6 +190,20 @@ pub enum JsonOperator { HashLongArrow, /// : Colon is used by Snowflake (Which is similar to LongArrow) Colon, + /// jsonb @> jsonb -> boolean: Test whether left json contains the right json + AtArrow, + /// jsonb <@ jsonb -> boolean: Test whether right json contains the left json + ArrowAt, + /// jsonb #- text[] -> jsonb: Deletes the field or array element at the specified + /// path, where path elements can be either field keys or array indexes. + HashMinus, + /// jsonb @? jsonpath -> boolean: Does JSON path return any item for the specified + /// JSON value? + AtQuestion, + /// jsonb @@ jsonpath → boolean: Returns the result of a JSON path predicate check + /// for the specified JSON value. Only the first item of the result is taken into + /// account. If the result is not Boolean, then NULL is returned. + AtAt, } impl fmt::Display for JsonOperator { @@ -210,6 +224,13 @@ impl fmt::Display for JsonOperator { JsonOperator::Colon => { write!(f, ":") } + JsonOperator::AtArrow => { + write!(f, "@>") + } + JsonOperator::ArrowAt => write!(f, "<@"), + JsonOperator::HashMinus => write!(f, "#-"), + JsonOperator::AtQuestion => write!(f, "@?"), + JsonOperator::AtAt => write!(f, "@@"), } } } diff --git a/src/parser.rs b/src/parser.rs index 1e3d71b34..26aa499e5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1602,12 +1602,22 @@ impl<'a> Parser<'a> { || Token::LongArrow == tok || Token::HashArrow == tok || Token::HashLongArrow == tok + || Token::AtArrow == tok + || Token::ArrowAt == tok + || Token::HashMinus == tok + || Token::AtQuestion == tok + || Token::AtAt == tok { let operator = match tok.token { Token::Arrow => JsonOperator::Arrow, Token::LongArrow => JsonOperator::LongArrow, Token::HashArrow => JsonOperator::HashArrow, Token::HashLongArrow => JsonOperator::HashLongArrow, + Token::AtArrow => JsonOperator::AtArrow, + Token::ArrowAt => JsonOperator::ArrowAt, + Token::HashMinus => JsonOperator::HashMinus, + Token::AtQuestion => JsonOperator::AtQuestion, + Token::AtAt => JsonOperator::AtAt, _ => unreachable!(), }; Ok(Expr::JsonAccess { @@ -1805,7 +1815,12 @@ impl<'a> Parser<'a> { | Token::LongArrow | Token::Arrow | Token::HashArrow - | Token::HashLongArrow => Ok(50), + | Token::HashLongArrow + | Token::AtArrow + | Token::ArrowAt + | Token::HashMinus + | Token::AtQuestion + | Token::AtAt => Ok(50), _ => Ok(0), } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 99dccaa0a..eb2cc2658 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -155,6 +155,20 @@ pub enum Token { HashArrow, /// #>> Extracts JSON sub-object at the specified path as text HashLongArrow, + /// jsonb @> jsonb -> boolean: Test whether left json contains the right json + AtArrow, + /// jsonb <@ jsonb -> boolean: Test whether right json contains the left json + ArrowAt, + /// jsonb #- text[] -> jsonb: Deletes the field or array element at the specified + /// path, where path elements can be either field keys or array indexes. + HashMinus, + /// jsonb @? jsonpath -> boolean: Does JSON path return any item for the specified + /// JSON value? + AtQuestion, + /// jsonb @@ jsonpath → boolean: Returns the result of a JSON path predicate check + /// for the specified JSON value. Only the first item of the result is taken into + /// account. If the result is not Boolean, then NULL is returned. + AtAt, } impl fmt::Display for Token { @@ -217,7 +231,12 @@ impl fmt::Display for Token { Token::LongArrow => write!(f, "->>"), Token::HashArrow => write!(f, "#>"), Token::HashLongArrow => write!(f, "#>>"), + Token::AtArrow => write!(f, "@>"), Token::DoubleDollarQuoting => write!(f, "$$"), + Token::ArrowAt => write!(f, "<@"), + Token::HashMinus => write!(f, "#-"), + Token::AtQuestion => write!(f, "@?"), + Token::AtAt => write!(f, "@@"), } } } @@ -708,6 +727,7 @@ impl<'a> Tokenizer<'a> { } Some('>') => self.consume_and_return(chars, Token::Neq), Some('<') => self.consume_and_return(chars, Token::ShiftLeft), + Some('@') => self.consume_and_return(chars, Token::ArrowAt), _ => Ok(Some(Token::Lt)), } } @@ -752,6 +772,7 @@ impl<'a> Tokenizer<'a> { '#' => { chars.next(); match chars.peek() { + Some('-') => self.consume_and_return(chars, Token::HashMinus), Some('>') => { chars.next(); match chars.peek() { @@ -765,7 +786,15 @@ impl<'a> Tokenizer<'a> { _ => Ok(Some(Token::Sharp)), } } - '@' => self.consume_and_return(chars, Token::AtSign), + '@' => { + chars.next(); + match chars.peek() { + Some('>') => self.consume_and_return(chars, Token::AtArrow), + Some('?') => self.consume_and_return(chars, Token::AtQuestion), + Some('@') => self.consume_and_return(chars, Token::AtAt), + _ => Ok(Some(Token::AtSign)), + } + } '?' => { chars.next(); let s = peeking_take_while(chars, |ch| ch.is_numeric()); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d8144a716..5f178a91e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1620,6 +1620,71 @@ fn test_json() { }), select.projection[0] ); + + let sql = "SELECT info FROM orders WHERE info @> '{\"a\": 1}'"; + let select = pg().verified_only_select(sql); + assert_eq!( + Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::new("info"))), + operator: JsonOperator::AtArrow, + right: Box::new(Expr::Value(Value::SingleQuotedString( + "{\"a\": 1}".to_string() + ))), + }, + select.selection.unwrap(), + ); + + let sql = "SELECT info FROM orders WHERE '{\"a\": 1}' <@ info"; + let select = pg().verified_only_select(sql); + assert_eq!( + Expr::JsonAccess { + left: Box::new(Expr::Value(Value::SingleQuotedString( + "{\"a\": 1}".to_string() + ))), + operator: JsonOperator::ArrowAt, + right: Box::new(Expr::Identifier(Ident::new("info"))), + }, + select.selection.unwrap(), + ); + + let sql = "SELECT info #- ARRAY['a', 'b'] FROM orders"; + let select = pg().verified_only_select(sql); + assert_eq!( + SelectItem::UnnamedExpr(Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::from("info"))), + operator: JsonOperator::HashMinus, + right: Box::new(Expr::Array(Array { + elem: vec![ + Expr::Value(Value::SingleQuotedString("a".to_string())), + Expr::Value(Value::SingleQuotedString("b".to_string())), + ], + named: true, + })), + }), + select.projection[0], + ); + + let sql = "SELECT info FROM orders WHERE info @? '$.a'"; + let select = pg().verified_only_select(sql); + assert_eq!( + Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::from("info"))), + operator: JsonOperator::AtQuestion, + right: Box::new(Expr::Value(Value::SingleQuotedString("$.a".to_string())),), + }, + select.selection.unwrap(), + ); + + let sql = "SELECT info FROM orders WHERE info @@ '$.a'"; + let select = pg().verified_only_select(sql); + assert_eq!( + Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::from("info"))), + operator: JsonOperator::AtAt, + right: Box::new(Expr::Value(Value::SingleQuotedString("$.a".to_string())),), + }, + select.selection.unwrap(), + ); } #[test] From 3d5cc54dcfcc1c2b921ebfe8c1aea319b4adcba7 Mon Sep 17 00:00:00 2001 From: Audun Skaugen Date: Sat, 17 Dec 2022 13:38:57 +0100 Subject: [PATCH 125/806] Generalize conflict target (#762) Postgres supports `ON CONFLICT ON CONSTRAINT ` to explicitly name the constraint that fails. Support this. --- src/ast/mod.rs | 20 ++++++++++++-- src/parser.rs | 10 ++++++- tests/sqlparser_postgres.rs | 55 +++++++++++++++++++++++++++++-------- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6a718702d..cf936d3c9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2728,11 +2728,17 @@ pub enum OnInsert { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct OnConflict { - pub conflict_target: Vec, + pub conflict_target: Option, pub action: OnConflictAction, } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ConflictTarget { + Columns(Vec), + OnConstraint(ObjectName), +} +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum OnConflictAction { DoNothing, DoUpdate(DoUpdate), @@ -2762,12 +2768,20 @@ impl fmt::Display for OnInsert { impl fmt::Display for OnConflict { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, " ON CONFLICT")?; - if !self.conflict_target.is_empty() { - write!(f, "({})", display_comma_separated(&self.conflict_target))?; + if let Some(target) = &self.conflict_target { + write!(f, "{}", target)?; } write!(f, " {}", self.action) } } +impl fmt::Display for ConflictTarget { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ConflictTarget::Columns(cols) => write!(f, "({})", display_comma_separated(cols)), + ConflictTarget::OnConstraint(name) => write!(f, " ON CONSTRAINT {}", name), + } + } +} impl fmt::Display for OnConflictAction { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/parser.rs b/src/parser.rs index 26aa499e5..528b8c6fe 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5630,7 +5630,15 @@ impl<'a> Parser<'a> { let on = if self.parse_keyword(Keyword::ON) { if self.parse_keyword(Keyword::CONFLICT) { let conflict_target = - self.parse_parenthesized_column_list(IsOptional::Optional)?; + if self.parse_keywords(&[Keyword::ON, Keyword::CONSTRAINT]) { + Some(ConflictTarget::OnConstraint(self.parse_object_name()?)) + } else if self.peek_token() == Token::LParen { + Some(ConflictTarget::Columns( + self.parse_parenthesized_column_list(IsOptional::Mandatory)?, + )) + } else { + None + }; self.expect_keyword(Keyword::DO)?; let action = if self.parse_keyword(Keyword::NOTHING) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5f178a91e..6b26fe48b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1112,12 +1112,12 @@ fn parse_pg_on_conflict() { Statement::Insert { on: Some(OnInsert::OnConflict(OnConflict { - conflict_target, + conflict_target: Some(ConflictTarget::Columns(cols)), action, })), .. } => { - assert_eq!(vec![Ident::from("did")], conflict_target); + assert_eq!(vec![Ident::from("did")], cols); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { @@ -1142,15 +1142,12 @@ fn parse_pg_on_conflict() { Statement::Insert { on: Some(OnInsert::OnConflict(OnConflict { - conflict_target, + conflict_target: Some(ConflictTarget::Columns(cols)), action, })), .. } => { - assert_eq!( - vec![Ident::from("did"), Ident::from("area"),], - conflict_target - ); + assert_eq!(vec![Ident::from("did"), Ident::from("area"),], cols); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![ @@ -1183,12 +1180,11 @@ fn parse_pg_on_conflict() { Statement::Insert { on: Some(OnInsert::OnConflict(OnConflict { - conflict_target, + conflict_target: None, action, })), .. } => { - assert_eq!(Vec::::new(), conflict_target); assert_eq!(OnConflictAction::DoNothing, action); } _ => unreachable!(), @@ -1204,12 +1200,49 @@ fn parse_pg_on_conflict() { Statement::Insert { on: Some(OnInsert::OnConflict(OnConflict { - conflict_target, + conflict_target: Some(ConflictTarget::Columns(cols)), + action, + })), + .. + } => { + assert_eq!(vec![Ident::from("did")], cols); + assert_eq!( + OnConflictAction::DoUpdate(DoUpdate { + assignments: vec![Assignment { + id: vec!["dname".into()], + value: Expr::Value(Value::Placeholder("$1".to_string())) + },], + selection: Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "dsize".to_string(), + quote_style: None + })), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(Value::Placeholder("$2".to_string()))) + }) + }), + action + ); + } + _ => unreachable!(), + }; + + let stmt = pg_and_generic().verified_stmt( + "INSERT INTO distributors (did, dname, dsize) \ + VALUES (5, 'Gizmo Transglobal', 1000), (6, 'Associated Computing, Inc', 1010) \ + ON CONFLICT ON CONSTRAINT distributors_did_pkey \ + DO UPDATE SET dname = $1 WHERE dsize > $2", + ); + match stmt { + Statement::Insert { + on: + Some(OnInsert::OnConflict(OnConflict { + conflict_target: Some(ConflictTarget::OnConstraint(cname)), action, })), .. } => { - assert_eq!(vec![Ident::from("did")], conflict_target); + assert_eq!(vec![Ident::from("distributors_did_pkey")], cname.0); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { From 2d801c9fb6cfe3aa138171dd6e7fbf4dbe845b1a Mon Sep 17 00:00:00 2001 From: zidaye <44500963+zidaye@users.noreply.github.com> Date: Wed, 28 Dec 2022 20:57:51 +0800 Subject: [PATCH 126/806] Support `DROP FUNCTION` syntax (#752) * drop function * update and to Option * fix review * update * fmt --- src/ast/mod.rs | 69 +++++++++++++++++++++++-- src/parser.rs | 53 +++++++++++++++++-- tests/sqlparser_postgres.rs | 100 ++++++++++++++++++++++++++++++++++-- 3 files changed, 207 insertions(+), 15 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cf936d3c9..6003cb1d4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1268,6 +1268,14 @@ pub enum Statement { /// deleted along with the dropped table purge: bool, }, + /// DROP Function + DropFunction { + if_exists: bool, + /// One or more function to drop + func_desc: Vec, + /// `CASCADE` or `RESTRICT` + option: Option, + }, /// DECLARE - Declaring Cursor Variables /// /// Note: this is a PostgreSQL-specific statement, @@ -1432,7 +1440,7 @@ pub enum Statement { or_replace: bool, temporary: bool, name: ObjectName, - args: Option>, + args: Option>, return_type: Option, /// Optional parameters. params: CreateFunctionBody, @@ -2284,6 +2292,22 @@ impl fmt::Display for Statement { if *restrict { " RESTRICT" } else { "" }, if *purge { " PURGE" } else { "" } ), + Statement::DropFunction { + if_exists, + func_desc, + option, + } => { + write!( + f, + "DROP FUNCTION{} {}", + if *if_exists { " IF EXISTS" } else { "" }, + display_comma_separated(func_desc), + )?; + if let Some(op) = option { + write!(f, " {}", op)?; + } + Ok(()) + } Statement::Discard { object_type } => { write!(f, "DISCARD {object_type}", object_type = object_type)?; Ok(()) @@ -3726,17 +3750,52 @@ impl fmt::Display for ContextModifier { } } -/// Function argument in CREATE FUNCTION. +/// Function describe in DROP FUNCTION. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum DropFunctionOption { + Restrict, + Cascade, +} + +impl fmt::Display for DropFunctionOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DropFunctionOption::Restrict => write!(f, "RESTRICT "), + DropFunctionOption::Cascade => write!(f, "CASCADE "), + } + } +} + +/// Function describe in DROP FUNCTION. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DropFunctionDesc { + pub name: ObjectName, + pub args: Option>, +} + +impl fmt::Display for DropFunctionDesc { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name)?; + if let Some(args) = &self.args { + write!(f, "({})", display_comma_separated(args))?; + } + Ok(()) + } +} + +/// Function argument in CREATE OR DROP FUNCTION. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct CreateFunctionArg { +pub struct OperateFunctionArg { pub mode: Option, pub name: Option, pub data_type: DataType, pub default_expr: Option, } -impl CreateFunctionArg { +impl OperateFunctionArg { /// Returns an unnamed argument. pub fn unnamed(data_type: DataType) -> Self { Self { @@ -3758,7 +3817,7 @@ impl CreateFunctionArg { } } -impl fmt::Display for CreateFunctionArg { +impl fmt::Display for OperateFunctionArg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(mode) = &self.mode { write!(f, "{} ", mode)?; diff --git a/src/parser.rs b/src/parser.rs index 528b8c6fe..21ab6d2c6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2343,7 +2343,13 @@ impl<'a> Parser<'a> { } else if dialect_of!(self is PostgreSqlDialect) { let name = self.parse_object_name()?; self.expect_token(&Token::LParen)?; - let args = self.parse_comma_separated(Parser::parse_create_function_arg)?; + let args = if self.consume_token(&Token::RParen) { + self.prev_token(); + None + } else { + Some(self.parse_comma_separated(Parser::parse_function_arg)?) + }; + self.expect_token(&Token::RParen)?; let return_type = if self.parse_keyword(Keyword::RETURNS) { @@ -2358,7 +2364,7 @@ impl<'a> Parser<'a> { or_replace, temporary, name, - args: Some(args), + args, return_type, params, }) @@ -2368,7 +2374,7 @@ impl<'a> Parser<'a> { } } - fn parse_create_function_arg(&mut self) -> Result { + fn parse_function_arg(&mut self) -> Result { let mode = if self.parse_keyword(Keyword::IN) { Some(ArgMode::In) } else if self.parse_keyword(Keyword::OUT) { @@ -2394,7 +2400,7 @@ impl<'a> Parser<'a> { } else { None }; - Ok(CreateFunctionArg { + Ok(OperateFunctionArg { mode, name, data_type, @@ -2767,9 +2773,11 @@ impl<'a> Parser<'a> { ObjectType::Schema } else if self.parse_keyword(Keyword::SEQUENCE) { ObjectType::Sequence + } else if self.parse_keyword(Keyword::FUNCTION) { + return self.parse_drop_function(); } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, SCHEMA, or SEQUENCE after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION or SEQUENCE after DROP", self.peek_token(), ); }; @@ -2796,6 +2804,41 @@ impl<'a> Parser<'a> { }) } + /// DROP FUNCTION [ IF EXISTS ] name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] [, ...] + /// [ CASCADE | RESTRICT ] + fn parse_drop_function(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let func_desc = self.parse_comma_separated(Parser::parse_drop_function_desc)?; + let option = match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { + Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade), + Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict), + _ => None, + }; + Ok(Statement::DropFunction { + if_exists, + func_desc, + option, + }) + } + + fn parse_drop_function_desc(&mut self) -> Result { + let name = self.parse_object_name()?; + + let args = if self.consume_token(&Token::LParen) { + if self.consume_token(&Token::RParen) { + None + } else { + let args = self.parse_comma_separated(Parser::parse_function_arg)?; + self.expect_token(&Token::RParen)?; + Some(args) + } + } else { + None + }; + + Ok(DropFunctionDesc { name, args }) + } + /// DECLARE name [ BINARY ] [ ASENSITIVE | INSENSITIVE ] [ [ NO ] SCROLL ] // CURSOR [ { WITH | WITHOUT } HOLD ] FOR query pub fn parse_declare(&mut self) -> Result { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6b26fe48b..6e190a01b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2348,8 +2348,8 @@ fn parse_create_function() { temporary: false, name: ObjectName(vec![Ident::new("add")]), args: Some(vec![ - CreateFunctionArg::unnamed(DataType::Integer(None)), - CreateFunctionArg::unnamed(DataType::Integer(None)), + OperateFunctionArg::unnamed(DataType::Integer(None)), + OperateFunctionArg::unnamed(DataType::Integer(None)), ]), return_type: Some(DataType::Integer(None)), params: CreateFunctionBody { @@ -2371,8 +2371,8 @@ fn parse_create_function() { temporary: false, name: ObjectName(vec![Ident::new("add")]), args: Some(vec![ - CreateFunctionArg::with_name("a", DataType::Integer(None)), - CreateFunctionArg { + OperateFunctionArg::with_name("a", DataType::Integer(None)), + OperateFunctionArg { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), @@ -2400,7 +2400,7 @@ fn parse_create_function() { or_replace: true, temporary: false, name: ObjectName(vec![Ident::new("increment")]), - args: Some(vec![CreateFunctionArg::with_name( + args: Some(vec![OperateFunctionArg::with_name( "i", DataType::Integer(None) )]), @@ -2417,3 +2417,93 @@ fn parse_create_function() { } ); } + +#[test] +fn parse_drop_function() { + let sql = "DROP FUNCTION IF EXISTS test_func"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropFunction { + if_exists: true, + func_desc: vec![DropFunctionDesc { + name: ObjectName(vec![Ident { + value: "test_func".to_string(), + quote_style: None + }]), + args: None + }], + option: None + } + ); + + let sql = "DROP FUNCTION IF EXISTS test_func(a INTEGER, IN b INTEGER = 1)"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropFunction { + if_exists: true, + func_desc: vec![DropFunctionDesc { + name: ObjectName(vec![Ident { + value: "test_func".to_string(), + quote_style: None + }]), + args: Some(vec![ + OperateFunctionArg::with_name("a", DataType::Integer(None)), + OperateFunctionArg { + mode: Some(ArgMode::In), + name: Some("b".into()), + data_type: DataType::Integer(None), + default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), + } + ]), + }], + option: None + } + ); + + let sql = "DROP FUNCTION IF EXISTS test_func1(a INTEGER, IN b INTEGER = 1), test_func2(a VARCHAR, IN b INTEGER = 1)"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropFunction { + if_exists: true, + func_desc: vec![ + DropFunctionDesc { + name: ObjectName(vec![Ident { + value: "test_func1".to_string(), + quote_style: None + }]), + args: Some(vec![ + OperateFunctionArg::with_name("a", DataType::Integer(None)), + OperateFunctionArg { + mode: Some(ArgMode::In), + name: Some("b".into()), + data_type: DataType::Integer(None), + default_expr: Some(Expr::Value(Value::Number( + "1".parse().unwrap(), + false + ))), + } + ]), + }, + DropFunctionDesc { + name: ObjectName(vec![Ident { + value: "test_func2".to_string(), + quote_style: None + }]), + args: Some(vec![ + OperateFunctionArg::with_name("a", DataType::Varchar(None)), + OperateFunctionArg { + mode: Some(ArgMode::In), + name: Some("b".into()), + data_type: DataType::Integer(None), + default_expr: Some(Expr::Value(Value::Number( + "1".parse().unwrap(), + false + ))), + } + ]), + } + ], + option: None + } + ); +} From 2c20ec0be57288b063f684db95b5df3d9131e89e Mon Sep 17 00:00:00 2001 From: Jeffrey <22608443+Jefffrey@users.noreply.github.com> Date: Thu, 29 Dec 2022 00:28:53 +1100 Subject: [PATCH 127/806] Support parsing scientific notation (such as `10e5`) (#768) --- src/test_utils.rs | 1 + src/tokenizer.rs | 66 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 51 ++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) diff --git a/src/test_utils.rs b/src/test_utils.rs index cbb929285..bbffb7711 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -144,6 +144,7 @@ pub fn all_dialects() -> TestedDialects { Box::new(RedshiftSqlDialect {}), Box::new(MySqlDialect {}), Box::new(BigQueryDialect {}), + Box::new(SQLiteDialect {}), ], } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index eb2cc2658..ddb8c4b21 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -541,6 +541,7 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume the first char let s = self.tokenize_word(ch, chars); + // TODO: implement parsing of exponent here if s.chars().all(|x| ('0'..='9').contains(&x) || x == '.') { let mut inner_state = State { peekable: s.chars().peekable(), @@ -617,6 +618,36 @@ impl<'a> Tokenizer<'a> { return Ok(Some(Token::Period)); } + // Parse exponent as number + if chars.peek() == Some(&'e') || chars.peek() == Some(&'E') { + let mut char_clone = chars.peekable.clone(); + let mut exponent_part = String::new(); + exponent_part.push(char_clone.next().unwrap()); + + // Optional sign + match char_clone.peek() { + Some(&c) if matches!(c, '+' | '-') => { + exponent_part.push(c); + char_clone.next(); + } + _ => (), + } + + match char_clone.peek() { + // Definitely an exponent, get original iterator up to speed and use it + Some(&c) if matches!(c, '0'..='9') => { + for _ in 0..exponent_part.len() { + chars.next(); + } + exponent_part += + &peeking_take_while(chars, |ch| matches!(ch, '0'..='9')); + s += exponent_part.as_str(); + } + // Not an exponent, discard the work done + _ => (), + } + } + let long = if chars.peek() == Some(&'L') { chars.next(); true @@ -1091,6 +1122,41 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_select_exponent() { + let sql = String::from("SELECT 1e10, 1e-10, 1e+10, 1ea, 1e-10a, 1e-10-10"); + let dialect = GenericDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, &sql); + let tokens = tokenizer.tokenize().unwrap(); + + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number(String::from("1e10"), false), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number(String::from("1e-10"), false), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number(String::from("1e+10"), false), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number(String::from("1"), false), + Token::make_word("ea", None), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number(String::from("1e-10"), false), + Token::make_word("a", None), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number(String::from("1e-10"), false), + Token::Minus, + Token::Number(String::from("10"), false), + ]; + + compare(expected, tokens); + } + #[test] fn tokenize_scalar_function() { let sql = String::from("SELECT sqrt(1)"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e5ed0bb80..cb9172bc6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -775,6 +775,57 @@ fn parse_null_in_select() { ); } +#[test] +fn parse_exponent_in_select() -> Result<(), ParserError> { + // all except Hive, as it allows numbers to start an identifier + let dialects = TestedDialects { + dialects: vec![ + Box::new(AnsiDialect {}), + Box::new(BigQueryDialect {}), + Box::new(ClickHouseDialect {}), + Box::new(GenericDialect {}), + // Box::new(HiveDialect {}), + Box::new(MsSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(SQLiteDialect {}), + ], + }; + let sql = "SELECT 10e-20, 1e3, 1e+3, 1e3a, 1e, 0.5e2"; + let mut select = dialects.parse_sql_statements(sql)?; + + let select = match select.pop().unwrap() { + Statement::Query(inner) => *inner, + _ => panic!("Expected Query"), + }; + let select = match *select.body { + SetExpr::Select(inner) => *inner, + _ => panic!("Expected SetExpr::Select"), + }; + + assert_eq!( + &vec![ + SelectItem::UnnamedExpr(Expr::Value(number("10e-20"))), + SelectItem::UnnamedExpr(Expr::Value(number("1e3"))), + SelectItem::UnnamedExpr(Expr::Value(number("1e+3"))), + SelectItem::ExprWithAlias { + expr: Expr::Value(number("1e3")), + alias: Ident::new("a") + }, + SelectItem::ExprWithAlias { + expr: Expr::Value(number("1")), + alias: Ident::new("e") + }, + SelectItem::UnnamedExpr(Expr::Value(number("0.5e2"))), + ], + &select.projection + ); + + Ok(()) +} + #[test] fn parse_select_with_date_column_name() { let sql = "SELECT date"; From 79d0baad738fbd81b6faa274ca7e01a11f8a3580 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 28 Dec 2022 08:29:51 -0500 Subject: [PATCH 128/806] Add configurable recursion limit to parser, to protect against stack overflows (#764) --- src/lib.rs | 8 +- src/parser.rs | 229 ++++++++++++++++++++++++++++++++++---- src/test_utils.rs | 6 +- tests/sqlparser_common.rs | 88 +++++++++++++++ 4 files changed, 301 insertions(+), 30 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 880c6d6e1..4a8c3c51d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,11 +12,15 @@ //! SQL Parser for Rust //! -//! Example code: -//! //! This crate provides an ANSI:SQL 2011 lexer and parser that can parse SQL //! into an Abstract Syntax Tree (AST). //! +//! See [`Parser::parse_sql`](crate::parser::Parser::parse_sql) and +//! [`Parser::new`](crate::parser::Parser::new) for the Parsing API +//! and the [`ast`](crate::ast) crate for the AST structure. +//! +//! Example: +//! //! ``` //! use sqlparser::dialect::GenericDialect; //! use sqlparser::parser::Parser; diff --git a/src/parser.rs b/src/parser.rs index 21ab6d2c6..b4042666d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -37,6 +37,7 @@ use crate::tokenizer::*; pub enum ParserError { TokenizerError(String), ParserError(String), + RecursionLimitExceeded, } // Use `Parser::expected` instead, if possible @@ -55,6 +56,92 @@ macro_rules! return_ok_if_some { }}; } +#[cfg(feature = "std")] +/// Implemenation [`RecursionCounter`] if std is available +mod recursion { + use core::sync::atomic::{AtomicUsize, Ordering}; + use std::rc::Rc; + + use super::ParserError; + + /// Tracks remaining recursion depth. This value is decremented on + /// each call to `try_decrease()`, when it reaches 0 an error will + /// be returned. + /// + /// Note: Uses an Rc and AtomicUsize in order to satisfy the Rust + /// borrow checker so the automatic DepthGuard decrement a + /// reference to the counter. The actual value is not modified + /// concurrently + pub(crate) struct RecursionCounter { + remaining_depth: Rc, + } + + impl RecursionCounter { + /// Creates a [`RecursionCounter`] with the specified maximum + /// depth + pub fn new(remaining_depth: usize) -> Self { + Self { + remaining_depth: Rc::new(remaining_depth.into()), + } + } + + /// Decreases the remaining depth by 1. + /// + /// Returns `Err` if the remaining depth falls to 0. + /// + /// Returns a [`DepthGuard`] which will adds 1 to the + /// remaining depth upon drop; + pub fn try_decrease(&self) -> Result { + let old_value = self.remaining_depth.fetch_sub(1, Ordering::SeqCst); + // ran out of space + if old_value == 0 { + Err(ParserError::RecursionLimitExceeded) + } else { + Ok(DepthGuard::new(Rc::clone(&self.remaining_depth))) + } + } + } + + /// Guard that increass the remaining depth by 1 on drop + pub struct DepthGuard { + remaining_depth: Rc, + } + + impl DepthGuard { + fn new(remaining_depth: Rc) -> Self { + Self { remaining_depth } + } + } + impl Drop for DepthGuard { + fn drop(&mut self) { + self.remaining_depth.fetch_add(1, Ordering::SeqCst); + } + } +} + +#[cfg(not(feature = "std"))] +mod recursion { + /// Implemenation [`RecursionCounter`] if std is NOT available (and does not + /// guard against stack overflow). + /// + /// Has the same API as the std RecursionCounter implementation + /// but does not actually limit stack depth. + pub(crate) struct RecursionCounter {} + + impl RecursionCounter { + pub fn new(_remaining_depth: usize) -> Self { + Self {} + } + pub fn try_decrease(&self) -> Result { + Ok(DepthGuard {}) + } + } + + pub struct DepthGuard {} +} + +use recursion::RecursionCounter; + #[derive(PartialEq, Eq)] pub enum IsOptional { Optional, @@ -96,6 +183,7 @@ impl fmt::Display for ParserError { match self { ParserError::TokenizerError(s) => s, ParserError::ParserError(s) => s, + ParserError::RecursionLimitExceeded => "recursion limit exceeded", } ) } @@ -104,22 +192,78 @@ impl fmt::Display for ParserError { #[cfg(feature = "std")] impl std::error::Error for ParserError {} +// By default, allow expressions up to this deep before erroring +const DEFAULT_REMAINING_DEPTH: usize = 50; + pub struct Parser<'a> { tokens: Vec, /// The index of the first unprocessed token in `self.tokens` index: usize, + /// The current dialect to use dialect: &'a dyn Dialect, + /// ensure the stack does not overflow by limiting recusion depth + recursion_counter: RecursionCounter, } impl<'a> Parser<'a> { - /// Parse the specified tokens - /// To avoid breaking backwards compatibility, this function accepts - /// bare tokens. - pub fn new(tokens: Vec, dialect: &'a dyn Dialect) -> Self { - Parser::new_without_locations(tokens, dialect) + /// Create a parser for a [`Dialect`] + /// + /// See also [`Parser::parse_sql`] + /// + /// Example: + /// ``` + /// # use sqlparser::{parser::{Parser, ParserError}, dialect::GenericDialect}; + /// # fn main() -> Result<(), ParserError> { + /// let dialect = GenericDialect{}; + /// let statements = Parser::new(&dialect) + /// .try_with_sql("SELECT * FROM foo")? + /// .parse_statements()?; + /// # Ok(()) + /// # } + /// ``` + pub fn new(dialect: &'a dyn Dialect) -> Self { + Self { + tokens: vec![], + index: 0, + dialect, + recursion_counter: RecursionCounter::new(DEFAULT_REMAINING_DEPTH), + } + } + + /// Specify the maximum recursion limit while parsing. + /// + /// + /// [`Parser`] prevents stack overflows by returning + /// [`ParserError::RecursionLimitExceeded`] if the parser exceeds + /// this depth while processing the query. + /// + /// Example: + /// ``` + /// # use sqlparser::{parser::{Parser, ParserError}, dialect::GenericDialect}; + /// # fn main() -> Result<(), ParserError> { + /// let dialect = GenericDialect{}; + /// let result = Parser::new(&dialect) + /// .with_recursion_limit(1) + /// .try_with_sql("SELECT * FROM foo WHERE (a OR (b OR (c OR d)))")? + /// .parse_statements(); + /// assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); + /// # Ok(()) + /// # } + /// ``` + pub fn with_recursion_limit(mut self, recursion_limit: usize) -> Self { + self.recursion_counter = RecursionCounter::new(recursion_limit); + self + } + + /// Reset this parser to parse the specified token stream + pub fn with_tokens_with_locations(mut self, tokens: Vec) -> Self { + self.tokens = tokens; + self.index = 0; + self } - pub fn new_without_locations(tokens: Vec, dialect: &'a dyn Dialect) -> Self { + /// Reset this parser state to parse the specified tokens + pub fn with_tokens(self, tokens: Vec) -> Self { // Put in dummy locations let tokens_with_locations: Vec = tokens .into_iter() @@ -128,49 +272,84 @@ impl<'a> Parser<'a> { location: Location { line: 0, column: 0 }, }) .collect(); - Parser::new_with_locations(tokens_with_locations, dialect) + self.with_tokens_with_locations(tokens_with_locations) } - /// Parse the specified tokens - pub fn new_with_locations(tokens: Vec, dialect: &'a dyn Dialect) -> Self { - Parser { - tokens, - index: 0, - dialect, - } + /// Tokenize the sql string and sets this [`Parser`]'s state to + /// parse the resulting tokens + /// + /// Returns an error if there was an error tokenizing the SQL string. + /// + /// See example on [`Parser::new()`] for an example + pub fn try_with_sql(self, sql: &str) -> Result { + debug!("Parsing sql '{}'...", sql); + let mut tokenizer = Tokenizer::new(self.dialect, sql); + let tokens = tokenizer.tokenize()?; + Ok(self.with_tokens(tokens)) } - /// Parse a SQL statement and produce an Abstract Syntax Tree (AST) - pub fn parse_sql(dialect: &dyn Dialect, sql: &str) -> Result, ParserError> { - let mut tokenizer = Tokenizer::new(dialect, sql); - let tokens = tokenizer.tokenize()?; - let mut parser = Parser::new(tokens, dialect); + /// Parse potentially multiple statements + /// + /// Example + /// ``` + /// # use sqlparser::{parser::{Parser, ParserError}, dialect::GenericDialect}; + /// # fn main() -> Result<(), ParserError> { + /// let dialect = GenericDialect{}; + /// let statements = Parser::new(&dialect) + /// // Parse a SQL string with 2 separate statements + /// .try_with_sql("SELECT * FROM foo; SELECT * FROM bar;")? + /// .parse_statements()?; + /// assert_eq!(statements.len(), 2); + /// # Ok(()) + /// # } + /// ``` + pub fn parse_statements(&mut self) -> Result, ParserError> { let mut stmts = Vec::new(); let mut expecting_statement_delimiter = false; - debug!("Parsing sql '{}'...", sql); loop { // ignore empty statements (between successive statement delimiters) - while parser.consume_token(&Token::SemiColon) { + while self.consume_token(&Token::SemiColon) { expecting_statement_delimiter = false; } - if parser.peek_token() == Token::EOF { + if self.peek_token() == Token::EOF { break; } if expecting_statement_delimiter { - return parser.expected("end of statement", parser.peek_token()); + return self.expected("end of statement", self.peek_token()); } - let statement = parser.parse_statement()?; + let statement = self.parse_statement()?; stmts.push(statement); expecting_statement_delimiter = true; } Ok(stmts) } + /// Convience method to parse a string with one or more SQL + /// statements into produce an Abstract Syntax Tree (AST). + /// + /// Example + /// ``` + /// # use sqlparser::{parser::{Parser, ParserError}, dialect::GenericDialect}; + /// # fn main() -> Result<(), ParserError> { + /// let dialect = GenericDialect{}; + /// let statements = Parser::parse_sql( + /// &dialect, "SELECT * FROM foo" + /// )?; + /// assert_eq!(statements.len(), 1); + /// # Ok(()) + /// # } + /// ``` + pub fn parse_sql(dialect: &dyn Dialect, sql: &str) -> Result, ParserError> { + Parser::new(dialect).try_with_sql(sql)?.parse_statements() + } + /// Parse a single top-level statement (such as SELECT, INSERT, CREATE, etc.), /// stopping before the statement separator, if any. pub fn parse_statement(&mut self) -> Result { + let _guard = self.recursion_counter.try_decrease()?; + // allow the dialect to override statement parsing if let Some(statement) = self.dialect.parse_statement(self) { return statement; @@ -364,6 +543,7 @@ impl<'a> Parser<'a> { /// Parse a new expression pub fn parse_expr(&mut self) -> Result { + let _guard = self.recursion_counter.try_decrease()?; self.parse_subexpr(0) } @@ -4512,6 +4692,7 @@ impl<'a> Parser<'a> { /// by `ORDER BY`. Unlike some other parse_... methods, this one doesn't /// expect the initial keyword to be already consumed pub fn parse_query(&mut self) -> Result { + let _guard = self.recursion_counter.try_decrease()?; let with = if self.parse_keyword(Keyword::WITH) { Some(With { recursive: self.parse_keyword(Keyword::RECURSIVE), diff --git a/src/test_utils.rs b/src/test_utils.rs index bbffb7711..79c24e292 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -29,7 +29,6 @@ use core::fmt::Debug; use crate::ast::*; use crate::dialect::*; use crate::parser::{Parser, ParserError}; -use crate::tokenizer::Tokenizer; /// Tests use the methods on this struct to invoke the parser on one or /// multiple dialects. @@ -65,9 +64,8 @@ impl TestedDialects { F: Fn(&mut Parser) -> T, { self.one_of_identical_results(|dialect| { - let mut tokenizer = Tokenizer::new(dialect, sql); - let tokens = tokenizer.tokenize().unwrap(); - f(&mut Parser::new(tokens, dialect)) + let mut parser = Parser::new(dialect).try_with_sql(sql).unwrap(); + f(&mut parser) }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index cb9172bc6..f0dd91e7d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6508,3 +6508,91 @@ fn parse_uncache_table() { res.unwrap_err() ); } + +#[test] +fn parse_deeply_nested_parens_hits_recursion_limits() { + let sql = "(".repeat(1000); + let res = parse_sql_statements(&sql); + assert_eq!(ParserError::RecursionLimitExceeded, res.unwrap_err()); +} + +#[test] +fn parse_deeply_nested_expr_hits_recursion_limits() { + let dialect = GenericDialect {}; + + let where_clause = make_where_clause(100); + let sql = format!("SELECT id, user_id FROM test WHERE {where_clause}"); + + let res = Parser::new(&dialect) + .try_with_sql(&sql) + .expect("tokenize to work") + .parse_statements(); + + assert_eq!(res, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn parse_deeply_nested_subquery_expr_hits_recursion_limits() { + let dialect = GenericDialect {}; + + let where_clause = make_where_clause(100); + let sql = format!("SELECT id, user_id where id IN (select id from t WHERE {where_clause})"); + + let res = Parser::new(&dialect) + .try_with_sql(&sql) + .expect("tokenize to work") + .parse_statements(); + + assert_eq!(res, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn parse_with_recursion_limit() { + let dialect = GenericDialect {}; + + let where_clause = make_where_clause(20); + let sql = format!("SELECT id, user_id FROM test WHERE {where_clause}"); + + // Expect the statement to parse with default limit + let res = Parser::new(&dialect) + .try_with_sql(&sql) + .expect("tokenize to work") + .parse_statements(); + + assert!(matches!(res, Ok(_)), "{:?}", res); + + // limit recursion to something smaller, expect parsing to fail + let res = Parser::new(&dialect) + .try_with_sql(&sql) + .expect("tokenize to work") + .with_recursion_limit(20) + .parse_statements(); + + assert_eq!(res, Err(ParserError::RecursionLimitExceeded)); + + // limit recursion to 50, expect it to succeed + let res = Parser::new(&dialect) + .try_with_sql(&sql) + .expect("tokenize to work") + .with_recursion_limit(50) + .parse_statements(); + + assert!(matches!(res, Ok(_)), "{:?}", res); +} + +/// Makes a predicate that looks like ((user_id = $id) OR user_id = $2...) +fn make_where_clause(num: usize) -> String { + use std::fmt::Write; + let mut output = "(".repeat(num - 1); + + for i in 0..num { + if i > 0 { + write!(&mut output, " OR ").unwrap(); + } + write!(&mut output, "user_id = {}", i).unwrap(); + if i < num - 1 { + write!(&mut output, ")").unwrap(); + } + } + output +} From f0870fd315c945876336f27a91f6762904a93fd1 Mon Sep 17 00:00:00 2001 From: Jeffrey <22608443+Jefffrey@users.noreply.github.com> Date: Thu, 29 Dec 2022 00:31:39 +1100 Subject: [PATCH 129/806] Enable grouping sets parsing for GenericDialect (#771) --- src/parser.rs | 2 +- tests/sqlparser_common.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index b4042666d..a400d5c2a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -943,7 +943,7 @@ impl<'a> Parser<'a> { /// parse a group by expr. a group by expr can be one of group sets, roll up, cube, or simple /// expr. fn parse_group_by_expr(&mut self) -> Result { - if dialect_of!(self is PostgreSqlDialect) { + if dialect_of!(self is PostgreSqlDialect | GenericDialect) { if self.parse_keywords(&[Keyword::GROUPING, Keyword::SETS]) { self.expect_token(&Token::LParen)?; let result = self.parse_comma_separated(|p| p.parse_tuple(false, true))?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f0dd91e7d..a815a9fd7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1513,7 +1513,7 @@ fn parse_select_group_by() { #[test] fn parse_select_group_by_grouping_sets() { let dialects = TestedDialects { - dialects: vec![Box::new(PostgreSqlDialect {})], + dialects: vec![Box::new(GenericDialect {}), Box::new(PostgreSqlDialect {})], }; let sql = "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, GROUPING SETS ((brand), (size), ())"; @@ -1534,7 +1534,7 @@ fn parse_select_group_by_grouping_sets() { #[test] fn parse_select_group_by_rollup() { let dialects = TestedDialects { - dialects: vec![Box::new(PostgreSqlDialect {})], + dialects: vec![Box::new(GenericDialect {}), Box::new(PostgreSqlDialect {})], }; let sql = "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, ROLLUP (brand, size)"; let select = dialects.verified_only_select(sql); @@ -1553,7 +1553,7 @@ fn parse_select_group_by_rollup() { #[test] fn parse_select_group_by_cube() { let dialects = TestedDialects { - dialects: vec![Box::new(PostgreSqlDialect {})], + dialects: vec![Box::new(GenericDialect {}), Box::new(PostgreSqlDialect {})], }; let sql = "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, CUBE (brand, size)"; let select = dialects.verified_only_select(sql); From 61c661c234518ae02ae2c50d317e4640e4b90e26 Mon Sep 17 00:00:00 2001 From: henry <51254761+devgony@users.noreply.github.com> Date: Wed, 28 Dec 2022 22:35:27 +0900 Subject: [PATCH 130/806] Support `ALTER INDEX {INDEX_NAME} RENAME TO {NEW_INDEX_NAME}` (#767) * test: add a case `rename_index` * feat: add AlterIndex match arm * fix: remove todo with self.expected * chore: add comment to unreachable --- src/ast/ddl.rs | 16 ++ src/ast/mod.rs | 11 +- src/parser.rs | 341 ++++++++++++++++++++------------------ tests/sqlparser_common.rs | 15 ++ 4 files changed, 224 insertions(+), 159 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 00bf83aba..d594d856d 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -94,6 +94,12 @@ pub enum AlterTableOperation { }, } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum AlterIndexOperation { + RenameIndex { index_name: ObjectName }, +} + impl fmt::Display for AlterTableOperation { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -200,6 +206,16 @@ impl fmt::Display for AlterTableOperation { } } +impl fmt::Display for AlterIndexOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterIndexOperation::RenameIndex { index_name } => { + write!(f, "RENAME TO {}", index_name) + } + } + } +} + /// An `ALTER COLUMN` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6003cb1d4..b497014dd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -26,8 +26,8 @@ pub use self::data_type::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, }; pub use self::ddl::{ - AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, IndexType, - KeyOrIndexDisplay, ReferentialAction, TableConstraint, + AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, + ColumnOptionDef, IndexType, KeyOrIndexDisplay, ReferentialAction, TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ @@ -1250,6 +1250,10 @@ pub enum Statement { name: ObjectName, operation: AlterTableOperation, }, + AlterIndex { + name: ObjectName, + operation: AlterIndexOperation, + }, /// DROP Drop { /// The type of the object to drop: TABLE, VIEW, etc. @@ -2275,6 +2279,9 @@ impl fmt::Display for Statement { Statement::AlterTable { name, operation } => { write!(f, "ALTER TABLE {} {}", name, operation) } + Statement::AlterIndex { name, operation } => { + write!(f, "ALTER INDEX {} {}", name, operation) + } Statement::Drop { object_type, if_exists, diff --git a/src/parser.rs b/src/parser.rs index a400d5c2a..01eb18bd6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3661,173 +3661,200 @@ impl<'a> Parser<'a> { } pub fn parse_alter(&mut self) -> Result { - self.expect_keyword(Keyword::TABLE)?; - let _ = self.parse_keyword(Keyword::ONLY); - let table_name = self.parse_object_name()?; - let operation = if self.parse_keyword(Keyword::ADD) { - if let Some(constraint) = self.parse_optional_table_constraint()? { - AlterTableOperation::AddConstraint(constraint) - } else { - let if_not_exists = - self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - if self.parse_keyword(Keyword::PARTITION) { + let object_type = self.expect_one_of_keywords(&[Keyword::TABLE, Keyword::INDEX])?; + match object_type { + Keyword::TABLE => { + let _ = self.parse_keyword(Keyword::ONLY); + let table_name = self.parse_object_name()?; + let operation = if self.parse_keyword(Keyword::ADD) { + if let Some(constraint) = self.parse_optional_table_constraint()? { + AlterTableOperation::AddConstraint(constraint) + } else { + let if_not_exists = + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + if self.parse_keyword(Keyword::PARTITION) { + self.expect_token(&Token::LParen)?; + let partitions = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::AddPartitions { + if_not_exists, + new_partitions: partitions, + } + } else { + let column_keyword = self.parse_keyword(Keyword::COLUMN); + + let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | GenericDialect) + { + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) + || if_not_exists + } else { + false + }; + + let column_def = self.parse_column_def()?; + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + } + } + } + } else if self.parse_keyword(Keyword::RENAME) { + if dialect_of!(self is PostgreSqlDialect) + && self.parse_keyword(Keyword::CONSTRAINT) + { + let old_name = self.parse_identifier()?; + self.expect_keyword(Keyword::TO)?; + let new_name = self.parse_identifier()?; + AlterTableOperation::RenameConstraint { old_name, new_name } + } else if self.parse_keyword(Keyword::TO) { + let table_name = self.parse_object_name()?; + AlterTableOperation::RenameTable { table_name } + } else { + let _ = self.parse_keyword(Keyword::COLUMN); + let old_column_name = self.parse_identifier()?; + self.expect_keyword(Keyword::TO)?; + let new_column_name = self.parse_identifier()?; + AlterTableOperation::RenameColumn { + old_column_name, + new_column_name, + } + } + } else if self.parse_keyword(Keyword::DROP) { + if self.parse_keywords(&[Keyword::IF, Keyword::EXISTS, Keyword::PARTITION]) { + self.expect_token(&Token::LParen)?; + let partitions = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::DropPartitions { + partitions, + if_exists: true, + } + } else if self.parse_keyword(Keyword::PARTITION) { + self.expect_token(&Token::LParen)?; + let partitions = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::DropPartitions { + partitions, + if_exists: false, + } + } else if self.parse_keyword(Keyword::CONSTRAINT) { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier()?; + let cascade = self.parse_keyword(Keyword::CASCADE); + AlterTableOperation::DropConstraint { + if_exists, + name, + cascade, + } + } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) + && dialect_of!(self is MySqlDialect | GenericDialect) + { + AlterTableOperation::DropPrimaryKey + } else { + let _ = self.parse_keyword(Keyword::COLUMN); + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let column_name = self.parse_identifier()?; + let cascade = self.parse_keyword(Keyword::CASCADE); + AlterTableOperation::DropColumn { + column_name, + if_exists, + cascade, + } + } + } else if self.parse_keyword(Keyword::PARTITION) { + self.expect_token(&Token::LParen)?; + let before = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + self.expect_keyword(Keyword::RENAME)?; + self.expect_keywords(&[Keyword::TO, Keyword::PARTITION])?; self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(Parser::parse_expr)?; + let renames = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; - AlterTableOperation::AddPartitions { - if_not_exists, - new_partitions: partitions, + AlterTableOperation::RenamePartitions { + old_partitions: before, + new_partitions: renames, + } + } else if self.parse_keyword(Keyword::CHANGE) { + let _ = self.parse_keyword(Keyword::COLUMN); + let old_name = self.parse_identifier()?; + let new_name = self.parse_identifier()?; + let data_type = self.parse_data_type()?; + let mut options = vec![]; + while let Some(option) = self.parse_optional_column_option()? { + options.push(option); } - } else { - let column_keyword = self.parse_keyword(Keyword::COLUMN); - let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | GenericDialect) + AlterTableOperation::ChangeColumn { + old_name, + new_name, + data_type, + options, + } + } else if self.parse_keyword(Keyword::ALTER) { + let _ = self.parse_keyword(Keyword::COLUMN); + let column_name = self.parse_identifier()?; + let is_postgresql = dialect_of!(self is PostgreSqlDialect); + + let op = if self.parse_keywords(&[Keyword::SET, Keyword::NOT, Keyword::NULL]) { + AlterColumnOperation::SetNotNull {} + } else if self.parse_keywords(&[Keyword::DROP, Keyword::NOT, Keyword::NULL]) { + AlterColumnOperation::DropNotNull {} + } else if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT]) { + AlterColumnOperation::SetDefault { + value: self.parse_expr()?, + } + } 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)) { - self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) - || if_not_exists + 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 { - false + return self.expected( + "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE after ALTER COLUMN", + self.peek_token(), + ); }; - - let column_def = self.parse_column_def()?; - AlterTableOperation::AddColumn { - column_keyword, - if_not_exists, - column_def, - } - } - } - } else if self.parse_keyword(Keyword::RENAME) { - if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::CONSTRAINT) { - let old_name = self.parse_identifier()?; - self.expect_keyword(Keyword::TO)?; - let new_name = self.parse_identifier()?; - AlterTableOperation::RenameConstraint { old_name, new_name } - } else if self.parse_keyword(Keyword::TO) { - let table_name = self.parse_object_name()?; - AlterTableOperation::RenameTable { table_name } - } else { - let _ = self.parse_keyword(Keyword::COLUMN); - let old_column_name = self.parse_identifier()?; - self.expect_keyword(Keyword::TO)?; - let new_column_name = self.parse_identifier()?; - AlterTableOperation::RenameColumn { - old_column_name, - new_column_name, - } - } - } else if self.parse_keyword(Keyword::DROP) { - if self.parse_keywords(&[Keyword::IF, Keyword::EXISTS, Keyword::PARTITION]) { - self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - AlterTableOperation::DropPartitions { - partitions, - if_exists: true, - } - } else if self.parse_keyword(Keyword::PARTITION) { - self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - AlterTableOperation::DropPartitions { - partitions, - if_exists: false, - } - } else if self.parse_keyword(Keyword::CONSTRAINT) { - let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let name = self.parse_identifier()?; - let cascade = self.parse_keyword(Keyword::CASCADE); - AlterTableOperation::DropConstraint { - if_exists, - name, - cascade, - } - } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) - && dialect_of!(self is MySqlDialect | GenericDialect) - { - AlterTableOperation::DropPrimaryKey - } else { - let _ = self.parse_keyword(Keyword::COLUMN); - let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let column_name = self.parse_identifier()?; - let cascade = self.parse_keyword(Keyword::CASCADE); - AlterTableOperation::DropColumn { - column_name, - if_exists, - cascade, - } + AlterTableOperation::AlterColumn { column_name, op } + } else { + return self.expected( + "ADD, RENAME, PARTITION or DROP after ALTER TABLE", + self.peek_token(), + ); + }; + Ok(Statement::AlterTable { + name: table_name, + operation, + }) } - } else if self.parse_keyword(Keyword::PARTITION) { - self.expect_token(&Token::LParen)?; - let before = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - self.expect_keyword(Keyword::RENAME)?; - self.expect_keywords(&[Keyword::TO, Keyword::PARTITION])?; - self.expect_token(&Token::LParen)?; - let renames = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - AlterTableOperation::RenamePartitions { - old_partitions: before, - new_partitions: renames, - } - } else if self.parse_keyword(Keyword::CHANGE) { - let _ = self.parse_keyword(Keyword::COLUMN); - let old_name = self.parse_identifier()?; - let new_name = self.parse_identifier()?; - let data_type = self.parse_data_type()?; - let mut options = vec![]; - while let Some(option) = self.parse_optional_column_option()? { - options.push(option); - } - - AlterTableOperation::ChangeColumn { - old_name, - new_name, - data_type, - options, - } - } else if self.parse_keyword(Keyword::ALTER) { - let _ = self.parse_keyword(Keyword::COLUMN); - let column_name = self.parse_identifier()?; - let is_postgresql = dialect_of!(self is PostgreSqlDialect); - - let op = if self.parse_keywords(&[Keyword::SET, Keyword::NOT, Keyword::NULL]) { - AlterColumnOperation::SetNotNull {} - } else if self.parse_keywords(&[Keyword::DROP, Keyword::NOT, Keyword::NULL]) { - AlterColumnOperation::DropNotNull {} - } else if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT]) { - AlterColumnOperation::SetDefault { - value: self.parse_expr()?, - } - } 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()?) + Keyword::INDEX => { + let _ = self.parse_keyword(Keyword::ONLY); + let index_name = self.parse_object_name()?; + let operation = if self.parse_keyword(Keyword::RENAME) { + if self.parse_keyword(Keyword::TO) { + let index_name = self.parse_object_name()?; + AlterIndexOperation::RenameIndex { index_name } + } else { + return self.expected("TO after RENAME", self.peek_token()); + } } else { - None + return self.expected("RENAME after ALTER INDEX", self.peek_token()); }; - AlterColumnOperation::SetDataType { data_type, using } - } else { - return self.expected( - "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE after ALTER COLUMN", - self.peek_token(), - ); - }; - AlterTableOperation::AlterColumn { column_name, op } - } else { - return self.expected( - "ADD, RENAME, PARTITION or DROP after ALTER TABLE", - self.peek_token(), - ); - }; - Ok(Statement::AlterTable { - name: table_name, - operation, - }) + + Ok(Statement::AlterIndex { + name: index_name, + operation, + }) + } + // unreachable because expect_one_of_keywords used above + _ => unreachable!(), + } } /// Parse a copy statement diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a815a9fd7..c880eee7c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2686,6 +2686,21 @@ fn parse_alter_table() { } } +#[test] +fn parse_alter_index() { + let rename_index = "ALTER INDEX idx RENAME TO new_idx"; + match verified_stmt(rename_index) { + Statement::AlterIndex { + name, + operation: AlterIndexOperation::RenameIndex { index_name }, + } => { + assert_eq!("idx", name.to_string()); + assert_eq!("new_idx", index_name.to_string()) + } + _ => unreachable!(), + }; +} + #[test] fn parse_alter_table_add_column() { match verified_stmt("ALTER TABLE tab ADD foo TEXT") { From b1a000f1493003867c840c493a94a25dd95e29c6 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 28 Dec 2022 08:52:12 -0500 Subject: [PATCH 131/806] Standardize comments on parsing optional keywords (#773) --- src/parser.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 01eb18bd6..4d0fe2678 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1230,7 +1230,7 @@ impl<'a> Parser<'a> { /// if `named` is `true`, came from an expression like `ARRAY[ex1, ex2]` pub fn parse_array_expr(&mut self, named: bool) -> Result { if self.peek_token().token == Token::RBracket { - let _ = self.next_token(); + let _ = self.next_token(); // consume ] Ok(Expr::Array(Array { elem: vec![], named, @@ -2723,8 +2723,7 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let names = self.parse_comma_separated(Parser::parse_object_name)?; - // Parse optional WITH - let _ = self.parse_keyword(Keyword::WITH); + let _ = self.parse_keyword(Keyword::WITH); // [ WITH ] let optional_keywords = if dialect_of!(self is MsSqlDialect) { vec![Keyword::AUTHORIZATION] @@ -3664,7 +3663,7 @@ impl<'a> Parser<'a> { let object_type = self.expect_one_of_keywords(&[Keyword::TABLE, Keyword::INDEX])?; match object_type { Keyword::TABLE => { - let _ = self.parse_keyword(Keyword::ONLY); + let _ = self.parse_keyword(Keyword::ONLY); // [ ONLY ] let table_name = self.parse_object_name()?; let operation = if self.parse_keyword(Keyword::ADD) { if let Some(constraint) = self.parse_optional_table_constraint()? { @@ -3711,7 +3710,7 @@ impl<'a> Parser<'a> { let table_name = self.parse_object_name()?; AlterTableOperation::RenameTable { table_name } } else { - let _ = self.parse_keyword(Keyword::COLUMN); + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let old_column_name = self.parse_identifier()?; self.expect_keyword(Keyword::TO)?; let new_column_name = self.parse_identifier()?; @@ -3751,7 +3750,7 @@ impl<'a> Parser<'a> { { AlterTableOperation::DropPrimaryKey } else { - let _ = self.parse_keyword(Keyword::COLUMN); + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let column_name = self.parse_identifier()?; let cascade = self.parse_keyword(Keyword::CASCADE); @@ -3775,7 +3774,7 @@ impl<'a> Parser<'a> { new_partitions: renames, } } else if self.parse_keyword(Keyword::CHANGE) { - let _ = self.parse_keyword(Keyword::COLUMN); + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let old_name = self.parse_identifier()?; let new_name = self.parse_identifier()?; let data_type = self.parse_data_type()?; @@ -3791,7 +3790,7 @@ impl<'a> Parser<'a> { options, } } else if self.parse_keyword(Keyword::ALTER) { - let _ = self.parse_keyword(Keyword::COLUMN); + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let column_name = self.parse_identifier()?; let is_postgresql = dialect_of!(self is PostgreSqlDialect); @@ -3834,7 +3833,6 @@ impl<'a> Parser<'a> { }) } Keyword::INDEX => { - let _ = self.parse_keyword(Keyword::ONLY); let index_name = self.parse_object_name()?; let operation = if self.parse_keyword(Keyword::RENAME) { if self.parse_keyword(Keyword::TO) { @@ -3879,7 +3877,7 @@ impl<'a> Parser<'a> { filename: self.parse_literal_string()?, } }; - let _ = self.parse_keyword(Keyword::WITH); + let _ = self.parse_keyword(Keyword::WITH); // [ WITH ] let mut options = vec![]; if self.consume_token(&Token::LParen) { options = self.parse_comma_separated(Parser::parse_copy_option)?; @@ -5376,12 +5374,12 @@ impl<'a> Parser<'a> { let join_operator_type = match peek_keyword { Keyword::INNER | Keyword::JOIN => { - let _ = self.parse_keyword(Keyword::INNER); + let _ = self.parse_keyword(Keyword::INNER); // [ INNER ] self.expect_keyword(Keyword::JOIN)?; JoinOperator::Inner } kw @ Keyword::LEFT | kw @ Keyword::RIGHT => { - let _ = self.next_token(); + let _ = self.next_token(); // consume LEFT/RIGHT let is_left = kw == Keyword::LEFT; let join_type = self.parse_one_of_keywords(&[ Keyword::OUTER, @@ -5430,8 +5428,8 @@ impl<'a> Parser<'a> { } } Keyword::FULL => { - let _ = self.next_token(); - let _ = self.parse_keyword(Keyword::OUTER); + let _ = self.next_token(); // consume FULL + let _ = self.parse_keyword(Keyword::OUTER); // [ OUTER ] self.expect_keyword(Keyword::JOIN)?; JoinOperator::FullOuter } From 3e990466f8f6a2dadc72a0e69db5923355afcf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=84=B1=20Kim=20Jinsung?= <55318896+CEOJINSUNG@users.noreply.github.com> Date: Wed, 28 Dec 2022 22:59:09 +0900 Subject: [PATCH 132/806] Support `CREATE TABLE ON UPDATE ` Function (#685) * feat : OnUpdate Function Implement * feat : add GenericDialect Options --- src/ast/ddl.rs | 2 ++ src/parser.rs | 7 +++---- tests/sqlparser_mysql.rs | 12 ++++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d594d856d..f55760984 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -558,6 +558,7 @@ pub enum ColumnOption { DialectSpecific(Vec), CharacterSet(ObjectName), Comment(String), + OnUpdate(Expr), } impl fmt::Display for ColumnOption { @@ -592,6 +593,7 @@ impl fmt::Display for ColumnOption { DialectSpecific(val) => write!(f, "{}", display_separated(val, " ")), CharacterSet(n) => write!(f, "CHARACTER SET {}", n), Comment(v) => write!(f, "COMMENT '{}'", escape_single_quote_string(v)), + OnUpdate(expr) => write!(f, "ON UPDATE {}", expr), } } } diff --git a/src/parser.rs b/src/parser.rs index 4d0fe2678..ba62ff9b7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3474,11 +3474,10 @@ impl<'a> Parser<'a> { Token::make_keyword("AUTOINCREMENT"), ]))) } else if self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) - && dialect_of!(self is MySqlDialect) + && dialect_of!(self is MySqlDialect | GenericDialect) { - Ok(Some(ColumnOption::DialectSpecific(vec![ - Token::make_keyword("ON UPDATE"), - ]))) + let expr = self.parse_expr()?; + Ok(Some(ColumnOption::OnUpdate(expr))) } else { Ok(None) } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 67850b1b9..63ea02597 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1031,7 +1031,7 @@ fn parse_kill() { #[test] fn parse_table_colum_option_on_update() { - let sql1 = "CREATE TABLE foo (`modification_time` DATETIME ON UPDATE)"; + let sql1 = "CREATE TABLE foo (`modification_time` DATETIME ON UPDATE CURRENT_TIMESTAMP())"; match mysql().verified_stmt(sql1) { Statement::CreateTable { name, columns, .. } => { assert_eq!(name.to_string(), "foo"); @@ -1042,9 +1042,13 @@ fn parse_table_colum_option_on_update() { collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::DialectSpecific(vec![Token::make_keyword( - "ON UPDATE" - )]), + option: ColumnOption::OnUpdate(Expr::Function(Function { + name: ObjectName(vec![Ident::new("CURRENT_TIMESTAMP")]), + args: vec![], + over: None, + distinct: false, + special: false, + })), },], }], columns From dec3c2b818bdedcc81f0e3990e2dd2fefcf9e872 Mon Sep 17 00:00:00 2001 From: Raphael Taylor-Davies <1781103+tustvold@users.noreply.github.com> Date: Wed, 28 Dec 2022 15:07:12 +0000 Subject: [PATCH 133/806] Add derive based AST visitor (#765) * Add derive based AST visitor * Fix BigDecimal * Fix no visitor feature * Add test * Rename visit_table to visit_relation * Review feedback * Add pre and post visit Co-authored-by: Andrew Lamb --- .gitignore | 1 + Cargo.toml | 4 +- derive/Cargo.toml | 23 ++ derive/README.md | 79 +++++++ derive/src/lib.rs | 184 ++++++++++++++++ src/ast/data_type.rs | 8 + src/ast/ddl.rs | 13 ++ src/ast/helpers/stmt_create_table.rs | 4 + src/ast/mod.rs | 106 +++++++++- src/ast/operator.rs | 6 + src/ast/query.rs | 33 +++ src/ast/value.rs | 7 + src/ast/visitor.rs | 301 +++++++++++++++++++++++++++ src/keywords.rs | 4 + src/lib.rs | 3 + src/tokenizer.rs | 6 + 16 files changed, 771 insertions(+), 11 deletions(-) create mode 100644 derive/Cargo.toml create mode 100644 derive/README.md create mode 100644 derive/src/lib.rs create mode 100644 src/ast/visitor.rs diff --git a/.gitignore b/.gitignore index baccda415..d41369207 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # will have compiled files and executables /target/ /sqlparser_bench/target/ +/derive/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock diff --git a/Cargo.toml b/Cargo.toml index 2355f4646..a3376a673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ version = "0.28.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" -keywords = [ "ansi", "sql", "lexer", "parser" ] +keywords = ["ansi", "sql", "lexer", "parser"] repository = "https://github.com/sqlparser-rs/sqlparser-rs" license = "Apache-2.0" include = [ @@ -23,6 +23,7 @@ default = ["std"] std = [] # Enable JSON output in the `cli` example: json_example = ["serde_json", "serde"] +visitor = ["sqlparser_derive"] [dependencies] bigdecimal = { version = "0.3", features = ["serde"], optional = true } @@ -32,6 +33,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } # of dev-dependencies because of # https://github.com/rust-lang/cargo/issues/1596 serde_json = { version = "1.0", optional = true } +sqlparser_derive = { version = "0.1", path = "derive", optional = true } [dev-dependencies] simple_logger = "4.0" diff --git a/derive/Cargo.toml b/derive/Cargo.toml new file mode 100644 index 000000000..221437a9e --- /dev/null +++ b/derive/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sqlparser_derive" +description = "proc macro for sqlparser" +version = "0.1.0" +authors = ["Andy Grove "] +homepage = "https://github.com/sqlparser-rs/sqlparser-rs" +documentation = "https://docs.rs/sqlparser/" +keywords = ["ansi", "sql", "lexer", "parser"] +repository = "https://github.com/sqlparser-rs/sqlparser-rs" +license = "Apache-2.0" +include = [ + "src/**/*.rs", + "Cargo.toml", +] +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +proc-macro2 = "1.0" +quote = "1.0" diff --git a/derive/README.md b/derive/README.md new file mode 100644 index 000000000..e5ab87496 --- /dev/null +++ b/derive/README.md @@ -0,0 +1,79 @@ +# SQL Parser Derive Macro + +## Visit + +This crate contains a procedural macro that can automatically derive implementations of the `Visit` trait + +```rust +#[derive(Visit)] +struct Foo { + boolean: bool, + bar: Bar, +} + +#[derive(Visit)] +enum Bar { + A(), + B(String, bool), + C { named: i32 }, +} +``` + +Will generate code akin to + +```rust +impl Visit for Foo { + fn visit(&self, visitor: &mut V) -> ControlFlow { + self.boolean.visit(visitor)?; + self.bar.visit(visitor)?; + ControlFlow::Continue(()) + } +} + +impl Visit for Bar { + fn visit(&self, visitor: &mut V) -> ControlFlow { + match self { + Self::A() => {} + Self::B(_1, _2) => { + _1.visit(visitor)?; + _2.visit(visitor)?; + } + Self::C { named } => { + named.visit(visitor)?; + } + } + ControlFlow::Continue(()) + } +} +``` + +Additionally certain types may wish to call a corresponding method on visitor before recursing + +```rust +#[derive(Visit)] +#[visit(with = "visit_expr")] +enum Expr { + A(), + B(String, #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] ObjectName, bool), +} +``` + +Will generate + +```rust +impl Visit for Bar { + fn visit(&self, visitor: &mut V) -> ControlFlow { + visitor.visit_expr(self)?; + match self { + Self::A() => {} + Self::B(_1, _2, _3) => { + _1.visit(visitor)?; + visitor.visit_relation(_3)?; + _2.visit(visitor)?; + _3.visit(visitor)?; + } + } + ControlFlow::Continue(()) + } +} +``` diff --git a/derive/src/lib.rs b/derive/src/lib.rs new file mode 100644 index 000000000..b211b767b --- /dev/null +++ b/derive/src/lib.rs @@ -0,0 +1,184 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use syn::spanned::Spanned; +use syn::{ + parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Fields, GenericParam, Generics, + Ident, Index, Lit, Meta, MetaNameValue, NestedMeta, +}; + +/// Implementation of `[#derive(Visit)]` +#[proc_macro_derive(Visit, attributes(visit))] +pub fn derive_visit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + // Parse the input tokens into a syntax tree. + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + let attributes = Attributes::parse(&input.attrs); + // Add a bound `T: HeapSize` to every type parameter T. + let generics = add_trait_bounds(input.generics); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let (pre_visit, post_visit) = attributes.visit(quote!(self)); + let children = visit_children(&input.data); + + let expanded = quote! { + // The generated impl. + impl #impl_generics sqlparser::ast::Visit for #name #ty_generics #where_clause { + fn visit(&self, visitor: &mut V) -> ::std::ops::ControlFlow { + #pre_visit + #children + #post_visit + ::std::ops::ControlFlow::Continue(()) + } + } + }; + + proc_macro::TokenStream::from(expanded) +} + +/// Parses attributes that can be provided to this macro +/// +/// `#[visit(leaf, with = "visit_expr")]` +#[derive(Default)] +struct Attributes { + /// Content for the `with` attribute + with: Option, +} + +impl Attributes { + fn parse(attrs: &[Attribute]) -> Self { + let mut out = Self::default(); + for attr in attrs.iter().filter(|a| a.path.is_ident("visit")) { + let meta = attr.parse_meta().expect("visit attribute"); + match meta { + Meta::List(l) => { + for nested in &l.nested { + match nested { + NestedMeta::Meta(Meta::NameValue(v)) => out.parse_name_value(v), + _ => panic!("Expected #[visit(key = \"value\")]"), + } + } + } + _ => panic!("Expected #[visit(...)]"), + } + } + out + } + + /// Updates self with a name value attribute + fn parse_name_value(&mut self, v: &MetaNameValue) { + if v.path.is_ident("with") { + match &v.lit { + Lit::Str(s) => self.with = Some(format_ident!("{}", s.value(), span = s.span())), + _ => panic!("Expected a string value, got {}", v.lit.to_token_stream()), + } + return; + } + panic!("Unrecognised kv attribute {}", v.path.to_token_stream()) + } + + /// Returns the pre and post visit token streams + fn visit(&self, s: TokenStream) -> (Option, Option) { + let pre_visit = self.with.as_ref().map(|m| { + let m = format_ident!("pre_{}", m); + quote!(visitor.#m(#s)?;) + }); + let post_visit = self.with.as_ref().map(|m| { + let m = format_ident!("post_{}", m); + quote!(visitor.#m(#s)?;) + }); + (pre_visit, post_visit) + } +} + +// Add a bound `T: Visit` to every type parameter T. +fn add_trait_bounds(mut generics: Generics) -> Generics { + for param in &mut generics.params { + if let GenericParam::Type(ref mut type_param) = *param { + type_param.bounds.push(parse_quote!(sqlparser::ast::Visit)); + } + } + generics +} + +// Generate the body of the visit implementation for the given type +fn visit_children(data: &Data) -> TokenStream { + match data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => { + let recurse = fields.named.iter().map(|f| { + let name = &f.ident; + let attributes = Attributes::parse(&f.attrs); + let (pre_visit, post_visit) = attributes.visit(quote!(&self.#name)); + quote_spanned!(f.span() => #pre_visit sqlparser::ast::Visit::visit(&self.#name, visitor)?; #post_visit) + }); + quote! { + #(#recurse)* + } + } + Fields::Unnamed(fields) => { + let recurse = fields.unnamed.iter().enumerate().map(|(i, f)| { + let index = Index::from(i); + let attributes = Attributes::parse(&f.attrs); + let (pre_visit, post_visit) = attributes.visit(quote!(&self.#index)); + quote_spanned!(f.span() => #pre_visit sqlparser::ast::Visit::visit(&self.#index, visitor)?; #post_visit) + }); + quote! { + #(#recurse)* + } + } + Fields::Unit => { + quote!() + } + }, + Data::Enum(data) => { + let statements = data.variants.iter().map(|v| { + let name = &v.ident; + match &v.fields { + Fields::Named(fields) => { + let names = fields.named.iter().map(|f| &f.ident); + let visit = fields.named.iter().map(|f| { + let name = &f.ident; + let attributes = Attributes::parse(&f.attrs); + let (pre_visit, post_visit) = attributes.visit(quote!(&#name)); + quote_spanned!(f.span() => #pre_visit sqlparser::ast::Visit::visit(#name, visitor)?; #post_visit) + }); + + quote!( + Self::#name { #(#names),* } => { + #(#visit)* + } + ) + } + Fields::Unnamed(fields) => { + let names = fields.unnamed.iter().enumerate().map(|(i, f)| format_ident!("_{}", i, span = f.span())); + let visit = fields.unnamed.iter().enumerate().map(|(i, f)| { + let name = format_ident!("_{}", i); + let attributes = Attributes::parse(&f.attrs); + let (pre_visit, post_visit) = attributes.visit(quote!(&#name)); + quote_spanned!(f.span() => #pre_visit sqlparser::ast::Visit::visit(#name, visitor)?; #post_visit) + }); + + quote! { + Self::#name ( #(#names),*) => { + #(#visit)* + } + } + } + Fields::Unit => { + quote! { + Self::#name => {} + } + } + } + }); + + quote! { + match self { + #(#statements),* + } + } + } + Data::Union(_) => unimplemented!(), + } +} diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 1353eca90..af8320d8f 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -17,6 +17,9 @@ use core::fmt; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "visitor")] +use sqlparser_derive::Visit; + use crate::ast::ObjectName; use super::value::escape_single_quote_string; @@ -24,6 +27,7 @@ use super::value::escape_single_quote_string; /// SQL data types #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum DataType { /// Fixed-length character type e.g. CHARACTER(10) Character(Option), @@ -337,6 +341,7 @@ fn format_datetime_precision_and_tz( /// guarantee compatibility with the input query we must maintain its exact information. #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum TimezoneInfo { /// No information about time zone. E.g., TIMESTAMP None, @@ -384,6 +389,7 @@ impl fmt::Display for TimezoneInfo { /// [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))] pub enum ExactNumberInfo { /// No additional information e.g. `DECIMAL` None, @@ -414,6 +420,7 @@ impl fmt::Display for ExactNumberInfo { /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-length #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct CharacterLength { /// Default (if VARYING) or maximum (if not VARYING) length pub length: u64, @@ -436,6 +443,7 @@ impl fmt::Display for CharacterLength { /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#char-length-units #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum CharLengthUnits { /// CHARACTERS unit Characters, diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f55760984..e5bee49ca 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -20,6 +20,9 @@ use core::fmt; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "visitor")] +use sqlparser_derive::Visit; + use crate::ast::value::escape_single_quote_string; use crate::ast::{display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName}; use crate::tokenizer::Token; @@ -27,6 +30,7 @@ use crate::tokenizer::Token; /// An `ALTER TABLE` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum AlterTableOperation { /// `ADD ` AddConstraint(TableConstraint), @@ -96,6 +100,7 @@ pub enum AlterTableOperation { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum AlterIndexOperation { RenameIndex { index_name: ObjectName }, } @@ -219,6 +224,7 @@ impl fmt::Display for AlterIndexOperation { /// An `ALTER COLUMN` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum AlterColumnOperation { /// `SET NOT NULL` SetNotNull, @@ -262,6 +268,7 @@ impl fmt::Display for AlterColumnOperation { /// `ALTER TABLE ADD ` statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum TableConstraint { /// `[ CONSTRAINT ] { PRIMARY KEY | UNIQUE } ()` Unique { @@ -425,6 +432,7 @@ impl fmt::Display for TableConstraint { /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum KeyOrIndexDisplay { /// Nothing to display None, @@ -460,6 +468,7 @@ impl fmt::Display for KeyOrIndexDisplay { /// [3]: https://www.postgresql.org/docs/14/sql-createindex.html #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum IndexType { BTree, Hash, @@ -478,6 +487,7 @@ impl fmt::Display for IndexType { /// SQL column definition #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct ColumnDef { pub name: Ident, pub data_type: DataType, @@ -513,6 +523,7 @@ impl fmt::Display for ColumnDef { /// "column options," and we allow any column option to be named. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct ColumnOptionDef { pub name: Option, pub option: ColumnOption, @@ -528,6 +539,7 @@ impl fmt::Display for ColumnOptionDef { /// TABLE` statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ColumnOption { /// `NULL` Null, @@ -617,6 +629,7 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { /// Used in foreign key constraints in `ON UPDATE` and `ON DELETE` options. #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ReferentialAction { Restrict, Cascade, diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 97c567b83..403d91131 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -4,6 +4,9 @@ use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "visitor")] +use sqlparser_derive::Visit; + use crate::ast::{ ColumnDef, FileFormat, HiveDistributionStyle, HiveFormat, ObjectName, OnCommit, Query, SqlOption, Statement, TableConstraint, @@ -40,6 +43,7 @@ use crate::parser::ParserError; /// [1]: crate::ast::Statement::CreateTable #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct CreateTableBuilder { pub or_replace: bool, pub temporary: bool, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b497014dd..80dff8504 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -22,6 +22,9 @@ use core::fmt; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "visitor")] +use sqlparser_derive::Visit; + pub use self::data_type::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, }; @@ -38,6 +41,9 @@ pub use self::query::{ }; pub use self::value::{escape_quoted_string, DateTimeField, TrimWhereField, Value}; +#[cfg(feature = "visitor")] +pub use visitor::*; + mod data_type; mod ddl; pub mod helpers; @@ -45,6 +51,9 @@ mod operator; mod query; mod value; +#[cfg(feature = "visitor")] +mod visitor; + struct DisplaySeparated<'a, T> where T: fmt::Display, @@ -85,6 +94,7 @@ where /// An identifier, decomposed into its value or character data and the quote style. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct Ident { /// The value of the identifier without quotes. pub value: String, @@ -145,6 +155,7 @@ impl fmt::Display for Ident { /// A name of a table, view, custom type, etc., possibly multi-part, i.e. db.schema.obj #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct ObjectName(pub Vec); impl fmt::Display for ObjectName { @@ -153,10 +164,11 @@ impl fmt::Display for ObjectName { } } -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] /// Represents an Array Expression, either /// `ARRAY[..]`, or `[..]` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct Array { /// The list of expressions between brackets pub elem: Vec, @@ -179,6 +191,7 @@ impl fmt::Display for Array { /// JsonOperator #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum JsonOperator { /// -> keeps the value as json Arrow, @@ -242,6 +255,7 @@ impl fmt::Display for JsonOperator { /// inappropriate type, like `WHERE 1` or `SELECT 1=1`, as necessary. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit), visit(with = "visit_expr"))] pub enum Expr { /// Identifier e.g. table name or column name Identifier(Ident), @@ -882,6 +896,7 @@ impl fmt::Display for Expr { /// A window specification (i.e. `OVER (PARTITION BY .. ORDER BY .. etc.)`) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct WindowSpec { pub partition_by: Vec, pub order_by: Vec, @@ -927,6 +942,7 @@ impl fmt::Display for WindowSpec { /// reject invalid bounds like `ROWS UNBOUNDED FOLLOWING` before execution. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct WindowFrame { pub units: WindowFrameUnits, pub start_bound: WindowFrameBound, @@ -952,6 +968,7 @@ impl Default for WindowFrame { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum WindowFrameUnits { Rows, Range, @@ -971,6 +988,7 @@ impl fmt::Display for WindowFrameUnits { /// Specifies [WindowFrame]'s `start_bound` and `end_bound` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum WindowFrameBound { /// `CURRENT ROW` CurrentRow, @@ -994,6 +1012,7 @@ impl fmt::Display for WindowFrameBound { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum AddDropSync { ADD, DROP, @@ -1012,6 +1031,7 @@ impl fmt::Display for AddDropSync { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ShowCreateObject { Event, Function, @@ -1036,6 +1056,7 @@ impl fmt::Display for ShowCreateObject { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum CommentObject { Column, Table, @@ -1052,6 +1073,7 @@ impl fmt::Display for CommentObject { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum Password { Password(Expr), NullPassword, @@ -1061,9 +1083,11 @@ pub enum Password { #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit), visit(with = "visit_statement"))] pub enum Statement { /// Analyze (Hive) Analyze { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, partitions: Option>, for_columns: bool, @@ -1074,11 +1098,13 @@ pub enum Statement { }, /// Truncate (Hive) Truncate { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, partitions: Option>, }, /// Msck (Hive) Msck { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, repair: bool, partition_action: Option, @@ -1092,6 +1118,7 @@ pub enum Statement { /// INTO - optional keyword into: bool, /// TABLE + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, /// COLUMNS columns: Vec, @@ -1119,6 +1146,7 @@ pub enum Statement { }, Copy { /// TABLE + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, /// COLUMNS columns: Vec, @@ -1181,6 +1209,7 @@ pub enum Statement { global: Option, if_not_exists: bool, /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] name: ObjectName, /// Optional schema columns: Vec, @@ -1205,6 +1234,7 @@ pub enum Statement { }, /// SQLite's `CREATE VIRTUAL TABLE .. USING ()` CreateVirtualTable { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] name: ObjectName, if_not_exists: bool, module_name: Ident, @@ -1214,6 +1244,7 @@ pub enum Statement { CreateIndex { /// index name name: ObjectName, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, using: Option, columns: Vec, @@ -1247,6 +1278,7 @@ pub enum Statement { /// ALTER TABLE AlterTable { /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] name: ObjectName, operation: AlterTableOperation, }, @@ -1383,6 +1415,7 @@ pub enum Statement { ShowColumns { extended: bool, full: bool, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, filter: Option, }, @@ -1498,9 +1531,10 @@ pub enum Statement { /// EXPLAIN TABLE /// Note: this is a MySQL-specific statement. See ExplainTable { - // If true, query used the MySQL `DESCRIBE` alias for explain + /// If true, query used the MySQL `DESCRIBE` alias for explain describe_alias: bool, - // Table name + /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, }, /// EXPLAIN / DESCRIBE for select_statement @@ -1534,19 +1568,22 @@ pub enum Statement { /// CACHE [ FLAG ] TABLE [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ ] /// Based on Spark SQL,see Cache { - // Table flag + /// Table flag table_flag: Option, - // Table name + /// Table name + + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, has_as: bool, - // Table confs + /// Table confs options: Vec, - // Cache table as a Query + /// Cache table as a Query query: Option, }, /// UNCACHE TABLE [ IF EXISTS ] UNCache { - // Table name + /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, if_exists: bool, }, @@ -2673,6 +2710,7 @@ impl fmt::Display for Statement { /// [ START [ WITH ] start ] [ CACHE cache ] [ [ NO ] CYCLE ] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum SequenceOptions { IncrementBy(Expr, bool), MinValue(MinMaxValue), @@ -2737,6 +2775,7 @@ impl fmt::Display for SequenceOptions { /// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum MinMaxValue { // clause is not specified Empty, @@ -2748,6 +2787,7 @@ pub enum MinMaxValue { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] #[non_exhaustive] pub enum OnInsert { /// ON DUPLICATE KEY UPDATE (MySQL when the key already exists, then execute an update instead) @@ -2758,18 +2798,21 @@ pub enum OnInsert { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct OnConflict { pub conflict_target: Option, pub action: OnConflictAction, } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ConflictTarget { Columns(Vec), OnConstraint(ObjectName), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum OnConflictAction { DoNothing, DoUpdate(DoUpdate), @@ -2777,6 +2820,7 @@ pub enum OnConflictAction { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct DoUpdate { /// Column assignments pub assignments: Vec, @@ -2838,6 +2882,7 @@ impl fmt::Display for OnConflictAction { /// Privileges granted in a GRANT statement or revoked in a REVOKE statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum Privileges { /// All privileges applicable to the object type All { @@ -2874,6 +2919,7 @@ impl fmt::Display for Privileges { /// Specific direction for FETCH statement #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum FetchDirection { Count { limit: Value }, Next, @@ -2937,6 +2983,7 @@ impl fmt::Display for FetchDirection { /// A privilege on a database object (table, sequence, etc.). #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum Action { Connect, Create, @@ -2986,6 +3033,7 @@ impl fmt::Display for Action { /// Objects on which privileges are granted in a GRANT statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum GrantObjects { /// Grant privileges on `ALL SEQUENCES IN SCHEMA [, ...]` AllSequencesInSchema { schemas: Vec }, @@ -3032,6 +3080,7 @@ impl fmt::Display for GrantObjects { /// SQL assignment `foo = expr` as used in SQLUpdate #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct Assignment { pub id: Vec, pub value: Expr, @@ -3045,6 +3094,7 @@ impl fmt::Display for Assignment { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum FunctionArgExpr { Expr(Expr), /// Qualified wildcard, e.g. `alias.*` or `schema.table.*`. @@ -3065,6 +3115,7 @@ impl fmt::Display for FunctionArgExpr { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum FunctionArg { Named { name: Ident, arg: FunctionArgExpr }, Unnamed(FunctionArgExpr), @@ -3081,6 +3132,7 @@ impl fmt::Display for FunctionArg { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum CloseCursor { All, Specific { name: Ident }, @@ -3098,6 +3150,7 @@ impl fmt::Display for CloseCursor { /// A function call #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct Function { pub name: ObjectName, pub args: Vec, @@ -3111,6 +3164,7 @@ pub struct Function { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum AnalyzeFormat { TEXT, GRAPHVIZ, @@ -3152,6 +3206,7 @@ impl fmt::Display for Function { /// External table's available file format #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum FileFormat { TEXTFILE, SEQUENCEFILE, @@ -3181,6 +3236,7 @@ impl fmt::Display for FileFormat { /// [ WITHIN GROUP (ORDER BY [, ...] ) ]` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct ListAgg { pub distinct: bool, pub expr: Box, @@ -3218,6 +3274,7 @@ impl fmt::Display for ListAgg { /// The `ON OVERFLOW` clause of a LISTAGG invocation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ListAggOnOverflow { /// `ON OVERFLOW ERROR` Error, @@ -3255,6 +3312,7 @@ impl fmt::Display for ListAggOnOverflow { /// ORDER BY position is defined differently for BigQuery, Postgres and Snowflake. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct ArrayAgg { pub distinct: bool, pub expr: Box, @@ -3291,6 +3349,7 @@ impl fmt::Display for ArrayAgg { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ObjectType { Table, View, @@ -3315,6 +3374,7 @@ impl fmt::Display for ObjectType { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum KillType { Connection, Query, @@ -3335,6 +3395,7 @@ impl fmt::Display for KillType { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum HiveDistributionStyle { PARTITIONED { columns: Vec, @@ -3354,14 +3415,15 @@ pub enum HiveDistributionStyle { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum HiveRowFormat { SERDE { class: String }, DELIMITED, } -#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] #[allow(clippy::large_enum_variant)] pub enum HiveIOFormat { IOF { @@ -3375,6 +3437,7 @@ pub enum HiveIOFormat { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct HiveFormat { pub row_format: Option, pub storage: Option, @@ -3383,6 +3446,7 @@ pub struct HiveFormat { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct SqlOption { pub name: Ident, pub value: Value, @@ -3396,6 +3460,7 @@ impl fmt::Display for SqlOption { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum TransactionMode { AccessMode(TransactionAccessMode), IsolationLevel(TransactionIsolationLevel), @@ -3413,6 +3478,7 @@ impl fmt::Display for TransactionMode { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum TransactionAccessMode { ReadOnly, ReadWrite, @@ -3430,6 +3496,7 @@ impl fmt::Display for TransactionAccessMode { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum TransactionIsolationLevel { ReadUncommitted, ReadCommitted, @@ -3451,6 +3518,7 @@ impl fmt::Display for TransactionIsolationLevel { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ShowStatementFilter { Like(String), ILike(String), @@ -3473,6 +3541,7 @@ impl fmt::Display for ShowStatementFilter { /// https://sqlite.org/lang_conflict.html #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum SqliteOnConflict { Rollback, Abort, @@ -3496,6 +3565,7 @@ impl fmt::Display for SqliteOnConflict { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum CopyTarget { Stdin, Stdout, @@ -3527,6 +3597,7 @@ impl fmt::Display for CopyTarget { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum OnCommit { DeleteRows, PreserveRows, @@ -3538,6 +3609,7 @@ pub enum OnCommit { /// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum CopyOption { /// FORMAT format_name Format(Ident), @@ -3591,6 +3663,7 @@ impl fmt::Display for CopyOption { /// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum CopyLegacyOption { /// BINARY Binary, @@ -3619,6 +3692,7 @@ impl fmt::Display for CopyLegacyOption { /// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum CopyLegacyCsvOption { /// HEADER Header, @@ -3650,6 +3724,7 @@ impl fmt::Display for CopyLegacyCsvOption { /// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum MergeClause { MatchedUpdate { predicate: Option, @@ -3711,6 +3786,7 @@ impl fmt::Display for MergeClause { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum DiscardObject { ALL, PLANS, @@ -3732,6 +3808,7 @@ impl fmt::Display for DiscardObject { /// Optional context modifier for statements that can be or `LOCAL`, or `SESSION`. #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ContextModifier { /// No context defined. Each dialect defines the default in this scenario. None, @@ -3777,6 +3854,7 @@ impl fmt::Display for DropFunctionOption { /// Function describe in DROP FUNCTION. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct DropFunctionDesc { pub name: ObjectName, pub args: Option>, @@ -3795,6 +3873,7 @@ impl fmt::Display for DropFunctionDesc { /// Function argument in CREATE OR DROP FUNCTION. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct OperateFunctionArg { pub mode: Option, pub name: Option, @@ -3843,6 +3922,7 @@ impl fmt::Display for OperateFunctionArg { /// The mode of an argument in CREATE FUNCTION. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum ArgMode { In, Out, @@ -3862,6 +3942,7 @@ impl fmt::Display for ArgMode { /// These attributes inform the query optimizer about the behavior of the function. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum FunctionBehavior { Immutable, Stable, @@ -3880,6 +3961,7 @@ impl fmt::Display for FunctionBehavior { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum FunctionDefinition { SingleQuotedDef(String), DoubleDollarDef(String), @@ -3898,6 +3980,7 @@ impl fmt::Display for FunctionDefinition { /// Postgres: https://www.postgresql.org/docs/15/sql-createfunction.html #[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct CreateFunctionBody { /// LANGUAGE lang_name pub language: Option, @@ -3936,6 +4019,7 @@ impl fmt::Display for CreateFunctionBody { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum CreateFunctionUsing { Jar(String), File(String), @@ -3958,6 +4042,7 @@ impl fmt::Display for CreateFunctionUsing { /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#schema-definition #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum SchemaName { /// Only schema name specified: ``. Simple(ObjectName), @@ -3988,6 +4073,7 @@ impl fmt::Display for SchemaName { /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum SearchModifier { /// `IN NATURAL LANGUAGE MODE`. InNaturalLanguageMode, diff --git a/src/ast/operator.rs b/src/ast/operator.rs index f22839474..b8f371be3 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -14,14 +14,19 @@ use core::fmt; #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "visitor")] +use sqlparser_derive::Visit; + use super::display_separated; /// Unary operators #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum UnaryOperator { Plus, Minus, @@ -59,6 +64,7 @@ impl fmt::Display for UnaryOperator { /// Binary operators #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum BinaryOperator { Plus, Minus, diff --git a/src/ast/query.rs b/src/ast/query.rs index 9d23b1ae7..7a009c2ed 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -16,12 +16,16 @@ use alloc::{boxed::Box, vec::Vec}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "visitor")] +use sqlparser_derive::Visit; + use crate::ast::*; /// The most complete variant of a `SELECT` query expression, optionally /// including `WITH`, `UNION` / other set operations, and `ORDER BY`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub struct Query { /// WITH (common table expressions, or CTEs) pub with: Option, @@ -69,6 +73,7 @@ impl fmt::Display for Query { #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] pub enum SetExpr { /// Restricted SELECT .. FROM .. HAVING (no ORDER BY or set operations) Select(Box), @@ -122,7 +122,7 @@ impl fmt::Display for SetExpr { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum SetOperator { Union, Except, @@ -144,7 +144,7 @@ impl fmt::Display for SetOperator { // For example, BigQuery does not support `DISTINCT` for `EXCEPT` and `INTERSECT` #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum SetQuantifier { All, Distinct, @@ -164,7 +164,7 @@ impl fmt::Display for SetQuantifier { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] /// A [`TABLE` command]( https://www.postgresql.org/docs/current/sql-select.html#SQL-TABLE) -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Table { pub table_name: Option, pub schema_name: Option, @@ -191,7 +191,7 @@ impl fmt::Display for Table { /// to a set operation like `UNION`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Select { pub distinct: bool, /// MSSQL syntax: `TOP () [ PERCENT ] [ WITH TIES ]` @@ -276,7 +276,7 @@ impl fmt::Display for Select { /// A hive LATERAL VIEW with potential column aliases #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct LateralView { /// LATERAL VIEW pub lateral_view: Expr, @@ -310,7 +310,7 @@ impl fmt::Display for LateralView { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct With { pub recursive: bool, pub cte_tables: Vec, @@ -333,7 +333,7 @@ impl fmt::Display for With { /// number of columns in the query matches the number of columns in the query. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Cte { pub alias: TableAlias, pub query: Box, @@ -353,7 +353,7 @@ impl fmt::Display for Cte { /// One item of the comma-separated list following `SELECT` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum SelectItem { /// Any expression, not followed by `[ AS ] alias` UnnamedExpr(Expr), @@ -368,7 +368,7 @@ pub enum SelectItem { /// Additional options for wildcards, e.g. Snowflake `EXCLUDE` and Bigquery `EXCEPT`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WildcardAdditionalOptions { /// `[EXCLUDE...]`. pub opt_exclude: Option, @@ -397,7 +397,7 @@ impl fmt::Display for WildcardAdditionalOptions { /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ExcludeSelectItem { /// Single column name without parenthesis. /// @@ -437,7 +437,7 @@ impl fmt::Display for ExcludeSelectItem { /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct ExceptSelectItem { /// First guaranteed column. pub fist_elemnt: Ident, @@ -483,7 +483,7 @@ impl fmt::Display for SelectItem { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct TableWithJoins { pub relation: TableFactor, pub joins: Vec, @@ -502,7 +502,7 @@ impl fmt::Display for TableWithJoins { /// A table name or a parenthesized subquery with an optional alias #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TableFactor { Table { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] @@ -633,7 +633,7 @@ impl fmt::Display for TableFactor { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct TableAlias { pub name: Ident, pub columns: Vec, @@ -651,7 +651,7 @@ impl fmt::Display for TableAlias { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Join { pub relation: TableFactor, pub join_operator: JoinOperator, @@ -746,7 +746,7 @@ impl fmt::Display for Join { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum JoinOperator { Inner(JoinConstraint), LeftOuter(JoinConstraint), @@ -769,7 +769,7 @@ pub enum JoinOperator { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum JoinConstraint { On(Expr), Using(Vec), @@ -780,7 +780,7 @@ pub enum JoinConstraint { /// An `ORDER BY` expression #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct OrderByExpr { pub expr: Expr, /// Optional `ASC` or `DESC` @@ -808,7 +808,7 @@ impl fmt::Display for OrderByExpr { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Offset { pub value: Expr, pub rows: OffsetRows, @@ -823,7 +823,7 @@ impl fmt::Display for Offset { /// Stores the keyword after `OFFSET ` #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum OffsetRows { /// Omitting ROW/ROWS is non-standard MySQL quirk. None, @@ -843,7 +843,7 @@ impl fmt::Display for OffsetRows { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Fetch { pub with_ties: bool, pub percent: bool, @@ -864,7 +864,7 @@ impl fmt::Display for Fetch { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct LockClause { pub lock_type: LockType, pub of: Option, @@ -886,7 +886,7 @@ impl fmt::Display for LockClause { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum LockType { Share, Update, @@ -904,7 +904,7 @@ impl fmt::Display for LockType { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum NonBlock { Nowait, SkipLocked, @@ -922,7 +922,7 @@ impl fmt::Display for NonBlock { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Top { /// SQL semantic equivalent of LIMIT but with same structure as FETCH. pub with_ties: bool, @@ -944,7 +944,7 @@ impl fmt::Display for Top { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Values { /// Was there an explict ROWs keyword (MySQL)? /// @@ -968,7 +968,7 @@ impl fmt::Display for Values { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct SelectInto { pub temporary: bool, pub unlogged: bool, diff --git a/src/ast/value.rs b/src/ast/value.rs index 0a64704a1..4072c0a4c 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -21,12 +21,12 @@ use bigdecimal::BigDecimal; use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] -use sqlparser_derive::Visit; +use sqlparser_derive::{Visit, VisitMut}; /// Primitive SQL values such as number and string #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum Value { /// Numeric literal #[cfg(not(feature = "bigdecimal"))] @@ -77,7 +77,7 @@ impl fmt::Display for Value { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct DollarQuotedString { pub value: String, pub tag: Option, @@ -98,7 +98,7 @@ impl fmt::Display for DollarQuotedString { #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum DateTimeField { Year, Month, @@ -229,7 +229,7 @@ pub fn escape_escaped_string(s: &str) -> EscapeEscapedStringLiteral<'_> { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TrimWhereField { Both, Leading, diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 85f9175ef..553e014a4 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -24,12 +24,27 @@ use core::ops::ControlFlow; /// using the [Visit](sqlparser_derive::Visit) proc macro. /// /// ```text -/// #[cfg_attr(feature = "visitor", derive(Visit))] +/// #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] /// ``` pub trait Visit { fn visit(&self, visitor: &mut V) -> ControlFlow; } +/// A type that can be visited by a [`VisitorMut`]. See [`VisitorMut`] for +/// recursively visiting parsed SQL statements. +/// +/// # Note +/// +/// This trait should be automatically derived for sqlparser AST nodes +/// using the [VisitMut](sqlparser_derive::VisitMut) proc macro. +/// +/// ```text +/// #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// ``` +pub trait VisitMut { + fn visit(&mut self, visitor: &mut V) -> ControlFlow; +} + impl Visit for Option { fn visit(&self, visitor: &mut V) -> ControlFlow { if let Some(s) = self { @@ -54,6 +69,30 @@ impl Visit for Box { } } +impl VisitMut for Option { + fn visit(&mut self, visitor: &mut V) -> ControlFlow { + if let Some(s) = self { + s.visit(visitor)?; + } + ControlFlow::Continue(()) + } +} + +impl VisitMut for Vec { + fn visit(&mut self, visitor: &mut V) -> ControlFlow { + for v in self { + v.visit(visitor)?; + } + ControlFlow::Continue(()) + } +} + +impl VisitMut for Box { + fn visit(&mut self, visitor: &mut V) -> ControlFlow { + T::visit(self, visitor) + } +} + macro_rules! visit_noop { ($($t:ty),+) => { $(impl Visit for $t { @@ -61,6 +100,11 @@ macro_rules! visit_noop { ControlFlow::Continue(()) } })+ + $(impl VisitMut for $t { + fn visit(&mut self, _visitor: &mut V) -> ControlFlow { + ControlFlow::Continue(()) + } + })+ }; } @@ -166,6 +210,84 @@ pub trait Visitor { } } +/// A visitor that can be used to mutate an AST tree. +/// +/// `previst_` methods are invoked before visiting all children of the +/// node and `postvisit_` methods are invoked after visiting all +/// children of the node. +/// +/// # See also +/// +/// These methods provide a more concise way of visiting nodes of a certain type: +/// * [visit_relations_mut] +/// * [visit_expressions_mut] +/// * [visit_statements_mut] +/// +/// # Example +/// ``` +/// # use sqlparser::parser::Parser; +/// # use sqlparser::dialect::GenericDialect; +/// # use sqlparser::ast::{VisitMut, VisitorMut, ObjectName, Expr, Ident}; +/// # use core::ops::ControlFlow; +/// +/// // A visitor that replaces "to_replace" with "replaced" in all expressions +/// struct Replacer; +/// +/// // Visit each expression after its children have been visited +/// impl VisitorMut for Replacer { +/// type Break = (); +/// +/// fn post_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow { +/// if let Expr::Identifier(Ident{ value, ..}) = expr { +/// *value = value.replace("to_replace", "replaced") +/// } +/// ControlFlow::Continue(()) +/// } +/// } +/// +/// let sql = "SELECT to_replace FROM foo where to_replace IN (SELECT to_replace FROM bar)"; +/// let mut statements = Parser::parse_sql(&GenericDialect{}, sql).unwrap(); +/// +/// // Drive the visitor through the AST +/// statements.visit(&mut Replacer); +/// +/// assert_eq!(statements[0].to_string(), "SELECT replaced FROM foo WHERE replaced IN (SELECT replaced FROM bar)"); +/// ``` +pub trait VisitorMut { + /// Type returned when the recursion returns early. + type Break; + + /// Invoked for any relations (e.g. tables) that appear in the AST before visiting children + fn pre_visit_relation(&mut self, _relation: &mut ObjectName) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any relations (e.g. tables) that appear in the AST after visiting children + fn post_visit_relation(&mut self, _relation: &mut ObjectName) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any expressions that appear in the AST before visiting children + fn pre_visit_expr(&mut self, _expr: &mut Expr) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any expressions that appear in the AST + fn post_visit_expr(&mut self, _expr: &mut Expr) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any statements that appear in the AST before visiting children + fn pre_visit_statement(&mut self, _statement: &mut Statement) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any statements that appear in the AST after visiting children + fn post_visit_statement(&mut self, _statement: &mut Statement) -> ControlFlow { + ControlFlow::Continue(()) + } +} + struct RelationVisitor(F); impl ControlFlow> Visitor for RelationVisitor { @@ -176,6 +298,14 @@ impl ControlFlow> Visitor for RelationVisitor } } +impl ControlFlow> VisitorMut for RelationVisitor { + type Break = E; + + fn pre_visit_relation(&mut self, relation: &mut ObjectName) -> ControlFlow { + self.0(relation) + } +} + /// Invokes the provided closure on all relations (e.g. table names) present in `v` /// /// # Example @@ -213,6 +343,36 @@ where ControlFlow::Continue(()) } +/// Invokes the provided closure on all relations (e.g. table names) present in `v` +/// +/// # Example +/// ``` +/// # use sqlparser::parser::Parser; +/// # use sqlparser::dialect::GenericDialect; +/// # use sqlparser::ast::{ObjectName, visit_relations_mut}; +/// # use core::ops::ControlFlow; +/// let sql = "SELECT a FROM foo"; +/// let mut statements = Parser::parse_sql(&GenericDialect{}, sql) +/// .unwrap(); +/// +/// // visit statements, renaming table foo to bar +/// visit_relations_mut(&mut statements, |table| { +/// table.0[0].value = table.0[0].value.replace("foo", "bar"); +/// ControlFlow::<()>::Continue(()) +/// }); +/// +/// assert_eq!(statements[0].to_string(), "SELECT a FROM bar"); +/// ``` +pub fn visit_relations_mut(v: &mut V, f: F) -> ControlFlow +where + V: VisitMut, + F: FnMut(&mut ObjectName) -> ControlFlow, +{ + let mut visitor = RelationVisitor(f); + v.visit(&mut visitor)?; + ControlFlow::Continue(()) +} + struct ExprVisitor(F); impl ControlFlow> Visitor for ExprVisitor { @@ -223,6 +383,14 @@ impl ControlFlow> Visitor for ExprVisitor { } } +impl ControlFlow> VisitorMut for ExprVisitor { + type Break = E; + + fn pre_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow { + self.0(expr) + } +} + /// Invokes the provided closure on all expressions (e.g. `1 + 2`) present in `v` /// /// # Example @@ -262,6 +430,36 @@ where ControlFlow::Continue(()) } +/// Invokes the provided closure on all expressions present in `v` +/// +/// # Example +/// ``` +/// # use sqlparser::parser::Parser; +/// # use sqlparser::dialect::GenericDialect; +/// # use sqlparser::ast::{Expr, visit_expressions_mut, visit_statements_mut}; +/// # use core::ops::ControlFlow; +/// let sql = "SELECT (SELECT y FROM z LIMIT 9) FROM t LIMIT 3"; +/// let mut statements = Parser::parse_sql(&GenericDialect{}, sql).unwrap(); +/// +/// // Remove all select limits in sub-queries +/// visit_expressions_mut(&mut statements, |expr| { +/// if let Expr::Subquery(q) = expr { +/// q.limit = None +/// } +/// ControlFlow::<()>::Continue(()) +/// }); +/// +/// assert_eq!(statements[0].to_string(), "SELECT (SELECT y FROM z) FROM t LIMIT 3"); +/// ``` +pub fn visit_expressions_mut(v: &mut V, f: F) -> ControlFlow +where + V: VisitMut, + F: FnMut(&mut Expr) -> ControlFlow, +{ + v.visit(&mut ExprVisitor(f))?; + ControlFlow::Continue(()) +} + struct StatementVisitor(F); impl ControlFlow> Visitor for StatementVisitor { @@ -272,6 +470,14 @@ impl ControlFlow> Visitor for StatementVisitor } } +impl ControlFlow> VisitorMut for StatementVisitor { + type Break = E; + + fn pre_visit_statement(&mut self, statement: &mut Statement) -> ControlFlow { + self.0(statement) + } +} + /// Invokes the provided closure on all statements (e.g. `SELECT`, `CREATE TABLE`, etc) present in `v` /// /// # Example @@ -309,6 +515,37 @@ where ControlFlow::Continue(()) } +/// Invokes the provided closure on all statements (e.g. `SELECT`, `CREATE TABLE`, etc) present in `v` +/// +/// # Example +/// ``` +/// # use sqlparser::parser::Parser; +/// # use sqlparser::dialect::GenericDialect; +/// # use sqlparser::ast::{Statement, visit_statements_mut}; +/// # use core::ops::ControlFlow; +/// let sql = "SELECT x FROM foo LIMIT 9+$limit; SELECT * FROM t LIMIT f()"; +/// let mut statements = Parser::parse_sql(&GenericDialect{}, sql).unwrap(); +/// +/// // 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 +/// } +/// ControlFlow::<()>::Continue(()) +/// }); +/// +/// assert_eq!(statements[0].to_string(), "SELECT x FROM foo"); +/// assert_eq!(statements[1].to_string(), "SELECT * FROM t"); +/// ``` +pub fn visit_statements_mut(v: &mut V, f: F) -> ControlFlow +where + V: VisitMut, + F: FnMut(&mut Statement) -> ControlFlow, +{ + v.visit(&mut StatementVisitor(f))?; + ControlFlow::Continue(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/keywords.rs b/src/keywords.rs index ac8f28ac2..ad4e51e14 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] -use sqlparser_derive::Visit; +use sqlparser_derive::{Visit, VisitMut}; /// Defines a string constant for a single keyword: `kw_def!(SELECT);` /// expands to `pub const SELECT = "SELECT";` @@ -47,7 +47,7 @@ macro_rules! define_keywords { ),*) => { #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - #[cfg_attr(feature = "visitor", derive(Visit))] + #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] #[allow(non_camel_case_types)] pub enum Keyword { NoKeyword, diff --git a/src/tokenizer.rs b/src/tokenizer.rs index c92012a5a..dcf128542 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -32,7 +32,7 @@ use core::str::Chars; use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] -use sqlparser_derive::Visit; +use sqlparser_derive::{Visit, VisitMut}; use crate::ast::DollarQuotedString; use crate::dialect::SnowflakeDialect; @@ -42,7 +42,7 @@ use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX}; /// SQL Token enumeration #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum Token { /// An end-of-file marker, not a real token EOF, @@ -269,7 +269,7 @@ impl Token { /// A keyword (like SELECT) or an optionally quoted SQL identifier #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Word { /// The value of the token, without the enclosing quotes, and with the /// escape sequences (if any) processed (TODO: escapes are not handled) @@ -308,7 +308,7 @@ impl Word { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum Whitespace { Space, Newline, From 98403c07b192cdcfcdefb1886292d5b491ca7010 Mon Sep 17 00:00:00 2001 From: Jeffrey <22608443+Jefffrey@users.noreply.github.com> Date: Mon, 2 Jan 2023 22:51:13 +1100 Subject: [PATCH 144/806] Allow parsing of mysql empty row inserts (#783) --- src/parser.rs | 78 +++++++++++++++++++++++----------------- tests/sqlparser_mysql.rs | 35 ++++++++++++++++++ 2 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 19914253f..76d370041 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1433,7 +1433,7 @@ impl<'a> Parser<'a> { /// /// [(1)]: Expr::MatchAgainst pub fn parse_match_against(&mut self) -> Result { - let columns = self.parse_parenthesized_column_list(Mandatory)?; + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; self.expect_keyword(Keyword::AGAINST)?; @@ -2419,7 +2419,7 @@ impl<'a> Parser<'a> { // general that the arguments can be made to appear as column // definitions in a traditional CREATE TABLE statement", but // we don't implement that. - let module_args = self.parse_parenthesized_column_list(Optional)?; + let module_args = self.parse_parenthesized_column_list(Optional, false)?; Ok(Statement::CreateVirtualTable { name: table_name, if_not_exists, @@ -2698,12 +2698,12 @@ impl<'a> Parser<'a> { // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. let name = self.parse_object_name()?; - let columns = self.parse_parenthesized_column_list(Optional)?; + let columns = self.parse_parenthesized_column_list(Optional, false)?; let with_options = self.parse_options(Keyword::WITH)?; let cluster_by = if self.parse_keyword(Keyword::CLUSTER) { self.expect_keyword(Keyword::BY)?; - self.parse_parenthesized_column_list(Optional)? + self.parse_parenthesized_column_list(Optional, false)? } else { vec![] }; @@ -3441,7 +3441,7 @@ impl<'a> Parser<'a> { let foreign_table = self.parse_object_name()?; // PostgreSQL allows omitting the column list and // uses the primary key column of the foreign table by default - let referred_columns = self.parse_parenthesized_column_list(Optional)?; + let referred_columns = self.parse_parenthesized_column_list(Optional, false)?; let mut on_delete = None; let mut on_update = None; loop { @@ -3525,7 +3525,7 @@ impl<'a> Parser<'a> { if is_primary { self.expect_keyword(Keyword::KEY)?; } - let columns = self.parse_parenthesized_column_list(Mandatory)?; + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; Ok(Some(TableConstraint::Unique { name, columns, @@ -3534,10 +3534,10 @@ impl<'a> Parser<'a> { } Token::Word(w) if w.keyword == Keyword::FOREIGN => { self.expect_keyword(Keyword::KEY)?; - let columns = self.parse_parenthesized_column_list(Mandatory)?; + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; self.expect_keyword(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name()?; - let referred_columns = self.parse_parenthesized_column_list(Mandatory)?; + let referred_columns = self.parse_parenthesized_column_list(Mandatory, false)?; let mut on_delete = None; let mut on_update = None; loop { @@ -3582,7 +3582,7 @@ impl<'a> Parser<'a> { } else { None }; - let columns = self.parse_parenthesized_column_list(Mandatory)?; + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; Ok(Some(TableConstraint::Index { display_as_key, @@ -3617,7 +3617,7 @@ impl<'a> Parser<'a> { let opt_index_name = self.maybe_parse(|parser| parser.parse_identifier()); - let columns = self.parse_parenthesized_column_list(Mandatory)?; + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; Ok(Some(TableConstraint::FulltextOrSpatial { fulltext, @@ -3864,7 +3864,7 @@ impl<'a> Parser<'a> { /// Parse a copy statement pub fn parse_copy(&mut self) -> Result { let table_name = self.parse_object_name()?; - let columns = self.parse_parenthesized_column_list(Optional)?; + let columns = self.parse_parenthesized_column_list(Optional, false)?; let to = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::TO]) { Some(Keyword::FROM) => false, Some(Keyword::TO) => true, @@ -3950,13 +3950,13 @@ impl<'a> Parser<'a> { Some(Keyword::QUOTE) => CopyOption::Quote(self.parse_literal_char()?), Some(Keyword::ESCAPE) => CopyOption::Escape(self.parse_literal_char()?), Some(Keyword::FORCE_QUOTE) => { - CopyOption::ForceQuote(self.parse_parenthesized_column_list(Mandatory)?) + CopyOption::ForceQuote(self.parse_parenthesized_column_list(Mandatory, false)?) } Some(Keyword::FORCE_NOT_NULL) => { - CopyOption::ForceNotNull(self.parse_parenthesized_column_list(Mandatory)?) + CopyOption::ForceNotNull(self.parse_parenthesized_column_list(Mandatory, false)?) } Some(Keyword::FORCE_NULL) => { - CopyOption::ForceNull(self.parse_parenthesized_column_list(Mandatory)?) + CopyOption::ForceNull(self.parse_parenthesized_column_list(Mandatory, false)?) } Some(Keyword::ENCODING) => CopyOption::Encoding(self.parse_literal_string()?), _ => self.expected("option", self.peek_token())?, @@ -4454,7 +4454,7 @@ impl<'a> Parser<'a> { ) -> Result, ParserError> { match self.parse_optional_alias(reserved_kwds)? { Some(name) => { - let columns = self.parse_parenthesized_column_list(Optional)?; + let columns = self.parse_parenthesized_column_list(Optional, false)?; Ok(Some(TableAlias { name, columns })) } None => Ok(None), @@ -4505,11 +4505,17 @@ impl<'a> Parser<'a> { pub fn parse_parenthesized_column_list( &mut self, optional: IsOptional, + allow_empty: bool, ) -> Result, ParserError> { if self.consume_token(&Token::LParen) { - let cols = self.parse_comma_separated(Parser::parse_identifier)?; - self.expect_token(&Token::RParen)?; - Ok(cols) + if allow_empty && self.peek_token().token == Token::RParen { + self.next_token(); + Ok(vec![]) + } else { + let cols = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + Ok(cols) + } } else if optional == Optional { Ok(vec![]) } else { @@ -4811,7 +4817,7 @@ impl<'a> Parser<'a> { from: None, } } else { - let columns = self.parse_parenthesized_column_list(Optional)?; + let columns = self.parse_parenthesized_column_list(Optional, false)?; self.expect_keyword(Keyword::AS)?; self.expect_token(&Token::LParen)?; let query = Box::new(self.parse_query()?); @@ -4848,7 +4854,8 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; SetExpr::Query(Box::new(subquery)) } else if self.parse_keyword(Keyword::VALUES) { - SetExpr::Values(self.parse_values()?) + let is_mysql = dialect_of!(self is MySqlDialect); + SetExpr::Values(self.parse_values(is_mysql)?) } else if self.parse_keyword(Keyword::TABLE) { SetExpr::Table(Box::new(self.parse_as_table()?)) } else { @@ -5645,7 +5652,7 @@ impl<'a> Parser<'a> { let constraint = self.parse_expr()?; Ok(JoinConstraint::On(constraint)) } else if self.parse_keyword(Keyword::USING) { - let columns = self.parse_parenthesized_column_list(Mandatory)?; + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; Ok(JoinConstraint::Using(columns)) } else { Ok(JoinConstraint::None) @@ -5770,7 +5777,7 @@ impl<'a> Parser<'a> { ]) { let columns = match kw { Keyword::INSERT | Keyword::REFERENCES | Keyword::SELECT | Keyword::UPDATE => { - let columns = self.parse_parenthesized_column_list(Optional)?; + let columns = self.parse_parenthesized_column_list(Optional, false)?; if columns.is_empty() { None } else { @@ -5856,7 +5863,8 @@ impl<'a> Parser<'a> { // Hive lets you put table here regardless let table = self.parse_keyword(Keyword::TABLE); let table_name = self.parse_object_name()?; - let columns = self.parse_parenthesized_column_list(Optional)?; + let is_mysql = dialect_of!(self is MySqlDialect); + let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?; let partitioned = if self.parse_keyword(Keyword::PARTITION) { self.expect_token(&Token::LParen)?; @@ -5868,7 +5876,7 @@ impl<'a> Parser<'a> { }; // Hive allows you to specify columns after partitions as well if you want. - let after_columns = self.parse_parenthesized_column_list(Optional)?; + let after_columns = self.parse_parenthesized_column_list(Optional, false)?; let source = Box::new(self.parse_query()?); let on = if self.parse_keyword(Keyword::ON) { @@ -5878,7 +5886,7 @@ impl<'a> Parser<'a> { Some(ConflictTarget::OnConstraint(self.parse_object_name()?)) } else if self.peek_token() == Token::LParen { Some(ConflictTarget::Columns( - self.parse_parenthesized_column_list(IsOptional::Mandatory)?, + self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?, )) } else { None @@ -6091,7 +6099,7 @@ impl<'a> Parser<'a> { &mut self, ) -> Result, ParserError> { let opt_except = if self.parse_keyword(Keyword::EXCEPT) { - let idents = self.parse_parenthesized_column_list(Mandatory)?; + let idents = self.parse_parenthesized_column_list(Mandatory, false)?; match &idents[..] { [] => { return self.expected( @@ -6236,7 +6244,7 @@ impl<'a> Parser<'a> { }) } - pub fn parse_values(&mut self) -> Result { + pub fn parse_values(&mut self, allow_empty: bool) -> Result { let mut explicit_row = false; let rows = self.parse_comma_separated(|parser| { @@ -6245,9 +6253,14 @@ impl<'a> Parser<'a> { } parser.expect_token(&Token::LParen)?; - let exprs = parser.parse_comma_separated(Parser::parse_expr)?; - parser.expect_token(&Token::RParen)?; - Ok(exprs) + if allow_empty && parser.peek_token().token == Token::RParen { + parser.next_token(); + Ok(vec![]) + } else { + let exprs = parser.parse_comma_separated(Parser::parse_expr)?; + parser.expect_token(&Token::RParen)?; + Ok(exprs) + } })?; Ok(Values { explicit_row, rows }) } @@ -6413,9 +6426,10 @@ impl<'a> Parser<'a> { "INSERT in MATCHED merge clause".to_string(), )); } - let columns = self.parse_parenthesized_column_list(Optional)?; + let is_mysql = dialect_of!(self is MySqlDialect); + let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?; self.expect_keyword(Keyword::VALUES)?; - let values = self.parse_values()?; + let values = self.parse_values(is_mysql)?; MergeClause::NotMatched { predicate, columns, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 63ea02597..508ae8461 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -692,6 +692,41 @@ fn parse_simple_insert() { } } +#[test] +fn parse_empty_row_insert() { + let sql = "INSERT INTO tb () VALUES (), ()"; + + match mysql().one_statement_parses_to(sql, "INSERT INTO tb VALUES (), ()") { + Statement::Insert { + table_name, + columns, + source, + on, + .. + } => { + assert_eq!(ObjectName(vec![Ident::new("tb")]), table_name); + assert!(columns.is_empty()); + assert!(on.is_none()); + assert_eq!( + Box::new(Query { + with: None, + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![vec![], vec![]] + })), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + locks: vec![], + }), + source + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_insert_with_on_duplicate_update() { let sql = "INSERT INTO permission_groups (name, description, perm_create, perm_read, perm_update, perm_delete) VALUES ('accounting_manager', 'Some description about the group', true, true, true, true) ON DUPLICATE KEY UPDATE description = VALUES(description), perm_create = VALUES(perm_create), perm_read = VALUES(perm_read), perm_update = VALUES(perm_update), perm_delete = VALUES(perm_delete)"; From 17f604f75708affac3652a6be001dfbba0a2bce7 Mon Sep 17 00:00:00 2001 From: Jeffrey <22608443+Jefffrey@users.noreply.github.com> Date: Tue, 3 Jan 2023 02:29:06 +1100 Subject: [PATCH 145/806] Support RENAME for wildcard SELECTs (#784) --- src/ast/mod.rs | 8 +- src/ast/query.rs | 74 ++++++++++++++++- src/parser.rs | 39 ++++++++- tests/sqlparser_bigquery.rs | 63 +++++---------- tests/sqlparser_snowflake.rs | 149 ++++++++++++++++++++--------------- 5 files changed, 215 insertions(+), 118 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c3eb688db..b8d5cf042 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -34,10 +34,10 @@ pub use self::ddl::{ }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, ExceptSelectItem, ExcludeSelectItem, Fetch, Join, JoinConstraint, JoinOperator, - LateralView, LockClause, LockType, NonBlock, Offset, OffsetRows, OrderByExpr, Query, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, - TableWithJoins, Top, Values, WildcardAdditionalOptions, With, + Cte, ExceptSelectItem, ExcludeSelectItem, Fetch, IdentWithAlias, Join, JoinConstraint, + JoinOperator, LateralView, LockClause, LockType, NonBlock, Offset, OffsetRows, OrderByExpr, + Query, RenameSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, + Table, TableAlias, TableFactor, TableWithJoins, Top, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, diff --git a/src/ast/query.rs b/src/ast/query.rs index 55699f40b..6d90c0cea 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -365,7 +365,27 @@ pub enum SelectItem { Wildcard(WildcardAdditionalOptions), } -/// Additional options for wildcards, e.g. Snowflake `EXCLUDE` and Bigquery `EXCEPT`. +/// Single aliased identifier +/// +/// # Syntax +/// ```plaintext +/// AS +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] +pub struct IdentWithAlias { + pub ident: Ident, + pub alias: Ident, +} + +impl fmt::Display for IdentWithAlias { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} AS {}", self.ident, self.alias) + } +} + +/// Additional options for wildcards, e.g. Snowflake `EXCLUDE`/`RENAME` and Bigquery `EXCEPT`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -374,6 +394,8 @@ pub struct WildcardAdditionalOptions { pub opt_exclude: Option, /// `[EXCEPT...]`. pub opt_except: Option, + /// `[RENAME ...]`. + pub opt_rename: Option, } impl fmt::Display for WildcardAdditionalOptions { @@ -384,6 +406,9 @@ impl fmt::Display for WildcardAdditionalOptions { if let Some(except) = &self.opt_except { write!(f, " {except}")?; } + if let Some(rename) = &self.opt_rename { + write!(f, " {rename}")?; + } Ok(()) } } @@ -429,6 +454,47 @@ impl fmt::Display for ExcludeSelectItem { } } +/// Snowflake `RENAME` information. +/// +/// # Syntax +/// ```plaintext +/// AS +/// | ( AS , AS , ...) +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit))] +pub enum RenameSelectItem { + /// Single column name with alias without parenthesis. + /// + /// # Syntax + /// ```plaintext + /// AS + /// ``` + Single(IdentWithAlias), + /// Multiple column names with aliases inside parenthesis. + /// # Syntax + /// ```plaintext + /// ( AS , AS , ...) + /// ``` + Multiple(Vec), +} + +impl fmt::Display for RenameSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RENAME")?; + match self { + Self::Single(column) => { + write!(f, " {column}")?; + } + Self::Multiple(columns) => { + write!(f, " ({})", display_comma_separated(columns))?; + } + } + Ok(()) + } +} + /// Bigquery `EXCEPT` information, with at least one column. /// /// # Syntax @@ -440,7 +506,7 @@ impl fmt::Display for ExcludeSelectItem { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct ExceptSelectItem { /// First guaranteed column. - pub fist_elemnt: Ident, + pub first_element: Ident, /// Additional columns. This list can be empty. pub additional_elements: Vec, } @@ -449,12 +515,12 @@ impl fmt::Display for ExceptSelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "EXCEPT ")?; if self.additional_elements.is_empty() { - write!(f, "({})", self.fist_elemnt)?; + write!(f, "({})", self.first_element)?; } else { write!( f, "({}, {})", - self.fist_elemnt, + self.first_element, display_comma_separated(&self.additional_elements) )?; } diff --git a/src/parser.rs b/src/parser.rs index 76d370041..34398ec4c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4401,6 +4401,14 @@ impl<'a> Parser<'a> { Ok(values) } + /// Strictly parse `identifier AS identifier` + pub fn parse_identifier_with_alias(&mut self) -> Result { + let ident = self.parse_identifier()?; + self.expect_keyword(Keyword::AS)?; + let alias = self.parse_identifier()?; + Ok(IdentWithAlias { ident, alias }) + } + /// Parse `AS identifier` (or simply `identifier` if it's not a reserved keyword) /// Some examples with aliases: `SELECT 1 foo`, `SELECT COUNT(*) AS cnt`, /// `SELECT ... FROM t1 foo, t2 bar`, `SELECT ... FROM (...) AS bar` @@ -4432,7 +4440,7 @@ impl<'a> Parser<'a> { // ignore the and treat the multiple strings as // a single ." Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))), - // Support for MySql dialect double qouted string, `AS "HOUR"` for example + // Support for MySql dialect double quoted string, `AS "HOUR"` for example Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))), _ => { if after_as { @@ -6063,10 +6071,16 @@ impl<'a> Parser<'a> { } else { None }; + let opt_rename = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_rename()? + } else { + None + }; Ok(WildcardAdditionalOptions { opt_exclude, opt_except, + opt_rename, }) } @@ -6108,7 +6122,7 @@ impl<'a> Parser<'a> { )?; } [first, idents @ ..] => Some(ExceptSelectItem { - fist_elemnt: first.clone(), + first_element: first.clone(), additional_elements: idents.to_vec(), }), } @@ -6119,6 +6133,27 @@ impl<'a> Parser<'a> { Ok(opt_except) } + /// Parse a [`Rename`](RenameSelectItem) information for wildcard select items. + pub fn parse_optional_select_item_rename( + &mut self, + ) -> Result, ParserError> { + let opt_rename = if self.parse_keyword(Keyword::RENAME) { + if self.consume_token(&Token::LParen) { + let idents = + self.parse_comma_separated(|parser| parser.parse_identifier_with_alias())?; + self.expect_token(&Token::RParen)?; + Some(RenameSelectItem::Multiple(idents)) + } else { + let ident = self.parse_identifier_with_alias()?; + Some(RenameSelectItem::Single(ident)) + } + } else { + None + }; + + Ok(opt_rename) + } + /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parse_expr()?; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 4bf26ba81..ada5fec5e 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -265,51 +265,26 @@ fn parse_array_agg_func() { #[test] fn test_select_wildcard_with_except() { - match bigquery_and_generic().verified_stmt("SELECT * EXCEPT (col_a) FROM data") { - Statement::Query(query) => match *query.body { - SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(WildcardAdditionalOptions { - opt_except: Some(except), - .. - }) => { - assert_eq!( - *except, - ExceptSelectItem { - fist_elemnt: Ident::new("col_a"), - additional_elements: vec![] - } - ) - } - _ => unreachable!(), - }, - _ => unreachable!(), - }, - _ => unreachable!(), - }; + let select = bigquery_and_generic().verified_only_select("SELECT * EXCEPT (col_a) FROM data"); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_except: Some(ExceptSelectItem { + first_element: Ident::new("col_a"), + additional_elements: vec![], + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); - match bigquery_and_generic() - .verified_stmt("SELECT * EXCEPT (department_id, employee_id) FROM employee_table") - { - Statement::Query(query) => match *query.body { - SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(WildcardAdditionalOptions { - opt_except: Some(except), - .. - }) => { - assert_eq!( - *except, - ExceptSelectItem { - fist_elemnt: Ident::new("department_id"), - additional_elements: vec![Ident::new("employee_id")] - } - ) - } - _ => unreachable!(), - }, - _ => unreachable!(), - }, - _ => unreachable!(), - }; + let select = bigquery_and_generic() + .verified_only_select("SELECT * EXCEPT (department_id, employee_id) FROM employee_table"); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_except: Some(ExceptSelectItem { + first_element: Ident::new("department_id"), + additional_elements: vec![Ident::new("employee_id")], + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); assert_eq!( bigquery_and_generic() diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e10141545..78a31e383 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -389,70 +389,91 @@ fn snowflake_and_generic() -> TestedDialects { #[test] fn test_select_wildcard_with_exclude() { - match snowflake_and_generic().verified_stmt("SELECT * EXCLUDE (col_a) FROM data") { - Statement::Query(query) => match *query.body { - SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: Some(exclude), - .. - }) => { - assert_eq!( - *exclude, - ExcludeSelectItem::Multiple(vec![Ident::new("col_a")]) - ) - } - _ => unreachable!(), - }, - _ => unreachable!(), + let select = snowflake_and_generic().verified_only_select("SELECT * EXCLUDE (col_a) FROM data"); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(ExcludeSelectItem::Multiple(vec![Ident::new("col_a")])), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); + + let select = snowflake_and_generic() + .verified_only_select("SELECT name.* EXCLUDE department_id FROM employee_table"); + let expected = SelectItem::QualifiedWildcard( + ObjectName(vec![Ident::new("name")]), + WildcardAdditionalOptions { + opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), + ..Default::default() }, - _ => unreachable!(), - }; - - match snowflake_and_generic() - .verified_stmt("SELECT name.* EXCLUDE department_id FROM employee_table") - { - Statement::Query(query) => match *query.body { - SetExpr::Select(select) => match &select.projection[0] { - SelectItem::QualifiedWildcard( - _, - WildcardAdditionalOptions { - opt_exclude: Some(exclude), - .. - }, - ) => { - assert_eq!( - *exclude, - ExcludeSelectItem::Single(Ident::new("department_id")) - ) - } - _ => unreachable!(), - }, - _ => unreachable!(), - }, - _ => unreachable!(), - }; - - match snowflake_and_generic() - .verified_stmt("SELECT * EXCLUDE (department_id, employee_id) FROM employee_table") - { - Statement::Query(query) => match *query.body { - SetExpr::Select(select) => match &select.projection[0] { - SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: Some(exclude), - .. - }) => { - assert_eq!( - *exclude, - ExcludeSelectItem::Multiple(vec![ - Ident::new("department_id"), - Ident::new("employee_id") - ]) - ) - } - _ => unreachable!(), - }, - _ => unreachable!(), + ); + assert_eq!(expected, select.projection[0]); + + let select = snowflake_and_generic() + .verified_only_select("SELECT * EXCLUDE (department_id, employee_id) FROM employee_table"); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(ExcludeSelectItem::Multiple(vec![ + Ident::new("department_id"), + Ident::new("employee_id"), + ])), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); +} + +#[test] +fn test_select_wildcard_with_rename() { + let select = + snowflake_and_generic().verified_only_select("SELECT * RENAME col_a AS col_b FROM data"); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_rename: Some(RenameSelectItem::Single(IdentWithAlias { + ident: Ident::new("col_a"), + alias: Ident::new("col_b"), + })), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); + + let select = snowflake_and_generic().verified_only_select( + "SELECT name.* RENAME (department_id AS new_dep, employee_id AS new_emp) FROM employee_table", + ); + let expected = SelectItem::QualifiedWildcard( + ObjectName(vec![Ident::new("name")]), + WildcardAdditionalOptions { + opt_rename: Some(RenameSelectItem::Multiple(vec![ + IdentWithAlias { + ident: Ident::new("department_id"), + alias: Ident::new("new_dep"), + }, + IdentWithAlias { + ident: Ident::new("employee_id"), + alias: Ident::new("new_emp"), + }, + ])), + ..Default::default() }, - _ => unreachable!(), - }; + ); + assert_eq!(expected, select.projection[0]); +} + +#[test] +fn test_select_wildcard_with_exclude_and_rename() { + let select = snowflake_and_generic() + .verified_only_select("SELECT * EXCLUDE col_z RENAME col_a AS col_b FROM data"); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("col_z"))), + opt_rename: Some(RenameSelectItem::Single(IdentWithAlias { + ident: Ident::new("col_a"), + alias: Ident::new("col_b"), + })), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); + + // rename cannot precede exclude + assert_eq!( + snowflake_and_generic() + .parse_sql_statements("SELECT * RENAME col_a AS col_b EXCLUDE col_z FROM data") + .unwrap_err() + .to_string(), + "sql parser error: Expected end of statement, found: EXCLUDE" + ); } From 204bdc5b62d18f4f82b25432049d972f7bcc7acd Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 2 Jan 2023 10:40:42 -0500 Subject: [PATCH 146/806] Fix logical conflict (#785) --- src/ast/query.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 6d90c0cea..f63234b09 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -373,7 +373,7 @@ pub enum SelectItem { /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct IdentWithAlias { pub ident: Ident, pub alias: Ident, @@ -463,7 +463,7 @@ impl fmt::Display for ExcludeSelectItem { /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum RenameSelectItem { /// Single column name with alias without parenthesis. /// From d0ef564fa1a56a5d984b972ff935c56e7e51b66e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 2 Jan 2023 10:46:27 -0500 Subject: [PATCH 147/806] Changelog for 0.30.0 (#786) --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa05f4066..ed14aa787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.30.0] 2023-01-02 + +### Added +* Support `RENAME` for wildcard `SELECTs` (#784) - Thanks @Jefffrey +* Add a mutable visitor (#782) - Thanks @lovasoa + +### Changed +* Allow parsing of mysql empty row inserts (#783) - Thanks @Jefffrey + +### Fixed +* Fix logical conflict (#785) - Thanks @alamb + ## [0.29.0] 2022-12-29 ### Highlights From 260a012f8c6894ae50b9f8957de45203b3c1eeab Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 2 Jan 2023 10:47:00 -0500 Subject: [PATCH 148/806] (cargo-release) version 0.30.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 19cc623d8..1cc7a33ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.29.0" +version = "0.30.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 82cf554394c8c05bac18bd996531a09991805dd3 Mon Sep 17 00:00:00 2001 From: Justin Joyce Date: Mon, 9 Jan 2023 22:00:55 +0000 Subject: [PATCH 149/806] Add User (#787) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c2ae31564..1195cc941 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,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] and [GlueSQL]. +[LocustDB], [Ballista], [GlueSQL], and [Opteryx]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. @@ -178,6 +178,7 @@ licensed as above, without any additional terms or conditions. [LocustDB]: https://github.com/cswinter/LocustDB [Ballista]: https://github.com/apache/arrow-ballista [GlueSQL]: https://github.com/gluesql/gluesql +[Opteryx]: https://github.com/mabel-dev/opteryx [Pratt Parser]: https://tdop.github.io/ [sql-2016-grammar]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html [sql-standard]: https://en.wikipedia.org/wiki/ISO/IEC_9075 From 3f874f4ab8ad17f5149b57183436a4cdf4cf0704 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 9 Jan 2023 23:04:00 +0100 Subject: [PATCH 150/806] use post_* visitors for mutable visits (#789) My initial implementation of mutable visitors closely matched the existing immutable visitor implementations, and used pre_* visitors (a mutable expression was first passed the visitor, then its children were visited). After some initial usage, I realize this actually impractical, and can easily lead to infinite recursion when one isn't careful. For instance a mutable visitor would replace x by f(x), then would be called again on the nested x and result in f(f(x)), then again resulting in f(f(f(x))), and so on. So, this commit changes the behavior of the visit_*_mut functions to call the visitor on an expression only once all of its children have been visited. It also makes the documentation more explicit and adds an example. --- src/ast/visitor.rs | 50 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 553e014a4..1a9eeaa24 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -301,7 +301,7 @@ impl ControlFlow> Visitor for RelationVisitor impl ControlFlow> VisitorMut for RelationVisitor { type Break = E; - fn pre_visit_relation(&mut self, relation: &mut ObjectName) -> ControlFlow { + fn post_visit_relation(&mut self, relation: &mut ObjectName) -> ControlFlow { self.0(relation) } } @@ -343,7 +343,10 @@ where ControlFlow::Continue(()) } -/// Invokes the provided closure on all relations (e.g. table names) present in `v` +/// Invokes the provided closure with a mutable reference to all relations (e.g. table names) +/// present in `v`. +/// +/// When the closure mutates its argument, the new mutated relation will not be visited again. /// /// # Example /// ``` @@ -386,7 +389,7 @@ impl ControlFlow> Visitor for ExprVisitor { impl ControlFlow> VisitorMut for ExprVisitor { type Break = E; - fn pre_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow { + fn post_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow { self.0(expr) } } @@ -430,9 +433,14 @@ where ControlFlow::Continue(()) } -/// Invokes the provided closure on all expressions present in `v` +/// Invokes the provided closure iteratively with a mutable reference to all expressions +/// present in `v`. +/// +/// This performs a depth-first search, so if the closure mutates the expression /// /// # Example +/// +/// ## Remove all select limits in sub-queries /// ``` /// # use sqlparser::parser::Parser; /// # use sqlparser::dialect::GenericDialect; @@ -451,6 +459,35 @@ where /// /// assert_eq!(statements[0].to_string(), "SELECT (SELECT y FROM z) FROM t LIMIT 3"); /// ``` +/// +/// ## Wrap column name in function call +/// +/// This demonstrates how to effectively replace an expression with another more complicated one +/// that references the original. This example avoids unnecessary allocations by using the +/// [`std::mem`](std::mem) family of functions. +/// +/// ``` +/// # use sqlparser::parser::Parser; +/// # use sqlparser::dialect::GenericDialect; +/// # use sqlparser::ast::{Expr, Function, FunctionArg, FunctionArgExpr, Ident, ObjectName, Value, visit_expressions_mut, visit_statements_mut}; +/// # use core::ops::ControlFlow; +/// let sql = "SELECT x, y FROM t"; +/// let mut statements = Parser::parse_sql(&GenericDialect{}, sql).unwrap(); +/// +/// visit_expressions_mut(&mut statements, |expr| { +/// if matches!(expr, Expr::Identifier(col_name) if col_name.value == "x") { +/// let old_expr = std::mem::replace(expr, Expr::Value(Value::Null)); +/// *expr = Expr::Function(Function { +/// name: ObjectName(vec![Ident::new("f")]), +/// args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(old_expr))], +/// over: None, distinct: false, special: false, +/// }); +/// } +/// ControlFlow::<()>::Continue(()) +/// }); +/// +/// assert_eq!(statements[0].to_string(), "SELECT f(x), y FROM t"); +/// ``` pub fn visit_expressions_mut(v: &mut V, f: F) -> ControlFlow where V: VisitMut, @@ -473,12 +510,13 @@ impl ControlFlow> Visitor for StatementVisitor impl ControlFlow> VisitorMut for StatementVisitor { type Break = E; - fn pre_visit_statement(&mut self, statement: &mut Statement) -> ControlFlow { + fn post_visit_statement(&mut self, statement: &mut Statement) -> ControlFlow { self.0(statement) } } -/// Invokes the provided closure on all statements (e.g. `SELECT`, `CREATE TABLE`, etc) present in `v` +/// Invokes the provided closure iteratively with a mutable reference to all statements +/// present in `v` (e.g. `SELECT`, `CREATE TABLE`, etc). /// /// # Example /// ``` From ca939413f2fa8367fc5f4c499b3a56561348b70b Mon Sep 17 00:00:00 2001 From: SARDONYX <68905624+SARDONYX-sard@users.noreply.github.com> Date: Sun, 15 Jan 2023 19:31:49 +0900 Subject: [PATCH 151/806] fix(Dialect): fix a typo (precendence -> precedence) (#794) * fix(Dialect): fix a typo * fix: fix a typo in tests --- src/dialect/mod.rs | 2 +- tests/sqlparser_custom_dialect.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 676344720..a13339a9d 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -88,7 +88,7 @@ pub trait Dialect: Debug + Any { &self, _parser: &mut Parser, _expr: &Expr, - _precendence: u8, + _precedence: u8, ) -> Option> { // return None to fall back to the default behavior None diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index c0fe4c1dd..a700dda11 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -69,7 +69,7 @@ fn custom_infix_parser() -> Result<(), ParserError> { &self, parser: &mut Parser, expr: &Expr, - _precendence: u8, + _precedence: u8, ) -> Option> { if parser.consume_token(&Token::Plus) { Some(Ok(Expr::BinaryOp { From 4955863bdf0e9d90e0fff72bca7912b3d20daac2 Mon Sep 17 00:00:00 2001 From: Ziinc Date: Sun, 22 Jan 2023 01:05:59 +0800 Subject: [PATCH 152/806] fix: handle bigquery offset in map key (#797) * fix: handle bigquery offset in map key * chore: add in more comments and tests * chore: fix all linting and compilation warnings --- src/parser.rs | 8 +++++++- tests/sqlparser_bigquery.rs | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 34398ec4c..3883a333d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4183,7 +4183,13 @@ impl<'a> Parser<'a> { pub fn parse_map_key(&mut self) -> Result { let next_token = self.next_token(); match next_token.token { - Token::Word(Word { value, keyword, .. }) if keyword == Keyword::NoKeyword => { + // handle bigquery offset subscript operator which overlaps with OFFSET operator + Token::Word(Word { value, keyword, .. }) + if (dialect_of!(self is BigQueryDialect) && keyword == Keyword::OFFSET) => + { + self.parse_function(ObjectName(vec![Ident::new(value)])) + } + Token::Word(Word { value, keyword, .. }) if (keyword == Keyword::NoKeyword) => { if self.peek_token() == Token::LParen { return self.parse_function(ObjectName(vec![Ident::new(value)])); } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index ada5fec5e..3fe5cd92b 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -13,10 +13,9 @@ #[macro_use] mod test_utils; -use test_utils::*; - use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; +use test_utils::*; #[test] fn parse_literal_string() { @@ -306,3 +305,37 @@ fn bigquery_and_generic() -> TestedDialects { dialects: vec![Box::new(BigQueryDialect {}), Box::new(GenericDialect {})], } } + +#[test] +fn parse_map_access_offset() { + let sql = "SELECT d[offset(0)]"; + let _select = bigquery().verified_only_select(sql); + #[cfg(not(feature = "bigdecimal"))] + assert_eq!( + _select.projection[0], + SelectItem::UnnamedExpr(Expr::MapAccess { + column: Box::new(Expr::Identifier(Ident { + value: "d".to_string(), + quote_style: None, + })), + keys: vec![Expr::Function(Function { + name: ObjectName(vec!["offset".into()]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::Number("0".into(), false) + ))),], + over: None, + distinct: false, + special: false, + })], + }) + ); + + // test other operators + for sql in [ + "SELECT d[SAFE_OFFSET(0)]", + "SELECT d[ORDINAL(0)]", + "SELECT d[SAFE_ORDINAL(0)]", + ] { + bigquery().verified_only_select(sql); + } +} From b31ede77331ad0cd14f50246714f68f9d2fa432f Mon Sep 17 00:00:00 2001 From: Y Togami <62130798+togami2864@users.noreply.github.com> Date: Sat, 18 Feb 2023 03:24:50 +0900 Subject: [PATCH 153/806] chore: fix clippy error in ci (#803) * chore: fix clippy error in ci * chore: fix fmt --- examples/cli.rs | 8 +- examples/parse_select.rs | 2 +- src/ast/data_type.rs | 22 +- src/ast/ddl.rs | 54 ++-- src/ast/mod.rs | 413 ++++++++++++++---------------- src/ast/query.rs | 92 ++++--- src/ast/value.rs | 18 +- src/ast/visitor.rs | 12 +- src/parser.rs | 26 +- src/test_utils.rs | 3 +- src/tokenizer.rs | 37 ++- tests/sqlparser_bigquery.rs | 4 +- tests/sqlparser_common.rs | 102 ++------ tests/sqlparser_custom_dialect.rs | 6 +- tests/sqlparser_mysql.rs | 2 +- tests/sqlparser_postgres.rs | 14 +- 16 files changed, 371 insertions(+), 444 deletions(-) diff --git a/examples/cli.rs b/examples/cli.rs index fcaf1b144..53ef1abb1 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -46,7 +46,7 @@ $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] "--hive" => Box::new(HiveDialect {}), "--redshift" => Box::new(RedshiftSqlDialect {}), "--generic" | "" => Box::new(GenericDialect {}), - s => panic!("Unexpected parameter: {}", s), + s => panic!("Unexpected parameter: {s}"), }; println!("Parsing from file '{}' using {:?}", &filename, dialect); @@ -75,16 +75,16 @@ $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] #[cfg(feature = "json_example")] { let serialized = serde_json::to_string_pretty(&statements).unwrap(); - println!("Serialized as JSON:\n{}", serialized); + println!("Serialized as JSON:\n{serialized}"); } } else { - println!("Parse results:\n{:#?}", statements); + println!("Parse results:\n{statements:#?}"); } std::process::exit(0); } Err(e) => { - println!("Error during parsing: {:?}", e); + println!("Error during parsing: {e:?}"); std::process::exit(1); } } diff --git a/examples/parse_select.rs b/examples/parse_select.rs index e7aa16307..71fe1fa1e 100644 --- a/examples/parse_select.rs +++ b/examples/parse_select.rs @@ -25,5 +25,5 @@ fn main() { let ast = Parser::parse_sql(&dialect, sql).unwrap(); - println!("AST: {:?}", ast); + println!("AST: {ast:?}"); } diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index ba90659ab..ec5f256f2 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -186,13 +186,13 @@ impl fmt::Display for DataType { } DataType::Blob(size) => format_type_with_optional_length(f, "BLOB", size, false), DataType::Numeric(info) => { - write!(f, "NUMERIC{}", info) + write!(f, "NUMERIC{info}") } DataType::Decimal(info) => { - write!(f, "DECIMAL{}", info) + write!(f, "DECIMAL{info}") } DataType::Dec(info) => { - write!(f, "DEC{}", info) + write!(f, "DEC{info}") } DataType::Float(size) => format_type_with_optional_length(f, "FLOAT", size, false), DataType::TinyInt(zerofill) => { @@ -250,14 +250,14 @@ impl fmt::Display for DataType { DataType::Bytea => write!(f, "BYTEA"), DataType::Array(ty) => { if let Some(t) = &ty { - write!(f, "{}[]", t) + write!(f, "{t}[]") } else { write!(f, "ARRAY") } } DataType::Custom(ty, modifiers) => { if modifiers.is_empty() { - write!(f, "{}", ty) + write!(f, "{ty}") } else { write!(f, "{}({})", ty, modifiers.join(", ")) } @@ -292,9 +292,9 @@ fn format_type_with_optional_length( len: &Option, unsigned: bool, ) -> fmt::Result { - write!(f, "{}", sql_type)?; + write!(f, "{sql_type}")?; if let Some(len) = len { - write!(f, "({})", len)?; + write!(f, "({len})")?; } if unsigned { write!(f, " UNSIGNED")?; @@ -307,9 +307,9 @@ fn format_character_string_type( sql_type: &str, size: &Option, ) -> fmt::Result { - write!(f, "{}", sql_type)?; + write!(f, "{sql_type}")?; if let Some(size) = size { - write!(f, "({})", size)?; + write!(f, "({size})")?; } Ok(()) } @@ -320,7 +320,7 @@ fn format_datetime_precision_and_tz( len: &Option, time_zone: &TimezoneInfo, ) -> fmt::Result { - write!(f, "{}", sql_type)?; + write!(f, "{sql_type}")?; let len_fmt = len.as_ref().map(|l| format!("({l})")).unwrap_or_default(); match time_zone { @@ -432,7 +432,7 @@ impl fmt::Display for CharacterLength { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.length)?; if let Some(unit) = &self.unit { - write!(f, " {}", unit)?; + write!(f, " {unit}")?; } Ok(()) } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 8576d7277..31e944b6b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -117,7 +117,7 @@ impl fmt::Display for AlterTableOperation { display_comma_separated(new_partitions), ine = if *if_not_exists { " IF NOT EXISTS" } else { "" } ), - AlterTableOperation::AddConstraint(c) => write!(f, "ADD {}", c), + AlterTableOperation::AddConstraint(c) => write!(f, "ADD {c}"), AlterTableOperation::AddColumn { column_keyword, if_not_exists, @@ -135,7 +135,7 @@ impl fmt::Display for AlterTableOperation { Ok(()) } AlterTableOperation::AlterColumn { column_name, op } => { - write!(f, "ALTER COLUMN {} {}", column_name, op) + write!(f, "ALTER COLUMN {column_name} {op}") } AlterTableOperation::DropPartitions { partitions, @@ -183,13 +183,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::RenameColumn { old_column_name, new_column_name, - } => write!( - f, - "RENAME COLUMN {} TO {}", - old_column_name, new_column_name - ), + } => write!(f, "RENAME COLUMN {old_column_name} TO {new_column_name}"), AlterTableOperation::RenameTable { table_name } => { - write!(f, "RENAME TO {}", table_name) + write!(f, "RENAME TO {table_name}") } AlterTableOperation::ChangeColumn { old_name, @@ -197,7 +193,7 @@ impl fmt::Display for AlterTableOperation { data_type, options, } => { - write!(f, "CHANGE COLUMN {} {} {}", old_name, new_name, data_type)?; + write!(f, "CHANGE COLUMN {old_name} {new_name} {data_type}")?; if options.is_empty() { Ok(()) } else { @@ -205,7 +201,7 @@ impl fmt::Display for AlterTableOperation { } } AlterTableOperation::RenameConstraint { old_name, new_name } => { - write!(f, "RENAME CONSTRAINT {} TO {}", old_name, new_name) + write!(f, "RENAME CONSTRAINT {old_name} TO {new_name}") } } } @@ -215,7 +211,7 @@ impl fmt::Display for AlterIndexOperation { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { AlterIndexOperation::RenameIndex { index_name } => { - write!(f, "RENAME TO {}", index_name) + write!(f, "RENAME TO {index_name}") } } } @@ -248,16 +244,16 @@ impl fmt::Display for AlterColumnOperation { AlterColumnOperation::SetNotNull => write!(f, "SET NOT NULL",), AlterColumnOperation::DropNotNull => write!(f, "DROP NOT NULL",), AlterColumnOperation::SetDefault { value } => { - write!(f, "SET DEFAULT {}", value) + write!(f, "SET DEFAULT {value}") } AlterColumnOperation::DropDefault {} => { write!(f, "DROP DEFAULT") } AlterColumnOperation::SetDataType { data_type, using } => { if let Some(expr) = using { - write!(f, "SET DATA TYPE {} USING {}", data_type, expr) + write!(f, "SET DATA TYPE {data_type} USING {expr}") } else { - write!(f, "SET DATA TYPE {}", data_type) + write!(f, "SET DATA TYPE {data_type}") } } } @@ -369,10 +365,10 @@ impl fmt::Display for TableConstraint { display_comma_separated(referred_columns), )?; if let Some(action) = on_delete { - write!(f, " ON DELETE {}", action)?; + write!(f, " ON DELETE {action}")?; } if let Some(action) = on_update { - write!(f, " ON UPDATE {}", action)?; + write!(f, " ON UPDATE {action}")?; } Ok(()) } @@ -387,10 +383,10 @@ impl fmt::Display for TableConstraint { } => { write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" })?; if let Some(name) = name { - write!(f, " {}", name)?; + write!(f, " {name}")?; } if let Some(index_type) = index_type { - write!(f, " USING {}", index_type)?; + write!(f, " USING {index_type}")?; } write!(f, " ({})", display_comma_separated(columns))?; @@ -409,11 +405,11 @@ impl fmt::Display for TableConstraint { } if !matches!(index_type_display, KeyOrIndexDisplay::None) { - write!(f, " {}", index_type_display)?; + write!(f, " {index_type_display}")?; } if let Some(name) = opt_index_name { - write!(f, " {}", name)?; + write!(f, " {name}")?; } write!(f, " ({})", display_comma_separated(columns))?; @@ -500,7 +496,7 @@ impl fmt::Display for ColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {}", self.name, self.data_type)?; for option in &self.options { - write!(f, " {}", option)?; + write!(f, " {option}")?; } Ok(()) } @@ -580,7 +576,7 @@ impl fmt::Display for ColumnOption { match self { Null => write!(f, "NULL"), NotNull => write!(f, "NOT NULL"), - Default(expr) => write!(f, "DEFAULT {}", expr), + Default(expr) => write!(f, "DEFAULT {expr}"), Unique { is_primary } => { write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }) } @@ -590,23 +586,23 @@ impl fmt::Display for ColumnOption { on_delete, on_update, } => { - write!(f, "REFERENCES {}", foreign_table)?; + write!(f, "REFERENCES {foreign_table}")?; if !referred_columns.is_empty() { write!(f, " ({})", display_comma_separated(referred_columns))?; } if let Some(action) = on_delete { - write!(f, " ON DELETE {}", action)?; + write!(f, " ON DELETE {action}")?; } if let Some(action) = on_update { - write!(f, " ON UPDATE {}", action)?; + write!(f, " ON UPDATE {action}")?; } Ok(()) } - Check(expr) => write!(f, "CHECK ({})", expr), + Check(expr) => write!(f, "CHECK ({expr})"), DialectSpecific(val) => write!(f, "{}", display_separated(val, " ")), - CharacterSet(n) => write!(f, "CHARACTER SET {}", n), + CharacterSet(n) => write!(f, "CHARACTER SET {n}"), Comment(v) => write!(f, "COMMENT '{}'", escape_single_quote_string(v)), - OnUpdate(expr) => write!(f, "ON UPDATE {}", expr), + OnUpdate(expr) => write!(f, "ON UPDATE {expr}"), } } } @@ -616,7 +612,7 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { impl<'a> fmt::Display for ConstraintName<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(name) = self.0 { - write!(f, "CONSTRAINT {} ", name)?; + write!(f, "CONSTRAINT {name} ")?; } Ok(()) } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b8d5cf042..a8c934396 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -71,9 +71,9 @@ where fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut delim = ""; for t in self.slice { - write!(f, "{}", delim)?; + write!(f, "{delim}")?; delim = self.sep; - write!(f, "{}", t)?; + write!(f, "{t}")?; } Ok(()) } @@ -145,7 +145,7 @@ impl fmt::Display for Ident { match self.quote_style { Some(q) if q == '"' || q == '\'' || q == '`' => { let escaped = value::escape_quoted_string(&self.value, q); - write!(f, "{}{}{}", q, escaped, q) + write!(f, "{q}{escaped}{q}") } Some(q) if q == '[' => write!(f, "[{}]", self.value), None => f.write_str(&self.value), @@ -529,27 +529,27 @@ pub enum Expr { impl fmt::Display for Expr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Expr::Identifier(s) => write!(f, "{}", s), + Expr::Identifier(s) => write!(f, "{s}"), Expr::MapAccess { column, keys } => { - write!(f, "{}", column)?; + write!(f, "{column}")?; for k in keys { match k { - k @ Expr::Value(Value::Number(_, _)) => write!(f, "[{}]", k)?, - Expr::Value(Value::SingleQuotedString(s)) => write!(f, "[\"{}\"]", s)?, - _ => write!(f, "[{}]", k)?, + k @ Expr::Value(Value::Number(_, _)) => write!(f, "[{k}]")?, + Expr::Value(Value::SingleQuotedString(s)) => write!(f, "[\"{s}\"]")?, + _ => write!(f, "[{k}]")?, } } Ok(()) } Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), - Expr::IsTrue(ast) => write!(f, "{} IS TRUE", ast), - Expr::IsNotTrue(ast) => write!(f, "{} IS NOT TRUE", ast), - Expr::IsFalse(ast) => write!(f, "{} IS FALSE", ast), - Expr::IsNotFalse(ast) => write!(f, "{} IS NOT FALSE", ast), - Expr::IsNull(ast) => write!(f, "{} IS NULL", ast), - Expr::IsNotNull(ast) => write!(f, "{} IS NOT NULL", ast), - Expr::IsUnknown(ast) => write!(f, "{} IS UNKNOWN", ast), - Expr::IsNotUnknown(ast) => write!(f, "{} IS NOT UNKNOWN", ast), + Expr::IsTrue(ast) => write!(f, "{ast} IS TRUE"), + Expr::IsNotTrue(ast) => write!(f, "{ast} IS NOT TRUE"), + Expr::IsFalse(ast) => write!(f, "{ast} IS FALSE"), + Expr::IsNotFalse(ast) => write!(f, "{ast} IS NOT FALSE"), + Expr::IsNull(ast) => write!(f, "{ast} IS NULL"), + Expr::IsNotNull(ast) => write!(f, "{ast} IS NOT NULL"), + Expr::IsUnknown(ast) => write!(f, "{ast} IS UNKNOWN"), + Expr::IsNotUnknown(ast) => write!(f, "{ast} IS NOT UNKNOWN"), Expr::InList { expr, list, @@ -596,7 +596,7 @@ impl fmt::Display for Expr { low, high ), - Expr::BinaryOp { left, op, right } => write!(f, "{} {} {}", left, op, right), + Expr::BinaryOp { left, op, right } => write!(f, "{left} {op} {right}"), Expr::Like { negated, expr, @@ -663,46 +663,46 @@ impl fmt::Display for Expr { pattern ), }, - Expr::AnyOp(expr) => write!(f, "ANY({})", expr), - Expr::AllOp(expr) => write!(f, "ALL({})", expr), + Expr::AnyOp(expr) => write!(f, "ANY({expr})"), + Expr::AllOp(expr) => write!(f, "ALL({expr})"), Expr::UnaryOp { op, expr } => { if op == &UnaryOperator::PGPostfixFactorial { - write!(f, "{}{}", expr, op) + write!(f, "{expr}{op}") } else if op == &UnaryOperator::Not { - write!(f, "{} {}", op, expr) + write!(f, "{op} {expr}") } else { - write!(f, "{}{}", op, expr) + write!(f, "{op}{expr}") } } - Expr::Cast { expr, data_type } => write!(f, "CAST({} AS {})", expr, data_type), - Expr::TryCast { expr, data_type } => write!(f, "TRY_CAST({} AS {})", expr, data_type), - Expr::SafeCast { expr, data_type } => write!(f, "SAFE_CAST({} AS {})", expr, data_type), - Expr::Extract { field, expr } => write!(f, "EXTRACT({} FROM {})", field, expr), + Expr::Cast { expr, data_type } => write!(f, "CAST({expr} AS {data_type})"), + Expr::TryCast { expr, data_type } => write!(f, "TRY_CAST({expr} AS {data_type})"), + Expr::SafeCast { expr, data_type } => write!(f, "SAFE_CAST({expr} AS {data_type})"), + Expr::Extract { field, expr } => write!(f, "EXTRACT({field} FROM {expr})"), Expr::Ceil { expr, field } => { if field == &DateTimeField::NoDateTime { - write!(f, "CEIL({})", expr) + write!(f, "CEIL({expr})") } else { - write!(f, "CEIL({} TO {})", expr, field) + write!(f, "CEIL({expr} TO {field})") } } Expr::Floor { expr, field } => { if field == &DateTimeField::NoDateTime { - write!(f, "FLOOR({})", expr) + write!(f, "FLOOR({expr})") } else { - write!(f, "FLOOR({} TO {})", expr, field) + write!(f, "FLOOR({expr} TO {field})") } } - Expr::Position { expr, r#in } => write!(f, "POSITION({} IN {})", expr, r#in), - Expr::Collate { expr, collation } => write!(f, "{} COLLATE {}", expr, collation), - Expr::Nested(ast) => write!(f, "({})", ast), - Expr::Value(v) => write!(f, "{}", v), + Expr::Position { expr, r#in } => write!(f, "POSITION({expr} IN {in})"), + Expr::Collate { expr, collation } => write!(f, "{expr} COLLATE {collation}"), + Expr::Nested(ast) => write!(f, "({ast})"), + Expr::Value(v) => write!(f, "{v}"), Expr::TypedString { data_type, value } => { - write!(f, "{}", data_type)?; + write!(f, "{data_type}")?; write!(f, " '{}'", &value::escape_single_quote_string(value)) } - Expr::Function(fun) => write!(f, "{}", fun), + Expr::Function(fun) => write!(f, "{fun}"), Expr::AggregateExpressionWithFilter { expr, filter } => { - write!(f, "{} FILTER (WHERE {})", expr, filter) + write!(f, "{expr} FILTER (WHERE {filter})") } Expr::Case { operand, @@ -712,14 +712,14 @@ impl fmt::Display for Expr { } => { write!(f, "CASE")?; if let Some(operand) = operand { - write!(f, " {}", operand)?; + write!(f, " {operand}")?; } for (c, r) in conditions.iter().zip(results) { - write!(f, " WHEN {} THEN {}", c, r)?; + write!(f, " WHEN {c} THEN {r}")?; } if let Some(else_result) = else_result { - write!(f, " ELSE {}", else_result)?; + write!(f, " ELSE {else_result}")?; } write!(f, " END") } @@ -729,15 +729,15 @@ impl fmt::Display for Expr { if *negated { "NOT " } else { "" }, subquery ), - Expr::Subquery(s) => write!(f, "({})", s), - Expr::ArraySubquery(s) => write!(f, "ARRAY({})", s), - Expr::ListAgg(listagg) => write!(f, "{}", listagg), - Expr::ArrayAgg(arrayagg) => write!(f, "{}", arrayagg), + Expr::Subquery(s) => write!(f, "({s})"), + Expr::ArraySubquery(s) => write!(f, "ARRAY({s})"), + Expr::ListAgg(listagg) => write!(f, "{listagg}"), + Expr::ArrayAgg(arrayagg) => write!(f, "{arrayagg}"), Expr::GroupingSets(sets) => { write!(f, "GROUPING SETS (")?; let mut sep = ""; for set in sets { - write!(f, "{}", sep)?; + write!(f, "{sep}")?; sep = ", "; write!(f, "({})", display_comma_separated(set))?; } @@ -747,7 +747,7 @@ impl fmt::Display for Expr { write!(f, "CUBE (")?; let mut sep = ""; for set in sets { - write!(f, "{}", sep)?; + write!(f, "{sep}")?; sep = ", "; if set.len() == 1 { write!(f, "{}", set[0])?; @@ -761,7 +761,7 @@ impl fmt::Display for Expr { write!(f, "ROLLUP (")?; let mut sep = ""; for set in sets { - write!(f, "{}", sep)?; + write!(f, "{sep}")?; sep = ", "; if set.len() == 1 { write!(f, "{}", set[0])?; @@ -776,12 +776,12 @@ impl fmt::Display for Expr { substring_from, substring_for, } => { - write!(f, "SUBSTRING({}", expr)?; + write!(f, "SUBSTRING({expr}")?; if let Some(from_part) = substring_from { - write!(f, " FROM {}", from_part)?; + write!(f, " FROM {from_part}")?; } if let Some(for_part) = substring_for { - write!(f, " FOR {}", for_part)?; + write!(f, " FOR {for_part}")?; } write!(f, ")") @@ -794,17 +794,16 @@ impl fmt::Display for Expr { } => { write!( f, - "OVERLAY({} PLACING {} FROM {}", - expr, overlay_what, overlay_from + "OVERLAY({expr} PLACING {overlay_what} FROM {overlay_from}" )?; if let Some(for_part) = overlay_for { - write!(f, " FOR {}", for_part)?; + write!(f, " FOR {for_part}")?; } write!(f, ")") } - Expr::IsDistinctFrom(a, b) => write!(f, "{} IS DISTINCT FROM {}", a, b), - Expr::IsNotDistinctFrom(a, b) => write!(f, "{} IS NOT DISTINCT FROM {}", a, b), + Expr::IsDistinctFrom(a, b) => write!(f, "{a} IS DISTINCT FROM {b}"), + Expr::IsNotDistinctFrom(a, b) => write!(f, "{a} IS NOT DISTINCT FROM {b}"), Expr::Trim { expr, trim_where, @@ -812,12 +811,12 @@ impl fmt::Display for Expr { } => { write!(f, "TRIM(")?; if let Some(ident) = trim_where { - write!(f, "{} ", ident)?; + write!(f, "{ident} ")?; } if let Some(trim_char) = trim_what { - write!(f, "{} FROM {}", trim_char, expr)?; + write!(f, "{trim_char} FROM {expr}")?; } else { - write!(f, "{}", expr)?; + write!(f, "{expr}")?; } write!(f, ")") @@ -826,14 +825,14 @@ impl fmt::Display for Expr { write!(f, "({})", display_comma_separated(exprs)) } Expr::ArrayIndex { obj, indexes } => { - write!(f, "{}", obj)?; + write!(f, "{obj}")?; for i in indexes { - write!(f, "[{}]", i)?; + write!(f, "[{i}]")?; } Ok(()) } Expr::Array(set) => { - write!(f, "{}", set) + write!(f, "{set}") } Expr::JsonAccess { left, @@ -841,19 +840,19 @@ impl fmt::Display for Expr { right, } => { if operator == &JsonOperator::Colon { - write!(f, "{}{}{}", left, operator, right) + write!(f, "{left}{operator}{right}") } else { - write!(f, "{} {} {}", left, operator, right) + write!(f, "{left} {operator} {right}") } } Expr::CompositeAccess { expr, key } => { - write!(f, "{}.{}", expr, key) + write!(f, "{expr}.{key}") } Expr::AtTimeZone { timestamp, time_zone, } => { - write!(f, "{} AT TIME ZONE '{}'", timestamp, time_zone) + write!(f, "{timestamp} AT TIME ZONE '{time_zone}'") } Expr::Interval { value, @@ -867,8 +866,7 @@ impl fmt::Display for Expr { assert!(last_field.is_none()); write!( f, - "INTERVAL {} SECOND ({}, {})", - value, leading_precision, fractional_seconds_precision + "INTERVAL {value} SECOND ({leading_precision}, {fractional_seconds_precision})" ) } Expr::Interval { @@ -878,18 +876,18 @@ impl fmt::Display for Expr { last_field, fractional_seconds_precision, } => { - write!(f, "INTERVAL {}", value)?; + write!(f, "INTERVAL {value}")?; if let Some(leading_field) = leading_field { - write!(f, " {}", leading_field)?; + write!(f, " {leading_field}")?; } if let Some(leading_precision) = leading_precision { - write!(f, " ({})", leading_precision)?; + write!(f, " ({leading_precision})")?; } if let Some(last_field) = last_field { - write!(f, " TO {}", last_field)?; + write!(f, " TO {last_field}")?; } if let Some(fractional_seconds_precision) = fractional_seconds_precision { - write!(f, " ({})", fractional_seconds_precision)?; + write!(f, " ({fractional_seconds_precision})")?; } Ok(()) } @@ -1023,8 +1021,8 @@ impl fmt::Display for WindowFrameBound { WindowFrameBound::CurrentRow => f.write_str("CURRENT ROW"), WindowFrameBound::Preceding(None) => f.write_str("UNBOUNDED PRECEDING"), WindowFrameBound::Following(None) => f.write_str("UNBOUNDED FOLLOWING"), - WindowFrameBound::Preceding(Some(n)) => write!(f, "{} PRECEDING", n), - WindowFrameBound::Following(Some(n)) => write!(f, "{} FOLLOWING", n), + WindowFrameBound::Preceding(Some(n)) => write!(f, "{n} PRECEDING"), + WindowFrameBound::Following(Some(n)) => write!(f, "{n} FOLLOWING"), } } } @@ -1644,10 +1642,10 @@ impl fmt::Display for Statement { write!(f, "KILL ")?; if let Some(m) = modifier { - write!(f, "{} ", m)?; + write!(f, "{m} ")?; } - write!(f, "{}", id) + write!(f, "{id}") } Statement::ExplainTable { describe_alias, @@ -1659,7 +1657,7 @@ impl fmt::Display for Statement { write!(f, "EXPLAIN ")?; } - write!(f, "{}", table_name) + write!(f, "{table_name}") } Statement::Explain { describe_alias, @@ -1683,12 +1681,12 @@ impl fmt::Display for Statement { } if let Some(format) = format { - write!(f, "FORMAT {} ", format)?; + write!(f, "FORMAT {format} ")?; } - write!(f, "{}", statement) + write!(f, "{statement}") } - Statement::Query(s) => write!(f, "{}", s), + Statement::Query(s) => write!(f, "{s}"), Statement::Declare { name, binary, @@ -1697,7 +1695,7 @@ impl fmt::Display for Statement { hold, query, } => { - write!(f, "DECLARE {} ", name)?; + write!(f, "DECLARE {name} ")?; if *binary { write!(f, "BINARY ")?; @@ -1729,19 +1727,19 @@ impl fmt::Display for Statement { } } - write!(f, "FOR {}", query) + write!(f, "FOR {query}") } Statement::Fetch { name, direction, into, } => { - write!(f, "FETCH {} ", direction)?; + write!(f, "FETCH {direction} ")?; - write!(f, "IN {}", name)?; + write!(f, "IN {name}")?; if let Some(into) = into { - write!(f, " INTO {}", into)?; + write!(f, " INTO {into}")?; } Ok(()) @@ -1761,9 +1759,9 @@ impl fmt::Display for Statement { path = path )?; if let Some(ref ff) = file_format { - write!(f, " STORED AS {}", ff)? + write!(f, " STORED AS {ff}")? } - write!(f, " {}", source) + write!(f, " {source}") } Statement::Msck { table_name, @@ -1777,7 +1775,7 @@ impl fmt::Display for Statement { table = table_name )?; if let Some(pa) = partition_action { - write!(f, " {}", pa)?; + write!(f, " {pa}")?; } Ok(()) } @@ -1785,7 +1783,7 @@ impl fmt::Display for Statement { table_name, partitions, } => { - write!(f, "TRUNCATE TABLE {}", table_name)?; + write!(f, "TRUNCATE TABLE {table_name}")?; if let Some(ref parts) = partitions { if !parts.is_empty() { write!(f, " PARTITION ({})", display_comma_separated(parts))?; @@ -1802,7 +1800,7 @@ impl fmt::Display for Statement { noscan, compute_statistics, } => { - write!(f, "ANALYZE TABLE {}", table_name)?; + write!(f, "ANALYZE TABLE {table_name}")?; if let Some(ref parts) = partitions { if !parts.is_empty() { write!(f, " PARTITION ({})", display_comma_separated(parts))?; @@ -1840,7 +1838,7 @@ impl fmt::Display for Statement { returning, } => { if let Some(action) = or { - write!(f, "INSERT OR {} INTO {} ", action, table_name)?; + write!(f, "INSERT OR {action} INTO {table_name} ")?; } else { write!( f, @@ -1862,10 +1860,10 @@ impl fmt::Display for Statement { if !after_columns.is_empty() { write!(f, "({}) ", display_comma_separated(after_columns))?; } - write!(f, "{}", source)?; + write!(f, "{source}")?; if let Some(on) = on { - write!(f, "{}", on)?; + write!(f, "{on}")?; } if let Some(returning) = returning { @@ -1884,7 +1882,7 @@ impl fmt::Display for Statement { legacy_options, values, } => { - write!(f, "COPY {}", table_name)?; + write!(f, "COPY {table_name}")?; if !columns.is_empty() { write!(f, " ({})", display_comma_separated(columns))?; } @@ -1899,10 +1897,10 @@ impl fmt::Display for Statement { writeln!(f, ";")?; let mut delim = ""; for v in values { - write!(f, "{}", delim)?; + write!(f, "{delim}")?; delim = "\t"; if let Some(v) = v { - write!(f, "{}", v)?; + write!(f, "{v}")?; } else { write!(f, "\\N")?; } @@ -1918,15 +1916,15 @@ impl fmt::Display for Statement { selection, returning, } => { - write!(f, "UPDATE {}", table)?; + write!(f, "UPDATE {table}")?; if !assignments.is_empty() { write!(f, " SET {}", display_comma_separated(assignments))?; } if let Some(from) = from { - write!(f, " FROM {}", from)?; + write!(f, " FROM {from}")?; } if let Some(selection) = selection { - write!(f, " WHERE {}", selection)?; + write!(f, " WHERE {selection}")?; } if let Some(returning) = returning { write!(f, " RETURNING {}", display_comma_separated(returning))?; @@ -1939,12 +1937,12 @@ impl fmt::Display for Statement { selection, returning, } => { - write!(f, "DELETE FROM {}", table_name)?; + write!(f, "DELETE FROM {table_name}")?; if let Some(using) = using { - write!(f, " USING {}", using)?; + write!(f, " USING {using}")?; } if let Some(selection) = selection { - write!(f, " WHERE {}", selection)?; + write!(f, " WHERE {selection}")?; } if let Some(returning) = returning { write!(f, " RETURNING {}", display_comma_separated(returning))?; @@ -1952,7 +1950,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::Close { cursor } => { - write!(f, "CLOSE {}", cursor)?; + write!(f, "CLOSE {cursor}")?; Ok(()) } @@ -1966,12 +1964,12 @@ impl fmt::Display for Statement { if *if_not_exists { write!(f, " IF NOT EXISTS")?; } - write!(f, " {}", db_name)?; + write!(f, " {db_name}")?; if let Some(l) = location { - write!(f, " LOCATION '{}'", l)?; + write!(f, " LOCATION '{l}'")?; } if let Some(ml) = managed_location { - write!(f, " MANAGEDLOCATION '{}'", ml)?; + write!(f, " MANAGEDLOCATION '{ml}'")?; } Ok(()) } @@ -1993,7 +1991,7 @@ impl fmt::Display for Statement { write!(f, "({})", display_comma_separated(args))?; } if let Some(return_type) = return_type { - write!(f, " RETURNS {}", return_type)?; + write!(f, " RETURNS {return_type}")?; } write!(f, "{params}")?; Ok(()) @@ -2023,7 +2021,7 @@ impl fmt::Display for Statement { if !cluster_by.is_empty() { write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; } - write!(f, " AS {}", query) + write!(f, " AS {query}") } Statement::CreateTable { name, @@ -2099,11 +2097,11 @@ impl fmt::Display for Statement { // Only for Hive if let Some(l) = like { - write!(f, " LIKE {}", l)?; + write!(f, " LIKE {l}")?; } if let Some(c) = clone { - write!(f, " CLONE {}", c)?; + write!(f, " CLONE {c}")?; } match hive_distribution { @@ -2120,7 +2118,7 @@ impl fmt::Display for Statement { write!(f, " SORTED BY ({})", display_comma_separated(sorted_by))?; } if *num_buckets > 0 { - write!(f, " INTO {} BUCKETS", num_buckets)?; + write!(f, " INTO {num_buckets} BUCKETS")?; } } HiveDistributionStyle::SKEWED { @@ -2149,7 +2147,7 @@ impl fmt::Display for Statement { { match row_format { Some(HiveRowFormat::SERDE { class }) => { - write!(f, " ROW FORMAT SERDE '{}'", class)? + write!(f, " ROW FORMAT SERDE '{class}'")? } Some(HiveRowFormat::DELIMITED) => write!(f, " ROW FORMAT DELIMITED")?, None => (), @@ -2160,17 +2158,16 @@ impl fmt::Display for Statement { output_format, }) => write!( f, - " STORED AS INPUTFORMAT {} OUTPUTFORMAT {}", - input_format, output_format + " STORED AS INPUTFORMAT {input_format} OUTPUTFORMAT {output_format}" )?, Some(HiveIOFormat::FileFormat { format }) if !*external => { - write!(f, " STORED AS {}", format)? + write!(f, " STORED AS {format}")? } _ => (), } if !*external { if let Some(loc) = location { - write!(f, " LOCATION '{}'", loc)?; + write!(f, " LOCATION '{loc}'")?; } } } @@ -2193,16 +2190,16 @@ impl fmt::Display for Statement { write!(f, " WITH ({})", display_comma_separated(with_options))?; } if let Some(query) = query { - write!(f, " AS {}", query)?; + write!(f, " AS {query}")?; } if let Some(engine) = engine { - write!(f, " ENGINE={}", engine)?; + write!(f, " ENGINE={engine}")?; } if let Some(default_charset) = default_charset { - write!(f, " DEFAULT CHARSET={}", default_charset)?; + write!(f, " DEFAULT CHARSET={default_charset}")?; } if let Some(collation) = collation { - write!(f, " COLLATE={}", collation)?; + write!(f, " COLLATE={collation}")?; } if on_commit.is_some() { @@ -2212,7 +2209,7 @@ impl fmt::Display for Statement { Some(OnCommit::Drop) => "ON COMMIT DROP", None => "", }; - write!(f, " {}", on_commit)?; + write!(f, " {on_commit}")?; } Ok(()) @@ -2252,7 +2249,7 @@ impl fmt::Display for Statement { table_name = table_name )?; if let Some(value) = using { - write!(f, " USING {} ", value)?; + write!(f, " USING {value} ")?; } write!(f, "({})", display_separated(columns, ",")) } @@ -2318,15 +2315,15 @@ impl fmt::Display for Statement { } )?; if let Some(limit) = connection_limit { - write!(f, " CONNECTION LIMIT {}", limit)?; + write!(f, " CONNECTION LIMIT {limit}")?; } match password { - Some(Password::Password(pass)) => write!(f, " PASSWORD {}", pass), + Some(Password::Password(pass)) => write!(f, " PASSWORD {pass}"), Some(Password::NullPassword) => write!(f, " PASSWORD NULL"), None => Ok(()), }?; if let Some(until) = valid_until { - write!(f, " VALID UNTIL {}", until)?; + write!(f, " VALID UNTIL {until}")?; } if !in_role.is_empty() { write!(f, " IN ROLE {}", display_comma_separated(in_role))?; @@ -2344,15 +2341,15 @@ impl fmt::Display for Statement { write!(f, " ADMIN {}", display_comma_separated(admin))?; } if let Some(owner) = authorization_owner { - write!(f, " AUTHORIZATION {}", owner)?; + write!(f, " AUTHORIZATION {owner}")?; } Ok(()) } Statement::AlterTable { name, operation } => { - write!(f, "ALTER TABLE {} {}", name, operation) + write!(f, "ALTER TABLE {name} {operation}") } Statement::AlterIndex { name, operation } => { - write!(f, "ALTER INDEX {} {}", name, operation) + write!(f, "ALTER INDEX {name} {operation}") } Statement::Drop { object_type, @@ -2383,12 +2380,12 @@ impl fmt::Display for Statement { display_comma_separated(func_desc), )?; if let Some(op) = option { - write!(f, " {}", op)?; + write!(f, " {op}")?; } Ok(()) } Statement::Discard { object_type } => { - write!(f, "DISCARD {object_type}", object_type = object_type)?; + write!(f, "DISCARD {object_type}")?; Ok(()) } Self::SetRole { @@ -2457,12 +2454,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::ShowCreate { obj_type, obj_name } => { - write!( - f, - "SHOW CREATE {obj_type} {obj_name}", - obj_type = obj_type, - obj_name = obj_name, - )?; + write!(f, "SHOW CREATE {obj_type} {obj_name}",)?; Ok(()) } Statement::ShowColumns { @@ -2479,7 +2471,7 @@ impl fmt::Display for Statement { table_name = table_name, )?; if let Some(filter) = filter { - write!(f, " {}", filter)?; + write!(f, " {filter}")?; } Ok(()) } @@ -2496,28 +2488,28 @@ impl fmt::Display for Statement { full = if *full { "FULL " } else { "" }, )?; if let Some(db_name) = db_name { - write!(f, " FROM {}", db_name)?; + write!(f, " FROM {db_name}")?; } if let Some(filter) = filter { - write!(f, " {}", filter)?; + write!(f, " {filter}")?; } Ok(()) } Statement::ShowFunctions { filter } => { write!(f, "SHOW FUNCTIONS")?; if let Some(filter) = filter { - write!(f, " {}", filter)?; + write!(f, " {filter}")?; } Ok(()) } Statement::Use { db_name } => { - write!(f, "USE {}", db_name)?; + write!(f, "USE {db_name}")?; Ok(()) } Statement::ShowCollation { filter } => { write!(f, "SHOW COLLATION")?; if let Some(filter) = filter { - write!(f, " {}", filter)?; + write!(f, " {filter}")?; } Ok(()) } @@ -2542,7 +2534,7 @@ impl fmt::Display for Statement { write!(f, " {}", display_comma_separated(modes))?; } if let Some(snapshot_id) = snapshot { - write!(f, " SNAPSHOT {}", snapshot_id)?; + write!(f, " SNAPSHOT {snapshot_id}")?; } Ok(()) } @@ -2562,9 +2554,9 @@ impl fmt::Display for Statement { name = schema_name ), Statement::Assert { condition, message } => { - write!(f, "ASSERT {}", condition)?; + write!(f, "ASSERT {condition}")?; if let Some(m) = message { - write!(f, " AS {}", m)?; + write!(f, " AS {m}")?; } Ok(()) } @@ -2575,14 +2567,14 @@ impl fmt::Display for Statement { with_grant_option, granted_by, } => { - write!(f, "GRANT {} ", privileges)?; - write!(f, "ON {} ", objects)?; + write!(f, "GRANT {privileges} ")?; + write!(f, "ON {objects} ")?; write!(f, "TO {}", display_comma_separated(grantees))?; if *with_grant_option { write!(f, " WITH GRANT OPTION")?; } if let Some(grantor) = granted_by { - write!(f, " GRANTED BY {}", grantor)?; + write!(f, " GRANTED BY {grantor}")?; } Ok(()) } @@ -2593,11 +2585,11 @@ impl fmt::Display for Statement { granted_by, cascade, } => { - write!(f, "REVOKE {} ", privileges)?; - write!(f, "ON {} ", objects)?; + write!(f, "REVOKE {privileges} ")?; + write!(f, "ON {objects} ")?; write!(f, "FROM {}", display_comma_separated(grantees))?; if let Some(grantor) = granted_by { - write!(f, " GRANTED BY {}", grantor)?; + write!(f, " GRANTED BY {grantor}")?; } write!(f, " {}", if *cascade { "CASCADE" } else { "RESTRICT" })?; Ok(()) @@ -2609,7 +2601,7 @@ impl fmt::Display for Statement { name = name, ), Statement::Execute { name, parameters } => { - write!(f, "EXECUTE {}", name)?; + write!(f, "EXECUTE {name}")?; if !parameters.is_empty() { write!(f, "({})", display_comma_separated(parameters))?; } @@ -2620,27 +2612,27 @@ impl fmt::Display for Statement { data_types, statement, } => { - write!(f, "PREPARE {} ", name)?; + write!(f, "PREPARE {name} ")?; if !data_types.is_empty() { write!(f, "({}) ", display_comma_separated(data_types))?; } - write!(f, "AS {}", statement) + write!(f, "AS {statement}") } Statement::Comment { object_type, object_name, comment, } => { - write!(f, "COMMENT ON {} {} IS ", object_type, object_name)?; + write!(f, "COMMENT ON {object_type} {object_name} IS ")?; if let Some(c) = comment { - write!(f, "'{}'", c) + write!(f, "'{c}'") } else { write!(f, "NULL") } } Statement::Savepoint { name } => { write!(f, "SAVEPOINT ")?; - write!(f, "{}", name) + write!(f, "{name}") } Statement::Merge { into, @@ -2654,7 +2646,7 @@ impl fmt::Display for Statement { "MERGE{int} {table} USING {source} ", int = if *into { " INTO" } else { "" } )?; - write!(f, "ON {} ", on)?; + write!(f, "ON {on} ")?; write!(f, "{}", display_separated(clauses, " ")) } Statement::Cache { @@ -2672,7 +2664,7 @@ impl fmt::Display for Statement { table_name = table_name, )?; } else { - write!(f, "CACHE TABLE {table_name}", table_name = table_name,)?; + write!(f, "CACHE TABLE {table_name}",)?; } if !options.is_empty() { @@ -2695,13 +2687,9 @@ impl fmt::Display for Statement { if_exists, } => { if *if_exists { - write!( - f, - "UNCACHE TABLE IF EXISTS {table_name}", - table_name = table_name - ) + write!(f, "UNCACHE TABLE IF EXISTS {table_name}") } else { - write!(f, "UNCACHE TABLE {table_name}", table_name = table_name) + write!(f, "UNCACHE TABLE {table_name}") } } Statement::CreateSequence { @@ -2728,10 +2716,10 @@ impl fmt::Display for Statement { as_type = as_type )?; for sequence_option in sequence_options { - write!(f, "{}", sequence_option)?; + write!(f, "{sequence_option}")?; } if let Some(ob) = owned_by.as_ref() { - write!(f, " OWNED BY {}", ob)?; + write!(f, " OWNED BY {ob}")?; } write!(f, "") } @@ -2776,7 +2764,7 @@ impl fmt::Display for SequenceOptions { write!(f, " NO MINVALUE") } MinMaxValue::Some(minvalue) => { - write!(f, " MINVALUE {minvalue}", minvalue = minvalue) + write!(f, " MINVALUE {minvalue}") } }, SequenceOptions::MaxValue(value) => match value { @@ -2787,7 +2775,7 @@ impl fmt::Display for SequenceOptions { write!(f, " NO MAXVALUE") } MinMaxValue::Some(maxvalue) => { - write!(f, " MAXVALUE {maxvalue}", maxvalue = maxvalue) + write!(f, " MAXVALUE {maxvalue}") } }, SequenceOptions::StartWith(start, with) => { @@ -2881,7 +2869,7 @@ impl fmt::Display for OnConflict { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, " ON CONFLICT")?; if let Some(target) = &self.conflict_target { - write!(f, "{}", target)?; + write!(f, "{target}")?; } write!(f, " {}", self.action) } @@ -2890,7 +2878,7 @@ impl fmt::Display for ConflictTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ConflictTarget::Columns(cols) => write!(f, "({})", display_comma_separated(cols)), - ConflictTarget::OnConstraint(name) => write!(f, " ON CONSTRAINT {}", name), + ConflictTarget::OnConstraint(name) => write!(f, " ON CONSTRAINT {name}"), } } } @@ -2908,7 +2896,7 @@ impl fmt::Display for OnConflictAction { )?; } if let Some(selection) = &do_update.selection { - write!(f, " WHERE {}", selection)?; + write!(f, " WHERE {selection}")?; } Ok(()) } @@ -3143,8 +3131,8 @@ pub enum FunctionArgExpr { impl fmt::Display for FunctionArgExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - FunctionArgExpr::Expr(expr) => write!(f, "{}", expr), - FunctionArgExpr::QualifiedWildcard(prefix) => write!(f, "{}.*", prefix), + FunctionArgExpr::Expr(expr) => write!(f, "{expr}"), + FunctionArgExpr::QualifiedWildcard(prefix) => write!(f, "{prefix}.*"), FunctionArgExpr::Wildcard => f.write_str("*"), } } @@ -3161,8 +3149,8 @@ pub enum FunctionArg { impl fmt::Display for FunctionArg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - FunctionArg::Named { name, arg } => write!(f, "{} => {}", name, arg), - FunctionArg::Unnamed(unnamed_arg) => write!(f, "{}", unnamed_arg), + FunctionArg::Named { name, arg } => write!(f, "{name} => {arg}"), + FunctionArg::Unnamed(unnamed_arg) => write!(f, "{unnamed_arg}"), } } } @@ -3179,7 +3167,7 @@ impl fmt::Display for CloseCursor { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { CloseCursor::All => write!(f, "ALL"), - CloseCursor::Specific { name } => write!(f, "{}", name), + CloseCursor::Specific { name } => write!(f, "{name}"), } } } @@ -3232,7 +3220,7 @@ impl fmt::Display for Function { )?; if let Some(o) = &self.over { - write!(f, " OVER ({})", o)?; + write!(f, " OVER ({o})")?; } } @@ -3291,10 +3279,10 @@ impl fmt::Display for ListAgg { self.expr )?; if let Some(separator) = &self.separator { - write!(f, ", {}", separator)?; + write!(f, ", {separator}")?; } if let Some(on_overflow) = &self.on_overflow { - write!(f, "{}", on_overflow)?; + write!(f, "{on_overflow}")?; } write!(f, ")")?; if !self.within_group.is_empty() { @@ -3331,7 +3319,7 @@ impl fmt::Display for ListAggOnOverflow { ListAggOnOverflow::Truncate { filler, with_count } => { write!(f, " TRUNCATE")?; if let Some(filler) = filler { - write!(f, " {}", filler)?; + write!(f, " {filler}")?; } if *with_count { write!(f, " WITH")?; @@ -3368,16 +3356,16 @@ impl fmt::Display for ArrayAgg { )?; if !self.within_group { if let Some(order_by) = &self.order_by { - write!(f, " ORDER BY {}", order_by)?; + write!(f, " ORDER BY {order_by}")?; } if let Some(limit) = &self.limit { - write!(f, " LIMIT {}", limit)?; + write!(f, " LIMIT {limit}")?; } } write!(f, ")")?; if self.within_group { if let Some(order_by) = &self.order_by { - write!(f, " WITHIN GROUP (ORDER BY {})", order_by)?; + write!(f, " WITHIN GROUP (ORDER BY {order_by})")?; } } Ok(()) @@ -3507,8 +3495,8 @@ impl fmt::Display for TransactionMode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use TransactionMode::*; match self { - AccessMode(access_mode) => write!(f, "{}", access_mode), - IsolationLevel(iso_level) => write!(f, "ISOLATION LEVEL {}", iso_level), + AccessMode(access_mode) => write!(f, "{access_mode}"), + IsolationLevel(iso_level) => write!(f, "ISOLATION LEVEL {iso_level}"), } } } @@ -3568,7 +3556,7 @@ impl fmt::Display for ShowStatementFilter { match self { Like(pattern) => write!(f, "LIKE '{}'", value::escape_single_quote_string(pattern)), ILike(pattern) => write!(f, "ILIKE {}", value::escape_single_quote_string(pattern)), - Where(expr) => write!(f, "WHERE {}", expr), + Where(expr) => write!(f, "WHERE {expr}"), } } } @@ -3677,15 +3665,15 @@ impl fmt::Display for CopyOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use CopyOption::*; match self { - Format(name) => write!(f, "FORMAT {}", name), + Format(name) => write!(f, "FORMAT {name}"), Freeze(true) => write!(f, "FREEZE"), Freeze(false) => write!(f, "FREEZE FALSE"), - Delimiter(char) => write!(f, "DELIMITER '{}'", char), + Delimiter(char) => write!(f, "DELIMITER '{char}'"), Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)), Header(true) => write!(f, "HEADER"), Header(false) => write!(f, "HEADER FALSE"), - Quote(char) => write!(f, "QUOTE '{}'", char), - Escape(char) => write!(f, "ESCAPE '{}'", char), + Quote(char) => write!(f, "QUOTE '{char}'"), + Escape(char) => write!(f, "ESCAPE '{char}'"), ForceQuote(columns) => write!(f, "FORCE_QUOTE ({})", display_comma_separated(columns)), ForceNotNull(columns) => { write!(f, "FORCE_NOT_NULL ({})", display_comma_separated(columns)) @@ -3718,7 +3706,7 @@ impl fmt::Display for CopyLegacyOption { use CopyLegacyOption::*; match self { Binary => write!(f, "BINARY"), - Delimiter(char) => write!(f, "DELIMITER '{}'", char), + Delimiter(char) => write!(f, "DELIMITER '{char}'"), Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)), Csv(opts) => write!(f, "CSV {}", display_separated(opts, " ")), } @@ -3749,8 +3737,8 @@ impl fmt::Display for CopyLegacyCsvOption { use CopyLegacyCsvOption::*; match self { Header => write!(f, "HEADER"), - Quote(char) => write!(f, "QUOTE '{}'", char), - Escape(char) => write!(f, "ESCAPE '{}'", char), + Quote(char) => write!(f, "QUOTE '{char}'"), + Escape(char) => write!(f, "ESCAPE '{char}'"), ForceQuote(columns) => write!(f, "FORCE QUOTE {}", display_comma_separated(columns)), ForceNotNull(columns) => { write!(f, "FORCE NOT NULL {}", display_comma_separated(columns)) @@ -3787,7 +3775,7 @@ impl fmt::Display for MergeClause { } => { write!(f, " MATCHED")?; if let Some(pred) = predicate { - write!(f, " AND {}", pred)?; + write!(f, " AND {pred}")?; } write!( f, @@ -3798,7 +3786,7 @@ impl fmt::Display for MergeClause { MatchedDelete(predicate) => { write!(f, " MATCHED")?; if let Some(pred) = predicate { - write!(f, " AND {}", pred)?; + write!(f, " AND {pred}")?; } write!(f, " THEN DELETE") } @@ -3809,7 +3797,7 @@ impl fmt::Display for MergeClause { } => { write!(f, " NOT MATCHED")?; if let Some(pred) = predicate { - write!(f, " AND {}", pred)?; + write!(f, " AND {pred}")?; } write!( f, @@ -3944,14 +3932,14 @@ impl OperateFunctionArg { impl fmt::Display for OperateFunctionArg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(mode) = &self.mode { - write!(f, "{} ", mode)?; + write!(f, "{mode} ")?; } if let Some(name) = &self.name { - write!(f, "{} ", name)?; + write!(f, "{name} ")?; } write!(f, "{}", self.data_type)?; if let Some(default_expr) = &self.default_expr { - write!(f, " = {}", default_expr)?; + write!(f, " = {default_expr}")?; } Ok(()) } @@ -4164,14 +4152,14 @@ mod tests { vec![Expr::Identifier(Ident::new("a"))], vec![Expr::Identifier(Ident::new("b"))], ]); - assert_eq!("GROUPING SETS ((a), (b))", format!("{}", grouping_sets)); + assert_eq!("GROUPING SETS ((a), (b))", format!("{grouping_sets}")); // a and b in the same group let grouping_sets = Expr::GroupingSets(vec![vec![ Expr::Identifier(Ident::new("a")), Expr::Identifier(Ident::new("b")), ]]); - assert_eq!("GROUPING SETS ((a, b))", format!("{}", grouping_sets)); + assert_eq!("GROUPING SETS ((a, b))", format!("{grouping_sets}")); // (a, b) and (c, d) in different group let grouping_sets = Expr::GroupingSets(vec![ @@ -4184,28 +4172,25 @@ mod tests { Expr::Identifier(Ident::new("d")), ], ]); - assert_eq!( - "GROUPING SETS ((a, b), (c, d))", - format!("{}", grouping_sets) - ); + assert_eq!("GROUPING SETS ((a, b), (c, d))", format!("{grouping_sets}")); } #[test] fn test_rollup_display() { let rollup = Expr::Rollup(vec![vec![Expr::Identifier(Ident::new("a"))]]); - assert_eq!("ROLLUP (a)", format!("{}", rollup)); + assert_eq!("ROLLUP (a)", format!("{rollup}")); let rollup = Expr::Rollup(vec![vec![ Expr::Identifier(Ident::new("a")), Expr::Identifier(Ident::new("b")), ]]); - assert_eq!("ROLLUP ((a, b))", format!("{}", rollup)); + assert_eq!("ROLLUP ((a, b))", format!("{rollup}")); let rollup = Expr::Rollup(vec![ vec![Expr::Identifier(Ident::new("a"))], vec![Expr::Identifier(Ident::new("b"))], ]); - assert_eq!("ROLLUP (a, b)", format!("{}", rollup)); + assert_eq!("ROLLUP (a, b)", format!("{rollup}")); let rollup = Expr::Rollup(vec![ vec![Expr::Identifier(Ident::new("a"))], @@ -4215,25 +4200,25 @@ mod tests { ], vec![Expr::Identifier(Ident::new("d"))], ]); - assert_eq!("ROLLUP (a, (b, c), d)", format!("{}", rollup)); + assert_eq!("ROLLUP (a, (b, c), d)", format!("{rollup}")); } #[test] fn test_cube_display() { let cube = Expr::Cube(vec![vec![Expr::Identifier(Ident::new("a"))]]); - assert_eq!("CUBE (a)", format!("{}", cube)); + assert_eq!("CUBE (a)", format!("{cube}")); let cube = Expr::Cube(vec![vec![ Expr::Identifier(Ident::new("a")), Expr::Identifier(Ident::new("b")), ]]); - assert_eq!("CUBE ((a, b))", format!("{}", cube)); + assert_eq!("CUBE ((a, b))", format!("{cube}")); let cube = Expr::Cube(vec![ vec![Expr::Identifier(Ident::new("a"))], vec![Expr::Identifier(Ident::new("b"))], ]); - assert_eq!("CUBE (a, b)", format!("{}", cube)); + assert_eq!("CUBE (a, b)", format!("{cube}")); let cube = Expr::Cube(vec![ vec![Expr::Identifier(Ident::new("a"))], @@ -4243,6 +4228,6 @@ mod tests { ], vec![Expr::Identifier(Ident::new("d"))], ]); - assert_eq!("CUBE (a, (b, c), d)", format!("{}", cube)); + assert_eq!("CUBE (a, (b, c), d)", format!("{cube}")); } } diff --git a/src/ast/query.rs b/src/ast/query.rs index f63234b09..d64babadf 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -46,20 +46,20 @@ 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)?; + write!(f, "{with} ")?; } write!(f, "{}", self.body)?; if !self.order_by.is_empty() { write!(f, " ORDER BY {}", display_comma_separated(&self.order_by))?; } if let Some(ref limit) = self.limit { - write!(f, " LIMIT {}", limit)?; + write!(f, " LIMIT {limit}")?; } if let Some(ref offset) = self.offset { - write!(f, " {}", offset)?; + write!(f, " {offset}")?; } if let Some(ref fetch) = self.fetch { - write!(f, " {}", fetch)?; + write!(f, " {fetch}")?; } if !self.locks.is_empty() { write!(f, " {}", display_separated(&self.locks, " "))?; @@ -95,25 +95,23 @@ pub enum 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::Table(t) => write!(f, "{}", t), + 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::Table(t) => write!(f, "{t}"), SetExpr::SetOperation { left, right, op, set_quantifier, } => { - write!(f, "{} {}", left, op)?; + write!(f, "{left} {op}")?; match set_quantifier { - SetQuantifier::All | SetQuantifier::Distinct => { - write!(f, " {}", set_quantifier)? - } - SetQuantifier::None => write!(f, "{}", set_quantifier)?, + SetQuantifier::All | SetQuantifier::Distinct => write!(f, " {set_quantifier}")?, + SetQuantifier::None => write!(f, "{set_quantifier}")?, } - write!(f, " {}", right)?; + write!(f, " {right}")?; Ok(()) } } @@ -224,12 +222,12 @@ impl fmt::Display for Select { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "SELECT{}", if self.distinct { " DISTINCT" } else { "" })?; if let Some(ref top) = self.top { - write!(f, " {}", top)?; + write!(f, " {top}")?; } write!(f, " {}", display_comma_separated(&self.projection))?; if let Some(ref into) = self.into { - write!(f, " {}", into)?; + write!(f, " {into}")?; } if !self.from.is_empty() { @@ -237,11 +235,11 @@ impl fmt::Display for Select { } if !self.lateral_views.is_empty() { for lv in &self.lateral_views { - write!(f, "{}", lv)?; + write!(f, "{lv}")?; } } if let Some(ref selection) = self.selection { - write!(f, " WHERE {}", selection)?; + write!(f, " WHERE {selection}")?; } if !self.group_by.is_empty() { write!(f, " GROUP BY {}", display_comma_separated(&self.group_by))?; @@ -264,10 +262,10 @@ impl fmt::Display for Select { write!(f, " SORT BY {}", display_comma_separated(&self.sort_by))?; } if let Some(ref having) = self.having { - write!(f, " HAVING {}", having)?; + write!(f, " HAVING {having}")?; } if let Some(ref qualify) = self.qualify { - write!(f, " QUALIFY {}", qualify)?; + write!(f, " QUALIFY {qualify}")?; } Ok(()) } @@ -344,7 +342,7 @@ impl fmt::Display for Cte { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} AS ({})", self.alias, self.query)?; if let Some(ref fr) = self.from { - write!(f, " FROM {}", fr)?; + write!(f, " FROM {fr}")?; } Ok(()) } @@ -531,10 +529,10 @@ impl fmt::Display for ExceptSelectItem { impl fmt::Display for SelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { - SelectItem::UnnamedExpr(expr) => write!(f, "{}", expr), - SelectItem::ExprWithAlias { expr, alias } => write!(f, "{} AS {}", expr, alias), + SelectItem::UnnamedExpr(expr) => write!(f, "{expr}"), + SelectItem::ExprWithAlias { expr, alias } => write!(f, "{expr} AS {alias}"), SelectItem::QualifiedWildcard(prefix, additional_options) => { - write!(f, "{}.*", prefix)?; + write!(f, "{prefix}.*")?; write!(f, "{additional_options}")?; Ok(()) } @@ -559,7 +557,7 @@ impl fmt::Display for TableWithJoins { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.relation)?; for join in &self.joins { - write!(f, "{}", join)?; + write!(f, "{join}")?; } Ok(()) } @@ -632,12 +630,12 @@ impl fmt::Display for TableFactor { args, with_hints, } => { - write!(f, "{}", name)?; + write!(f, "{name}")?; if let Some(args) = args { write!(f, "({})", display_comma_separated(args))?; } if let Some(alias) = alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } if !with_hints.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_hints))?; @@ -652,16 +650,16 @@ impl fmt::Display for TableFactor { if *lateral { write!(f, "LATERAL ")?; } - write!(f, "({})", subquery)?; + write!(f, "({subquery})")?; if let Some(alias) = alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } Ok(()) } TableFactor::TableFunction { expr, alias } => { - write!(f, "TABLE({})", expr)?; + write!(f, "TABLE({expr})")?; if let Some(alias) = alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } Ok(()) } @@ -671,15 +669,15 @@ impl fmt::Display for TableFactor { with_offset, with_offset_alias, } => { - write!(f, "UNNEST({})", array_expr)?; + write!(f, "UNNEST({array_expr})")?; if let Some(alias) = alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } if *with_offset { write!(f, " WITH OFFSET")?; } if let Some(alias) = with_offset_alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } Ok(()) } @@ -687,9 +685,9 @@ impl fmt::Display for TableFactor { table_with_joins, alias, } => { - write!(f, "({})", table_with_joins)?; + write!(f, "({table_with_joins})")?; if let Some(alias) = alias { - write!(f, " AS {}", alias)?; + write!(f, " AS {alias}")?; } Ok(()) } @@ -736,7 +734,7 @@ impl fmt::Display for Join { impl<'a> fmt::Display for Suffix<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.0 { - JoinConstraint::On(expr) => write!(f, " ON {}", expr), + JoinConstraint::On(expr) => write!(f, " ON {expr}"), JoinConstraint::Using(attrs) => { write!(f, " USING({})", display_comma_separated(attrs)) } @@ -921,9 +919,9 @@ impl fmt::Display for Fetch { let extension = if self.with_ties { "WITH TIES" } else { "ONLY" }; if let Some(ref quantity) = self.quantity { let percent = if self.percent { " PERCENT" } else { "" }; - write!(f, "FETCH FIRST {}{} ROWS {}", quantity, percent, extension) + write!(f, "FETCH FIRST {quantity}{percent} ROWS {extension}") } else { - write!(f, "FETCH FIRST ROWS {}", extension) + write!(f, "FETCH FIRST ROWS {extension}") } } } @@ -941,10 +939,10 @@ impl fmt::Display for LockClause { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "FOR {}", &self.lock_type)?; if let Some(ref of) = self.of { - write!(f, " OF {}", of)?; + write!(f, " OF {of}")?; } if let Some(ref nb) = self.nonblock { - write!(f, " {}", nb)?; + write!(f, " {nb}")?; } Ok(()) } @@ -964,7 +962,7 @@ impl fmt::Display for LockType { LockType::Share => "SHARE", LockType::Update => "UPDATE", }; - write!(f, "{}", select_lock) + write!(f, "{select_lock}") } } @@ -982,7 +980,7 @@ impl fmt::Display for NonBlock { NonBlock::Nowait => "NOWAIT", NonBlock::SkipLocked => "SKIP LOCKED", }; - write!(f, "{}", nonblock) + write!(f, "{nonblock}") } } @@ -1001,9 +999,9 @@ impl fmt::Display for Top { let extension = if self.with_ties { " WITH TIES" } else { "" }; if let Some(ref quantity) = self.quantity { let percent = if self.percent { " PERCENT" } else { "" }; - write!(f, "TOP ({}){}{}", quantity, percent, extension) + write!(f, "TOP ({quantity}){percent}{extension}") } else { - write!(f, "TOP{}", extension) + write!(f, "TOP{extension}") } } } @@ -1024,7 +1022,7 @@ impl fmt::Display for Values { let prefix = if self.explicit_row { "ROW" } else { "" }; let mut delim = ""; for row in &self.rows { - write!(f, "{}", delim)?; + write!(f, "{delim}")?; delim = ", "; write!(f, "{prefix}({})", display_comma_separated(row))?; } diff --git a/src/ast/value.rs b/src/ast/value.rs index 4072c0a4c..0adb2d5dc 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -61,16 +61,16 @@ impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Value::Number(v, l) => write!(f, "{}{long}", v, long = if *l { "L" } else { "" }), - Value::DoubleQuotedString(v) => write!(f, "\"{}\"", v), + Value::DoubleQuotedString(v) => write!(f, "\"{v}\""), Value::SingleQuotedString(v) => write!(f, "'{}'", escape_single_quote_string(v)), - Value::DollarQuotedString(v) => write!(f, "{}", v), + Value::DollarQuotedString(v) => write!(f, "{v}"), Value::EscapedStringLiteral(v) => write!(f, "E'{}'", escape_escaped_string(v)), - Value::NationalStringLiteral(v) => write!(f, "N'{}'", v), - Value::HexStringLiteral(v) => write!(f, "X'{}'", v), - Value::Boolean(v) => write!(f, "{}", v), + Value::NationalStringLiteral(v) => write!(f, "N'{v}'"), + Value::HexStringLiteral(v) => write!(f, "X'{v}'"), + Value::Boolean(v) => write!(f, "{v}"), Value::Null => write!(f, "NULL"), - Value::Placeholder(v) => write!(f, "{}", v), - Value::UnQuotedString(v) => write!(f, "{}", v), + Value::Placeholder(v) => write!(f, "{v}"), + Value::UnQuotedString(v) => write!(f, "{v}"), } } } @@ -178,7 +178,7 @@ impl<'a> fmt::Display for EscapeQuotedString<'a> { if c == self.quote { write!(f, "{q}{q}", q = self.quote)?; } else { - write!(f, "{}", c)?; + write!(f, "{c}")?; } } Ok(()) @@ -215,7 +215,7 @@ impl<'a> fmt::Display for EscapeEscapedStringLiteral<'a> { write!(f, r#"\r"#)?; } _ => { - write!(f, "{}", c)?; + write!(f, "{c}")?; } } } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 1a9eeaa24..6787bfd68 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -600,32 +600,32 @@ mod tests { type Break = (); fn pre_visit_relation(&mut self, relation: &ObjectName) -> ControlFlow { - self.visited.push(format!("PRE: RELATION: {}", relation)); + self.visited.push(format!("PRE: RELATION: {relation}")); ControlFlow::Continue(()) } fn post_visit_relation(&mut self, relation: &ObjectName) -> ControlFlow { - self.visited.push(format!("POST: RELATION: {}", relation)); + self.visited.push(format!("POST: RELATION: {relation}")); ControlFlow::Continue(()) } fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow { - self.visited.push(format!("PRE: EXPR: {}", expr)); + self.visited.push(format!("PRE: EXPR: {expr}")); ControlFlow::Continue(()) } fn post_visit_expr(&mut self, expr: &Expr) -> ControlFlow { - self.visited.push(format!("POST: EXPR: {}", expr)); + self.visited.push(format!("POST: EXPR: {expr}")); ControlFlow::Continue(()) } fn pre_visit_statement(&mut self, statement: &Statement) -> ControlFlow { - self.visited.push(format!("PRE: STATEMENT: {}", statement)); + self.visited.push(format!("PRE: STATEMENT: {statement}")); ControlFlow::Continue(()) } fn post_visit_statement(&mut self, statement: &Statement) -> ControlFlow { - self.visited.push(format!("POST: STATEMENT: {}", statement)); + self.visited.push(format!("POST: STATEMENT: {statement}")); ControlFlow::Continue(()) } } diff --git a/src/parser.rs b/src/parser.rs index 3883a333d..58252d3fe 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -805,7 +805,7 @@ impl<'a> Parser<'a> { let tok = self.next_token(); let key = match tok.token { Token::Word(word) => word.to_ident(), - _ => return parser_err!(format!("Expected identifier, found: {}", tok)), + _ => return parser_err!(format!("Expected identifier, found: {tok}")), }; Ok(Expr::CompositeAccess { expr: Box::new(expr), @@ -2083,7 +2083,7 @@ impl<'a> Parser<'a> { /// Report unexpected token pub fn expected(&self, expected: &str, found: TokenWithLocation) -> Result { - parser_err!(format!("Expected {}, found: {}", expected, found)) + parser_err!(format!("Expected {expected}, found: {found}")) } /// Look for an expected keyword and consume it if it exists @@ -2135,7 +2135,7 @@ impl<'a> Parser<'a> { if let Some(keyword) = self.parse_one_of_keywords(keywords) { Ok(keyword) } else { - let keywords: Vec = keywords.iter().map(|x| format!("{:?}", x)).collect(); + let keywords: Vec = keywords.iter().map(|x| format!("{x:?}")).collect(); self.expected( &format!("one of {}", keywords.join(" or ")), self.peek_token(), @@ -2495,7 +2495,7 @@ impl<'a> Parser<'a> { Keyword::ARCHIVE => Ok(Some(CreateFunctionUsing::Archive(uri))), _ => self.expected( "JAR, FILE or ARCHIVE, got {:?}", - TokenWithLocation::wrap(Token::make_keyword(format!("{:?}", keyword).as_str())), + TokenWithLocation::wrap(Token::make_keyword(format!("{keyword:?}").as_str())), ), } } @@ -4028,7 +4028,7 @@ impl<'a> Parser<'a> { fn parse_literal_char(&mut self) -> Result { let s = self.parse_literal_string()?; if s.len() != 1 { - return parser_err!(format!("Expect a char, found {:?}", s)); + return parser_err!(format!("Expect a char, found {s:?}")); } Ok(s.chars().next().unwrap()) } @@ -4107,7 +4107,7 @@ impl<'a> Parser<'a> { // (i.e., it returns the input string). Token::Number(ref n, l) => match n.parse() { Ok(n) => Ok(Value::Number(n, l)), - Err(e) => parser_err!(format!("Could not parse '{}' as number: {}", n, e)), + Err(e) => parser_err!(format!("Could not parse '{n}' as number: {e}")), }, Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), @@ -4147,7 +4147,7 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { Token::Number(s, _) => s.parse::().map_err(|e| { - ParserError::ParserError(format!("Could not parse '{}' as u64: {}", s, e)) + ParserError::ParserError(format!("Could not parse '{s}' as u64: {e}")) }), _ => self.expected("literal int", next_token), } @@ -5267,8 +5267,7 @@ impl<'a> Parser<'a> { Keyword::EVENT => Ok(ShowCreateObject::Event), Keyword::VIEW => Ok(ShowCreateObject::View), keyword => Err(ParserError::ParserError(format!( - "Unable to map keyword to ShowCreateObject: {:?}", - keyword + "Unable to map keyword to ShowCreateObject: {keyword:?}" ))), }?; @@ -5437,8 +5436,7 @@ impl<'a> Parser<'a> { } _ => { return Err(ParserError::ParserError(format!( - "expected OUTER, SEMI, ANTI or JOIN after {:?}", - kw + "expected OUTER, SEMI, ANTI or JOIN after {kw:?}" ))) } } @@ -5561,8 +5559,7 @@ impl<'a> Parser<'a> { // but not `FROM (mytable AS alias1) AS alias2`. if let Some(inner_alias) = alias { return Err(ParserError::ParserError(format!( - "duplicate alias {}", - inner_alias + "duplicate alias {inner_alias}" ))); } // Act as if the alias was specified normally next @@ -5731,8 +5728,7 @@ impl<'a> Parser<'a> { if !err.is_empty() { let errors: Vec = err.into_iter().filter_map(|x| x.err()).collect(); return Err(ParserError::ParserError(format!( - "INTERNAL ERROR: GRANT/REVOKE unexpected keyword(s) - {:?}", - errors + "INTERNAL ERROR: GRANT/REVOKE unexpected keyword(s) - {errors:?}" ))); } let act = actions.into_iter().filter_map(|x| x.ok()).collect(); diff --git a/src/test_utils.rs b/src/test_utils.rs index 79c24e292..d5bafd90f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -49,8 +49,7 @@ impl TestedDialects { if let Some((prev_dialect, prev_parsed)) = s { assert_eq!( prev_parsed, parsed, - "Parse results with {:?} are different from {:?}", - prev_dialect, dialect + "Parse results with {prev_dialect:?} are different from {dialect:?}" ); } Some((dialect, parsed)) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index dcf128542..eef4cb7b4 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -180,17 +180,17 @@ impl fmt::Display for Token { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Token::EOF => f.write_str("EOF"), - Token::Word(ref w) => write!(f, "{}", w), + Token::Word(ref w) => write!(f, "{w}"), Token::Number(ref n, l) => write!(f, "{}{long}", n, long = if *l { "L" } else { "" }), - Token::Char(ref c) => write!(f, "{}", c), - Token::SingleQuotedString(ref s) => write!(f, "'{}'", s), - Token::DoubleQuotedString(ref s) => write!(f, "\"{}\"", s), - Token::DollarQuotedString(ref s) => write!(f, "{}", s), - Token::NationalStringLiteral(ref s) => write!(f, "N'{}'", s), - Token::EscapedStringLiteral(ref s) => write!(f, "E'{}'", s), - Token::HexStringLiteral(ref s) => write!(f, "X'{}'", s), + Token::Char(ref c) => write!(f, "{c}"), + Token::SingleQuotedString(ref s) => write!(f, "'{s}'"), + Token::DoubleQuotedString(ref s) => write!(f, "\"{s}\""), + Token::DollarQuotedString(ref s) => write!(f, "{s}"), + Token::NationalStringLiteral(ref s) => write!(f, "N'{s}'"), + Token::EscapedStringLiteral(ref s) => write!(f, "E'{s}'"), + Token::HexStringLiteral(ref s) => write!(f, "X'{s}'"), Token::Comma => f.write_str(","), - Token::Whitespace(ws) => write!(f, "{}", ws), + Token::Whitespace(ws) => write!(f, "{ws}"), Token::DoubleEq => f.write_str("=="), Token::Spaceship => f.write_str("<=>"), Token::Eq => f.write_str("="), @@ -232,7 +232,7 @@ impl fmt::Display for Token { Token::ShiftRight => f.write_str(">>"), Token::PGSquareRoot => f.write_str("|/"), Token::PGCubeRoot => f.write_str("||/"), - Token::Placeholder(ref s) => write!(f, "{}", s), + Token::Placeholder(ref s) => write!(f, "{s}"), Token::Arrow => write!(f, "->"), Token::LongArrow => write!(f, "->>"), Token::HashArrow => write!(f, "#>"), @@ -323,8 +323,8 @@ impl fmt::Display for Whitespace { Whitespace::Space => f.write_str(" "), Whitespace::Newline => f.write_str("\n"), Whitespace::Tab => f.write_str("\t"), - Whitespace::SingleLineComment { prefix, comment } => write!(f, "{}{}", prefix, comment), - Whitespace::MultiLineComment(s) => write!(f, "/*{}*/", s), + Whitespace::SingleLineComment { prefix, comment } => write!(f, "{prefix}{comment}"), + Whitespace::MultiLineComment(s) => write!(f, "/*{s}*/"), } } } @@ -595,13 +595,13 @@ impl<'a> Tokenizer<'a> { } else { self.tokenizer_error( error_loc, - format!("Expected close delimiter '{}' before EOF.", quote_end), + format!("Expected close delimiter '{quote_end}' before EOF."), ) } } // numbers and period '0'..='9' | '.' => { - let mut s = peeking_take_while(chars, |ch| matches!(ch, '0'..='9')); + let mut s = peeking_take_while(chars, |ch| ch.is_ascii_digit()); // match binary literal that starts with 0x if s == "0" && chars.peek() == Some(&'x') { @@ -618,7 +618,7 @@ impl<'a> Tokenizer<'a> { s.push('.'); chars.next(); } - s += &peeking_take_while(chars, |ch| matches!(ch, '0'..='9')); + s += &peeking_take_while(chars, |ch| ch.is_ascii_digit()); // No number -> Token::Period if s == "." { @@ -642,12 +642,12 @@ impl<'a> Tokenizer<'a> { match char_clone.peek() { // Definitely an exponent, get original iterator up to speed and use it - Some(&c) if matches!(c, '0'..='9') => { + Some(&c) if c.is_ascii_digit() => { for _ in 0..exponent_part.len() { chars.next(); } exponent_part += - &peeking_take_while(chars, |ch| matches!(ch, '0'..='9')); + &peeking_take_while(chars, |ch| ch.is_ascii_digit()); s += exponent_part.as_str(); } // Not an exponent, discard the work done @@ -907,8 +907,7 @@ impl<'a> Tokenizer<'a> { return self.tokenizer_error( chars.location(), format!( - "Unterminated dollar-quoted string at or near \"{}\"", - value + "Unterminated dollar-quoted string at or near \"{value}\"" ), ); } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3fe5cd92b..24d3fdcc5 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -35,7 +35,7 @@ fn parse_literal_string() { #[test] fn parse_table_identifiers() { fn test_table_ident(ident: &str, expected: Vec) { - let sql = format!("SELECT 1 FROM {}", ident); + let sql = format!("SELECT 1 FROM {ident}"); let select = bigquery().verified_only_select(&sql); assert_eq!( select.from, @@ -51,7 +51,7 @@ fn parse_table_identifiers() { ); } fn test_table_ident_err(ident: &str) { - let sql = format!("SELECT 1 FROM {}", ident); + let sql = format!("SELECT 1 FROM {ident}"); assert!(bigquery().parse_sql_statements(&sql).is_err()); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c880eee7c..ae693e34a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2435,7 +2435,7 @@ fn parse_create_or_replace_table() { #[test] fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), ParserError> { let sql = |options: &str| -> String { - format!("create table X (y_id int references Y (id) {})", options) + format!("create table X (y_id int references Y (id) {options})") }; parse_sql_statements(&sql("on update cascade on delete no action"))?; @@ -2777,7 +2777,7 @@ fn parse_alter_table_constraints() { check_one("CHECK (end_date > start_date OR end_date IS NULL)"); fn check_one(constraint_text: &str) { - match verified_stmt(&format!("ALTER TABLE tab ADD {}", constraint_text)) { + match verified_stmt(&format!("ALTER TABLE tab ADD {constraint_text}")) { Statement::AlterTable { name, operation: AlterTableOperation::AddConstraint(constraint), @@ -2787,7 +2787,7 @@ fn parse_alter_table_constraints() { } _ => unreachable!(), } - verified_stmt(&format!("CREATE TABLE foo (id INT, {})", constraint_text)); + verified_stmt(&format!("CREATE TABLE foo (id INT, {constraint_text})")); } } @@ -2804,7 +2804,7 @@ fn parse_alter_table_drop_column() { ); fn check_one(constraint_text: &str) { - match verified_stmt(&format!("ALTER TABLE tab {}", constraint_text)) { + match verified_stmt(&format!("ALTER TABLE tab {constraint_text}")) { Statement::AlterTable { name, operation: @@ -2827,10 +2827,7 @@ fn parse_alter_table_drop_column() { #[test] fn parse_alter_table_alter_column() { let alter_stmt = "ALTER TABLE tab"; - match verified_stmt(&format!( - "{} ALTER COLUMN is_active SET NOT NULL", - alter_stmt - )) { + match verified_stmt(&format!("{alter_stmt} ALTER COLUMN is_active SET NOT NULL")) { Statement::AlterTable { name, operation: AlterTableOperation::AlterColumn { column_name, op }, @@ -2848,8 +2845,7 @@ fn parse_alter_table_alter_column() { ); match verified_stmt(&format!( - "{} ALTER COLUMN is_active SET DEFAULT false", - alter_stmt + "{alter_stmt} ALTER COLUMN is_active SET DEFAULT false" )) { Statement::AlterTable { name, @@ -2867,10 +2863,7 @@ fn parse_alter_table_alter_column() { _ => unreachable!(), } - match verified_stmt(&format!( - "{} ALTER COLUMN is_active DROP DEFAULT", - alter_stmt - )) { + match verified_stmt(&format!("{alter_stmt} ALTER COLUMN is_active DROP DEFAULT")) { Statement::AlterTable { name, operation: AlterTableOperation::AlterColumn { column_name, op }, @@ -2906,7 +2899,7 @@ fn parse_alter_table_alter_column_type() { let res = Parser::parse_sql( &GenericDialect {}, - &format!("{} ALTER COLUMN is_active TYPE TEXT", alter_stmt), + &format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT"), ); assert_eq!( ParserError::ParserError("Expected SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE after ALTER COLUMN, found: TYPE".to_string()), @@ -2915,10 +2908,7 @@ fn parse_alter_table_alter_column_type() { let res = Parser::parse_sql( &GenericDialect {}, - &format!( - "{} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'", - alter_stmt - ), + &format!("{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'"), ); assert_eq!( ParserError::ParserError("Expected end of statement, found: USING".to_string()), @@ -2966,7 +2956,7 @@ fn parse_alter_table_drop_constraint() { let res = Parser::parse_sql( &GenericDialect {}, - &format!("{} DROP CONSTRAINT is_active TEXT", alter_stmt), + &format!("{alter_stmt} DROP CONSTRAINT is_active TEXT"), ); assert_eq!( ParserError::ParserError("Expected end of statement, found: TEXT".to_string()), @@ -2997,7 +2987,7 @@ fn parse_scalar_function_in_projection() { for function_name in names { // like SELECT sqrt(id) FROM foo - let sql = dbg!(format!("SELECT {}(id) FROM foo", function_name)); + let sql = dbg!(format!("SELECT {function_name}(id) FROM foo")); let select = verified_only_select(&sql); assert_eq!( &Expr::Function(Function { @@ -4221,7 +4211,7 @@ fn parse_ctes() { // Top-level CTE assert_ctes_in_select(&cte_sqls, &verified_query(with)); // CTE in a subquery - let sql = &format!("SELECT ({})", with); + let sql = &format!("SELECT ({with})"); let select = verified_only_select(sql); match expr_from_projection(only(&select.projection)) { Expr::Subquery(ref subquery) => { @@ -4230,7 +4220,7 @@ fn parse_ctes() { _ => panic!("Expected subquery"), } // CTE in a derived table - let sql = &format!("SELECT * FROM ({})", with); + let sql = &format!("SELECT * FROM ({with})"); let select = verified_only_select(sql); match only(select.from).relation { TableFactor::Derived { subquery, .. } => { @@ -4239,13 +4229,13 @@ fn parse_ctes() { _ => panic!("Expected derived table"), } // CTE in a view - let sql = &format!("CREATE VIEW v AS {}", with); + let sql = &format!("CREATE VIEW v AS {with}"); match verified_stmt(sql) { Statement::CreateView { query, .. } => assert_ctes_in_select(&cte_sqls, &query), _ => panic!("Expected CREATE VIEW"), } // CTE in a CTE... - let sql = &format!("WITH outer_cte AS ({}) SELECT * FROM outer_cte", with); + let sql = &format!("WITH outer_cte AS ({with}) SELECT * FROM outer_cte"); let select = verified_query(sql); assert_ctes_in_select(&cte_sqls, &only(&select.with.unwrap().cte_tables).query); } @@ -4270,10 +4260,7 @@ fn parse_cte_renamed_columns() { #[test] fn parse_recursive_cte() { let cte_query = "SELECT 1 UNION ALL SELECT val + 1 FROM nums WHERE val < 10".to_owned(); - let sql = &format!( - "WITH RECURSIVE nums (val) AS ({}) SELECT * FROM nums", - cte_query - ); + let sql = &format!("WITH RECURSIVE nums (val) AS ({cte_query}) SELECT * FROM nums"); let cte_query = verified_query(&cte_query); let query = verified_query(sql); @@ -5029,9 +5016,8 @@ fn lateral_derived() { fn chk(lateral_in: bool) { let lateral_str = if lateral_in { "LATERAL " } else { "" }; let sql = format!( - "SELECT * FROM customer LEFT JOIN {}\ - (SELECT * FROM order WHERE order.customer = customer.id LIMIT 3) AS order ON true", - lateral_str + "SELECT * FROM customer LEFT JOIN {lateral_str}\ + (SELECT * FROM order WHERE order.customer = customer.id LIMIT 3) AS order ON true" ); let select = verified_only_select(&sql); let from = only(select.from); @@ -6286,9 +6272,7 @@ fn parse_cache_table() { let query = all_dialects().verified_query(sql); assert_eq!( - verified_stmt( - format!("CACHE TABLE '{table_name}'", table_name = cache_table_name).as_str() - ), + verified_stmt(format!("CACHE TABLE '{cache_table_name}'").as_str()), Statement::Cache { table_flag: None, table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), @@ -6299,14 +6283,7 @@ fn parse_cache_table() { ); assert_eq!( - verified_stmt( - format!( - "CACHE {flag} TABLE '{table_name}'", - flag = table_flag, - table_name = cache_table_name - ) - .as_str() - ), + verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}'").as_str()), Statement::Cache { table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), @@ -6319,9 +6296,7 @@ fn parse_cache_table() { assert_eq!( verified_stmt( format!( - "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88)", - flag = table_flag, - table_name = cache_table_name, + "CACHE {table_flag} TABLE '{cache_table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88)", ) .as_str() ), @@ -6346,10 +6321,7 @@ fn parse_cache_table() { assert_eq!( verified_stmt( format!( - "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) {sql}", - flag = table_flag, - table_name = cache_table_name, - sql = sql, + "CACHE {table_flag} TABLE '{cache_table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) {sql}", ) .as_str() ), @@ -6374,10 +6346,7 @@ fn parse_cache_table() { assert_eq!( verified_stmt( format!( - "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) AS {sql}", - flag = table_flag, - table_name = cache_table_name, - sql = sql, + "CACHE {table_flag} TABLE '{cache_table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) AS {sql}", ) .as_str() ), @@ -6400,15 +6369,7 @@ fn parse_cache_table() { ); assert_eq!( - verified_stmt( - format!( - "CACHE {flag} TABLE '{table_name}' {sql}", - flag = table_flag, - table_name = cache_table_name, - sql = sql - ) - .as_str() - ), + verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}' {sql}").as_str()), Statement::Cache { table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), @@ -6419,14 +6380,7 @@ fn parse_cache_table() { ); assert_eq!( - verified_stmt( - format!( - "CACHE {flag} TABLE '{table_name}' AS {sql}", - flag = table_flag, - table_name = cache_table_name - ) - .as_str() - ), + verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}' AS {sql}").as_str()), Statement::Cache { table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), @@ -6574,7 +6528,7 @@ fn parse_with_recursion_limit() { .expect("tokenize to work") .parse_statements(); - assert!(matches!(res, Ok(_)), "{:?}", res); + assert!(matches!(res, Ok(_)), "{res:?}"); // limit recursion to something smaller, expect parsing to fail let res = Parser::new(&dialect) @@ -6592,7 +6546,7 @@ fn parse_with_recursion_limit() { .with_recursion_limit(50) .parse_statements(); - assert!(matches!(res, Ok(_)), "{:?}", res); + assert!(matches!(res, Ok(_)), "{res:?}"); } /// Makes a predicate that looks like ((user_id = $id) OR user_id = $2...) @@ -6604,7 +6558,7 @@ fn make_where_clause(num: usize) -> String { if i > 0 { write!(&mut output, " OR ").unwrap(); } - write!(&mut output, "user_id = {}", i).unwrap(); + write!(&mut output, "user_id = {i}").unwrap(); if i < num - 1 { write!(&mut output, ")").unwrap(); } diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index a700dda11..465ce8720 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -47,7 +47,7 @@ fn custom_prefix_parser() -> Result<(), ParserError> { let sql = "SELECT 1 + 2"; let ast = Parser::parse_sql(&dialect, sql)?; let query = &ast[0]; - assert_eq!("SELECT NULL + 2", &format!("{}", query)); + assert_eq!("SELECT NULL + 2", &format!("{query}")); Ok(()) } @@ -87,7 +87,7 @@ fn custom_infix_parser() -> Result<(), ParserError> { let sql = "SELECT 1 + 2"; let ast = Parser::parse_sql(&dialect, sql)?; let query = &ast[0]; - assert_eq!("SELECT 1 * 2", &format!("{}", query)); + assert_eq!("SELECT 1 * 2", &format!("{query}")); Ok(()) } @@ -121,7 +121,7 @@ fn custom_statement_parser() -> Result<(), ParserError> { let sql = "SELECT 1 + 2"; let ast = Parser::parse_sql(&dialect, sql)?; let query = &ast[0]; - assert_eq!("COMMIT", &format!("{}", query)); + assert_eq!("COMMIT", &format!("{query}")); Ok(()) } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 508ae8461..3bf74255f 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -209,7 +209,7 @@ fn parse_show_create() { ShowCreateObject::View, ] { assert_eq!( - mysql_and_generic().verified_stmt(format!("SHOW CREATE {} myident", obj_type).as_str()), + mysql_and_generic().verified_stmt(format!("SHOW CREATE {obj_type} myident").as_str()), Statement::ShowCreate { obj_type: *obj_type, obj_name: obj_name.clone(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 496a61843..08cb0b343 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -485,7 +485,7 @@ PHP ₱ USD $ \N Some other value \\."#; let ast = pg_and_generic().one_statement_parses_to(sql, ""); - println!("{:#?}", ast); + println!("{ast:#?}"); //assert_eq!(sql, ast.to_string()); } @@ -2069,7 +2069,7 @@ fn parse_create_role() { assert_eq!(*login, Some(true)); assert_eq!(*password, Some(Password::NullPassword)); } - err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE abc WITH LOGIN PASSWORD NULL"; @@ -2086,7 +2086,7 @@ fn parse_create_role() { assert_eq!(*login, Some(true)); assert_eq!(*password, Some(Password::NullPassword)); } - err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE magician WITH SUPERUSER CREATEROLE NOCREATEDB BYPASSRLS INHERIT PASSWORD 'abcdef' LOGIN VALID UNTIL '2025-01-01' IN ROLE role1, role2 ROLE role3 ADMIN role4, role5 REPLICATION"; @@ -2141,7 +2141,7 @@ fn parse_create_role() { assert_eq_vec(&["role4", "role5"], admin); assert_eq!(*authorization_owner, None); } - err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE abc WITH USER foo, bar ROLE baz "; @@ -2155,7 +2155,7 @@ fn parse_create_role() { assert_eq_vec(&["foo", "bar"], user); assert_eq_vec(&["baz"], role); } - err => panic!("Failed to parse CREATE ROLE test case: {:?}", err), + err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let negatables = vec![ @@ -2169,9 +2169,9 @@ fn parse_create_role() { ]; for negatable_kw in negatables.iter() { - let sql = format!("CREATE ROLE abc {kw} NO{kw}", kw = negatable_kw); + let sql = format!("CREATE ROLE abc {negatable_kw} NO{negatable_kw}"); if pg().parse_sql_statements(&sql).is_ok() { - panic!("Should not be able to parse CREATE ROLE containing both negated and non-negated versions of the same keyword: {}", negatable_kw) + panic!("Should not be able to parse CREATE ROLE containing both negated and non-negated versions of the same keyword: {negatable_kw}") } } } From 488e8a8156cb38f770403787a348ef7634d20fc5 Mon Sep 17 00:00:00 2001 From: Maciej Skrzypkowski Date: Fri, 17 Feb 2023 19:38:43 +0100 Subject: [PATCH 154/806] Support MySQL Character Set Introducers (#788) * MySQL Character Set Introducers * Documentation fix * Parsing string introducer from Token::word * Fixed lint * fix clippy --------- Co-authored-by: Maciej Skrzypkowski Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 3 +++ src/parser.rs | 29 +++++++++++++++++++++++++++- src/tokenizer.rs | 9 +++++---- tests/sqlparser_mysql.rs | 41 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a8c934396..81d0e22f6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -437,6 +437,8 @@ pub enum Expr { Nested(Box), /// A literal value, such as string, number, date or NULL Value(Value), + /// + IntroducedString { introducer: String, value: Value }, /// A constant of form ` 'value'`. /// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`), /// as well as constants of other types (a non-standard PostgreSQL extension). @@ -696,6 +698,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::TypedString { data_type, value } => { write!(f, "{data_type}")?; write!(f, " '{}'", &value::escape_single_quote_string(value)) diff --git a/src/parser.rs b/src/parser.rs index 58252d3fe..eaf47d42b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -734,6 +734,17 @@ impl<'a> Parser<'a> { Ok(Expr::CompoundIdentifier(id_parts)) } } + // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html + Token::SingleQuotedString(_) + | Token::DoubleQuotedString(_) + | Token::HexStringLiteral(_) + if w.value.starts_with('_') => + { + Ok(Expr::IntroducedString { + introducer: w.value, + value: self.parse_introduced_string_value()?, + }) + } _ => Ok(Expr::Identifier(w.to_ident())), }, }, // End of Token::Word @@ -784,7 +795,6 @@ impl<'a> Parser<'a> { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } - Token::LParen => { let expr = if self.parse_keyword(Keyword::SELECT) || self.parse_keyword(Keyword::WITH) { @@ -4142,6 +4152,23 @@ impl<'a> Parser<'a> { } } + fn parse_introduced_string_value(&mut self) -> Result { + let next_token = self.next_token(); + let location = next_token.location; + 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())), + unexpected => self.expected( + "a string value", + TokenWithLocation { + token: unexpected, + location, + }, + ), + } + } + /// Parse an unsigned literal integer/long pub fn parse_literal_uint(&mut self) -> Result { let next_token = self.next_token(); diff --git a/src/tokenizer.rs b/src/tokenizer.rs index eef4cb7b4..9780de046 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -546,12 +546,12 @@ impl<'a> Tokenizer<'a> { // identifier or keyword ch if self.dialect.is_identifier_start(ch) => { chars.next(); // consume the first char - let s = self.tokenize_word(ch, chars); + let word = self.tokenize_word(ch, chars); // TODO: implement parsing of exponent here - if s.chars().all(|x| ('0'..='9').contains(&x) || x == '.') { + if word.chars().all(|x| ('0'..='9').contains(&x) || x == '.') { let mut inner_state = State { - peekable: s.chars().peekable(), + peekable: word.chars().peekable(), line: 0, col: 0, }; @@ -562,7 +562,8 @@ impl<'a> Tokenizer<'a> { s += s2.as_str(); return Ok(Some(Token::Number(s, false))); } - Ok(Some(Token::make_word(&s, None))) + + Ok(Some(Token::make_word(&word, None))) } // single quoted string '\'' => { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 3bf74255f..04c86cb15 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1264,3 +1264,44 @@ fn parse_values() { mysql().verified_stmt("VALUES ROW(1, true, 'a')"); mysql().verified_stmt("SELECT a, c FROM (VALUES ROW(1, true, 'a'), ROW(2, false, 'b'), ROW(3, false, 'c')) AS t (a, b, c)"); } + +#[test] +fn parse_hex_string_introducer() { + assert_eq!( + mysql().verified_stmt("SELECT _latin1 X'4D7953514C'"), + Statement::Query(Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::IntroducedString { + introducer: "_latin1".to_string(), + value: Value::HexStringLiteral("4D7953514C".to_string()) + })], + from: vec![], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + qualify: None, + into: None + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + locks: vec![], + })) + ) +} + +#[test] +fn parse_string_introducers() { + mysql().verified_stmt("SELECT _binary 'abc'"); + mysql().one_statement_parses_to("SELECT _utf8'abc'", "SELECT _utf8 'abc'"); + mysql().one_statement_parses_to("SELECT _utf8mb4'abc'", "SELECT _utf8mb4 'abc'"); + mysql().verified_stmt("SELECT _binary 'abc', _utf8mb4 'abc'"); +} From 79009f5448a6775d2c4c6cb8123e4eb59a514ff0 Mon Sep 17 00:00:00 2001 From: Y Togami <62130798+togami2864@users.noreply.github.com> Date: Sat, 18 Feb 2023 03:46:30 +0900 Subject: [PATCH 155/806] feat: support JSON keyword (#799) * feat: support json keyword for bigquery * chore: fix test --------- Co-authored-by: Andrew Lamb --- src/ast/data_type.rs | 3 +++ src/parser.rs | 1 + tests/sqlparser_common.rs | 52 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index ec5f256f2..bd1c468e9 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -140,6 +140,8 @@ pub enum DataType { Timestamp(Option, TimezoneInfo), /// Interval Interval, + /// JSON type used in BigQuery + JSON, /// Regclass used in postgresql serial Regclass, /// Text @@ -244,6 +246,7 @@ impl fmt::Display for DataType { format_datetime_precision_and_tz(f, "TIMESTAMP", precision, timezone_info) } DataType::Interval => write!(f, "INTERVAL"), + DataType::JSON => write!(f, "JSON"), DataType::Regclass => write!(f, "REGCLASS"), DataType::Text => write!(f, "TEXT"), DataType::String => write!(f, "STRING"), diff --git a/src/parser.rs b/src/parser.rs index eaf47d42b..3d22a8e00 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4365,6 +4365,7 @@ impl<'a> Parser<'a> { // qualifier that we don't currently support. See // parse_interval for a taste. Keyword::INTERVAL => Ok(DataType::Interval), + Keyword::JSON => Ok(DataType::JSON), Keyword::REGCLASS => Ok(DataType::Regclass), Keyword::STRING => Ok(DataType::String), Keyword::TEXT => Ok(DataType::Text), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ae693e34a..76b0dff56 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3598,6 +3598,58 @@ fn parse_at_timezone() { ); } +#[test] +fn parse_json_keyword() { + let sql = r#"SELECT JSON '{ + "id": 10, + "type": "fruit", + "name": "apple", + "on_menu": true, + "recipes": + { + "salads": + [ + { "id": 2001, "type": "Walnut Apple Salad" }, + { "id": 2002, "type": "Apple Spinach Salad" } + ], + "desserts": + [ + { "id": 3001, "type": "Apple Pie" }, + { "id": 3002, "type": "Apple Scones" }, + { "id": 3003, "type": "Apple Crumble" } + ] + } +}'"#; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::JSON, + value: r#"{ + "id": 10, + "type": "fruit", + "name": "apple", + "on_menu": true, + "recipes": + { + "salads": + [ + { "id": 2001, "type": "Walnut Apple Salad" }, + { "id": 2002, "type": "Apple Spinach Salad" } + ], + "desserts": + [ + { "id": 3001, "type": "Apple Pie" }, + { "id": 3002, "type": "Apple Scones" }, + { "id": 3003, "type": "Apple Crumble" } + ] + } +}"# + .into() + }, + expr_from_projection(only(&select.projection)), + ); +} + #[test] fn parse_simple_math_expr_plus() { let sql = "SELECT a + b, 2 + a, 2.5 + a, a_f + b_f, 2 + a_f, 2.5 + a_f FROM c"; From a2fea10f89e7efacec0854ec6bf2da5362ef610d Mon Sep 17 00:00:00 2001 From: Maciej Obuchowski Date: Fri, 17 Feb 2023 19:55:56 +0100 Subject: [PATCH 156/806] snowflake: add support for TRANSIENT keyword (#807) * snowflake: add support for TRANSIENT keyword Signed-off-by: Maciej Obuchowski * fix clippy --------- Signed-off-by: Maciej Obuchowski Co-authored-by: Andrew Lamb --- src/ast/helpers/stmt_create_table.rs | 10 ++++++++++ src/ast/mod.rs | 5 ++++- src/keywords.rs | 1 + src/parser.rs | 5 ++++- tests/sqlparser_snowflake.rs | 14 ++++++++++++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 14bcd3633..60307d4ef 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -50,6 +50,7 @@ pub struct CreateTableBuilder { pub external: bool, pub global: Option, pub if_not_exists: bool, + pub transient: bool, pub name: ObjectName, pub columns: Vec, pub constraints: Vec, @@ -78,6 +79,7 @@ impl CreateTableBuilder { external: false, global: None, if_not_exists: false, + transient: false, name, columns: vec![], constraints: vec![], @@ -123,6 +125,11 @@ impl CreateTableBuilder { self } + pub fn transient(mut self, transient: bool) -> Self { + self.transient = transient; + self + } + pub fn columns(mut self, columns: Vec) -> Self { self.columns = columns; self @@ -213,6 +220,7 @@ impl CreateTableBuilder { external: self.external, global: self.global, if_not_exists: self.if_not_exists, + transient: self.transient, name: self.name, columns: self.columns, constraints: self.constraints, @@ -248,6 +256,7 @@ impl TryFrom for CreateTableBuilder { external, global, if_not_exists, + transient, name, columns, constraints, @@ -272,6 +281,7 @@ impl TryFrom for CreateTableBuilder { external, global, if_not_exists, + transient, name, columns, constraints, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 81d0e22f6..34099520b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1232,6 +1232,7 @@ pub enum Statement { external: bool, global: Option, if_not_exists: bool, + transient: bool, /// Table name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] name: ObjectName, @@ -2034,6 +2035,7 @@ impl fmt::Display for Statement { with_options, or_replace, if_not_exists, + transient, hive_distribution, hive_formats, external, @@ -2060,7 +2062,7 @@ impl fmt::Display for Statement { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {or_replace}{external}{global}{temporary}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{temporary}{transient}TABLE {if_not_exists}{name}", or_replace = if *or_replace { "OR REPLACE " } else { "" }, external = if *external { "EXTERNAL " } else { "" }, global = global @@ -2074,6 +2076,7 @@ impl fmt::Display for Statement { .unwrap_or(""), if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, temporary = if *temporary { "TEMPORARY " } else { "" }, + transient = if *transient { "TRANSIENT " } else { "" }, name = name, )?; if let Some(on_cluster) = on_cluster { diff --git a/src/keywords.rs b/src/keywords.rs index ad4e51e14..d43f8ebf0 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -570,6 +570,7 @@ define_keywords!( TOP, TRAILING, TRANSACTION, + TRANSIENT, TRANSLATE, TRANSLATE_REGEX, TRANSLATION, diff --git a/src/parser.rs b/src/parser.rs index 3d22a8e00..5d659060c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2266,6 +2266,7 @@ impl<'a> Parser<'a> { let or_replace = self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); let local = self.parse_one_of_keywords(&[Keyword::LOCAL]).is_some(); let global = self.parse_one_of_keywords(&[Keyword::GLOBAL]).is_some(); + let transient = self.parse_one_of_keywords(&[Keyword::TRANSIENT]).is_some(); let global: Option = if global { Some(true) } else if local { @@ -2277,7 +2278,7 @@ impl<'a> Parser<'a> { .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) .is_some(); if self.parse_keyword(Keyword::TABLE) { - self.parse_create_table(or_replace, temporary, global) + 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) @@ -3248,6 +3249,7 @@ impl<'a> Parser<'a> { or_replace: bool, temporary: bool, global: Option, + transient: bool, ) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let table_name = self.parse_object_name()?; @@ -3352,6 +3354,7 @@ impl<'a> Parser<'a> { .table_properties(table_properties) .or_replace(or_replace) .if_not_exists(if_not_exists) + .transient(transient) .hive_distribution(hive_distribution) .hive_formats(Some(hive_formats)) .global(global) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 78a31e383..ec43e030c 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -34,6 +34,20 @@ fn test_snowflake_create_table() { } } +#[test] +fn test_snowflake_create_transient_table() { + let sql = "CREATE TRANSIENT TABLE CUSTOMER (id INT, name VARCHAR(255))"; + match snowflake_and_generic().verified_stmt(sql) { + Statement::CreateTable { + name, transient, .. + } => { + assert_eq!("CUSTOMER", name.to_string()); + assert!(transient) + } + _ => unreachable!(), + } +} + #[test] fn test_snowflake_single_line_tokenize() { let sql = "CREATE TABLE# this is a comment \ntable_1"; From c35dcc93a78f72a9060d76bba5ee6c292909e54a Mon Sep 17 00:00:00 2001 From: Maciej Skrzypkowski Date: Fri, 17 Feb 2023 20:04:59 +0100 Subject: [PATCH 157/806] Support redshift's columns definition list for system information functions (#769) * parsing of redshift's column definition list for pg_get_late_binding_view_cols pg_get_cols pg_get_grantee_by_iam_role pg_get_iam_role_by_user * Renamed ColsDefinition to TableAliasDefinition added generic dialect * Tests fixed * Visitor for IdentPair * Parsing redshift table alias based on indentifier and parentheses instead of function name * fix clippy --------- Co-authored-by: Maciej Skrzypkowski Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 29 ++++++++++++++ src/ast/query.rs | 7 ++++ src/keywords.rs | 2 + src/parser.rs | 46 ++++++++++++++++++++++ src/test_utils.rs | 1 + tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_clickhouse.rs | 3 ++ tests/sqlparser_common.rs | 21 ++++++++++ tests/sqlparser_hive.rs | 2 + tests/sqlparser_mssql.rs | 2 + tests/sqlparser_mysql.rs | 3 ++ tests/sqlparser_postgres.rs | 2 + tests/sqlparser_redshift.rs | 73 +++++++++++++++++++++++++++++++++++ tests/sqlparser_snowflake.rs | 2 + 14 files changed, 194 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 34099520b..cce26c562 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4141,6 +4141,35 @@ impl fmt::Display for SearchModifier { } } +/// A result table definition i.e. `cols(view_schema name, view_name name, col_name name, col_type varchar, col_num int)` +/// used for redshift functions: pg_get_late_binding_view_cols, pg_get_cols, pg_get_grantee_by_iam_role,pg_get_iam_role_by_user +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TableAliasDefinition { + pub name: Ident, + pub args: Vec, +} + +impl fmt::Display for TableAliasDefinition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}({})", self.name, display_comma_separated(&self.args))?; + 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 IdentPair(pub Ident, pub Ident); + +impl fmt::Display for IdentPair { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.0, self.1)?; + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/query.rs b/src/ast/query.rs index d64babadf..a97013eb9 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -580,6 +580,9 @@ pub enum TableFactor { /// vector of arguments, in the case of a table-valued function call, /// whereas it's `None` in the case of a regular table name. args: Option>, + /// A table alias definition i.e. `cols(view_schema name, view_name name, col_name name, col_type varchar, col_num int)` + /// used for redshift functions: pg_get_late_binding_view_cols, pg_get_cols, pg_get_grantee_by_iam_role,pg_get_iam_role_by_user) + columns_definition: Option, /// MSSQL-specific `WITH (...)` hints such as NOLOCK. with_hints: Vec, }, @@ -628,6 +631,7 @@ impl fmt::Display for TableFactor { name, alias, args, + columns_definition, with_hints, } => { write!(f, "{name}")?; @@ -637,6 +641,9 @@ impl fmt::Display for TableFactor { if let Some(alias) = alias { write!(f, " AS {alias}")?; } + if let Some(columns_definition) = columns_definition { + write!(f, " {columns_definition}")?; + } if !with_hints.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_hints))?; } diff --git a/src/keywords.rs b/src/keywords.rs index d43f8ebf0..18338ccdd 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -669,6 +669,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::OUTER, Keyword::SET, Keyword::QUALIFY, + Keyword::AS, ]; /// Can't be used as a column alias, so that `SELECT alias` @@ -698,4 +699,5 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ // Reserved only as a column alias in the `SELECT` clause Keyword::FROM, Keyword::INTO, + Keyword::AS, ]; diff --git a/src/parser.rs b/src/parser.rs index 5d659060c..f962f9db1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5649,6 +5649,7 @@ impl<'a> Parser<'a> { } else { None }; + let columns_definition = self.parse_redshift_columns_definition_list()?; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; // MSSQL-specific table hints: let mut with_hints = vec![]; @@ -5665,11 +5666,56 @@ impl<'a> Parser<'a> { name, alias, args, + columns_definition, with_hints, }) } } + fn parse_redshift_columns_definition_list( + &mut self, + ) -> Result, ParserError> { + if !dialect_of!(self is RedshiftSqlDialect | GenericDialect) { + return Ok(None); + } + + if let Some(col_definition_list_name) = self.parse_optional_columns_definition_list_alias() + { + if self.consume_token(&Token::LParen) { + let names = self.parse_comma_separated(Parser::parse_ident_pair)?; + self.expect_token(&Token::RParen)?; + Ok(Some(TableAliasDefinition { + name: col_definition_list_name, + args: names, + })) + } else { + self.prev_token(); + Ok(None) + } + } else { + Ok(None) + } + } + + fn parse_optional_columns_definition_list_alias(&mut self) -> Option { + match self.next_token().token { + Token::Word(w) if !keywords::RESERVED_FOR_TABLE_ALIAS.contains(&w.keyword) => { + Some(w.to_ident()) + } + _ => { + self.prev_token(); + None + } + } + } + + fn parse_ident_pair(&mut self) -> Result { + Ok(IdentPair( + self.parse_identifier()?, + self.parse_identifier()?, + )) + } + pub fn parse_derived_table_factor( &mut self, lateral: IsLateral, diff --git a/src/test_utils.rs b/src/test_utils.rs index d5bafd90f..b2bff0812 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -185,6 +185,7 @@ pub fn table(name: impl Into) -> TableFactor { name: ObjectName(vec![Ident::new(name.into())]), alias: None, args: None, + columns_definition: None, with_hints: vec![], } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 24d3fdcc5..4ed80df99 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -44,6 +44,7 @@ fn parse_table_identifiers() { name: ObjectName(expected), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![] diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 3e974d56d..10e0d9ea5 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -60,6 +60,7 @@ fn parse_map_access_expr() { name: ObjectName(vec![Ident::new("foos")]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![] @@ -164,11 +165,13 @@ fn parse_delimited_identifiers() { name, alias, args, + columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); + assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 76b0dff56..faab5049c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -210,6 +210,7 @@ fn parse_update_set_from() { name: ObjectName(vec![Ident::new("t1")]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -236,6 +237,7 @@ fn parse_update_set_from() { name: ObjectName(vec![Ident::new("t1")]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -298,6 +300,7 @@ fn parse_update_with_table_alias() { columns: vec![], }), args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -353,6 +356,7 @@ fn parse_delete_statement() { name: ObjectName(vec![Ident::with_quote('"', "table")]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, table_name @@ -379,6 +383,7 @@ fn parse_where_delete_statement() { name: ObjectName(vec![Ident::new("foo")]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, table_name, @@ -419,6 +424,7 @@ fn parse_where_delete_with_alias_statement() { columns: vec![], }), args: None, + columns_definition: None, with_hints: vec![], }, table_name, @@ -432,6 +438,7 @@ fn parse_where_delete_with_alias_statement() { columns: vec![], }), args: None, + columns_definition: None, with_hints: vec![], }), using @@ -3447,6 +3454,7 @@ fn parse_interval_and_or_xor() { }]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -3893,6 +3901,7 @@ fn parse_implicit_join() { name: ObjectName(vec!["t1".into()]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -3902,6 +3911,7 @@ fn parse_implicit_join() { name: ObjectName(vec!["t2".into()]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -3919,6 +3929,7 @@ fn parse_implicit_join() { name: ObjectName(vec!["t1a".into()]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![Join { @@ -3926,6 +3937,7 @@ fn parse_implicit_join() { name: ObjectName(vec!["t1b".into()]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -3936,6 +3948,7 @@ fn parse_implicit_join() { name: ObjectName(vec!["t2a".into()]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![Join { @@ -3943,6 +3956,7 @@ fn parse_implicit_join() { name: ObjectName(vec!["t2b".into()]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -3963,6 +3977,7 @@ fn parse_cross_join() { name: ObjectName(vec![Ident::new("t2")]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::CrossJoin, @@ -3983,6 +3998,7 @@ fn parse_joins_on() { name: ObjectName(vec![Ident::new(relation.into())]), alias, args: None, + columns_definition: None, with_hints: vec![], }, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -4052,6 +4068,7 @@ fn parse_joins_using() { name: ObjectName(vec![Ident::new(relation.into())]), alias, args: None, + columns_definition: None, with_hints: vec![], }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), @@ -4113,6 +4130,7 @@ fn parse_natural_join() { name: ObjectName(vec![Ident::new("t2")]), alias, args: None, + columns_definition: None, with_hints: vec![], }, join_operator: f(JoinConstraint::Natural), @@ -4377,6 +4395,7 @@ fn parse_derived_tables() { name: ObjectName(vec!["t2".into()]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -5668,6 +5687,7 @@ fn parse_merge() { columns: vec![], }), args: None, + columns_definition: None, with_hints: vec![], } ); @@ -5691,6 +5711,7 @@ fn parse_merge() { name: ObjectName(vec![Ident::new("s"), Ident::new("foo")]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 064a090f7..7c17ea120 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -320,11 +320,13 @@ fn parse_delimited_identifiers() { name, alias, args, + columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); + assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 41b0803e4..874dccfab 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -152,11 +152,13 @@ fn parse_delimited_identifiers() { name, alias, args, + columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); + assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 04c86cb15..b4ae5cd5e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -870,6 +870,7 @@ fn parse_update_with_joins() { columns: vec![] }), args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![Join { @@ -880,6 +881,7 @@ fn parse_update_with_joins() { columns: vec![] }), args: None, + columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { @@ -1001,6 +1003,7 @@ fn parse_substring_in_select() { }]), alias: None, args: None, + columns_definition: None, with_hints: vec![] }, joins: vec![] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 08cb0b343..2fbcf977f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2188,11 +2188,13 @@ fn parse_delimited_identifiers() { name, alias, args, + columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); + assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 7597ee981..fdbd66e79 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -44,6 +44,7 @@ fn test_square_brackets_over_db_schema_table_name() { ]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -88,6 +89,7 @@ fn test_double_quotes_over_db_schema_table_name() { ]), alias: None, args: None, + columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -107,11 +109,13 @@ fn parse_delimited_identifiers() { name, alias, args, + columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); + assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), @@ -272,3 +276,72 @@ fn test_sharp() { select.projection[0] ); } + +#[test] +fn test_parse_pg_get_late_binding_view_cols() { + let sql = "select * from pg_get_late_binding_view_cols() some_name_cols(view_schema name, view_name name, col_name name)"; + let expected = "SELECT * FROM pg_get_late_binding_view_cols() some_name_cols(view_schema name, view_name name, col_name name)"; + redshift().one_statement_parses_to(sql, expected); + + let select = redshift().verified_only_select(expected); + assert_eq!( + TableFactor::Table { + name: ObjectName(vec![Ident::new("pg_get_late_binding_view_cols")],), + args: Some(vec![]), + alias: None, + columns_definition: Some(TableAliasDefinition { + name: Ident::new("some_name_cols"), + args: vec![ + IdentPair(Ident::new("view_schema"), Ident::new("name")), + IdentPair(Ident::new("view_name"), Ident::new("name")), + IdentPair(Ident::new("col_name"), Ident::new("name")) + ] + }), + with_hints: vec![] + }, + select.from[0].relation + ); +} + +#[test] +fn test_parse_pg_get_cols() { + let sql = + "SELECT * FROM pg_get_cols() some_name(view_schema name, view_name name, col_name name)"; + redshift().verified_stmt(sql); +} + +#[test] +fn test_parse_pg_get_grantee_by_iam_role() { + let sql = "SELECT grantee, grantee_type, cmd_type FROM pg_get_grantee_by_iam_role('arn:aws:iam::123456789012:role/Redshift-S3-Write') res_grantee(grantee text, grantee_type text, cmd_type text)"; + redshift().verified_stmt(sql); +} + +#[test] +fn test_parse_pg_get_iam_role_by_user() { + let sql = "SELECT username, iam_role, cmd FROM pg_get_iam_role_by_user('reg_user1') res_iam_role(username text, iam_role text, cmd text)"; + redshift().verified_stmt(sql); +} + +#[test] +fn test_parse_pg_get_late_binding_view_cols_in_select() { + let sql = "SELECT pg_get_late_binding_view_cols()"; + redshift().verified_stmt(sql); +} + +#[test] +fn test_parse_pg_get_cols_in_select() { + let sql = "SELECT pg_get_cols()"; + redshift().verified_stmt(sql); +} + +#[test] +fn test_parse_pg_get_grantee_by_iam_role_in_select() { + let sql = "SELECT pg_get_grantee_by_iam_role()"; + redshift().verified_stmt(sql); +} + +#[test] +fn test_parse_pg_get_iam_role_by_user_in_select() { + let sql = "SELECT pg_get_iam_role_by_user()"; + redshift().verified_stmt(sql); +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ec43e030c..eeaebf118 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -220,11 +220,13 @@ fn parse_delimited_identifiers() { name, alias, args, + columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); + assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), From 0c0d088ec21d7f3883165d36c34300745816c5c7 Mon Sep 17 00:00:00 2001 From: Y Togami <62130798+togami2864@users.noreply.github.com> Date: Mon, 20 Feb 2023 00:38:03 +0900 Subject: [PATCH 158/806] feat: support byte string literal in bq (#802) * rebase * review * lint --- src/ast/value.rs | 6 ++++++ src/parser.rs | 8 ++++++++ src/tokenizer.rs | 27 ++++++++++++++++++++++++++- tests/sqlparser_bigquery.rs | 18 ++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index 0adb2d5dc..154aafc76 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -41,6 +41,10 @@ pub enum Value { /// See [Postgres docs](https://www.postgresql.org/docs/8.3/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS) /// for more details. EscapedStringLiteral(String), + /// B'string value' + SingleQuotedByteStringLiteral(String), + /// B"string value" + DoubleQuotedByteStringLiteral(String), /// N'string value' NationalStringLiteral(String), /// X'hex value' @@ -68,6 +72,8 @@ impl fmt::Display for Value { Value::NationalStringLiteral(v) => write!(f, "N'{v}'"), Value::HexStringLiteral(v) => write!(f, "X'{v}'"), Value::Boolean(v) => write!(f, "{v}"), + Value::SingleQuotedByteStringLiteral(v) => write!(f, "B'{v}'"), + Value::DoubleQuotedByteStringLiteral(v) => write!(f, "B\"{v}\""), Value::Null => write!(f, "NULL"), Value::Placeholder(v) => write!(f, "{v}"), Value::UnQuotedString(v) => write!(f, "{v}"), diff --git a/src/parser.rs b/src/parser.rs index f962f9db1..a1ecdfe96 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -790,6 +790,8 @@ impl<'a> Parser<'a> { | Token::SingleQuotedString(_) | Token::DoubleQuotedString(_) | Token::DollarQuotedString(_) + | Token::SingleQuotedByteStringLiteral(_) + | Token::DoubleQuotedByteStringLiteral(_) | Token::NationalStringLiteral(_) | Token::HexStringLiteral(_) => { self.prev_token(); @@ -4125,6 +4127,12 @@ impl<'a> Parser<'a> { Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), Token::DollarQuotedString(ref s) => Ok(Value::DollarQuotedString(s.clone())), + Token::SingleQuotedByteStringLiteral(ref s) => { + Ok(Value::SingleQuotedByteStringLiteral(s.clone())) + } + Token::DoubleQuotedByteStringLiteral(ref s) => { + Ok(Value::DoubleQuotedByteStringLiteral(s.clone())) + } Token::NationalStringLiteral(ref s) => Ok(Value::NationalStringLiteral(s.to_string())), Token::EscapedStringLiteral(ref s) => Ok(Value::EscapedStringLiteral(s.to_string())), Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 9780de046..b05667c2b 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -35,7 +35,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::DollarQuotedString; -use crate::dialect::SnowflakeDialect; +use crate::dialect::{BigQueryDialect, GenericDialect, SnowflakeDialect}; use crate::dialect::{Dialect, MySqlDialect}; use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX}; @@ -58,6 +58,10 @@ pub enum Token { DoubleQuotedString(String), /// Dollar quoted string: i.e: $$string$$ or $tag_name$string$tag_name$ DollarQuotedString(DollarQuotedString), + /// Byte string literal: i.e: b'string' or B'string' + SingleQuotedByteStringLiteral(String), + /// Byte string literal: i.e: b"string" or B"string" + DoubleQuotedByteStringLiteral(String), /// "National" string literal: i.e: N'string' NationalStringLiteral(String), /// "escaped" string literal, which are an extension to the SQL standard: i.e: e'first \n second' or E 'first \n second' @@ -189,6 +193,8 @@ impl fmt::Display for Token { Token::NationalStringLiteral(ref s) => write!(f, "N'{s}'"), Token::EscapedStringLiteral(ref s) => write!(f, "E'{s}'"), Token::HexStringLiteral(ref s) => write!(f, "X'{s}'"), + Token::SingleQuotedByteStringLiteral(ref s) => write!(f, "B'{s}'"), + Token::DoubleQuotedByteStringLiteral(ref s) => write!(f, "B\"{s}\""), Token::Comma => f.write_str(","), Token::Whitespace(ws) => write!(f, "{ws}"), Token::DoubleEq => f.write_str("=="), @@ -493,6 +499,25 @@ impl<'a> Tokenizer<'a> { } Ok(Some(Token::Whitespace(Whitespace::Newline))) } + // BigQuery uses b or B for byte string literal + b @ 'B' | b @ 'b' if dialect_of!(self is BigQueryDialect | GenericDialect) => { + chars.next(); // consume + match chars.peek() { + Some('\'') => { + let s = self.tokenize_quoted_string(chars, '\'')?; + Ok(Some(Token::SingleQuotedByteStringLiteral(s))) + } + Some('\"') => { + let s = self.tokenize_quoted_string(chars, '\"')?; + Ok(Some(Token::DoubleQuotedByteStringLiteral(s))) + } + _ => { + // regular identifier starting with an "b" or "B" + let s = self.tokenize_word(b, chars); + Ok(Some(Token::make_word(&s, None))) + } + } + } // Redshift uses lower case n for national string literal n @ 'N' | n @ 'n' => { chars.next(); // consume, to check the next char diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 4ed80df99..f4020436d 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -32,6 +32,24 @@ fn parse_literal_string() { ); } +#[test] +fn parse_byte_literal() { + let sql = r#"SELECT B'abc', B"abc""#; + let select = bigquery().verified_only_select(sql); + assert_eq!(2, select.projection.len()); + assert_eq!( + &Expr::Value(Value::SingleQuotedByteStringLiteral("abc".to_string())), + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Value(Value::DoubleQuotedByteStringLiteral("abc".to_string())), + expr_from_projection(&select.projection[1]) + ); + + let sql = r#"SELECT b'abc', b"abc""#; + bigquery().one_statement_parses_to(sql, r#"SELECT B'abc', B"abc""#); +} + #[test] fn parse_table_identifiers() { fn test_table_ident(ident: &str, expected: Vec) { From 70917a59ed0b7d5b0bd5167d806e4804c98c9794 Mon Sep 17 00:00:00 2001 From: Y Togami <62130798+togami2864@users.noreply.github.com> Date: Thu, 2 Mar 2023 03:52:25 +0900 Subject: [PATCH 159/806] feat: `SELECT * REPLACE AS ` for bigquery (#798) * chore: add test for wildcard replace * feat: define opt_replace for wildcard replace * fix: modify replace option ast * fix: add test cases * chore: fmt * redefine ast * feat: parse select replace items * ci * Update src/ast/query.rs --------- Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 5 ++-- src/ast/query.rs | 51 +++++++++++++++++++++++++++++++++ src/parser.rs | 39 +++++++++++++++++++++++++ tests/sqlparser_bigquery.rs | 57 +++++++++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cce26c562..497b7ac32 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -36,8 +36,9 @@ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ Cte, ExceptSelectItem, ExcludeSelectItem, Fetch, IdentWithAlias, Join, JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NonBlock, Offset, OffsetRows, OrderByExpr, - Query, RenameSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, - Table, TableAlias, TableFactor, TableWithJoins, Top, Values, WildcardAdditionalOptions, With, + Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, + SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, + TableWithJoins, Top, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, diff --git a/src/ast/query.rs b/src/ast/query.rs index a97013eb9..b85efedbf 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -394,6 +394,9 @@ pub struct WildcardAdditionalOptions { pub opt_except: Option, /// `[RENAME ...]`. pub opt_rename: Option, + /// `[REPLACE]` + /// BigQuery syntax: + pub opt_replace: Option, } impl fmt::Display for WildcardAdditionalOptions { @@ -407,6 +410,9 @@ impl fmt::Display for WildcardAdditionalOptions { if let Some(rename) = &self.opt_rename { write!(f, " {rename}")?; } + if let Some(replace) = &self.opt_replace { + write!(f, " {replace}")?; + } Ok(()) } } @@ -526,6 +532,51 @@ impl fmt::Display for ExceptSelectItem { } } +/// Bigquery `REPLACE` information. +/// +/// # Syntax +/// ```plaintext +/// REPLACE ( [AS] ) +/// REPLACE ( [AS] , [AS] , ...) +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ReplaceSelectItem { + pub items: Vec>, +} + +impl fmt::Display for ReplaceSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "REPLACE")?; + write!(f, " ({})", display_comma_separated(&self.items))?; + Ok(()) + } +} + +/// # Syntax +/// ```plaintext +/// [AS] +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ReplaceSelectElement { + pub expr: Expr, + pub colum_name: Ident, + pub as_keyword: bool, +} + +impl fmt::Display for ReplaceSelectElement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.as_keyword { + write!(f, "{} AS {}", self.expr, self.colum_name) + } else { + write!(f, "{} {}", self.expr, self.colum_name) + } + } +} + impl fmt::Display for SelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { diff --git a/src/parser.rs b/src/parser.rs index a1ecdfe96..0473b5181 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6164,10 +6164,17 @@ impl<'a> Parser<'a> { None }; + let opt_replace = if dialect_of!(self is GenericDialect | BigQueryDialect) { + self.parse_optional_select_item_replace()? + } else { + None + }; + Ok(WildcardAdditionalOptions { opt_exclude, opt_except, opt_rename, + opt_replace, }) } @@ -6241,6 +6248,38 @@ impl<'a> Parser<'a> { Ok(opt_rename) } + /// Parse a [`Replace`](ReplaceSelectItem) information for wildcard select items. + pub fn parse_optional_select_item_replace( + &mut self, + ) -> Result, ParserError> { + let opt_replace = if self.parse_keyword(Keyword::REPLACE) { + if self.consume_token(&Token::LParen) { + let items = self.parse_comma_separated(|parser| { + Ok(Box::new(parser.parse_replace_elements()?)) + })?; + self.expect_token(&Token::RParen)?; + Some(ReplaceSelectItem { items }) + } else { + let tok = self.next_token(); + return self.expected("( after REPLACE but", tok); + } + } else { + None + }; + + Ok(opt_replace) + } + pub fn parse_replace_elements(&mut self) -> Result { + let expr = self.parse_expr()?; + let as_keyword = self.parse_keyword(Keyword::AS); + let ident = self.parse_identifier()?; + Ok(ReplaceSelectElement { + expr, + colum_name: ident, + as_keyword, + }) + } + /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parse_expr()?; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index f4020436d..1a04bc8ba 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -17,6 +17,11 @@ use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; use test_utils::*; +#[cfg(feature = "bigdecimal")] +use bigdecimal::*; +#[cfg(feature = "bigdecimal")] +use std::str::FromStr; + #[test] fn parse_literal_string() { let sql = r#"SELECT 'single', "double""#; @@ -313,6 +318,58 @@ fn test_select_wildcard_with_except() { ); } +#[test] +fn test_select_wildcard_with_replace() { + let select = bigquery_and_generic() + .verified_only_select(r#"SELECT * REPLACE ('widget' AS item_name) FROM orders"#); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_replace: Some(ReplaceSelectItem { + items: vec![Box::new(ReplaceSelectElement { + expr: Expr::Value(Value::SingleQuotedString("widget".to_owned())), + colum_name: Ident::new("item_name"), + as_keyword: true, + })], + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); + + let select = bigquery_and_generic().verified_only_select( + r#"SELECT * REPLACE (quantity / 2 AS quantity, 3 AS order_id) FROM orders"#, + ); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_replace: Some(ReplaceSelectItem { + items: vec![ + Box::new(ReplaceSelectElement { + expr: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("quantity"))), + op: BinaryOperator::Divide, + #[cfg(not(feature = "bigdecimal"))] + right: Box::new(Expr::Value(Value::Number("2".to_string(), false))), + #[cfg(feature = "bigdecimal")] + right: Box::new(Expr::Value(Value::Number( + BigDecimal::from_str("2").unwrap(), + false, + ))), + }, + colum_name: Ident::new("quantity"), + as_keyword: true, + }), + Box::new(ReplaceSelectElement { + #[cfg(not(feature = "bigdecimal"))] + expr: Expr::Value(Value::Number("3".to_string(), false)), + #[cfg(feature = "bigdecimal")] + expr: Expr::Value(Value::Number(BigDecimal::from_str("3").unwrap(), false)), + colum_name: Ident::new("order_id"), + as_keyword: true, + }), + ], + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); +} + fn bigquery() -> TestedDialects { TestedDialects { dialects: vec![Box::new(BigQueryDialect {})], From 58de3c1222669c86691e2a9220a6262e9e599001 Mon Sep 17 00:00:00 2001 From: Y Togami <62130798+togami2864@users.noreply.github.com> Date: Thu, 2 Mar 2023 04:11:42 +0900 Subject: [PATCH 160/806] feat: support raw string literal of BigQuery (#812) * add tests * feat: parse raw literal of bq * merge double quoted & single quoted to raw string literal * Update src/ast/value.rs --------- Co-authored-by: Andrew Lamb --- src/ast/value.rs | 4 ++++ src/parser.rs | 2 ++ src/tokenizer.rs | 22 ++++++++++++++++++++++ tests/sqlparser_bigquery.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/src/ast/value.rs b/src/ast/value.rs index 154aafc76..95ea978d0 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -45,6 +45,9 @@ pub enum Value { SingleQuotedByteStringLiteral(String), /// B"string value" DoubleQuotedByteStringLiteral(String), + /// R'string value' or r'string value' or r"string value" + /// + RawStringLiteral(String), /// N'string value' NationalStringLiteral(String), /// X'hex value' @@ -74,6 +77,7 @@ impl fmt::Display for Value { Value::Boolean(v) => write!(f, "{v}"), Value::SingleQuotedByteStringLiteral(v) => write!(f, "B'{v}'"), Value::DoubleQuotedByteStringLiteral(v) => write!(f, "B\"{v}\""), + Value::RawStringLiteral(v) => write!(f, "R'{v}'"), Value::Null => write!(f, "NULL"), Value::Placeholder(v) => write!(f, "{v}"), Value::UnQuotedString(v) => write!(f, "{v}"), diff --git a/src/parser.rs b/src/parser.rs index 0473b5181..4da20e15e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -792,6 +792,7 @@ impl<'a> Parser<'a> { | Token::DollarQuotedString(_) | Token::SingleQuotedByteStringLiteral(_) | Token::DoubleQuotedByteStringLiteral(_) + | Token::RawStringLiteral(_) | Token::NationalStringLiteral(_) | Token::HexStringLiteral(_) => { self.prev_token(); @@ -4133,6 +4134,7 @@ impl<'a> Parser<'a> { Token::DoubleQuotedByteStringLiteral(ref s) => { Ok(Value::DoubleQuotedByteStringLiteral(s.clone())) } + Token::RawStringLiteral(ref s) => Ok(Value::RawStringLiteral(s.clone())), Token::NationalStringLiteral(ref s) => Ok(Value::NationalStringLiteral(s.to_string())), Token::EscapedStringLiteral(ref s) => Ok(Value::EscapedStringLiteral(s.to_string())), Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b05667c2b..8134947df 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -62,6 +62,8 @@ pub enum Token { SingleQuotedByteStringLiteral(String), /// Byte string literal: i.e: b"string" or B"string" DoubleQuotedByteStringLiteral(String), + /// Raw string literal: i.e: r'string' or R'string' or r"string" or R"string" + RawStringLiteral(String), /// "National" string literal: i.e: N'string' NationalStringLiteral(String), /// "escaped" string literal, which are an extension to the SQL standard: i.e: e'first \n second' or E 'first \n second' @@ -195,6 +197,7 @@ impl fmt::Display for Token { Token::HexStringLiteral(ref s) => write!(f, "X'{s}'"), Token::SingleQuotedByteStringLiteral(ref s) => write!(f, "B'{s}'"), Token::DoubleQuotedByteStringLiteral(ref s) => write!(f, "B\"{s}\""), + Token::RawStringLiteral(ref s) => write!(f, "R'{s}'"), Token::Comma => f.write_str(","), Token::Whitespace(ws) => write!(f, "{ws}"), Token::DoubleEq => f.write_str("=="), @@ -518,6 +521,25 @@ impl<'a> Tokenizer<'a> { } } } + // BigQuery uses r or R for raw string literal + b @ 'R' | b @ 'r' if dialect_of!(self is BigQueryDialect | GenericDialect) => { + chars.next(); // consume + match chars.peek() { + Some('\'') => { + let s = self.tokenize_quoted_string(chars, '\'')?; + Ok(Some(Token::RawStringLiteral(s))) + } + Some('\"') => { + let s = self.tokenize_quoted_string(chars, '\"')?; + Ok(Some(Token::RawStringLiteral(s))) + } + _ => { + // regular identifier starting with an "r" or "R" + let s = self.tokenize_word(b, chars); + Ok(Some(Token::make_word(&s, None))) + } + } + } // Redshift uses lower case n for national string literal n @ 'N' | n @ 'n' => { chars.next(); // consume, to check the next char diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 1a04bc8ba..85b540e35 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -55,6 +55,38 @@ fn parse_byte_literal() { bigquery().one_statement_parses_to(sql, r#"SELECT B'abc', B"abc""#); } +#[test] +fn parse_raw_literal() { + let sql = r#"SELECT R'abc', R"abc", R'f\(abc,(.*),def\)', R"f\(abc,(.*),def\)""#; + let stmt = bigquery().one_statement_parses_to( + sql, + r#"SELECT R'abc', R'abc', R'f\(abc,(.*),def\)', R'f\(abc,(.*),def\)'"#, + ); + if let Statement::Query(query) = stmt { + if let SetExpr::Select(select) = *query.body { + assert_eq!(4, select.projection.len()); + assert_eq!( + &Expr::Value(Value::RawStringLiteral("abc".to_string())), + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Value(Value::RawStringLiteral("abc".to_string())), + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Value(Value::RawStringLiteral(r#"f\(abc,(.*),def\)"#.to_string())), + expr_from_projection(&select.projection[2]) + ); + assert_eq!( + &Expr::Value(Value::RawStringLiteral(r#"f\(abc,(.*),def\)"#.to_string())), + expr_from_projection(&select.projection[3]) + ); + return; + } + } + panic!("invalid query") +} + #[test] fn parse_table_identifiers() { fn test_table_ident(ident: &str, expected: Vec) { From 66ec634c67173c661ef3dee6b02bed35f20a0f54 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Mar 2023 20:23:15 +0100 Subject: [PATCH 161/806] Update CHANGELOG for version 0.31 (#820) --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed14aa787..6003b39d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,29 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. + +## [0.31.0] 2023-03-1 + +### Added +* Support raw string literals for BigQuery dialect (#812) - Thanks @togami2864 +* Support `SELECT * REPLACE AS ` in BigQuery dialect (#798) - Thanks @togami2864 +* Support byte string literals for BigQuery dialect (#802) - Thanks @togami2864 +* Support columns definition list for system information functions in RedShift dialect (#769) - Thanks @mskrzypkows +* Support `TRANSIENT` keyword in Snowflake dialect (#807) - Thanks @mobuchowski +* Support `JSON` keyword (#799) - Thanks @togami2864 +* Support MySQL Character Set Introducers (#788) - Thanks @mskrzypkows + +### Fixed + +Fix clippy error in ci (#803) - Thanks @togami2864 +Handle offset in map key in BigQuery dialect (#797) - Thanks @Ziinc +Fix a typo (precendence -> precedence) (#794) - Thanks @SARDONYX-sard +use post_* visitors for mutable visits (#789) - Thanks @lovasoa + +### Changed +Add another known user (#787) - Thanks @joocer + + ## [0.30.0] 2023-01-02 ### Added From b8384152769ec22febe143c471d9d2fa292c9d37 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Mar 2023 14:25:38 -0500 Subject: [PATCH 162/806] (cargo-release) version 0.31.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1cc7a33ec..776524b88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.30.0" +version = "0.31.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 2285bb44ba55f2256c17d72b33dc620573f225b6 Mon Sep 17 00:00:00 2001 From: Y Togami <62130798+togami2864@users.noreply.github.com> Date: Fri, 3 Mar 2023 00:32:20 +0900 Subject: [PATCH 163/806] chore: fix typo (#822) --- src/ast/query.rs | 6 +++--- src/parser.rs | 2 +- tests/sqlparser_bigquery.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index b85efedbf..1feb7a4aa 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -563,16 +563,16 @@ impl fmt::Display for ReplaceSelectItem { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct ReplaceSelectElement { pub expr: Expr, - pub colum_name: Ident, + pub column_name: Ident, pub as_keyword: bool, } impl fmt::Display for ReplaceSelectElement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.as_keyword { - write!(f, "{} AS {}", self.expr, self.colum_name) + write!(f, "{} AS {}", self.expr, self.column_name) } else { - write!(f, "{} {}", self.expr, self.colum_name) + write!(f, "{} {}", self.expr, self.column_name) } } } diff --git a/src/parser.rs b/src/parser.rs index 4da20e15e..de60801d2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6277,7 +6277,7 @@ impl<'a> Parser<'a> { let ident = self.parse_identifier()?; Ok(ReplaceSelectElement { expr, - colum_name: ident, + column_name: ident, as_keyword, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 85b540e35..b701140b6 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -358,7 +358,7 @@ fn test_select_wildcard_with_replace() { opt_replace: Some(ReplaceSelectItem { items: vec![Box::new(ReplaceSelectElement { expr: Expr::Value(Value::SingleQuotedString("widget".to_owned())), - colum_name: Ident::new("item_name"), + column_name: Ident::new("item_name"), as_keyword: true, })], }), @@ -384,7 +384,7 @@ fn test_select_wildcard_with_replace() { false, ))), }, - colum_name: Ident::new("quantity"), + column_name: Ident::new("quantity"), as_keyword: true, }), Box::new(ReplaceSelectElement { @@ -392,7 +392,7 @@ fn test_select_wildcard_with_replace() { expr: Expr::Value(Value::Number("3".to_string(), false)), #[cfg(feature = "bigdecimal")] expr: Expr::Value(Value::Number(BigDecimal::from_str("3").unwrap(), false)), - colum_name: Ident::new("order_id"), + column_name: Ident::new("order_id"), as_keyword: true, }), ], From b45306819c34397fc65f116ab1cc85178ee1bb6e Mon Sep 17 00:00:00 2001 From: Ankur Goyal Date: Thu, 2 Mar 2023 07:35:46 -0800 Subject: [PATCH 164/806] Add support for trailing commas (#810) * Add support for trailing commas * Support trailing commas for brace/bracket * Andrew's comments --- src/parser.rs | 82 +++++++++++++++++++++++++++---------- tests/sqlparser_bigquery.rs | 2 + 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index de60801d2..e7731e2a3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -195,12 +195,20 @@ impl std::error::Error for ParserError {} // By default, allow expressions up to this deep before erroring const DEFAULT_REMAINING_DEPTH: usize = 50; +#[derive(Default)] +pub struct ParserOptions { + pub trailing_commas: bool, +} + pub struct Parser<'a> { tokens: Vec, /// The index of the first unprocessed token in `self.tokens` index: usize, /// The current dialect to use dialect: &'a dyn Dialect, + /// Additional options that allow you to mix & match behavior otherwise + /// constrained to certain dialects (e.g. trailing commas) + options: ParserOptions, /// ensure the stack does not overflow by limiting recusion depth recursion_counter: RecursionCounter, } @@ -227,6 +235,7 @@ impl<'a> Parser<'a> { index: 0, dialect, recursion_counter: RecursionCounter::new(DEFAULT_REMAINING_DEPTH), + options: ParserOptions::default(), } } @@ -255,6 +264,31 @@ impl<'a> Parser<'a> { self } + /// Specify additional parser options + /// + /// + /// [`Parser`] supports additional options ([`ParserOptions`]) that allow you to + /// mix & match behavior otherwise constrained to certain dialects (e.g. trailing + /// commas). + /// + /// Example: + /// ``` + /// # use sqlparser::{parser::{Parser, ParserError, ParserOptions}, dialect::GenericDialect}; + /// # fn main() -> Result<(), ParserError> { + /// let dialect = GenericDialect{}; + /// let result = Parser::new(&dialect) + /// .with_options(ParserOptions { trailing_commas: true }) + /// .try_with_sql("SELECT a, b, COUNT(*), FROM foo GROUP BY a, b,")? + /// .parse_statements(); + /// assert!(matches!(result, Ok(_))); + /// # Ok(()) + /// # } + /// ``` + pub fn with_options(mut self, options: ParserOptions) -> Self { + self.options = options; + self + } + /// Reset this parser to parse the specified token stream pub fn with_tokens_with_locations(mut self, tokens: Vec) -> Self { self.tokens = tokens; @@ -2196,15 +2230,32 @@ impl<'a> Parser<'a> { /// Parse a comma-separated list of 1+ SelectItem pub fn parse_projection(&mut self) -> Result, ParserError> { + // BigQuery allows trailing commas, but only in project lists + // e.g. `SELECT 1, 2, FROM t` + // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#trailing_commas + // + // This pattern could be captured better with RAII type semantics, but it's quite a bit of + // code to add for just one case, so we'll just do it manually here. + let old_value = self.options.trailing_commas; + self.options.trailing_commas |= dialect_of!(self is BigQueryDialect); + + let ret = self.parse_comma_separated(|p| p.parse_select_item()); + self.options.trailing_commas = old_value; + + ret + } + + /// Parse a comma-separated list of 1+ items accepted by `F` + pub fn parse_comma_separated(&mut self, mut f: F) -> Result, ParserError> + where + F: FnMut(&mut Parser<'a>) -> Result, + { let mut values = vec![]; loop { - values.push(self.parse_select_item()?); + values.push(f(self)?); if !self.consume_token(&Token::Comma) { break; - } else if dialect_of!(self is BigQueryDialect) { - // BigQuery allows trailing commas. - // e.g. `SELECT 1, 2, FROM t` - // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#trailing_commas + } else if self.options.trailing_commas { match self.peek_token().token { Token::Word(kw) if keywords::RESERVED_FOR_COLUMN_ALIAS @@ -2213,7 +2264,11 @@ impl<'a> Parser<'a> { { break; } - Token::RParen | Token::EOF => break, + Token::RParen + | Token::SemiColon + | Token::EOF + | Token::RBracket + | Token::RBrace => break, _ => continue, } } @@ -2221,21 +2276,6 @@ impl<'a> Parser<'a> { Ok(values) } - /// Parse a comma-separated list of 1+ items accepted by `F` - pub fn parse_comma_separated(&mut self, mut f: F) -> Result, ParserError> - where - F: FnMut(&mut Parser<'a>) -> Result, - { - let mut values = vec![]; - loop { - values.push(f(self)?); - if !self.consume_token(&Token::Comma) { - break; - } - } - Ok(values) - } - /// Run a parser method `f`, reverting back to the current position /// if unsuccessful. #[must_use] diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index b701140b6..ac3a55cc7 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -181,6 +181,8 @@ fn parse_join_constraint_unnest_alias() { fn parse_trailing_comma() { for (sql, canonical) in [ ("SELECT a,", "SELECT a"), + ("SELECT 1,", "SELECT 1"), + ("SELECT 1,2,", "SELECT 1, 2"), ("SELECT a, b,", "SELECT a, b"), ("SELECT a, b AS c,", "SELECT a, b AS c"), ("SELECT a, b AS c, FROM t", "SELECT a, b AS c FROM t"), From fbbf1a4e848bcaed8740854e91ce9e4481b2c115 Mon Sep 17 00:00:00 2001 From: Y Togami <62130798+togami2864@users.noreply.github.com> Date: Fri, 3 Mar 2023 00:38:00 +0900 Subject: [PATCH 165/806] feat: support `BIGNUMERIC` of bigquery (#811) * add tests * bignumeric data type * bignumeric keyword * fix doc * add exact_number_info * fix doc * check result string --- src/ast/data_type.rs | 10 ++++++ src/keywords.rs | 2 ++ src/parser.rs | 6 ++++ tests/sqlparser_common.rs | 69 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index bd1c468e9..647e7b35e 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -79,6 +79,14 @@ pub enum DataType { /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type Decimal(ExactNumberInfo), + /// [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 + /// + /// [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] /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type @@ -196,6 +204,8 @@ impl fmt::Display for DataType { DataType::Dec(info) => { write!(f, "DEC{info}") } + DataType::BigNumeric(info) => write!(f, "BIGNUMERIC{info}"), + DataType::BigDecimal(info) => write!(f, "BIGDECIMAL{info}"), DataType::Float(size) => format_type_with_optional_length(f, "FLOAT", size, false), DataType::TinyInt(zerofill) => { format_type_with_optional_length(f, "TINYINT", zerofill, false) diff --git a/src/keywords.rs b/src/keywords.rs index 18338ccdd..af741aeef 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -104,7 +104,9 @@ define_keywords!( BEGIN_FRAME, BEGIN_PARTITION, BETWEEN, + BIGDECIMAL, BIGINT, + BIGNUMERIC, BINARY, BLOB, BOOLEAN, diff --git a/src/parser.rs b/src/parser.rs index e7731e2a3..dbbad7f0a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4432,6 +4432,12 @@ impl<'a> Parser<'a> { Keyword::DEC => Ok(DataType::Dec( self.parse_exact_number_optional_precision_scale()?, )), + Keyword::BIGNUMERIC => Ok(DataType::BigNumeric( + self.parse_exact_number_optional_precision_scale()?, + )), + Keyword::BIGDECIMAL => Ok(DataType::BigDecimal( + self.parse_exact_number_optional_precision_scale()?, + )), Keyword::ENUM => Ok(DataType::Enum(self.parse_string_values()?)), Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)), Keyword::ARRAY => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index faab5049c..0d91aac0b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3658,6 +3658,75 @@ fn parse_json_keyword() { ); } +#[test] +fn parse_bignumeric_keyword() { + let sql = r#"SELECT BIGNUMERIC '0'"#; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::BigNumeric(ExactNumberInfo::None), + value: r#"0"#.into() + }, + expr_from_projection(only(&select.projection)), + ); + verified_stmt("SELECT BIGNUMERIC '0'"); + + let sql = r#"SELECT BIGNUMERIC '123456'"#; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::BigNumeric(ExactNumberInfo::None), + value: r#"123456"#.into() + }, + expr_from_projection(only(&select.projection)), + ); + verified_stmt("SELECT BIGNUMERIC '123456'"); + + let sql = r#"SELECT BIGNUMERIC '-3.14'"#; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::BigNumeric(ExactNumberInfo::None), + value: r#"-3.14"#.into() + }, + expr_from_projection(only(&select.projection)), + ); + verified_stmt("SELECT BIGNUMERIC '-3.14'"); + + let sql = r#"SELECT BIGNUMERIC '-0.54321'"#; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::BigNumeric(ExactNumberInfo::None), + value: r#"-0.54321"#.into() + }, + expr_from_projection(only(&select.projection)), + ); + verified_stmt("SELECT BIGNUMERIC '-0.54321'"); + + let sql = r#"SELECT BIGNUMERIC '1.23456e05'"#; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::BigNumeric(ExactNumberInfo::None), + value: r#"1.23456e05"#.into() + }, + expr_from_projection(only(&select.projection)), + ); + verified_stmt("SELECT BIGNUMERIC '1.23456e05'"); + + let sql = r#"SELECT BIGNUMERIC '-9.876e-3'"#; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::BigNumeric(ExactNumberInfo::None), + value: r#"-9.876e-3"#.into() + }, + expr_from_projection(only(&select.projection)), + ); + verified_stmt("SELECT BIGNUMERIC '-9.876e-3'"); +} + #[test] fn parse_simple_math_expr_plus() { let sql = "SELECT a + b, 2 + a, 2.5 + a, a_f + b_f, 2 + a_f, 2.5 + a_f FROM c"; From 1cf913e7173a27897c0d198782e385de806dbcbd Mon Sep 17 00:00:00 2001 From: Mykhailo Bondarenko <70747718+michael-2956@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:39:39 +0200 Subject: [PATCH 166/806] feat: Support PostgreSQL exponentiation. (#813) * Add Postgres exponent operator * Parse caret as BinaryOperator::PGExp in PostgreSQL * Update sqlparser_postgres.rs * update tests to support PGExp * cargo fmt * improve extensibility * cargo fmt * redundant code and documentation lionks --- src/ast/operator.rs | 2 ++ src/parser.rs | 10 +++++++++- tests/sqlparser_common.rs | 21 ++++++++++++++++----- tests/sqlparser_postgres.rs | 9 +++++---- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index ca609941b..75877c949 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -88,6 +88,7 @@ pub enum BinaryOperator { PGBitwiseXor, PGBitwiseShiftLeft, PGBitwiseShiftRight, + PGExp, PGRegexMatch, PGRegexIMatch, PGRegexNotMatch, @@ -124,6 +125,7 @@ impl fmt::Display for BinaryOperator { BinaryOperator::PGBitwiseXor => f.write_str("#"), BinaryOperator::PGBitwiseShiftLeft => f.write_str("<<"), BinaryOperator::PGBitwiseShiftRight => f.write_str(">>"), + BinaryOperator::PGExp => f.write_str("^"), BinaryOperator::PGRegexMatch => f.write_str("~"), BinaryOperator::PGRegexIMatch => f.write_str("~*"), BinaryOperator::PGRegexNotMatch => f.write_str("!~"), diff --git a/src/parser.rs b/src/parser.rs index dbbad7f0a..526cf58d4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1648,7 +1648,15 @@ impl<'a> Parser<'a> { Token::Mod => Some(BinaryOperator::Modulo), Token::StringConcat => Some(BinaryOperator::StringConcat), Token::Pipe => Some(BinaryOperator::BitwiseOr), - Token::Caret => Some(BinaryOperator::BitwiseXor), + Token::Caret => { + // In PostgreSQL, ^ stands for the exponentiation operation, + // and # stands for XOR. See https://www.postgresql.org/docs/current/functions-math.html + if dialect_of!(self is PostgreSqlDialect) { + Some(BinaryOperator::PGExp) + } else { + Some(BinaryOperator::BitwiseXor) + } + } Token::Ampersand => Some(BinaryOperator::BitwiseAnd), Token::Div => Some(BinaryOperator::Divide), Token::ShiftLeft if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0d91aac0b..1e56b2168 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1228,16 +1228,27 @@ fn parse_string_agg() { ); } +/// selects all dialects but PostgreSQL +pub fn all_dialects_but_pg() -> TestedDialects { + TestedDialects { + dialects: all_dialects() + .dialects + .into_iter() + .filter(|x| !x.is::()) + .collect(), + } +} + #[test] fn parse_bitwise_ops() { let bitwise_ops = &[ - ("^", BinaryOperator::BitwiseXor), - ("|", BinaryOperator::BitwiseOr), - ("&", BinaryOperator::BitwiseAnd), + ("^", BinaryOperator::BitwiseXor, all_dialects_but_pg()), + ("|", BinaryOperator::BitwiseOr, all_dialects()), + ("&", BinaryOperator::BitwiseAnd, all_dialects()), ]; - for (str_op, op) in bitwise_ops { - let select = verified_only_select(&format!("SELECT a {} b", &str_op)); + for (str_op, op, dialects) in bitwise_ops { + let select = dialects.verified_only_select(&format!("SELECT a {} b", &str_op)); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("a"))), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2fbcf977f..72f364650 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1322,15 +1322,16 @@ fn parse_pg_returning() { } #[test] -fn parse_pg_bitwise_binary_ops() { - let bitwise_ops = &[ - // Sharp char cannot be used with Generic Dialect, it conflicts with identifiers +fn parse_pg_binary_ops() { + let binary_ops = &[ + // Sharp char and Caret cannot be used with Generic Dialect, it conflicts with identifiers ("#", BinaryOperator::PGBitwiseXor, pg()), + ("^", BinaryOperator::PGExp, pg()), (">>", BinaryOperator::PGBitwiseShiftRight, pg_and_generic()), ("<<", BinaryOperator::PGBitwiseShiftLeft, pg_and_generic()), ]; - for (str_op, op, dialects) in bitwise_ops { + for (str_op, op, dialects) in binary_ops { let select = dialects.verified_only_select(&format!("SELECT a {} b", &str_op)); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { From d69b875367385cfc1d18296f390674cfc3729e4a Mon Sep 17 00:00:00 2001 From: Ankur Goyal Date: Mon, 6 Mar 2023 06:55:55 -0800 Subject: [PATCH 167/806] ClickHouse CREATE TABLE Fixes: add ORDER BY and fix clause ordering (#824) * Fix ClickHouse (add ORDER BY) * Improve test case --- src/ast/helpers/stmt_create_table.rs | 12 ++++++++++- src/ast/mod.rs | 16 +++++++++++---- src/parser.rs | 30 ++++++++++++++++++++++------ tests/sqlparser_clickhouse.rs | 12 +++++++++++ 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 60307d4ef..cb1ad45e7 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{ - ColumnDef, FileFormat, HiveDistributionStyle, HiveFormat, ObjectName, OnCommit, Query, + ColumnDef, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, Query, SqlOption, Statement, TableConstraint, }; use crate::parser::ParserError; @@ -69,6 +69,7 @@ pub struct CreateTableBuilder { pub collation: Option, pub on_commit: Option, pub on_cluster: Option, + pub order_by: Option>, } impl CreateTableBuilder { @@ -98,6 +99,7 @@ impl CreateTableBuilder { collation: None, on_commit: None, on_cluster: None, + order_by: None, } } pub fn or_replace(mut self, or_replace: bool) -> Self { @@ -213,6 +215,11 @@ impl CreateTableBuilder { self } + pub fn order_by(mut self, order_by: Option>) -> Self { + self.order_by = order_by; + self + } + pub fn build(self) -> Statement { Statement::CreateTable { or_replace: self.or_replace, @@ -239,6 +246,7 @@ impl CreateTableBuilder { collation: self.collation, on_commit: self.on_commit, on_cluster: self.on_cluster, + order_by: self.order_by, } } } @@ -275,6 +283,7 @@ impl TryFrom for CreateTableBuilder { collation, on_commit, on_cluster, + order_by, } => Ok(Self { or_replace, temporary, @@ -300,6 +309,7 @@ impl TryFrom for CreateTableBuilder { collation, on_commit, on_cluster, + order_by, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 497b7ac32..0d2303cad 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1254,9 +1254,13 @@ pub enum Statement { default_charset: Option, collation: Option, on_commit: Option, - /// Click house "ON CLUSTER" clause: + /// ClickHouse "ON CLUSTER" clause: /// on_cluster: Option, + /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different + /// than empty (represented as ()), the latter meaning "no sorting". + /// + order_by: Option>, }, /// SQLite's `CREATE VIRTUAL TABLE .. USING ()` CreateVirtualTable { @@ -2053,6 +2057,7 @@ impl fmt::Display for Statement { collation, on_commit, on_cluster, + order_by, } => { // We want to allow the following options // Empty column list, allowed by PostgreSQL: @@ -2196,12 +2201,15 @@ impl fmt::Display for Statement { if !with_options.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_options))?; } - if let Some(query) = query { - write!(f, " AS {query}")?; - } if let Some(engine) = engine { write!(f, " ENGINE={engine}")?; } + if let Some(order_by) = order_by { + write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; + } + if let Some(query) = query { + write!(f, " AS {query}")?; + } if let Some(default_charset) = default_charset { write!(f, " DEFAULT CHARSET={default_charset}")?; } diff --git a/src/parser.rs b/src/parser.rs index 526cf58d4..f2009d31a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3340,12 +3340,6 @@ impl<'a> Parser<'a> { // PostgreSQL supports `WITH ( options )`, before `AS` let with_options = self.parse_options(Keyword::WITH)?; let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; - // Parse optional `AS ( query )` - let query = if self.parse_keyword(Keyword::AS) { - Some(Box::new(self.parse_query()?)) - } else { - None - }; let engine = if self.parse_keyword(Keyword::ENGINE) { self.expect_token(&Token::Eq)?; @@ -3358,6 +3352,29 @@ impl<'a> Parser<'a> { None }; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + if self.consume_token(&Token::LParen) { + let columns = if self.peek_token() != Token::RParen { + self.parse_comma_separated(Parser::parse_identifier)? + } else { + vec![] + }; + self.expect_token(&Token::RParen)?; + Some(columns) + } else { + Some(vec![self.parse_identifier()?]) + } + } else { + None + }; + + // Parse optional `AS ( query )` + let query = if self.parse_keyword(Keyword::AS) { + Some(Box::new(self.parse_query()?)) + } else { + None + }; + let default_charset = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARSET]) { self.expect_token(&Token::Eq)?; let next_token = self.next_token(); @@ -3414,6 +3431,7 @@ impl<'a> Parser<'a> { .like(like) .clone_clause(clone) .engine(engine) + .order_by(order_by) .default_charset(default_charset) .collation(collation) .on_commit(on_commit) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 10e0d9ea5..dfea7f18c 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -317,6 +317,18 @@ fn parse_similar_to() { chk(true); } +#[test] +fn parse_create_table() { + clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#); + clickhouse().one_statement_parses_to( + r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x""#, + 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"#, + ); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], From 7f4c9132d71029636f00a5c7ebe1d21898887c47 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 6 Mar 2023 17:43:22 +0100 Subject: [PATCH 168/806] Fix table alias parsing regression in 0.31.0 by backing out redshift column definition list (#827) * Fix table alias parsing regression * Revert "Support redshift's columns definition list for system information functions (#769)" This reverts commit c35dcc93a78f72a9060d76bba5ee6c292909e54a. --- src/ast/mod.rs | 29 -------------- src/ast/query.rs | 7 ---- src/keywords.rs | 2 - src/parser.rs | 46 ---------------------- src/test_utils.rs | 1 - tests/sqlparser_bigquery.rs | 1 - tests/sqlparser_clickhouse.rs | 3 -- tests/sqlparser_common.rs | 58 ++++++++++++++++++---------- tests/sqlparser_hive.rs | 2 - tests/sqlparser_mssql.rs | 2 - tests/sqlparser_mysql.rs | 3 -- tests/sqlparser_postgres.rs | 2 - tests/sqlparser_redshift.rs | 73 ----------------------------------- tests/sqlparser_snowflake.rs | 2 - 14 files changed, 37 insertions(+), 194 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0d2303cad..73dd1f5b2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4150,35 +4150,6 @@ impl fmt::Display for SearchModifier { } } -/// A result table definition i.e. `cols(view_schema name, view_name name, col_name name, col_type varchar, col_num int)` -/// used for redshift functions: pg_get_late_binding_view_cols, pg_get_cols, pg_get_grantee_by_iam_role,pg_get_iam_role_by_user -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct TableAliasDefinition { - pub name: Ident, - pub args: Vec, -} - -impl fmt::Display for TableAliasDefinition { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}({})", self.name, display_comma_separated(&self.args))?; - 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 IdentPair(pub Ident, pub Ident); - -impl fmt::Display for IdentPair { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {}", self.0, self.1)?; - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/query.rs b/src/ast/query.rs index 1feb7a4aa..361edf0bc 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -631,9 +631,6 @@ pub enum TableFactor { /// vector of arguments, in the case of a table-valued function call, /// whereas it's `None` in the case of a regular table name. args: Option>, - /// A table alias definition i.e. `cols(view_schema name, view_name name, col_name name, col_type varchar, col_num int)` - /// used for redshift functions: pg_get_late_binding_view_cols, pg_get_cols, pg_get_grantee_by_iam_role,pg_get_iam_role_by_user) - columns_definition: Option, /// MSSQL-specific `WITH (...)` hints such as NOLOCK. with_hints: Vec, }, @@ -682,7 +679,6 @@ impl fmt::Display for TableFactor { name, alias, args, - columns_definition, with_hints, } => { write!(f, "{name}")?; @@ -692,9 +688,6 @@ impl fmt::Display for TableFactor { if let Some(alias) = alias { write!(f, " AS {alias}")?; } - if let Some(columns_definition) = columns_definition { - write!(f, " {columns_definition}")?; - } if !with_hints.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_hints))?; } diff --git a/src/keywords.rs b/src/keywords.rs index af741aeef..dca9375e8 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -671,7 +671,6 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::OUTER, Keyword::SET, Keyword::QUALIFY, - Keyword::AS, ]; /// Can't be used as a column alias, so that `SELECT alias` @@ -701,5 +700,4 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ // Reserved only as a column alias in the `SELECT` clause Keyword::FROM, Keyword::INTO, - Keyword::AS, ]; diff --git a/src/parser.rs b/src/parser.rs index f2009d31a..f85e8d1b7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5731,7 +5731,6 @@ impl<'a> Parser<'a> { } else { None }; - let columns_definition = self.parse_redshift_columns_definition_list()?; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; // MSSQL-specific table hints: let mut with_hints = vec![]; @@ -5748,56 +5747,11 @@ impl<'a> Parser<'a> { name, alias, args, - columns_definition, with_hints, }) } } - fn parse_redshift_columns_definition_list( - &mut self, - ) -> Result, ParserError> { - if !dialect_of!(self is RedshiftSqlDialect | GenericDialect) { - return Ok(None); - } - - if let Some(col_definition_list_name) = self.parse_optional_columns_definition_list_alias() - { - if self.consume_token(&Token::LParen) { - let names = self.parse_comma_separated(Parser::parse_ident_pair)?; - self.expect_token(&Token::RParen)?; - Ok(Some(TableAliasDefinition { - name: col_definition_list_name, - args: names, - })) - } else { - self.prev_token(); - Ok(None) - } - } else { - Ok(None) - } - } - - fn parse_optional_columns_definition_list_alias(&mut self) -> Option { - match self.next_token().token { - Token::Word(w) if !keywords::RESERVED_FOR_TABLE_ALIAS.contains(&w.keyword) => { - Some(w.to_ident()) - } - _ => { - self.prev_token(); - None - } - } - } - - fn parse_ident_pair(&mut self) -> Result { - Ok(IdentPair( - self.parse_identifier()?, - self.parse_identifier()?, - )) - } - pub fn parse_derived_table_factor( &mut self, lateral: IsLateral, diff --git a/src/test_utils.rs b/src/test_utils.rs index b2bff0812..d5bafd90f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -185,7 +185,6 @@ pub fn table(name: impl Into) -> TableFactor { name: ObjectName(vec![Ident::new(name.into())]), alias: None, args: None, - columns_definition: None, with_hints: vec![], } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index ac3a55cc7..11a8c6e5c 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -99,7 +99,6 @@ fn parse_table_identifiers() { name: ObjectName(expected), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![] diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index dfea7f18c..3ee3fbc03 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -60,7 +60,6 @@ fn parse_map_access_expr() { name: ObjectName(vec![Ident::new("foos")]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![] @@ -165,13 +164,11 @@ fn parse_delimited_identifiers() { name, alias, args, - columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); - assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1e56b2168..55690da3a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -210,7 +210,6 @@ fn parse_update_set_from() { name: ObjectName(vec![Ident::new("t1")]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -237,7 +236,6 @@ fn parse_update_set_from() { name: ObjectName(vec![Ident::new("t1")]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -300,7 +298,6 @@ fn parse_update_with_table_alias() { columns: vec![], }), args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -333,6 +330,43 @@ fn parse_update_with_table_alias() { } } +#[test] +fn parse_select_with_table_alias_as() { + // AS is optional + one_statement_parses_to( + "SELECT a, b, c FROM lineitem l (A, B, C)", + "SELECT a, b, c FROM lineitem AS l (A, B, C)", + ); +} + +#[test] +fn parse_select_with_table_alias() { + let select = verified_only_select("SELECT a, b, c FROM lineitem AS l (A, B, C)"); + assert_eq!( + select.projection, + vec![ + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("a")),), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("b")),), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("c")),), + ] + ); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("lineitem")]), + alias: Some(TableAlias { + name: Ident::new("l"), + columns: vec![Ident::new("A"), Ident::new("B"), Ident::new("C"),], + }), + args: None, + with_hints: vec![], + }, + joins: vec![], + }] + ); +} + #[test] fn parse_invalid_table_name() { let ast = all_dialects() @@ -356,7 +390,6 @@ fn parse_delete_statement() { name: ObjectName(vec![Ident::with_quote('"', "table")]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, table_name @@ -383,7 +416,6 @@ fn parse_where_delete_statement() { name: ObjectName(vec![Ident::new("foo")]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, table_name, @@ -424,7 +456,6 @@ fn parse_where_delete_with_alias_statement() { columns: vec![], }), args: None, - columns_definition: None, with_hints: vec![], }, table_name, @@ -438,7 +469,6 @@ fn parse_where_delete_with_alias_statement() { columns: vec![], }), args: None, - columns_definition: None, with_hints: vec![], }), using @@ -3465,7 +3495,6 @@ fn parse_interval_and_or_xor() { }]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -3981,7 +4010,6 @@ fn parse_implicit_join() { name: ObjectName(vec!["t1".into()]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -3991,7 +4019,6 @@ fn parse_implicit_join() { name: ObjectName(vec!["t2".into()]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -4009,7 +4036,6 @@ fn parse_implicit_join() { name: ObjectName(vec!["t1a".into()]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![Join { @@ -4017,7 +4043,6 @@ fn parse_implicit_join() { name: ObjectName(vec!["t1b".into()]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -4028,7 +4053,6 @@ fn parse_implicit_join() { name: ObjectName(vec!["t2a".into()]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![Join { @@ -4036,7 +4060,6 @@ fn parse_implicit_join() { name: ObjectName(vec!["t2b".into()]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -4057,7 +4080,6 @@ fn parse_cross_join() { name: ObjectName(vec![Ident::new("t2")]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::CrossJoin, @@ -4078,7 +4100,6 @@ fn parse_joins_on() { name: ObjectName(vec![Ident::new(relation.into())]), alias, args: None, - columns_definition: None, with_hints: vec![], }, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -4148,7 +4169,6 @@ fn parse_joins_using() { name: ObjectName(vec![Ident::new(relation.into())]), alias, args: None, - columns_definition: None, with_hints: vec![], }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), @@ -4210,7 +4230,6 @@ fn parse_natural_join() { name: ObjectName(vec![Ident::new("t2")]), alias, args: None, - columns_definition: None, with_hints: vec![], }, join_operator: f(JoinConstraint::Natural), @@ -4475,7 +4494,6 @@ fn parse_derived_tables() { name: ObjectName(vec!["t2".into()]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -5767,7 +5785,6 @@ fn parse_merge() { columns: vec![], }), args: None, - columns_definition: None, with_hints: vec![], } ); @@ -5791,7 +5808,6 @@ fn parse_merge() { name: ObjectName(vec![Ident::new("s"), Ident::new("foo")]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 7c17ea120..064a090f7 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -320,13 +320,11 @@ fn parse_delimited_identifiers() { name, alias, args, - columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); - assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 874dccfab..41b0803e4 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -152,13 +152,11 @@ fn parse_delimited_identifiers() { name, alias, args, - columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); - assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b4ae5cd5e..04c86cb15 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -870,7 +870,6 @@ fn parse_update_with_joins() { columns: vec![] }), args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![Join { @@ -881,7 +880,6 @@ fn parse_update_with_joins() { columns: vec![] }), args: None, - columns_definition: None, with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { @@ -1003,7 +1001,6 @@ fn parse_substring_in_select() { }]), alias: None, args: None, - columns_definition: None, with_hints: vec![] }, joins: vec![] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 72f364650..6a3f88e37 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2189,13 +2189,11 @@ fn parse_delimited_identifiers() { name, alias, args, - columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); - assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index fdbd66e79..7597ee981 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -44,7 +44,6 @@ fn test_square_brackets_over_db_schema_table_name() { ]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -89,7 +88,6 @@ fn test_double_quotes_over_db_schema_table_name() { ]), alias: None, args: None, - columns_definition: None, with_hints: vec![], }, joins: vec![], @@ -109,13 +107,11 @@ fn parse_delimited_identifiers() { name, alias, args, - columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); - assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), @@ -276,72 +272,3 @@ fn test_sharp() { select.projection[0] ); } - -#[test] -fn test_parse_pg_get_late_binding_view_cols() { - let sql = "select * from pg_get_late_binding_view_cols() some_name_cols(view_schema name, view_name name, col_name name)"; - let expected = "SELECT * FROM pg_get_late_binding_view_cols() some_name_cols(view_schema name, view_name name, col_name name)"; - redshift().one_statement_parses_to(sql, expected); - - let select = redshift().verified_only_select(expected); - assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::new("pg_get_late_binding_view_cols")],), - args: Some(vec![]), - alias: None, - columns_definition: Some(TableAliasDefinition { - name: Ident::new("some_name_cols"), - args: vec![ - IdentPair(Ident::new("view_schema"), Ident::new("name")), - IdentPair(Ident::new("view_name"), Ident::new("name")), - IdentPair(Ident::new("col_name"), Ident::new("name")) - ] - }), - with_hints: vec![] - }, - select.from[0].relation - ); -} - -#[test] -fn test_parse_pg_get_cols() { - let sql = - "SELECT * FROM pg_get_cols() some_name(view_schema name, view_name name, col_name name)"; - redshift().verified_stmt(sql); -} - -#[test] -fn test_parse_pg_get_grantee_by_iam_role() { - let sql = "SELECT grantee, grantee_type, cmd_type FROM pg_get_grantee_by_iam_role('arn:aws:iam::123456789012:role/Redshift-S3-Write') res_grantee(grantee text, grantee_type text, cmd_type text)"; - redshift().verified_stmt(sql); -} - -#[test] -fn test_parse_pg_get_iam_role_by_user() { - let sql = "SELECT username, iam_role, cmd FROM pg_get_iam_role_by_user('reg_user1') res_iam_role(username text, iam_role text, cmd text)"; - redshift().verified_stmt(sql); -} - -#[test] -fn test_parse_pg_get_late_binding_view_cols_in_select() { - let sql = "SELECT pg_get_late_binding_view_cols()"; - redshift().verified_stmt(sql); -} - -#[test] -fn test_parse_pg_get_cols_in_select() { - let sql = "SELECT pg_get_cols()"; - redshift().verified_stmt(sql); -} - -#[test] -fn test_parse_pg_get_grantee_by_iam_role_in_select() { - let sql = "SELECT pg_get_grantee_by_iam_role()"; - redshift().verified_stmt(sql); -} - -#[test] -fn test_parse_pg_get_iam_role_by_user_in_select() { - let sql = "SELECT pg_get_iam_role_by_user()"; - redshift().verified_stmt(sql); -} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index eeaebf118..ec43e030c 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -220,13 +220,11 @@ fn parse_delimited_identifiers() { name, alias, args, - columns_definition, with_hints, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); - assert!(columns_definition.is_none()); assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), From 1d358592ab0052305ee8ec8da5bb4059eed36e34 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 6 Mar 2023 17:57:27 +0100 Subject: [PATCH 169/806] Changelog for 0.32.0 (#830) --- CHANGELOG.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6003b39d0..840630fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ Given that the parser produces a typed AST, any changes to the AST will technica Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.32.0] 2023-03-6 + +### Added +* Support ClickHouse `CREATE TABLE` with `ORDER BY` (#824) - Thanks @ankrgyl +* Support PostgreSQL exponentiation `^` operator (#813) - Thanks @michael-2956 +* Support `BIGNUMERIC` type in BigQuery (#811) - Thanks @togami2864 +* Support for optional trailing commas (#810) - Thanks @ankrgyl + +### Fixed +* Fix table alias parsing regression by backing out redshift column definition list (#827) - Thanks @alamb +* Fix typo in `ReplaceSelectElement` `colum_name` --> `column_name` (#822) - Thanks @togami2864 + ## [0.31.0] 2023-03-1 ### Added @@ -21,15 +33,13 @@ Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented * Support MySQL Character Set Introducers (#788) - Thanks @mskrzypkows ### Fixed - -Fix clippy error in ci (#803) - Thanks @togami2864 -Handle offset in map key in BigQuery dialect (#797) - Thanks @Ziinc -Fix a typo (precendence -> precedence) (#794) - Thanks @SARDONYX-sard -use post_* visitors for mutable visits (#789) - Thanks @lovasoa +* Fix clippy error in ci (#803) - Thanks @togami2864 +* Handle offset in map key in BigQuery dialect (#797) - Thanks @Ziinc +* Fix a typo (precendence -> precedence) (#794) - Thanks @SARDONYX-sard +* use post_* visitors for mutable visits (#789) - Thanks @lovasoa ### Changed -Add another known user (#787) - Thanks @joocer - +* Add another known user (#787) - Thanks @joocer ## [0.30.0] 2023-01-02 From 5f815c2b08e7e916412ed326ae425ada5bfa4c08 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 6 Mar 2023 11:58:23 -0500 Subject: [PATCH 170/806] (cargo-release) version 0.32.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 776524b88..1d3f244d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.31.0" +version = "0.32.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 9ea396d0da08084cdbd812a6b6316565b32e867c Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 7 Mar 2023 13:07:29 +0100 Subject: [PATCH 171/806] Improve documentation on `verified_*` methods (#828) --- src/test_utils.rs | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index d5bafd90f..422ecc77d 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -68,17 +68,25 @@ impl TestedDialects { }) } + /// Parses a single SQL string into multiple statements, ensuring + /// the result is the same for all tested dialects. pub fn parse_sql_statements(&self, sql: &str) -> Result, ParserError> { self.one_of_identical_results(|dialect| Parser::parse_sql(dialect, sql)) // To fail the `ensure_multiple_dialects_are_tested` test: // Parser::parse_sql(&**self.dialects.first().unwrap(), sql) } - /// Ensures that `sql` parses as a single statement and returns it. - /// If non-empty `canonical` SQL representation is provided, - /// additionally asserts that parsing `sql` results in the same parse - /// tree as parsing `canonical`, and that serializing it back to string - /// results in the `canonical` representation. + /// Ensures that `sql` parses as a single [Statement] for all tested + /// dialects. + /// + /// If `canonical` is non empty,this function additionally asserts + /// that: + /// + /// 1. parsing `sql` results in the same [`Statement`] as parsing + /// `canonical`. + /// + /// 2. re-serializing the result of parsing `sql` produces the same + /// `canonical` sql string pub fn one_statement_parses_to(&self, sql: &str, canonical: &str) -> Statement { let mut statements = self.parse_sql_statements(sql).unwrap(); assert_eq!(statements.len(), 1); @@ -94,14 +102,16 @@ impl TestedDialects { only_statement } - /// Ensures that `sql` parses as a single [Statement], and is not modified - /// after a serialization round-trip. - pub fn verified_stmt(&self, query: &str) -> Statement { - self.one_statement_parses_to(query, query) + /// Ensures that `sql` parses as a single [Statement], and that + /// re-serializing the parse result produces the same `sql` + /// string (is not modified after a serialization round-trip). + pub fn verified_stmt(&self, sql: &str) -> Statement { + self.one_statement_parses_to(sql, sql) } - /// Ensures that `sql` parses as a single [Query], and is not modified - /// after a serialization round-trip. + /// Ensures that `sql` parses as a single [Query], and that + /// re-serializing the parse result produces the same `sql` + /// string (is not modified after a serialization round-trip). pub fn verified_query(&self, sql: &str) -> Query { match self.verified_stmt(sql) { Statement::Query(query) => *query, @@ -109,8 +119,9 @@ impl TestedDialects { } } - /// Ensures that `sql` parses as a single [Select], and is not modified - /// after a serialization round-trip. + /// Ensures that `sql` parses as a single [Select], and that + /// re-serializing the parse result produces the same `sql` + /// string (is not modified after a serialization round-trip). pub fn verified_only_select(&self, query: &str) -> Select { match *self.verified_query(query).body { SetExpr::Select(s) => *s, @@ -118,8 +129,9 @@ impl TestedDialects { } } - /// Ensures that `sql` parses as an expression, and is not modified - /// after a serialization round-trip. + /// Ensures that `sql` parses as an [`Expr`], and that + /// re-serializing the parse result produces the same `sql` + /// string (is not modified after a serialization round-trip). pub fn verified_expr(&self, sql: &str) -> Expr { let ast = self .run_parser_method(sql, |parser| parser.parse_expr()) From 548191814c3ba53c131a53b5035951851bef11f5 Mon Sep 17 00:00:00 2001 From: "pawel.leszczynski" Date: Tue, 7 Mar 2023 13:16:39 +0100 Subject: [PATCH 172/806] support snowflake alter table swap with (#825) Signed-off-by: Pawel Leszczynski --- src/ast/ddl.rs | 7 +++++++ src/keywords.rs | 1 + src/parser.rs | 6 +++++- tests/sqlparser_snowflake.rs | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 31e944b6b..3edabd7f8 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -96,6 +96,10 @@ pub enum AlterTableOperation { column_name: Ident, op: AlterColumnOperation, }, + /// 'SWAP WITH ' + /// + /// Note: this is Snowflake specific + SwapWith { table_name: ObjectName }, } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -203,6 +207,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::RenameConstraint { old_name, new_name } => { write!(f, "RENAME CONSTRAINT {old_name} TO {new_name}") } + AlterTableOperation::SwapWith { table_name } => { + write!(f, "SWAP WITH {table_name}") + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index dca9375e8..dd5ead7e1 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -545,6 +545,7 @@ define_keywords!( SUM, SUPER, SUPERUSER, + SWAP, SYMMETRIC, SYNC, SYSTEM, diff --git a/src/parser.rs b/src/parser.rs index f85e8d1b7..da487e36f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3909,9 +3909,13 @@ impl<'a> Parser<'a> { ); }; AlterTableOperation::AlterColumn { column_name, op } + } else if self.parse_keyword(Keyword::SWAP) { + self.expect_keyword(Keyword::WITH)?; + let table_name = self.parse_object_name()?; + AlterTableOperation::SwapWith { table_name } } else { return self.expected( - "ADD, RENAME, PARTITION or DROP after ALTER TABLE", + "ADD, RENAME, PARTITION, SWAP or DROP after ALTER TABLE", self.peek_token(), ); }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ec43e030c..3a02cb627 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -491,3 +491,18 @@ fn test_select_wildcard_with_exclude_and_rename() { "sql parser error: Expected end of statement, found: EXCLUDE" ); } + +#[test] +fn test_alter_table_swap_with() { + let sql = "ALTER TABLE tab1 SWAP WITH tab2"; + match snowflake_and_generic().verified_stmt(sql) { + Statement::AlterTable { + name, + operation: AlterTableOperation::SwapWith { table_name }, + } => { + assert_eq!("tab1", name.to_string()); + assert_eq!("tab2", table_name.to_string()); + } + _ => unreachable!(), + }; +} From 4ff3aeb040f7094827cf47404b16cc990afab9b8 Mon Sep 17 00:00:00 2001 From: "pawel.leszczynski" Date: Thu, 9 Mar 2023 14:06:39 +0100 Subject: [PATCH 173/806] support IF EXISTS in COMMENT statements (#831) * support IF EXISTS in COMMENT statements Signed-off-by: Pawel Leszczynski * Update src/ast/mod.rs Co-authored-by: Andrew Lamb --------- Signed-off-by: Pawel Leszczynski Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 10 +++++++++- src/dialect/postgresql.rs | 3 +++ tests/sqlparser_postgres.rs | 8 +++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 73dd1f5b2..bd31949ed 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1487,6 +1487,9 @@ pub enum Statement { object_type: CommentObject, object_name: ObjectName, comment: Option, + /// An optional `IF EXISTS` clause. (Non-standard.) + /// See + if_exists: bool, }, /// `COMMIT [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ]` Commit { chain: bool }, @@ -2637,8 +2640,13 @@ impl fmt::Display for Statement { object_type, object_name, comment, + if_exists, } => { - write!(f, "COMMENT ON {object_type} {object_name} IS ")?; + write!(f, "COMMENT ")?; + if *if_exists { + write!(f, "IF EXISTS ")? + }; + write!(f, "ON {object_type} {object_name} IS ")?; if let Some(c) = comment { write!(f, "'{c}'") } else { diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index fe1953d2a..da07fefe9 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -49,6 +49,8 @@ impl Dialect for PostgreSqlDialect { } pub fn parse_comment(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + parser.expect_keyword(Keyword::ON)?; let token = parser.next_token(); @@ -74,5 +76,6 @@ pub fn parse_comment(parser: &mut Parser) -> Result { object_type, object_name, comment, + if_exists, }) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6a3f88e37..e541e5158 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1790,10 +1790,12 @@ fn parse_comments() { object_type, object_name, comment: Some(comment), + if_exists, } => { assert_eq!("comment", comment); assert_eq!("tab.name", object_name.to_string()); assert_eq!(CommentObject::Column, object_type); + assert!(!if_exists); } _ => unreachable!(), } @@ -1803,22 +1805,26 @@ fn parse_comments() { object_type, object_name, comment: Some(comment), + if_exists, } => { assert_eq!("comment", comment); assert_eq!("public.tab", object_name.to_string()); assert_eq!(CommentObject::Table, object_type); + assert!(!if_exists); } _ => unreachable!(), } - match pg().verified_stmt("COMMENT ON TABLE public.tab IS NULL") { + match pg().verified_stmt("COMMENT IF EXISTS ON TABLE public.tab IS NULL") { Statement::Comment { object_type, object_name, comment: None, + if_exists, } => { assert_eq!("public.tab", object_name.to_string()); assert_eq!(CommentObject::Table, object_type); + assert!(if_exists); } _ => unreachable!(), } From a8a8e65b7c7dddaba79d1d1553aabd88ae9c2568 Mon Sep 17 00:00:00 2001 From: sam <2740878+sam-mmm@users.noreply.github.com> Date: Thu, 16 Mar 2023 15:24:00 +0530 Subject: [PATCH 174/806] PostgreSQL: GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY and GENERATED ALWAYS AS ( generation_expr ) support (#832) * GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) basic impl - test are failing. * PostgreSQL GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) and GENERATED ALWAYS AS ( generation_expr ) STORED implementation. --- src/ast/ddl.rs | 64 +++++++++- src/ast/mod.rs | 2 +- src/dialect/ansi.rs | 7 +- src/dialect/bigquery.rs | 8 +- src/dialect/clickhouse.rs | 4 +- src/dialect/generic.rs | 12 +- src/dialect/hive.rs | 11 +- src/dialect/mssql.rs | 12 +- src/dialect/mysql.rs | 6 +- src/dialect/postgresql.rs | 8 +- src/dialect/snowflake.rs | 8 +- src/dialect/sqlite.rs | 6 +- src/keywords.rs | 2 + src/parser.rs | 49 ++++++++ src/tokenizer.rs | 2 +- tests/sqlparser_custom_dialect.rs | 8 +- tests/sqlparser_postgres.rs | 198 +++++++++++++++++++++++++++++- 17 files changed, 350 insertions(+), 57 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3edabd7f8..b515c261e 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -24,7 +24,9 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; -use crate::ast::{display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName}; +use crate::ast::{ + display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName, SequenceOptions, +}; use crate::tokenizer::Token; /// An `ALTER TABLE` (`Statement::AlterTable`) operation @@ -575,6 +577,13 @@ pub enum ColumnOption { CharacterSet(ObjectName), Comment(String), OnUpdate(Expr), + /// `Generated`s are modifiers that follow a column definition in a `CREATE + /// TABLE` statement. + Generated { + generated_as: GeneratedAs, + sequence_options: Option>, + generation_expr: Option, + }, } impl fmt::Display for ColumnOption { @@ -610,10 +619,63 @@ impl fmt::Display for ColumnOption { CharacterSet(n) => write!(f, "CHARACTER SET {n}"), Comment(v) => write!(f, "COMMENT '{}'", escape_single_quote_string(v)), OnUpdate(expr) => write!(f, "ON UPDATE {expr}"), + Generated { + generated_as, + sequence_options, + generation_expr, + } => match generated_as { + GeneratedAs::Always => { + write!(f, "GENERATED ALWAYS AS IDENTITY")?; + if sequence_options.is_some() { + let so = sequence_options.as_ref().unwrap(); + if !so.is_empty() { + write!(f, " (")?; + } + for sequence_option in so { + write!(f, "{sequence_option}")?; + } + if !so.is_empty() { + write!(f, " )")?; + } + } + Ok(()) + } + GeneratedAs::ByDefault => { + write!(f, "GENERATED BY DEFAULT AS IDENTITY")?; + if sequence_options.is_some() { + let so = sequence_options.as_ref().unwrap(); + if !so.is_empty() { + write!(f, " (")?; + } + for sequence_option in so { + write!(f, "{sequence_option}")?; + } + if !so.is_empty() { + write!(f, " )")?; + } + } + Ok(()) + } + GeneratedAs::ExpStored => { + let expr = generation_expr.as_ref().unwrap(); + write!(f, "GENERATED ALWAYS AS ({expr}) STORED") + } + }, } } } +/// `GeneratedAs`s are modifiers that follow a column option in a `generated`. +/// 'ExpStored' is PostgreSQL specific +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum GeneratedAs { + Always, + ByDefault, + ExpStored, +} + fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); impl<'a> fmt::Display for ConstraintName<'a> { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bd31949ed..c19466ce0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -30,7 +30,7 @@ pub use self::data_type::{ }; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, - ColumnOptionDef, IndexType, KeyOrIndexDisplay, ReferentialAction, TableConstraint, + ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, ReferentialAction, TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ diff --git a/src/dialect/ansi.rs b/src/dialect/ansi.rs index 1015ca2d3..14c83ae16 100644 --- a/src/dialect/ansi.rs +++ b/src/dialect/ansi.rs @@ -17,13 +17,10 @@ pub struct AnsiDialect {} impl Dialect for AnsiDialect { fn is_identifier_start(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() } fn is_identifier_part(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) - || ch == '_' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_' } } diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index e42676b22..8266a32f0 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -22,13 +22,13 @@ impl Dialect for BigQueryDialect { } fn is_identifier_start(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } fn is_identifier_part(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() + || ch.is_ascii_digit() || ch == '_' || ch == '-' } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 24ec5e49f..395116f9c 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -18,10 +18,10 @@ pub struct ClickHouseDialect {} impl Dialect for ClickHouseDialect { fn is_identifier_start(&self, ch: char) -> bool { // See https://clickhouse.com/docs/en/sql-reference/syntax/#syntax-identifiers - ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } fn is_identifier_part(&self, ch: char) -> bool { - self.is_identifier_start(ch) || ('0'..='9').contains(&ch) + self.is_identifier_start(ch) || ch.is_ascii_digit() } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 818fa0d0a..51ae3dafd 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -17,17 +17,13 @@ pub struct GenericDialect; impl Dialect for GenericDialect { fn is_identifier_start(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ch == '_' - || ch == '#' - || ch == '@' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' || ch == '#' || ch == '@' } fn is_identifier_part(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() + || ch.is_ascii_digit() || ch == '@' || ch == '$' || ch == '#' diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index ceb5488ef..96cefb1d9 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -21,16 +21,13 @@ impl Dialect for HiveDialect { } fn is_identifier_start(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) - || ch == '$' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '$' } fn is_identifier_part(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() + || ch.is_ascii_digit() || ch == '_' || ch == '$' || ch == '{' diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 539a17a9f..682376b17 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -23,17 +23,13 @@ impl Dialect for MsSqlDialect { fn is_identifier_start(&self, ch: char) -> bool { // See https://docs.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-2017#rules-for-regular-identifiers // We don't support non-latin "letters" currently. - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ch == '_' - || ch == '#' - || ch == '@' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' || ch == '#' || ch == '@' } fn is_identifier_part(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() + || ch.is_ascii_digit() || ch == '@' || ch == '$' || ch == '#' diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index d6095262c..441b2c240 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -20,8 +20,8 @@ impl Dialect for MySqlDialect { // See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html. // We don't yet support identifiers beginning with numbers, as that // makes it hard to distinguish numeric literals. - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() || ch == '_' || ch == '$' || ch == '@' @@ -29,7 +29,7 @@ impl Dialect for MySqlDialect { } fn is_identifier_part(&self, ch: char) -> bool { - self.is_identifier_start(ch) || ('0'..='9').contains(&ch) + self.is_identifier_start(ch) || ch.is_ascii_digit() } fn is_delimited_identifier_start(&self, ch: char) -> bool { diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index da07fefe9..97b6b0ba8 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -24,13 +24,13 @@ impl Dialect for PostgreSqlDialect { // See https://www.postgresql.org/docs/11/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS // We don't yet support identifiers beginning with "letters with // diacritical marks and non-Latin letters" - ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } fn is_identifier_part(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() + || ch.is_ascii_digit() || ch == '$' || ch == '_' } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 11108e973..4fc8de735 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -18,13 +18,13 @@ pub struct SnowflakeDialect; impl Dialect for SnowflakeDialect { // see https://docs.snowflake.com/en/sql-reference/identifiers-syntax.html fn is_identifier_start(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } fn is_identifier_part(&self, ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() + || ch.is_ascii_digit() || ch == '$' || ch == '_' } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 64d7f62fd..fa21224f6 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -28,15 +28,15 @@ impl Dialect for SQLiteDialect { fn is_identifier_start(&self, ch: char) -> bool { // See https://www.sqlite.org/draft/tokenreq.html - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() || ch == '_' || ch == '$' || ('\u{007f}'..='\u{ffff}').contains(&ch) } fn is_identifier_part(&self, ch: char) -> bool { - self.is_identifier_start(ch) || ('0'..='9').contains(&ch) + self.is_identifier_start(ch) || ch.is_ascii_digit() } fn parse_statement(&self, parser: &mut Parser) -> Option> { diff --git a/src/keywords.rs b/src/keywords.rs index dd5ead7e1..a2db1a353 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -77,6 +77,7 @@ define_keywords!( ALL, ALLOCATE, ALTER, + ALWAYS, ANALYZE, AND, ANTI, @@ -270,6 +271,7 @@ define_keywords!( FUNCTION, FUNCTIONS, FUSION, + GENERATED, GET, GLOBAL, GRANT, diff --git a/src/parser.rs b/src/parser.rs index da487e36f..98fe589d6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3567,6 +3567,55 @@ impl<'a> Parser<'a> { { let expr = self.parse_expr()?; Ok(Some(ColumnOption::OnUpdate(expr))) + } else if self.parse_keyword(Keyword::GENERATED) { + self.parse_optional_column_option_generated() + } else { + Ok(None) + } + } + fn parse_optional_column_option_generated( + &mut self, + ) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::ALWAYS, Keyword::AS, Keyword::IDENTITY]) { + let mut sequence_options = vec![]; + if self.expect_token(&Token::LParen).is_ok() { + sequence_options = self.parse_create_sequence_options()?; + self.expect_token(&Token::RParen)?; + } + Ok(Some(ColumnOption::Generated { + generated_as: GeneratedAs::Always, + sequence_options: Some(sequence_options), + generation_expr: None, + })) + } else if self.parse_keywords(&[ + Keyword::BY, + Keyword::DEFAULT, + Keyword::AS, + Keyword::IDENTITY, + ]) { + let mut sequence_options = vec![]; + if self.expect_token(&Token::LParen).is_ok() { + sequence_options = self.parse_create_sequence_options()?; + self.expect_token(&Token::RParen)?; + } + Ok(Some(ColumnOption::Generated { + generated_as: GeneratedAs::ByDefault, + sequence_options: Some(sequence_options), + generation_expr: None, + })) + } else if self.parse_keywords(&[Keyword::ALWAYS, Keyword::AS]) { + if self.expect_token(&Token::LParen).is_ok() { + let expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + let _ = self.parse_keywords(&[Keyword::STORED]); + Ok(Some(ColumnOption::Generated { + generated_as: GeneratedAs::ExpStored, + sequence_options: None, + generation_expr: Some(expr), + })) + } else { + Ok(None) + } } else { Ok(None) } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 8134947df..bc6e3addb 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -596,7 +596,7 @@ impl<'a> Tokenizer<'a> { let word = self.tokenize_word(ch, chars); // TODO: implement parsing of exponent here - if word.chars().all(|x| ('0'..='9').contains(&x) || x == '.') { + if word.chars().all(|x| x.is_ascii_digit() || x == '.') { let mut inner_state = State { peekable: word.chars().peekable(), line: 0, diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index 465ce8720..516591382 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -126,13 +126,13 @@ fn custom_statement_parser() -> Result<(), ParserError> { } fn is_identifier_start(ch: char) -> bool { - ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } fn is_identifier_part(ch: char) -> bool { - ('a'..='z').contains(&ch) - || ('A'..='Z').contains(&ch) - || ('0'..='9').contains(&ch) + ch.is_ascii_lowercase() + || ch.is_ascii_uppercase() + || ch.is_ascii_digit() || ch == '$' || ch == '_' } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e541e5158..359889aac 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -22,6 +22,202 @@ use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; +#[test] +fn parse_create_table_generated_always_as_identity() { + //With primary key + let sql = "CREATE TABLE table2 ( + column21 bigint primary key generated always as identity , + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column21 BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \ + column30 TEXT)", + ); + + let sql = "CREATE TABLE table2 ( + column21 bigint primary key generated by default as identity , + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column21 BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, \ + column30 TEXT)", + ); + + //With out primary key + let sql = "CREATE TABLE table2 ( + column22 bigint generated always as identity , + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column22 BIGINT GENERATED ALWAYS AS IDENTITY, \ + column30 TEXT)", + ); + + let sql = "CREATE TABLE table2 ( + column22 bigint generated by default as identity , + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column22 BIGINT GENERATED BY DEFAULT AS IDENTITY, \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column23 bigint generated by default as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 NO CYCLE ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column23 BIGINT GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 NO CYCLE ), \ + column30 TEXT)", + ); + + let sql = "CREATE TABLE table2 ( + column24 bigint generated by default as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 CYCLE ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column24 BIGINT GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 CYCLE ), \ + column30 TEXT)", + ); + + let sql = "CREATE TABLE table2 ( + column25 bigint generated by default as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column25 BIGINT GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column26 bigint generated by default as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column26 BIGINT GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column27 bigint generated by default as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column27 BIGINT GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column28 bigint generated by default as identity ( INCREMENT 1 MINVALUE 1 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column28 BIGINT GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 MINVALUE 1 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column29 bigint generated by default as identity ( INCREMENT 1 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column29 BIGINT GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 ), \ + column30 TEXT)", + ); + + let sql = "CREATE TABLE table2 ( + column22 bigint generated always as identity , + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column22 BIGINT GENERATED ALWAYS AS IDENTITY, \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column23 bigint generated always as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 NO CYCLE ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column23 BIGINT GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 NO CYCLE ), \ + column30 TEXT)", + ); + + let sql = "CREATE TABLE table2 ( + column24 bigint generated always as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 CYCLE ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column24 BIGINT GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 CYCLE ), \ + column30 TEXT)", + ); + + let sql = "CREATE TABLE table2 ( + column25 bigint generated always as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column25 BIGINT GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 CACHE 2 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column26 bigint generated always as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column26 BIGINT GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 START WITH 10 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column27 bigint generated always as identity ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column27 BIGINT GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 MINVALUE 1 MAXVALUE 20 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column28 bigint generated always as identity ( INCREMENT 1 MINVALUE 1 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column28 BIGINT GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 MINVALUE 1 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + column29 bigint generated always as identity ( INCREMENT 1 ), + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + column29 BIGINT GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 ), \ + column30 TEXT)", + ); + let sql = "CREATE TABLE table2 ( + priceInDollar numeric, + princeInPound numeric GENERATED ALWAYS AS (priceInDollar * 0.22) STORED, + column30 text );"; + pg().one_statement_parses_to( + sql, + "CREATE TABLE table2 (\ + priceInDollar NUMERIC, \ + princeInPound NUMERIC GENERATED ALWAYS AS (priceInDollar * 0.22) STORED, \ + column30 TEXT)", + ); +} + #[test] fn parse_create_sequence() { // SimpleLogger::new().init().unwrap(); @@ -1408,12 +1604,10 @@ fn parse_pg_regex_match_ops() { fn parse_array_index_expr() { #[cfg(feature = "bigdecimal")] let num: Vec = (0..=10) - .into_iter() .map(|s| Expr::Value(Value::Number(bigdecimal::BigDecimal::from(s), false))) .collect(); #[cfg(not(feature = "bigdecimal"))] let num: Vec = (0..=10) - .into_iter() .map(|s| Expr::Value(Value::Number(s.to_string(), false))) .collect(); From eb67d489bb449de141975267c12d23d1ebdbecdd Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Sat, 18 Mar 2023 23:23:18 +0800 Subject: [PATCH 175/806] Correct typos in parser.rs (#838) --- src/parser.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 98fe589d6..f48700fd8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -57,7 +57,7 @@ macro_rules! return_ok_if_some { } #[cfg(feature = "std")] -/// Implemenation [`RecursionCounter`] if std is available +/// Implementation [`RecursionCounter`] if std is available mod recursion { use core::sync::atomic::{AtomicUsize, Ordering}; use std::rc::Rc; @@ -121,7 +121,7 @@ mod recursion { #[cfg(not(feature = "std"))] mod recursion { - /// Implemenation [`RecursionCounter`] if std is NOT available (and does not + /// Implementation [`RecursionCounter`] if std is NOT available (and does not /// guard against stack overflow). /// /// Has the same API as the std RecursionCounter implementation @@ -209,7 +209,7 @@ pub struct Parser<'a> { /// Additional options that allow you to mix & match behavior otherwise /// constrained to certain dialects (e.g. trailing commas) options: ParserOptions, - /// ensure the stack does not overflow by limiting recusion depth + /// ensure the stack does not overflow by limiting recursion depth recursion_counter: RecursionCounter, } @@ -360,7 +360,7 @@ impl<'a> Parser<'a> { Ok(stmts) } - /// Convience method to parse a string with one or more SQL + /// Convenience method to parse a string with one or more SQL /// statements into produce an Abstract Syntax Tree (AST). /// /// Example From a1b7341b87e96c15a8ed75c6222060e45a6a36ae Mon Sep 17 00:00:00 2001 From: Maciej Skrzypkowski Date: Thu, 23 Mar 2023 12:07:17 +0100 Subject: [PATCH 176/806] Non-Latin characters support (#840) * Non latin characters --------- Co-authored-by: Maciej Skrzypkowski * Test for mysql --------- Co-authored-by: Maciej Skrzypkowski --- src/dialect/generic.rs | 5 ++--- src/dialect/mssql.rs | 6 ++---- src/dialect/mysql.rs | 3 +-- src/dialect/postgresql.rs | 10 +++------- src/tokenizer.rs | 20 ++++++-------------- tests/sqlparser_common.rs | 20 ++++++++++++++++++++ 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 51ae3dafd..8d6ccb5e6 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -17,12 +17,11 @@ pub struct GenericDialect; impl Dialect for GenericDialect { fn is_identifier_start(&self, ch: char) -> bool { - ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' || ch == '#' || ch == '@' + ch.is_alphabetic() || ch == '_' || ch == '#' || ch == '@' } fn is_identifier_part(&self, ch: char) -> bool { - ch.is_ascii_lowercase() - || ch.is_ascii_uppercase() + ch.is_alphabetic() || ch.is_ascii_digit() || ch == '@' || ch == '$' diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 682376b17..a9056350d 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -22,13 +22,11 @@ impl Dialect for MsSqlDialect { fn is_identifier_start(&self, ch: char) -> bool { // See https://docs.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-2017#rules-for-regular-identifiers - // We don't support non-latin "letters" currently. - ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' || ch == '#' || ch == '@' + ch.is_alphabetic() || ch == '_' || ch == '#' || ch == '@' } fn is_identifier_part(&self, ch: char) -> bool { - ch.is_ascii_lowercase() - || ch.is_ascii_uppercase() + ch.is_alphabetic() || ch.is_ascii_digit() || ch == '@' || ch == '$' diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 441b2c240..82cbc5364 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -20,8 +20,7 @@ impl Dialect for MySqlDialect { // See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html. // We don't yet support identifiers beginning with numbers, as that // makes it hard to distinguish numeric literals. - ch.is_ascii_lowercase() - || ch.is_ascii_uppercase() + ch.is_alphabetic() || ch == '_' || ch == '$' || ch == '@' diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 97b6b0ba8..4ba6229eb 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -23,16 +23,12 @@ impl Dialect for PostgreSqlDialect { fn is_identifier_start(&self, ch: char) -> bool { // See https://www.postgresql.org/docs/11/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS // We don't yet support identifiers beginning with "letters with - // diacritical marks and non-Latin letters" - ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' + // diacritical marks" + ch.is_alphabetic() || ch == '_' } fn is_identifier_part(&self, ch: char) -> bool { - ch.is_ascii_lowercase() - || ch.is_ascii_uppercase() - || ch.is_ascii_digit() - || ch == '$' - || ch == '_' + ch.is_alphabetic() || ch.is_ascii_digit() || ch == '$' || ch == '_' } fn parse_statement(&self, parser: &mut Parser) -> Option> { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index bc6e3addb..d4f161b02 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1519,7 +1519,7 @@ mod tests { #[test] fn tokenize_invalid_string() { - let sql = String::from("\nمصطفىh"); + let sql = String::from("\n💝مصطفىh"); let dialect = GenericDialect {}; let mut tokenizer = Tokenizer::new(&dialect, &sql); @@ -1527,12 +1527,8 @@ mod tests { // println!("tokens: {:#?}", tokens); let expected = vec![ Token::Whitespace(Whitespace::Newline), - Token::Char('م'), - Token::Char('ص'), - Token::Char('ط'), - Token::Char('ف'), - Token::Char('ى'), - Token::make_word("h", None), + Token::Char('💝'), + Token::make_word("مصطفىh", None), ]; compare(expected, tokens); } @@ -1582,7 +1578,7 @@ mod tests { #[test] fn tokenize_invalid_string_cols() { - let sql = String::from("\n\nSELECT * FROM table\tمصطفىh"); + let sql = String::from("\n\nSELECT * FROM table\t💝مصطفىh"); let dialect = GenericDialect {}; let mut tokenizer = Tokenizer::new(&dialect, &sql); @@ -1599,12 +1595,8 @@ mod tests { Token::Whitespace(Whitespace::Space), Token::make_keyword("table"), Token::Whitespace(Whitespace::Tab), - Token::Char('م'), - Token::Char('ص'), - Token::Char('ط'), - Token::Char('ف'), - Token::Char('ى'), - Token::make_word("h", None), + Token::Char('💝'), + Token::make_word("مصطفىh", None), ]; compare(expected, tokens); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 55690da3a..73df48012 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6734,3 +6734,23 @@ fn make_where_clause(num: usize) -> String { } output } + +#[test] +fn parse_non_latin_identifiers() { + let supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + 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"); + assert!(supported_dialects + .parse_sql_statements("SELECT 💝 FROM table1") + .is_err()); +} From 79c7ac73df1a3696d158512866797e6551c5d35b Mon Sep 17 00:00:00 2001 From: "pawel.leszczynski" Date: Sun, 26 Mar 2023 13:31:37 +0200 Subject: [PATCH 177/806] support CREATE/DROP STAGE for Snowflake (#833) Signed-off-by: Pawel Leszczynski --- src/ast/helpers/mod.rs | 1 + src/ast/helpers/stmt_data_loading.rs | 123 +++++++++++++++ src/ast/mod.rs | 51 ++++++ src/dialect/snowflake.rs | 214 +++++++++++++++++++++++++ src/keywords.rs | 8 + src/parser.rs | 4 +- tests/sqlparser_snowflake.rs | 228 +++++++++++++++++++++++++++ 7 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 src/ast/helpers/stmt_data_loading.rs diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index 41098b014..b54e59b6d 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -1 +1,2 @@ pub mod stmt_create_table; +pub mod stmt_data_loading; diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs new file mode 100644 index 000000000..d5ba6eda0 --- /dev/null +++ b/src/ast/helpers/stmt_data_loading.rs @@ -0,0 +1,123 @@ +// Licensed 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. + +//! AST types specific to loading and unloading syntax, like one available in Snowflake which +//! contains: STAGE ddl operations, PUT upload or COPY INTO +//! See [this page](https://docs.snowflake.com/en/sql-reference/commands-data-loading) for more details. + +#[cfg(not(feature = "std"))] +use alloc::string::String; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use core::fmt; +use core::fmt::Formatter; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct StageParamsObject { + pub url: Option, + pub encryption: DataLoadingOptions, + pub endpoint: Option, + pub storage_integration: Option, + pub credentials: DataLoadingOptions, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DataLoadingOptions { + pub options: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DataLoadingOptionType { + STRING, + BOOLEAN, + ENUM, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DataLoadingOption { + pub option_name: String, + pub option_type: DataLoadingOptionType, + pub value: String, +} + +impl fmt::Display for StageParamsObject { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let url = &self.url.as_ref(); + let storage_integration = &self.storage_integration.as_ref(); + let endpoint = &self.endpoint.as_ref(); + + if url.is_some() { + write!(f, " URL='{}'", url.unwrap())?; + } + if storage_integration.is_some() { + write!(f, " STORAGE_INTEGRATION={}", storage_integration.unwrap())?; + } + if endpoint.is_some() { + write!(f, " ENDPOINT='{}'", endpoint.unwrap())?; + } + if !self.credentials.options.is_empty() { + write!(f, " CREDENTIALS=({})", self.credentials)?; + } + if !self.encryption.options.is_empty() { + write!(f, " ENCRYPTION=({})", self.encryption)?; + } + + Ok(()) + } +} + +impl fmt::Display for DataLoadingOptions { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if !self.options.is_empty() { + for option in &self.options { + write!(f, "{}", option)?; + if !option.eq(self.options.last().unwrap()) { + write!(f, " ")?; + } + } + } + Ok(()) + } +} + +impl fmt::Display for DataLoadingOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.option_type { + DataLoadingOptionType::STRING => { + write!(f, "{}='{}'", self.option_name, self.value)?; + } + DataLoadingOptionType::ENUM => { + // single quote is omitted + write!(f, "{}={}", self.option_name, self.value)?; + } + DataLoadingOptionType::BOOLEAN => { + // single quote is omitted + write!(f, "{}={}", self.option_name, self.value)?; + } + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c19466ce0..94b237b4f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -44,6 +44,7 @@ pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, }; +use crate::ast::helpers::stmt_data_loading::{DataLoadingOptions, StageParamsObject}; #[cfg(feature = "visitor")] pub use visitor::*; @@ -1524,6 +1525,21 @@ pub enum Statement { /// Optional parameters. params: CreateFunctionBody, }, + /// ```sql + /// CREATE STAGE + /// ``` + /// See + CreateStage { + or_replace: bool, + temporary: bool, + if_not_exists: bool, + name: ObjectName, + stage_params: StageParamsObject, + directory_table_params: DataLoadingOptions, + file_format: DataLoadingOptions, + copy_options: DataLoadingOptions, + comment: Option, + }, /// `ASSERT [AS ]` Assert { condition: Expr, @@ -2746,6 +2762,39 @@ impl fmt::Display for Statement { } write!(f, "") } + Statement::CreateStage { + or_replace, + temporary, + if_not_exists, + name, + stage_params, + directory_table_params, + file_format, + copy_options, + comment, + .. + } => { + write!( + f, + "CREATE {or_replace}{temp}STAGE {if_not_exists}{name}{stage_params}", + temp = if *temporary { "TEMPORARY " } else { "" }, + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if !directory_table_params.options.is_empty() { + write!(f, " DIRECTORY=({})", directory_table_params)?; + } + if !file_format.options.is_empty() { + write!(f, " FILE_FORMAT=({})", file_format)?; + } + if !copy_options.options.is_empty() { + write!(f, " COPY_OPTIONS=({})", copy_options)?; + } + if comment.is_some() { + write!(f, " COMMENT='{}'", comment.as_ref().unwrap())?; + } + Ok(()) + } } } } @@ -3405,6 +3454,7 @@ pub enum ObjectType { Schema, Role, Sequence, + Stage, } impl fmt::Display for ObjectType { @@ -3416,6 +3466,7 @@ impl fmt::Display for ObjectType { ObjectType::Schema => "SCHEMA", ObjectType::Role => "ROLE", ObjectType::Sequence => "SEQUENCE", + ObjectType::Stage => "STAGE", }) } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 4fc8de735..0be5e1df6 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -10,7 +10,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(not(feature = "std"))] +use crate::alloc::string::ToString; +use crate::ast::helpers::stmt_data_loading::{ + DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageParamsObject, +}; +use crate::ast::Statement; use crate::dialect::Dialect; +use crate::keywords::Keyword; +use crate::parser::{Parser, ParserError}; +use crate::tokenizer::Token; +#[cfg(not(feature = "std"))] +use alloc::vec; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; #[derive(Debug, Default)] pub struct SnowflakeDialect; @@ -32,4 +45,205 @@ impl Dialect for SnowflakeDialect { fn supports_within_after_array_aggregation(&self) -> bool { true } + + fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keyword(Keyword::CREATE) { + // possibly CREATE STAGE + //[ OR REPLACE ] + let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); + //[ TEMPORARY ] + let temporary = parser.parse_keyword(Keyword::TEMPORARY); + + if parser.parse_keyword(Keyword::STAGE) { + // OK - this is CREATE STAGE statement + return Some(parse_create_stage(or_replace, temporary, parser)); + } else { + // need to go back with the cursor + let mut back = 1; + if or_replace { + back += 2 + } + if temporary { + back += 1 + } + for _i in 0..back { + parser.prev_token(); + } + } + } + None + } +} + +pub fn parse_create_stage( + or_replace: bool, + temporary: bool, + parser: &mut Parser, +) -> Result { + //[ IF NOT EXISTS ] + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = parser.parse_object_name()?; + let mut directory_table_params = Vec::new(); + let mut file_format = Vec::new(); + let mut copy_options = Vec::new(); + let mut comment = None; + + // [ internalStageParams | externalStageParams ] + let stage_params = parse_stage_params(parser)?; + + // [ directoryTableParams ] + if parser.parse_keyword(Keyword::DIRECTORY) { + parser.expect_token(&Token::Eq)?; + directory_table_params = parse_parentheses_options(parser)?; + } + + // [ file_format] + if parser.parse_keyword(Keyword::FILE_FORMAT) { + parser.expect_token(&Token::Eq)?; + file_format = parse_parentheses_options(parser)?; + } + + // [ copy_options ] + if parser.parse_keyword(Keyword::COPY_OPTIONS) { + parser.expect_token(&Token::Eq)?; + copy_options = parse_parentheses_options(parser)?; + } + + // [ 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()), + }?) + } + + Ok(Statement::CreateStage { + or_replace, + temporary, + if_not_exists, + name, + stage_params, + directory_table_params: DataLoadingOptions { + options: directory_table_params, + }, + file_format: DataLoadingOptions { + options: file_format, + }, + copy_options: DataLoadingOptions { + options: copy_options, + }, + comment, + }) +} + +fn parse_stage_params(parser: &mut Parser) -> Result { + let (mut url, mut storage_integration, mut endpoint) = (None, None, None); + let mut encryption: DataLoadingOptions = DataLoadingOptions { options: vec![] }; + let mut credentials: DataLoadingOptions = DataLoadingOptions { options: vec![] }; + + // URL + if parser.parse_keyword(Keyword::URL) { + parser.expect_token(&Token::Eq)?; + url = Some(match parser.next_token().token { + Token::SingleQuotedString(word) => Ok(word), + _ => parser.expected("a URL statement", parser.peek_token()), + }?) + } + + // STORAGE INTEGRATION + if parser.parse_keyword(Keyword::STORAGE_INTEGRATION) { + parser.expect_token(&Token::Eq)?; + storage_integration = Some(parser.next_token().token.to_string()); + } + + // ENDPOINT + if parser.parse_keyword(Keyword::ENDPOINT) { + parser.expect_token(&Token::Eq)?; + endpoint = Some(match parser.next_token().token { + Token::SingleQuotedString(word) => Ok(word), + _ => parser.expected("an endpoint statement", parser.peek_token()), + }?) + } + + // CREDENTIALS + if parser.parse_keyword(Keyword::CREDENTIALS) { + parser.expect_token(&Token::Eq)?; + credentials = DataLoadingOptions { + options: parse_parentheses_options(parser)?, + }; + } + + // ENCRYPTION + if parser.parse_keyword(Keyword::ENCRYPTION) { + parser.expect_token(&Token::Eq)?; + encryption = DataLoadingOptions { + options: parse_parentheses_options(parser)?, + }; + } + + Ok(StageParamsObject { + url, + encryption, + endpoint, + storage_integration, + credentials, + }) +} + +/// Parses options provided within parentheses like: +/// ( ENABLE = { TRUE | FALSE } +/// [ AUTO_REFRESH = { TRUE | FALSE } ] +/// [ REFRESH_ON_CREATE = { TRUE | FALSE } ] +/// [ NOTIFICATION_INTEGRATION = '' ] ) +/// +fn parse_parentheses_options(parser: &mut Parser) -> Result, ParserError> { + let mut options: Vec = Vec::new(); + + parser.expect_token(&Token::LParen)?; + loop { + match parser.next_token().token { + Token::RParen => break, + Token::Word(key) => { + parser.expect_token(&Token::Eq)?; + if parser.parse_keyword(Keyword::TRUE) { + options.push(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::BOOLEAN, + value: "TRUE".to_string(), + }); + Ok(()) + } else if parser.parse_keyword(Keyword::FALSE) { + options.push(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::BOOLEAN, + value: "FALSE".to_string(), + }); + Ok(()) + } else { + match parser.next_token().token { + Token::SingleQuotedString(value) => { + options.push(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::STRING, + value, + }); + Ok(()) + } + Token::Word(word) => { + options.push(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::ENUM, + value: word.value, + }); + Ok(()) + } + _ => parser.expected("expected option value", parser.peek_token()), + } + } + } + _ => parser.expected("another option or ')'", parser.peek_token()), + }?; + } + Ok(options) } diff --git a/src/keywords.rs b/src/keywords.rs index a2db1a353..6002b5c48 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -159,6 +159,7 @@ define_keywords!( CONTAINS, CONVERT, COPY, + COPY_OPTIONS, CORR, CORRESPONDING, COUNT, @@ -167,6 +168,7 @@ define_keywords!( CREATE, CREATEDB, CREATEROLE, + CREDENTIALS, CROSS, CSV, CUBE, @@ -220,8 +222,10 @@ define_keywords!( ELEMENT, ELSE, ENCODING, + ENCRYPTION, END, END_EXEC = "END-EXEC", + ENDPOINT, END_FRAME, END_PARTITION, ENGINE, @@ -248,6 +252,7 @@ define_keywords!( FETCH, FIELDS, FILE, + FILE_FORMAT, FILTER, FIRST, FIRST_VALUE, @@ -531,6 +536,7 @@ define_keywords!( SQLWARNING, SQRT, STABLE, + STAGE, START, STATIC, STATISTICS, @@ -538,6 +544,7 @@ define_keywords!( STDDEV_SAMP, STDIN, STDOUT, + STORAGE_INTEGRATION, STORED, STRING, SUBMULTISET, @@ -600,6 +607,7 @@ define_keywords!( UNTIL, UPDATE, UPPER, + URL, USAGE, USE, USER, diff --git a/src/parser.rs b/src/parser.rs index f48700fd8..87c5e1d07 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3017,11 +3017,13 @@ impl<'a> Parser<'a> { ObjectType::Schema } else if self.parse_keyword(Keyword::SEQUENCE) { ObjectType::Sequence + } else if self.parse_keyword(Keyword::STAGE) { + ObjectType::Stage } else if self.parse_keyword(Keyword::FUNCTION) { return self.parse_drop_function(); } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION or SEQUENCE after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION, STAGE or SEQUENCE after DROP", self.peek_token(), ); }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 3a02cb627..ea14cc44b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -14,6 +14,7 @@ //! Test SQL syntax specific to Snowflake. The parser based on the //! generic dialect is also tested (on the inputs it can handle). +use sqlparser::ast::helpers::stmt_data_loading::{DataLoadingOption, DataLoadingOptionType}; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SnowflakeDialect}; use sqlparser::parser::ParserError; @@ -506,3 +507,230 @@ fn test_alter_table_swap_with() { _ => unreachable!(), }; } + +#[test] +fn test_drop_stage() { + match snowflake_and_generic().verified_stmt("DROP STAGE s1") { + Statement::Drop { + names, if_exists, .. + } => { + assert!(!if_exists); + assert_eq!("s1", names[0].to_string()); + } + _ => unreachable!(), + }; + match snowflake_and_generic().verified_stmt("DROP STAGE IF EXISTS s1") { + Statement::Drop { + names, if_exists, .. + } => { + assert!(if_exists); + assert_eq!("s1", names[0].to_string()); + } + _ => unreachable!(), + }; + + snowflake_and_generic().one_statement_parses_to("DROP STAGE s1", "DROP STAGE s1"); + + snowflake_and_generic() + .one_statement_parses_to("DROP STAGE IF EXISTS s1", "DROP STAGE IF EXISTS s1"); +} + +#[test] +fn test_create_stage() { + let sql = "CREATE STAGE s1.s2"; + match snowflake().verified_stmt(sql) { + Statement::CreateStage { + or_replace, + temporary, + if_not_exists, + name, + comment, + .. + } => { + assert!(!or_replace); + assert!(!temporary); + assert!(!if_not_exists); + assert_eq!("s1.s2", name.to_string()); + assert!(comment.is_none()); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + let extended_sql = concat!( + "CREATE OR REPLACE TEMPORARY STAGE IF NOT EXISTS s1.s2 ", + "COMMENT='some-comment'" + ); + match snowflake().verified_stmt(extended_sql) { + Statement::CreateStage { + or_replace, + temporary, + if_not_exists, + name, + stage_params, + comment, + .. + } => { + assert!(or_replace); + assert!(temporary); + assert!(if_not_exists); + assert!(stage_params.url.is_none()); + assert!(stage_params.endpoint.is_none()); + assert_eq!("s1.s2", name.to_string()); + assert_eq!("some-comment", comment.unwrap()); + } + _ => unreachable!(), + }; + assert_eq!( + snowflake().verified_stmt(extended_sql).to_string(), + extended_sql + ); +} + +#[test] +fn test_create_stage_with_stage_params() { + let sql = concat!( + "CREATE OR REPLACE STAGE my_ext_stage ", + "URL='s3://load/files/' ", + "STORAGE_INTEGRATION=myint ", + "ENDPOINT='' ", + "CREDENTIALS=(AWS_KEY_ID='1a2b3c' AWS_SECRET_KEY='4x5y6z') ", + "ENCRYPTION=(MASTER_KEY='key' TYPE='AWS_SSE_KMS')" + ); + + match snowflake().verified_stmt(sql) { + Statement::CreateStage { stage_params, .. } => { + assert_eq!("s3://load/files/", stage_params.url.unwrap()); + assert_eq!("myint", stage_params.storage_integration.unwrap()); + assert_eq!( + "", + stage_params.endpoint.unwrap() + ); + assert!(stage_params + .credentials + .options + .contains(&DataLoadingOption { + option_name: "AWS_KEY_ID".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "1a2b3c".to_string() + })); + assert!(stage_params + .credentials + .options + .contains(&DataLoadingOption { + option_name: "AWS_SECRET_KEY".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "4x5y6z".to_string() + })); + assert!(stage_params + .encryption + .options + .contains(&DataLoadingOption { + option_name: "MASTER_KEY".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "key".to_string() + })); + assert!(stage_params + .encryption + .options + .contains(&DataLoadingOption { + option_name: "TYPE".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "AWS_SSE_KMS".to_string() + })); + } + _ => unreachable!(), + }; + + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} + +#[test] +fn test_create_stage_with_directory_table_params() { + let sql = concat!( + "CREATE OR REPLACE STAGE my_ext_stage ", + "URL='s3://load/files/' ", + "DIRECTORY=(ENABLE=TRUE REFRESH_ON_CREATE=FALSE NOTIFICATION_INTEGRATION='some-string')" + ); + + match snowflake().verified_stmt(sql) { + Statement::CreateStage { + directory_table_params, + .. + } => { + assert!(directory_table_params.options.contains(&DataLoadingOption { + option_name: "ENABLE".to_string(), + option_type: DataLoadingOptionType::BOOLEAN, + value: "TRUE".to_string() + })); + assert!(directory_table_params.options.contains(&DataLoadingOption { + option_name: "REFRESH_ON_CREATE".to_string(), + option_type: DataLoadingOptionType::BOOLEAN, + value: "FALSE".to_string() + })); + assert!(directory_table_params.options.contains(&DataLoadingOption { + option_name: "NOTIFICATION_INTEGRATION".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "some-string".to_string() + })); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} + +#[test] +fn test_create_stage_with_file_format() { + let sql = concat!( + "CREATE OR REPLACE STAGE my_ext_stage ", + "URL='s3://load/files/' ", + "FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')" + ); + + match snowflake().verified_stmt(sql) { + Statement::CreateStage { file_format, .. } => { + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "COMPRESSION".to_string(), + option_type: DataLoadingOptionType::ENUM, + value: "AUTO".to_string() + })); + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "BINARY_FORMAT".to_string(), + option_type: DataLoadingOptionType::ENUM, + value: "HEX".to_string() + })); + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "ESCAPE".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "\\".to_string() + })); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} + +#[test] +fn test_create_stage_with_copy_options() { + let sql = concat!( + "CREATE OR REPLACE STAGE my_ext_stage ", + "URL='s3://load/files/' ", + "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=TRUE)" + ); + match snowflake().verified_stmt(sql) { + Statement::CreateStage { copy_options, .. } => { + assert!(copy_options.options.contains(&DataLoadingOption { + option_name: "ON_ERROR".to_string(), + option_type: DataLoadingOptionType::ENUM, + value: "CONTINUE".to_string() + })); + assert!(copy_options.options.contains(&DataLoadingOption { + option_name: "FORCE".to_string(), + option_type: DataLoadingOptionType::BOOLEAN, + value: "TRUE".to_string() + })); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} From 29dea5d017b03ea57bad0515356721b94d209341 Mon Sep 17 00:00:00 2001 From: "pawel.leszczynski" Date: Sun, 26 Mar 2023 13:33:35 +0200 Subject: [PATCH 178/806] support PIVOT table syntax (#836) Signed-off-by: Pawel Leszczynski --- src/ast/query.rs | 42 ++++++++++++++++++++++++++ src/keywords.rs | 2 ++ src/parser.rs | 40 +++++++++++++++++++++++++ tests/sqlparser_common.rs | 62 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index 361edf0bc..8269309e5 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -670,6 +670,18 @@ pub enum TableFactor { table_with_joins: Box, alias: Option, }, + /// Represents PIVOT operation on a table. + /// For example `FROM monthly_sales PIVOT(sum(amount) FOR MONTH IN ('JAN', 'FEB'))` + /// See + Pivot { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + name: ObjectName, + table_alias: Option, + aggregate_function: Expr, // Function expression + value_column: Vec, + pivot_values: Vec, + pivot_alias: Option, + }, } impl fmt::Display for TableFactor { @@ -742,6 +754,36 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::Pivot { + name, + table_alias, + aggregate_function, + value_column, + pivot_values, + pivot_alias, + } => { + write!(f, "{}", name)?; + if table_alias.is_some() { + write!(f, " AS {}", table_alias.as_ref().unwrap())?; + } + write!( + f, + " PIVOT({} FOR {} IN (", + aggregate_function, + Expr::CompoundIdentifier(value_column.to_vec()) + )?; + for value in pivot_values { + write!(f, "{}", value)?; + if !value.eq(pivot_values.last().unwrap()) { + write!(f, ", ")?; + } + } + write!(f, "))")?; + if pivot_alias.is_some() { + write!(f, " AS {}", pivot_alias.as_ref().unwrap())?; + } + Ok(()) + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index 6002b5c48..8ec5e1d54 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -438,6 +438,7 @@ define_keywords!( PERCENTILE_DISC, PERCENT_RANK, PERIOD, + PIVOT, PLACING, PLANS, PORTION, @@ -657,6 +658,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::SORT, Keyword::HAVING, Keyword::ORDER, + Keyword::PIVOT, Keyword::TOP, Keyword::LATERAL, Keyword::VIEW, diff --git a/src/parser.rs b/src/parser.rs index 87c5e1d07..ffe79251d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5723,6 +5723,9 @@ impl<'a> Parser<'a> { | TableFactor::Table { alias, .. } | TableFactor::UNNEST { alias, .. } | TableFactor::TableFunction { alias, .. } + | TableFactor::Pivot { + pivot_alias: alias, .. + } | TableFactor::NestedJoin { alias, .. } => { // but not `FROM (mytable AS alias1) AS alias2`. if let Some(inner_alias) = alias { @@ -5780,13 +5783,21 @@ impl<'a> Parser<'a> { }) } else { let name = self.parse_object_name()?; + // Postgres, MSSQL: table-valued functions: let args = if self.consume_token(&Token::LParen) { Some(self.parse_optional_args()?) } else { None }; + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + + // Pivot + if self.parse_keyword(Keyword::PIVOT) { + return self.parse_pivot_table_factor(name, alias); + } + // MSSQL-specific table hints: let mut with_hints = vec![]; if self.parse_keyword(Keyword::WITH) { @@ -5824,6 +5835,35 @@ impl<'a> Parser<'a> { }) } + pub fn parse_pivot_table_factor( + &mut self, + name: ObjectName, + table_alias: Option, + ) -> Result { + self.expect_token(&Token::LParen)?; + let function_name = match self.next_token().token { + Token::Word(w) => Ok(w.value), + _ => self.expected("an aggregate function name", self.peek_token()), + }?; + let function = self.parse_function(ObjectName(vec![Ident::new(function_name)]))?; + self.expect_keyword(Keyword::FOR)?; + let value_column = self.parse_object_name()?.0; + self.expect_keyword(Keyword::IN)?; + self.expect_token(&Token::LParen)?; + let pivot_values = self.parse_comma_separated(Parser::parse_value)?; + self.expect_token(&Token::RParen)?; + self.expect_token(&Token::RParen)?; + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + Ok(TableFactor::Pivot { + name, + table_alias, + aggregate_function: function, + value_column, + pivot_values, + pivot_alias: alias, + }) + } + pub fn parse_join_constraint(&mut self, natural: bool) -> Result { if natural { Ok(JoinConstraint::Natural) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 73df48012..814c3fe01 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21,6 +21,7 @@ use matches::assert_matches; use sqlparser::ast::SelectItem::UnnamedExpr; +use sqlparser::ast::TableFactor::Pivot; use sqlparser::ast::*; use sqlparser::dialect::{ AnsiDialect, BigQueryDialect, ClickHouseDialect, GenericDialect, HiveDialect, MsSqlDialect, @@ -6718,6 +6719,67 @@ fn parse_with_recursion_limit() { assert!(matches!(res, Ok(_)), "{res:?}"); } +#[test] +fn parse_pivot_table() { + let sql = concat!( + "SELECT * FROM monthly_sales AS a ", + "PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ", + "ORDER BY EMPID" + ); + + assert_eq!( + verified_only_select(sql).from[0].relation, + Pivot { + name: ObjectName(vec![Ident::new("monthly_sales")]), + table_alias: Some(TableAlias { + name: Ident::new("a"), + columns: vec![] + }), + aggregate_function: Expr::Function(Function { + name: ObjectName(vec![Ident::new("SUM")]), + args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::CompoundIdentifier(vec![Ident::new("a"), Ident::new("amount"),]) + ))]), + over: None, + distinct: false, + special: false, + }), + value_column: vec![Ident::new("a"), Ident::new("MONTH")], + pivot_values: vec![ + Value::SingleQuotedString("JAN".to_string()), + Value::SingleQuotedString("FEB".to_string()), + Value::SingleQuotedString("MAR".to_string()), + Value::SingleQuotedString("APR".to_string()), + ], + pivot_alias: Some(TableAlias { + name: Ident { + value: "p".to_string(), + quote_style: None + }, + columns: vec![Ident::new("c"), Ident::new("d")], + }), + } + ); + assert_eq!(verified_stmt(sql).to_string(), sql); + + let sql_without_table_alias = concat!( + "SELECT * FROM monthly_sales ", + "PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ", + "ORDER BY EMPID" + ); + assert_matches!( + verified_only_select(sql_without_table_alias).from[0].relation, + Pivot { + table_alias: None, // parsing should succeed with empty alias + .. + } + ); + assert_eq!( + verified_stmt(sql_without_table_alias).to_string(), + sql_without_table_alias + ); +} + /// Makes a predicate that looks like ((user_id = $id) OR user_id = $2...) fn make_where_clause(num: usize) -> String { use std::fmt::Write; From 784a19138f269dab4ea3e1ad3c9da565d20c9155 Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Sun, 9 Apr 2023 05:41:56 -0600 Subject: [PATCH 179/806] Support "UPDATE" statement in "WITH" subquery (#842) * fix(WITH): allow "UPDATE" statement in "WITH" subquery" * add test case * update test to validate round-trip --- src/ast/query.rs | 2 ++ src/parser.rs | 44 +++++++++++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 8269309e5..6762730c5 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -89,6 +89,7 @@ pub enum SetExpr { }, Values(Values), Insert(Statement), + Update(Statement), Table(Box
), } @@ -99,6 +100,7 @@ impl fmt::Display for SetExpr { 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::Table(t) => write!(f, "{t}"), SetExpr::SetOperation { left, diff --git a/src/parser.rs b/src/parser.rs index ffe79251d..bdd5169fb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4910,7 +4910,30 @@ impl<'a> Parser<'a> { None }; - if !self.parse_keyword(Keyword::INSERT) { + if self.parse_keyword(Keyword::INSERT) { + let insert = self.parse_insert()?; + + Ok(Query { + with, + body: Box::new(SetExpr::Insert(insert)), + limit: None, + order_by: vec![], + offset: None, + fetch: None, + locks: vec![], + }) + } else if self.parse_keyword(Keyword::UPDATE) { + let update = self.parse_update()?; + Ok(Query { + with, + body: Box::new(SetExpr::Update(update)), + limit: None, + order_by: vec![], + offset: None, + fetch: None, + locks: vec![], + }) + } else { let body = Box::new(self.parse_query_body(0)?); let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { @@ -4966,18 +4989,6 @@ impl<'a> Parser<'a> { fetch, locks, }) - } else { - let insert = self.parse_insert()?; - - Ok(Query { - with, - body: Box::new(SetExpr::Insert(insert)), - limit: None, - order_by: vec![], - offset: None, - fetch: None, - locks: vec![], - }) } } @@ -7447,4 +7458,11 @@ mod tests { )) ); } + + #[test] + fn test_update_in_with_subquery() { + let sql = 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""#; + let ast = Parser::parse_sql(&GenericDialect, sql).unwrap(); + assert_eq!(ast[0].to_string(), sql); + } } From 00d071286b2d0beab2a7951f9bece2574e2d8383 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 10 Apr 2023 09:48:23 -0400 Subject: [PATCH 180/806] Move tests from parser.rs to appropriate parse_XX tests (#845) * Move tests from parser.rs to appropriate parse_XX tests * move to postgres tests * move more tests --- src/parser.rs | 59 ----------------------------- tests/sqlparser_common.rs | 64 +++----------------------------- tests/sqlparser_mysql.rs | 1 + tests/sqlparser_postgres.rs | 74 ++++++++++++++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 119 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index bdd5169fb..b06e6bd25 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6912,40 +6912,6 @@ mod tests { }); } - #[test] - fn test_parse_limit() { - let sql = "SELECT * FROM user LIMIT 1"; - all_dialects().run_parser_method(sql, |parser| { - let ast = parser.parse_query().unwrap(); - assert_eq!(ast.to_string(), sql.to_string()); - }); - - let sql = "SELECT * FROM user LIMIT $1 OFFSET $2"; - let dialects = TestedDialects { - dialects: vec![ - Box::new(PostgreSqlDialect {}), - Box::new(ClickHouseDialect {}), - Box::new(GenericDialect {}), - Box::new(MsSqlDialect {}), - Box::new(SnowflakeDialect {}), - ], - }; - - dialects.run_parser_method(sql, |parser| { - let ast = parser.parse_query().unwrap(); - assert_eq!(ast.to_string(), sql.to_string()); - }); - - let sql = "SELECT * FROM user LIMIT ? OFFSET ?"; - let dialects = TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - }; - dialects.run_parser_method(sql, |parser| { - let ast = parser.parse_query().unwrap(); - assert_eq!(ast.to_string(), sql.to_string()); - }); - } - #[cfg(test)] mod test_parse_data_type { use crate::ast::{ @@ -7402,24 +7368,6 @@ mod tests { ); } - #[test] - fn test_update_has_keyword() { - let sql = r#"UPDATE test SET name=$1, - value=$2, - where=$3, - create=$4, - is_default=$5, - classification=$6, - sort=$7 - WHERE id=$8"#; - let pg_dialect = PostgreSqlDialect {}; - let ast = Parser::parse_sql(&pg_dialect, sql).unwrap(); - assert_eq!( - ast[0].to_string(), - r#"UPDATE test SET name = $1, value = $2, where = $3, create = $4, is_default = $5, classification = $6, sort = $7 WHERE id = $8"# - ); - } - #[test] fn test_tokenizer_error_loc() { let sql = "foo '"; @@ -7458,11 +7406,4 @@ mod tests { )) ); } - - #[test] - fn test_update_in_with_subquery() { - let sql = 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""#; - let ast = Parser::parse_sql(&GenericDialect, sql).unwrap(); - assert_eq!(ast[0].to_string(), sql); - } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 814c3fe01..55350e5a6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -513,6 +513,11 @@ fn parse_simple_select() { assert_eq!(Some(Expr::Value(number("5"))), select.limit); } +#[test] +fn parse_limit() { + verified_stmt("SELECT * FROM user LIMIT 1"); +} + #[test] fn parse_limit_is_not_an_alias() { // In dialects supporting LIMIT it shouldn't be parsed as a table alias @@ -1559,65 +1564,6 @@ fn parse_select_group_by() { ); } -#[test] -fn parse_select_group_by_grouping_sets() { - let dialects = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(PostgreSqlDialect {})], - }; - let sql = - "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, GROUPING SETS ((brand), (size), ())"; - let select = dialects.verified_only_select(sql); - assert_eq!( - vec![ - Expr::Identifier(Ident::new("size")), - Expr::GroupingSets(vec![ - vec![Expr::Identifier(Ident::new("brand"))], - vec![Expr::Identifier(Ident::new("size"))], - vec![], - ]), - ], - select.group_by - ); -} - -#[test] -fn parse_select_group_by_rollup() { - let dialects = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(PostgreSqlDialect {})], - }; - let sql = "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, ROLLUP (brand, size)"; - let select = dialects.verified_only_select(sql); - assert_eq!( - vec![ - Expr::Identifier(Ident::new("size")), - Expr::Rollup(vec![ - vec![Expr::Identifier(Ident::new("brand"))], - vec![Expr::Identifier(Ident::new("size"))], - ]), - ], - select.group_by - ); -} - -#[test] -fn parse_select_group_by_cube() { - let dialects = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(PostgreSqlDialect {})], - }; - let sql = "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, CUBE (brand, size)"; - let select = dialects.verified_only_select(sql); - assert_eq!( - vec![ - Expr::Identifier(Ident::new("size")), - Expr::Cube(vec![ - vec![Expr::Identifier(Ident::new("brand"))], - vec![Expr::Identifier(Ident::new("size"))], - ]), - ], - select.group_by - ); -} - #[test] fn parse_select_having() { let sql = "SELECT foo FROM bar GROUP BY foo HAVING COUNT(*) > 1"; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 04c86cb15..ca2a979de 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1134,6 +1134,7 @@ fn parse_limit_my_sql_syntax() { "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 * FROM user LIMIT ? OFFSET ?"); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 359889aac..471421215 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2427,7 +2427,27 @@ fn parse_delimited_identifiers() { pg().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); pg().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); - //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); + pg().verified_stmt(r#"UPDATE foo SET "bar" = 5"#); +} + +#[test] +fn parse_update_has_keyword() { + pg().one_statement_parses_to( + r#"UPDATE test SET name=$1, + value=$2, + where=$3, + create=$4, + is_default=$5, + classification=$6, + sort=$7 + WHERE id=$8"#, + r#"UPDATE test SET name = $1, value = $2, where = $3, create = $4, is_default = $5, classification = $6, sort = $7 WHERE id = $8"# + ); +} + +#[test] +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] @@ -2789,3 +2809,55 @@ fn parse_incorrect_dollar_quoted_string() { let sql = "SELECT $$$"; assert!(pg().parse_sql_statements(sql).is_err()); } + +#[test] +fn parse_select_group_by_grouping_sets() { + let select = pg_and_generic().verified_only_select( + "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, GROUPING SETS ((brand), (size), ())" + ); + assert_eq!( + vec![ + Expr::Identifier(Ident::new("size")), + Expr::GroupingSets(vec![ + vec![Expr::Identifier(Ident::new("brand"))], + vec![Expr::Identifier(Ident::new("size"))], + vec![], + ]), + ], + select.group_by + ); +} + +#[test] +fn parse_select_group_by_rollup() { + let select = pg_and_generic().verified_only_select( + "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, ROLLUP (brand, size)", + ); + assert_eq!( + vec![ + Expr::Identifier(Ident::new("size")), + Expr::Rollup(vec![ + vec![Expr::Identifier(Ident::new("brand"))], + vec![Expr::Identifier(Ident::new("size"))], + ]), + ], + select.group_by + ); +} + +#[test] +fn parse_select_group_by_cube() { + let select = pg_and_generic().verified_only_select( + "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, CUBE (brand, size)", + ); + assert_eq!( + vec![ + Expr::Identifier(Ident::new("size")), + Expr::Cube(vec![ + vec![Expr::Identifier(Ident::new("brand"))], + vec![Expr::Identifier(Ident::new("size"))], + ]), + ], + select.group_by + ); +} From 04d9f3af2eb2dfded30ce29f7351d2fa4ee07d59 Mon Sep 17 00:00:00 2001 From: Coby Geralnik Date: Mon, 10 Apr 2023 16:56:01 +0300 Subject: [PATCH 181/806] Added support for Mysql Backslash escapes (enabled by default) (#844) --- src/tokenizer.rs | 27 ++++++++++++++++------- tests/sqlparser_mysql.rs | 47 ++++++++++++++++++++-------------------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index d4f161b02..010566d74 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1094,16 +1094,11 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume the opening quote - // slash escaping is specific to MySQL dialect - let mut is_escaped = false; while let Some(&ch) = chars.peek() { match ch { char if char == quote_style => { chars.next(); // consume - if is_escaped { - s.push(ch); - is_escaped = false; - } else if chars.peek().map(|c| *c == quote_style).unwrap_or(false) { + if chars.peek().map(|c| *c == quote_style).unwrap_or(false) { s.push(ch); chars.next(); } else { @@ -1111,12 +1106,28 @@ impl<'a> Tokenizer<'a> { } } '\\' => { + // consume + chars.next(); + // slash escaping is specific to MySQL dialect if dialect_of!(self is MySqlDialect) { - is_escaped = !is_escaped; + if let Some(next) = chars.peek() { + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences + let n = match next { + '\'' | '\"' | '\\' | '%' | '_' => *next, + '0' => '\0', + 'b' => '\u{8}', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'Z' => '\u{1a}', + _ => *next, + }; + s.push(n); + chars.next(); // consume next + } } else { s.push(ch); } - chars.next(); } _ => { chars.next(); // consume diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ca2a979de..0133cc9df 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -518,35 +518,34 @@ fn parse_unterminated_escape() { #[test] fn parse_escaped_string() { - let sql = r#"SELECT 'I\'m fine'"#; - - let stmt = mysql().one_statement_parses_to(sql, ""); - - match stmt { - Statement::Query(query) => match *query.body { - SetExpr::Select(value) => { - let expr = expr_from_projection(only(&value.projection)); - assert_eq!( - *expr, - Expr::Value(Value::SingleQuotedString("I'm fine".to_string())) - ); - } + fn assert_mysql_query_value(sql: &str, quoted: &str) { + let stmt = mysql().one_statement_parses_to(sql, ""); + + match stmt { + Statement::Query(query) => match *query.body { + SetExpr::Select(value) => { + let expr = expr_from_projection(only(&value.projection)); + assert_eq!( + *expr, + Expr::Value(Value::SingleQuotedString(quoted.to_string())) + ); + } + _ => unreachable!(), + }, _ => unreachable!(), - }, - _ => unreachable!(), - }; + }; + } + let sql = r#"SELECT 'I\'m fine'"#; + assert_mysql_query_value(sql, "I'm fine"); let sql = r#"SELECT 'I''m fine'"#; + assert_mysql_query_value(sql, "I'm fine"); - let projection = mysql().verified_only_select(sql).projection; - let item = projection.get(0).unwrap(); + let sql = r#"SELECT 'I\"m fine'"#; + assert_mysql_query_value(sql, "I\"m fine"); - match &item { - SelectItem::UnnamedExpr(Expr::Value(value)) => { - assert_eq!(*value, Value::SingleQuotedString("I'm fine".to_string())); - } - _ => unreachable!(), - } + let sql = r#"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"#; + assert_mysql_query_value(sql, "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} a "); } #[test] From 28e558c28639611a7d49d3917689cde4d6734f53 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 10 Apr 2023 12:11:14 -0400 Subject: [PATCH 182/806] Changelog for `0.33.0` (#846) * Changelog for `0.33.0` * typo --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 840630fa6..afcd9ad2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,22 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.33.0] 2023-04-10 + +### Added +* Support for Mysql Backslash escapes (enabled by default) (#844) - Thanks @cobyge +* Support "UPDATE" statement in "WITH" subquery (#842) - Thanks @nicksrandall +* Support PIVOT table syntax (#836) - Thanks @pawel-big-lebowski +* Support CREATE/DROP STAGE for Snowflake (#833) - Thanks @pawel-big-lebowski +* Support Non-Latin characters (#840) - Thanks @mskrzypkows +* Support PostgreSQL: GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY and GENERATED - Thanks @sam-mmm +* Support IF EXISTS in COMMENT statements (#831) - Thanks @pawel-big-lebowski +* Support snowflake alter table swap with (#825) - Thanks @pawel-big-lebowski + +### Changed +* Move tests from parser.rs to appropriate parse_XX tests (#845) - Thanks @alamb +* Correct typos in parser.rs (#838) - Thanks @felixonmars +* Improve documentation on verified_* methods (#828) - Thanks @alamb ## [0.32.0] 2023-03-6 From 00719e3cf3ca59492446ab3ae412736f5fefb369 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 10 Apr 2023 12:13:03 -0400 Subject: [PATCH 183/806] (cargo-release) version 0.33.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1d3f244d6..a1126d278 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.32.0" +version = "0.33.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 6b2b3f1f6c903bddc87ba0858b5ccdb94c5e2242 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Wed, 12 Apr 2023 15:04:46 -0400 Subject: [PATCH 184/806] Adds clickhouse to example (#849) --- examples/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/cli.rs b/examples/cli.rs index 53ef1abb1..a320a00bc 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -45,6 +45,7 @@ $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] "--snowflake" => Box::new(SnowflakeDialect {}), "--hive" => Box::new(HiveDialect {}), "--redshift" => Box::new(RedshiftSqlDialect {}), + "--clickhouse" => Box::new(ClickHouseDialect {}), "--generic" | "" => Box::new(GenericDialect {}), s => panic!("Unexpected parameter: {s}"), }; From 3e92ad349fe5149e694d2e5204474a1ae0efbed3 Mon Sep 17 00:00:00 2001 From: AviRaboah <91826424+AviRaboah@users.noreply.github.com> Date: Wed, 26 Apr 2023 16:27:04 +0300 Subject: [PATCH 185/806] Support identifiers beginning with digits in MySQL (#856) --- src/dialect/mysql.rs | 4 +- src/tokenizer.rs | 14 +++++- tests/sqlparser_mysql.rs | 100 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 82cbc5364..4a2ff0165 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -18,8 +18,8 @@ pub struct MySqlDialect {} impl Dialect for MySqlDialect { fn is_identifier_start(&self, ch: char) -> bool { // See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html. - // We don't yet support identifiers beginning with numbers, as that - // makes it hard to distinguish numeric literals. + // Identifiers which begin with a digit are recognized while tokenizing numbers, + // so they can be distinguished from exponent numeric literals. ch.is_alphabetic() || ch == '_' || ch == '$' diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 010566d74..a550c4f5d 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -673,10 +673,10 @@ impl<'a> Tokenizer<'a> { return Ok(Some(Token::Period)); } + let mut exponent_part = String::new(); // Parse exponent as number if chars.peek() == Some(&'e') || chars.peek() == Some(&'E') { let mut char_clone = chars.peekable.clone(); - let mut exponent_part = String::new(); exponent_part.push(char_clone.next().unwrap()); // Optional sign @@ -703,6 +703,18 @@ impl<'a> Tokenizer<'a> { } } + // mysql dialect supports identifiers that start with a numeric prefix, + // as long as they aren't an exponent number. + if dialect_of!(self is MySqlDialect) && exponent_part.is_empty() { + let word = + peeking_take_while(chars, |ch| self.dialect.is_identifier_part(ch)); + + if !word.is_empty() { + s += word.as_str(); + return Ok(Some(Token::make_word(s.as_str(), None))); + } + } + let long = if chars.peek() == Some(&'L') { chars.next(); true diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 0133cc9df..2b312dc5e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -849,6 +849,106 @@ fn parse_insert_with_on_duplicate_update() { } } +#[test] +fn parse_select_with_numeric_prefix_column_name() { + let sql = "SELECT 123col_$@123abc FROM \"table\""; + match mysql().verified_stmt(sql) { + Statement::Query(q) => { + assert_eq!( + q.body, + Box::new(SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new( + "123col_$@123abc" + )))], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::with_quote('"', "table")]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![] + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + qualify: None, + }))) + ); + } + _ => unreachable!(), + } +} + +#[cfg(not(feature = "bigdecimal"))] +#[test] +fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { + let sql = "SELECT 123e4, 123col_$@123abc FROM \"table\""; + match mysql().verified_stmt(sql) { + Statement::Query(q) => { + assert_eq!( + q.body, + Box::new(SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![ + SelectItem::UnnamedExpr(Expr::Value(Value::Number( + "123e4".to_string(), + false + ))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("123col_$@123abc"))) + ], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::with_quote('"', "table")]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![] + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + qualify: None, + }))) + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_insert_with_numeric_prefix_column_name() { + let sql = "INSERT INTO s1.t1 (123col_$@length123) VALUES (67.654)"; + match mysql().verified_stmt(sql) { + Statement::Insert { + table_name, + columns, + .. + } => { + assert_eq!( + ObjectName(vec![Ident::new("s1"), Ident::new("t1")]), + table_name + ); + assert_eq!(vec![Ident::new("123col_$@length123")], columns); + } + _ => unreachable!(), + } +} + #[test] fn parse_update_with_joins() { let sql = "UPDATE orders AS o JOIN customers AS c ON o.customer_id = c.id SET o.completed = true WHERE c.firstname = 'Peter'"; From d8af92536ccde6102d519133bb8308b7d03b3b65 Mon Sep 17 00:00:00 2001 From: "pawel.leszczynski" Date: Thu, 27 Apr 2023 17:30:48 +0200 Subject: [PATCH 186/806] support COPY INTO in snowflake (#841) Signed-off-by: Pawel Leszczynski --- src/ast/helpers/stmt_data_loading.rs | 27 +++ src/ast/mod.rs | 82 ++++++- src/dialect/snowflake.rs | 221 ++++++++++++++++++- src/keywords.rs | 3 + tests/sqlparser_snowflake.rs | 309 ++++++++++++++++++++++++++- 5 files changed, 635 insertions(+), 7 deletions(-) diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index d5ba6eda0..a259e664b 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -24,6 +24,7 @@ use core::fmt::Formatter; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::ast::Ident; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; @@ -63,6 +64,16 @@ pub struct DataLoadingOption { pub value: String, } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct StageLoadSelectItem { + pub alias: Option, + pub file_col_num: i32, + pub element: Option, + pub item_as: Option, +} + impl fmt::Display for StageParamsObject { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let url = &self.url.as_ref(); @@ -121,3 +132,19 @@ impl fmt::Display for DataLoadingOption { Ok(()) } } + +impl fmt::Display for StageLoadSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.alias.is_some() { + write!(f, "{}.", self.alias.as_ref().unwrap())?; + } + write!(f, "${}", self.file_col_num)?; + if self.element.is_some() { + write!(f, ":{}", self.element.as_ref().unwrap())?; + } + if self.item_as.is_some() { + write!(f, " AS {}", self.item_as.as_ref().unwrap())?; + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 94b237b4f..e412c7820 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -44,7 +44,9 @@ pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, }; -use crate::ast::helpers::stmt_data_loading::{DataLoadingOptions, StageParamsObject}; +use crate::ast::helpers::stmt_data_loading::{ + DataLoadingOptions, StageLoadSelectItem, StageParamsObject, +}; #[cfg(feature = "visitor")] pub use visitor::*; @@ -1187,6 +1189,26 @@ pub enum Statement { /// VALUES a vector of values to be copied values: Vec>, }, + /// ```sql + /// COPY INTO + /// ``` + /// See + /// Copy Into syntax available for Snowflake is different than the one implemented in + /// Postgres. Although they share common prefix, it is reasonable to implement them + /// in different enums. This can be refactored later once custom dialects + /// are allowed to have custom Statements. + CopyIntoSnowflake { + into: ObjectName, + from_stage: ObjectName, + from_stage_alias: Option, + stage_params: StageParamsObject, + from_transformations: Option>, + files: Option>, + pattern: Option, + file_format: DataLoadingOptions, + copy_options: DataLoadingOptions, + validation_mode: Option, + }, /// Close - closes the portal underlying an open cursor. Close { /// Cursor name @@ -2795,6 +2817,64 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CopyIntoSnowflake { + into, + from_stage, + from_stage_alias, + stage_params, + from_transformations, + files, + pattern, + file_format, + copy_options, + validation_mode, + } => { + write!(f, "COPY INTO {}", into)?; + if from_transformations.is_none() { + // Standard data load + write!(f, " FROM {}{}", from_stage, stage_params)?; + if from_stage_alias.as_ref().is_some() { + write!(f, " AS {}", from_stage_alias.as_ref().unwrap())?; + } + } else { + // Data load with transformation + write!( + f, + " FROM (SELECT {} FROM {}{}", + display_separated(from_transformations.as_ref().unwrap(), ", "), + from_stage, + stage_params, + )?; + if from_stage_alias.as_ref().is_some() { + write!(f, " AS {}", from_stage_alias.as_ref().unwrap())?; + } + write!(f, ")")?; + } + if files.is_some() { + write!( + f, + " FILES = ('{}')", + display_separated(files.as_ref().unwrap(), "', '") + )?; + } + if pattern.is_some() { + write!(f, " PATTERN = '{}'", pattern.as_ref().unwrap())?; + } + if !file_format.options.is_empty() { + write!(f, " FILE_FORMAT=({})", file_format)?; + } + if !copy_options.options.is_empty() { + write!(f, " COPY_OPTIONS=({})", copy_options)?; + } + if validation_mode.is_some() { + write!( + f, + " VALIDATION_MODE = {}", + validation_mode.as_ref().unwrap() + )?; + } + Ok(()) + } } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 0be5e1df6..713394a1e 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -13,17 +13,20 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; use crate::ast::helpers::stmt_data_loading::{ - DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageParamsObject, + DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageLoadSelectItem, + StageParamsObject, }; -use crate::ast::Statement; +use crate::ast::{Ident, ObjectName, Statement}; use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; #[cfg(not(feature = "std"))] -use alloc::vec; +use alloc::string::String; #[cfg(not(feature = "std"))] use alloc::vec::Vec; +#[cfg(not(feature = "std"))] +use alloc::{format, vec}; #[derive(Debug, Default)] pub struct SnowflakeDialect; @@ -31,7 +34,7 @@ pub struct SnowflakeDialect; impl Dialect for SnowflakeDialect { // see https://docs.snowflake.com/en/sql-reference/identifiers-syntax.html fn is_identifier_start(&self, ch: char) -> bool { - ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' || ch == '@' || ch == '%' } fn is_identifier_part(&self, ch: char) -> bool { @@ -40,6 +43,8 @@ impl Dialect for SnowflakeDialect { || ch.is_ascii_digit() || ch == '$' || ch == '_' + || ch == '/' + || ch == '~' } fn supports_within_after_array_aggregation(&self) -> bool { @@ -71,6 +76,11 @@ impl Dialect for SnowflakeDialect { } } } + if parser.parse_keywords(&[Keyword::COPY, Keyword::INTO]) { + // COPY INTO + return Some(parse_copy_into(parser)); + } + None } } @@ -137,6 +147,209 @@ pub fn parse_create_stage( }) } +pub fn parse_copy_into(parser: &mut Parser) -> Result { + let into: ObjectName = parser.parse_object_name()?; + let mut files: Vec = vec![]; + let mut from_transformations: Option> = None; + let from_stage_alias; + let from_stage: ObjectName; + let stage_params: StageParamsObject; + + parser.expect_keyword(Keyword::FROM)?; + // check if data load transformations are present + match parser.next_token().token { + Token::LParen => { + // data load with transformations + parser.expect_keyword(Keyword::SELECT)?; + from_transformations = parse_select_items_for_data_load(parser)?; + + parser.expect_keyword(Keyword::FROM)?; + from_stage = parser.parse_object_name()?; + 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 + }; + parser.expect_token(&Token::RParen)?; + } + _ => { + parser.prev_token(); + from_stage = parser.parse_object_name()?; + 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 + }; + } + }; + + // [ files ] + if parser.parse_keyword(Keyword::FILES) { + parser.expect_token(&Token::Eq)?; + parser.expect_token(&Token::LParen)?; + let mut continue_loop = true; + while continue_loop { + continue_loop = false; + let next_token = parser.next_token(); + match next_token.token { + Token::SingleQuotedString(s) => files.push(s), + _ => parser.expected("file token", next_token)?, + }; + if parser.next_token().token.eq(&Token::Comma) { + continue_loop = true; + } else { + parser.prev_token(); // not a comma, need to go back + } + } + parser.expect_token(&Token::RParen)?; + } + + // [ pattern ] + let mut pattern = None; + if parser.parse_keyword(Keyword::PATTERN) { + parser.expect_token(&Token::Eq)?; + let next_token = parser.next_token(); + pattern = Some(match next_token.token { + Token::SingleQuotedString(s) => s, + _ => parser.expected("pattern", next_token)?, + }); + } + + // [ file_format] + let mut file_format = Vec::new(); + if parser.parse_keyword(Keyword::FILE_FORMAT) { + parser.expect_token(&Token::Eq)?; + file_format = parse_parentheses_options(parser)?; + } + + // [ copy_options ] + let mut copy_options = Vec::new(); + if parser.parse_keyword(Keyword::COPY_OPTIONS) { + parser.expect_token(&Token::Eq)?; + copy_options = parse_parentheses_options(parser)?; + } + + // [ VALIDATION_MODE ] + let mut validation_mode = None; + if parser.parse_keyword(Keyword::VALIDATION_MODE) { + parser.expect_token(&Token::Eq)?; + validation_mode = Some(parser.next_token().token.to_string()); + } + + Ok(Statement::CopyIntoSnowflake { + into, + from_stage, + from_stage_alias, + stage_params, + from_transformations, + files: if files.is_empty() { None } else { Some(files) }, + pattern, + file_format: DataLoadingOptions { + options: file_format, + }, + copy_options: DataLoadingOptions { + options: copy_options, + }, + validation_mode, + }) +} + +fn parse_select_items_for_data_load( + parser: &mut Parser, +) -> 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; + + 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(()) + } + _ => 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()), + }?)); + } + _ => { + // 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; + } + } + } + Ok(Some(select_items)) +} + fn parse_stage_params(parser: &mut Parser) -> Result { let (mut url, mut storage_integration, mut endpoint) = (None, None, None); let mut encryption: DataLoadingOptions = DataLoadingOptions { options: vec![] }; diff --git a/src/keywords.rs b/src/keywords.rs index 8ec5e1d54..a0c5b68cb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -252,6 +252,7 @@ define_keywords!( FETCH, FIELDS, FILE, + FILES, FILE_FORMAT, FILTER, FIRST, @@ -433,6 +434,7 @@ define_keywords!( PARTITIONED, PARTITIONS, PASSWORD, + PATTERN, PERCENT, PERCENTILE_CONT, PERCENTILE_DISC, @@ -615,6 +617,7 @@ define_keywords!( USING, UUID, VALID, + VALIDATION_MODE, VALUE, VALUES, VALUE_OF, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ea14cc44b..8bcbb7b5a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -14,7 +14,9 @@ //! Test SQL syntax specific to Snowflake. The parser based on the //! generic dialect is also tested (on the inputs it can handle). -use sqlparser::ast::helpers::stmt_data_loading::{DataLoadingOption, DataLoadingOptionType}; +use sqlparser::ast::helpers::stmt_data_loading::{ + DataLoadingOption, DataLoadingOptionType, StageLoadSelectItem, +}; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SnowflakeDialect}; use sqlparser::parser::ParserError; @@ -69,7 +71,7 @@ fn test_snowflake_single_line_tokenize() { assert_eq!(expected, tokens); - let sql = "CREATE TABLE// this is a comment \ntable_1"; + let sql = "CREATE TABLE // this is a comment \ntable_1"; let mut tokenizer = Tokenizer::new(&dialect, sql); let tokens = tokenizer.tokenize().unwrap(); @@ -77,6 +79,7 @@ fn test_snowflake_single_line_tokenize() { Token::make_keyword("CREATE"), Token::Whitespace(Whitespace::Space), Token::make_keyword("TABLE"), + Token::Whitespace(Whitespace::Space), Token::Whitespace(Whitespace::SingleLineComment { prefix: "//".to_string(), comment: " this is a comment \n".to_string(), @@ -734,3 +737,305 @@ fn test_create_stage_with_copy_options() { }; assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); } + +#[test] +fn test_copy_into() { + let sql = concat!( + "COPY INTO my_company.emp_basic ", + "FROM 'gcs://mybucket/./../a.csv'" + ); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + into, + from_stage, + files, + pattern, + validation_mode, + .. + } => { + assert_eq!( + into, + ObjectName(vec![Ident::new("my_company"), Ident::new("emp_basic")]) + ); + assert_eq!( + from_stage, + ObjectName(vec![Ident::with_quote('\'', "gcs://mybucket/./../a.csv")]) + ); + assert!(files.is_none()); + assert!(pattern.is_none()); + assert!(validation_mode.is_none()); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} + +#[test] +fn test_copy_into_with_stage_params() { + let sql = concat!( + "COPY INTO my_company.emp_basic ", + "FROM 's3://load/files/' ", + "STORAGE_INTEGRATION=myint ", + "ENDPOINT='' ", + "CREDENTIALS=(AWS_KEY_ID='1a2b3c' AWS_SECRET_KEY='4x5y6z') ", + "ENCRYPTION=(MASTER_KEY='key' TYPE='AWS_SSE_KMS')" + ); + + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + from_stage, + stage_params, + .. + } => { + //assert_eq!("s3://load/files/", stage_params.url.unwrap()); + assert_eq!( + from_stage, + ObjectName(vec![Ident::with_quote('\'', "s3://load/files/")]) + ); + assert_eq!("myint", stage_params.storage_integration.unwrap()); + assert_eq!( + "", + stage_params.endpoint.unwrap() + ); + assert!(stage_params + .credentials + .options + .contains(&DataLoadingOption { + option_name: "AWS_KEY_ID".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "1a2b3c".to_string() + })); + assert!(stage_params + .credentials + .options + .contains(&DataLoadingOption { + option_name: "AWS_SECRET_KEY".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "4x5y6z".to_string() + })); + assert!(stage_params + .encryption + .options + .contains(&DataLoadingOption { + option_name: "MASTER_KEY".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "key".to_string() + })); + assert!(stage_params + .encryption + .options + .contains(&DataLoadingOption { + option_name: "TYPE".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "AWS_SSE_KMS".to_string() + })); + } + _ => unreachable!(), + }; + + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + // stage params within copy into with transformations + let sql = concat!( + "COPY INTO my_company.emp_basic FROM ", + "(SELECT t1.$1 FROM 's3://load/files/' STORAGE_INTEGRATION=myint)", + ); + + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + from_stage, + stage_params, + .. + } => { + assert_eq!( + from_stage, + ObjectName(vec![Ident::with_quote('\'', "s3://load/files/")]) + ); + assert_eq!("myint", stage_params.storage_integration.unwrap()); + } + _ => unreachable!(), + } +} + +#[test] +fn test_copy_into_with_files_and_pattern_and_verification() { + let sql = concat!( + "COPY INTO my_company.emp_basic ", + "FROM 'gcs://mybucket/./../a.csv' AS some_alias ", + "FILES = ('file1.json', 'file2.json') ", + "PATTERN = '.*employees0[1-5].csv.gz' ", + "VALIDATION_MODE = RETURN_7_ROWS" + ); + + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + files, + pattern, + validation_mode, + from_stage_alias, + .. + } => { + assert_eq!(files.unwrap(), vec!["file1.json", "file2.json"]); + assert_eq!(pattern.unwrap(), ".*employees0[1-5].csv.gz"); + assert_eq!(validation_mode.unwrap(), "RETURN_7_ROWS"); + assert_eq!(from_stage_alias.unwrap(), Ident::new("some_alias")); + } + _ => unreachable!(), + } + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} + +#[test] +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) ", + "FILES = ('file1.json', 'file2.json') ", + "PATTERN = '.*employees0[1-5].csv.gz' ", + "VALIDATION_MODE = RETURN_7_ROWS" + ); + + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + from_stage, + from_transformations, + .. + } => { + assert_eq!( + from_stage, + ObjectName(vec![Ident::new("@schema"), Ident::new("general_finished")]) + ); + assert_eq!( + from_transformations.as_ref().unwrap()[0], + 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 { + alias: None, + file_col_num: 1, + element: Some(Ident::new("index")), + item_as: None + } + ); + assert_eq!( + from_transformations.as_ref().unwrap()[2], + StageLoadSelectItem { + alias: Some(Ident::new("t2")), + file_col_num: 1, + element: None, + item_as: None + } + ); + } + _ => unreachable!(), + } + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} + +#[test] +fn test_copy_into_file_format() { + let sql = concat!( + "COPY INTO my_company.emp_basic ", + "FROM 'gcs://mybucket/./../a.csv' ", + "FILES = ('file1.json', 'file2.json') ", + "PATTERN = '.*employees0[1-5].csv.gz' ", + "FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')" + ); + + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { file_format, .. } => { + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "COMPRESSION".to_string(), + option_type: DataLoadingOptionType::ENUM, + value: "AUTO".to_string() + })); + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "BINARY_FORMAT".to_string(), + option_type: DataLoadingOptionType::ENUM, + value: "HEX".to_string() + })); + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "ESCAPE".to_string(), + option_type: DataLoadingOptionType::STRING, + value: "\\".to_string() + })); + } + _ => unreachable!(), + } + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} + +#[test] +fn test_copy_into_copy_options() { + let sql = concat!( + "COPY INTO my_company.emp_basic ", + "FROM 'gcs://mybucket/./../a.csv' ", + "FILES = ('file1.json', 'file2.json') ", + "PATTERN = '.*employees0[1-5].csv.gz' ", + "COPY_OPTIONS=(ON_ERROR=CONTINUE FORCE=TRUE)" + ); + + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { copy_options, .. } => { + assert!(copy_options.options.contains(&DataLoadingOption { + option_name: "ON_ERROR".to_string(), + option_type: DataLoadingOptionType::ENUM, + value: "CONTINUE".to_string() + })); + assert!(copy_options.options.contains(&DataLoadingOption { + option_name: "FORCE".to_string(), + option_type: DataLoadingOptionType::BOOLEAN, + value: "TRUE".to_string() + })); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); +} + +#[test] +fn test_snowflake_stage_object_names() { + let allowed_formatted_names = vec![ + "my_company.emp_basic", + "@namespace.%table_name", + "@namespace.%table_name/path", + "@namespace.stage_name/path", + "@~/path", + ]; + let mut allowed_object_names = vec![ + ObjectName(vec![Ident::new("my_company"), Ident::new("emp_basic")]), + ObjectName(vec![Ident::new("@namespace"), Ident::new("%table_name")]), + ObjectName(vec![ + Ident::new("@namespace"), + Ident::new("%table_name/path"), + ]), + ObjectName(vec![ + Ident::new("@namespace"), + Ident::new("stage_name/path"), + ]), + ObjectName(vec![Ident::new("@~/path")]), + ]; + + for it in allowed_formatted_names + .iter() + .zip(allowed_object_names.iter_mut()) + { + let (formatted_name, object_name) = it; + let sql = format!( + "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'", + formatted_name + ); + match snowflake().verified_stmt(&sql) { + Statement::CopyIntoSnowflake { into, .. } => { + assert_eq!(into.0, object_name.0) + } + _ => unreachable!(), + } + } +} From 5ecf633e311cb2ad7c37205c93a8702145395f51 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 27 Apr 2023 11:37:11 -0400 Subject: [PATCH 187/806] Add `dialect_from_str` and improve `Dialect` documentation (#848) * Add `dialect_from_str` and improve `Dialect` documentation * cleanup * fix compilation with nostd --- src/dialect/mod.rs | 86 ++++++++++++++++++++++++++++++++++++++++++-- src/dialect/mssql.rs | 1 + src/dialect/mysql.rs | 1 + src/lib.rs | 19 ++++++---- 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a13339a9d..5744ae65e 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -42,14 +42,47 @@ pub use self::sqlite::SQLiteDialect; pub use crate::keywords; use crate::parser::{Parser, ParserError}; -/// `dialect_of!(parser is SQLiteDialect | GenericDialect)` evaluates -/// to `true` if `parser.dialect` is one of the `Dialect`s specified. +#[cfg(not(feature = "std"))] +use alloc::boxed::Box; + +/// Convenience check if a [`Parser`] uses a certain dialect. +/// +/// `dialect_of!(parser Is SQLiteDialect | GenericDialect)` evaluates +/// to `true` if `parser.dialect` is one of the [`Dialect`]s specified. macro_rules! dialect_of { ( $parsed_dialect: ident is $($dialect_type: ty)|+ ) => { ($($parsed_dialect.dialect.is::<$dialect_type>())||+) }; } +/// Encapsulates the differences between SQL implementations. +/// +/// # SQL Dialects +/// SQL implementations deviatiate from one another, either due to +/// custom extensions or various historical reasons. This trait +/// encapsulates the parsing differences between dialects. +/// +/// # Examples +/// Most users create a [`Dialect`] directly, as shown on the [module +/// level documentation]: +/// +/// ``` +/// # use sqlparser::dialect::AnsiDialect; +/// let dialect = AnsiDialect {}; +/// ``` +/// +/// It is also possible to dynamically create a [`Dialect`] from its +/// name. For example: +/// +/// ``` +/// # use sqlparser::dialect::{AnsiDialect, dialect_from_str}; +/// let dialect = dialect_from_str("ansi").unwrap(); +/// +/// // Parsed dialect is an instance of `AnsiDialect`: +/// assert!(dialect.is::()); +/// ``` +/// +/// [module level documentation]: crate pub trait Dialect: Debug + Any { /// Determine if a character starts a quoted identifier. The default /// implementation, accepting "double quoted" ids is both ANSI-compliant @@ -113,6 +146,27 @@ impl dyn Dialect { } } +/// Returns the built in [`Dialect`] corresponding to `dialect_name`. +/// +/// See [`Dialect`] documentation for an example. +pub fn dialect_from_str(dialect_name: impl AsRef) -> Option> { + let dialect_name = dialect_name.as_ref(); + match dialect_name.to_lowercase().as_str() { + "generic" => Some(Box::new(GenericDialect)), + "mysql" => Some(Box::new(MySqlDialect {})), + "postgresql" | "postgres" => Some(Box::new(PostgreSqlDialect {})), + "hive" => Some(Box::new(HiveDialect {})), + "sqlite" => Some(Box::new(SQLiteDialect {})), + "snowflake" => Some(Box::new(SnowflakeDialect)), + "redshift" => Some(Box::new(RedshiftSqlDialect {})), + "mssql" => Some(Box::new(MsSqlDialect {})), + "clickhouse" => Some(Box::new(ClickHouseDialect {})), + "bigquery" => Some(Box::new(BigQueryDialect)), + "ansi" => Some(Box::new(AnsiDialect {})), + _ => None, + } +} + #[cfg(test)] mod tests { use super::ansi::AnsiDialect; @@ -141,4 +195,32 @@ mod tests { assert!(dialect_of!(ansi_holder is GenericDialect | AnsiDialect)); assert!(!dialect_of!(ansi_holder is GenericDialect | MsSqlDialect)); } + + #[test] + fn test_dialect_from_str() { + assert!(parse_dialect("generic").is::()); + assert!(parse_dialect("mysql").is::()); + assert!(parse_dialect("MySql").is::()); + assert!(parse_dialect("postgresql").is::()); + assert!(parse_dialect("postgres").is::()); + assert!(parse_dialect("hive").is::()); + assert!(parse_dialect("sqlite").is::()); + assert!(parse_dialect("snowflake").is::()); + assert!(parse_dialect("SnowFlake").is::()); + assert!(parse_dialect("MsSql").is::()); + assert!(parse_dialect("clickhouse").is::()); + assert!(parse_dialect("ClickHouse").is::()); + assert!(parse_dialect("bigquery").is::()); + assert!(parse_dialect("BigQuery").is::()); + assert!(parse_dialect("ansi").is::()); + assert!(parse_dialect("ANSI").is::()); + + // error cases + assert!(dialect_from_str("Unknown").is_none()); + assert!(dialect_from_str("").is_none()); + } + + fn parse_dialect(v: &str) -> Box { + dialect_from_str(v).unwrap() + } } diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index a9056350d..6d1f49cd7 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -12,6 +12,7 @@ use crate::dialect::Dialect; +// [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/) dialect #[derive(Debug)] pub struct MsSqlDialect {} diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 4a2ff0165..ceab34810 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -12,6 +12,7 @@ use crate::dialect::Dialect; +/// [MySQL](https://www.mysql.com/) #[derive(Debug)] pub struct MySqlDialect {} diff --git a/src/lib.rs b/src/lib.rs index 75209b054..5bcd32949 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,17 +10,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! SQL Parser for Rust +//! # SQL Parser for Rust //! //! This crate provides an ANSI:SQL 2011 lexer and parser that can parse SQL -//! into an Abstract Syntax Tree (AST). See the [sqlparser crates.io page] +//! into an Abstract Syntax Tree ([`AST`]). See the [sqlparser crates.io page] //! for more information. //! -//! See [`Parser::parse_sql`](crate::parser::Parser::parse_sql) and -//! [`Parser::new`](crate::parser::Parser::new) for the Parsing API -//! and the [`ast`](crate::ast) crate for the AST structure. +//! For more information: +//! 1. [`Parser::parse_sql`] and [`Parser::new`] for the Parsing API +//! 2. [`ast`] for the AST structure +//! 3. [`Dialect`] for supported SQL dialects //! -//! Example: +//! # Example //! //! ``` //! use sqlparser::dialect::GenericDialect; @@ -37,7 +38,13 @@ //! //! println!("AST: {:?}", ast); //! ``` +//! //! [sqlparser crates.io page]: https://crates.io/crates/sqlparser +//! [`Parser::parse_sql`]: crate::parser::Parser::parse_sql +//! [`Parser::new`]: crate::parser::Parser::new +//! [`AST`]: crate::ast +//! [`ast`]: crate::ast +//! [`Dialect`]: crate::dialect::Dialect #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::upper_case_acronyms)] From f72e2ec382c465ae9a9c7098f7f48aa3428aa8b6 Mon Sep 17 00:00:00 2001 From: AviRaboah <91826424+AviRaboah@users.noreply.github.com> Date: Thu, 27 Apr 2023 18:41:20 +0300 Subject: [PATCH 188/806] Support multiple-table DELETE syntax (#855) --- src/ast/mod.rs | 19 ++++-- src/parser.rs | 16 +++-- tests/sqlparser_common.rs | 122 +++++++++++++++++++++++++++++++++----- 3 files changed, 131 insertions(+), 26 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e412c7820..271325a50 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1229,10 +1229,12 @@ pub enum Statement { }, /// DELETE Delete { + /// Multi tables delete are supported in mysql + tables: Vec, /// FROM - table_name: TableFactor, - /// USING (Snowflake, Postgres) - using: Option, + from: Vec, + /// USING (Snowflake, Postgres, MySQL) + using: Option>, /// WHERE selection: Option, /// RETURNING @@ -1982,14 +1984,19 @@ impl fmt::Display for Statement { Ok(()) } Statement::Delete { - table_name, + tables, + from, using, selection, returning, } => { - write!(f, "DELETE FROM {table_name}")?; + write!(f, "DELETE ")?; + if !tables.is_empty() { + write!(f, "{} ", display_comma_separated(tables))?; + } + write!(f, "FROM {}", display_comma_separated(from))?; if let Some(using) = using { - write!(f, " USING {using}")?; + write!(f, " USING {}", display_comma_separated(using))?; } if let Some(selection) = selection { write!(f, " WHERE {selection}")?; diff --git a/src/parser.rs b/src/parser.rs index b06e6bd25..003369de4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4813,10 +4813,17 @@ impl<'a> Parser<'a> { } pub fn parse_delete(&mut self) -> Result { - self.expect_keyword(Keyword::FROM)?; - let table_name = self.parse_table_factor()?; + let tables = if !self.parse_keyword(Keyword::FROM) { + let tables = self.parse_comma_separated(Parser::parse_object_name)?; + self.expect_keyword(Keyword::FROM)?; + tables + } else { + vec![] + }; + + let from = self.parse_comma_separated(Parser::parse_table_and_joins)?; let using = if self.parse_keyword(Keyword::USING) { - Some(self.parse_table_factor()?) + Some(self.parse_comma_separated(Parser::parse_table_and_joins)?) } else { None }; @@ -4833,7 +4840,8 @@ impl<'a> Parser<'a> { }; Ok(Statement::Delete { - table_name, + tables, + from, using, selection, returning, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 55350e5a6..aee9efd70 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -385,7 +385,7 @@ fn parse_no_table_name() { fn parse_delete_statement() { let sql = "DELETE FROM \"table\""; match verified_stmt(sql) { - Statement::Delete { table_name, .. } => { + Statement::Delete { from, .. } => { assert_eq!( TableFactor::Table { name: ObjectName(vec![Ident::with_quote('"', "table")]), @@ -393,7 +393,93 @@ fn parse_delete_statement() { args: None, with_hints: vec![], }, - table_name + from[0].relation + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_delete_statement_for_multi_tables() { + let sql = "DELETE schema1.table1, schema2.table2 FROM schema1.table1 JOIN schema2.table2 ON schema2.table2.col1 = schema1.table1.col1 WHERE schema2.table2.col2 = 1"; + match verified_stmt(sql) { + Statement::Delete { tables, from, .. } => { + assert_eq!( + ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), + tables[0] + ); + assert_eq!( + ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), + tables[1] + ); + assert_eq!( + TableFactor::Table { + name: ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), + alias: None, + args: None, + with_hints: vec![], + }, + from[0].relation + ); + assert_eq!( + TableFactor::Table { + name: ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), + alias: None, + args: None, + with_hints: vec![], + }, + from[0].joins[0].relation + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_delete_statement_for_multi_tables_with_using() { + let sql = "DELETE FROM schema1.table1, schema2.table2 USING schema1.table1 JOIN schema2.table2 ON schema2.table2.pk = schema1.table1.col1 WHERE schema2.table2.col2 = 1"; + match verified_stmt(sql) { + Statement::Delete { + from, + using: Some(using), + .. + } => { + assert_eq!( + TableFactor::Table { + name: ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), + alias: None, + args: None, + with_hints: vec![], + }, + from[0].relation + ); + assert_eq!( + TableFactor::Table { + name: ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), + alias: None, + args: None, + with_hints: vec![], + }, + from[1].relation + ); + assert_eq!( + TableFactor::Table { + name: ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), + alias: None, + args: None, + with_hints: vec![], + }, + using[0].relation + ); + assert_eq!( + TableFactor::Table { + name: ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), + alias: None, + args: None, + with_hints: vec![], + }, + using[0].joins[0].relation ); } _ => unreachable!(), @@ -407,7 +493,8 @@ fn parse_where_delete_statement() { let sql = "DELETE FROM foo WHERE name = 5"; match verified_stmt(sql) { Statement::Delete { - table_name, + tables: _, + from, using, selection, returning, @@ -419,7 +506,7 @@ fn parse_where_delete_statement() { args: None, with_hints: vec![], }, - table_name, + from[0].relation, ); assert_eq!(None, using); @@ -444,7 +531,8 @@ fn parse_where_delete_with_alias_statement() { let sql = "DELETE FROM basket AS a USING basket AS b WHERE a.id < b.id"; match verified_stmt(sql) { Statement::Delete { - table_name, + tables: _, + from, using, selection, returning, @@ -459,19 +547,21 @@ fn parse_where_delete_with_alias_statement() { args: None, with_hints: vec![], }, - table_name, + from[0].relation, ); - assert_eq!( - Some(TableFactor::Table { - name: ObjectName(vec![Ident::new("basket")]), - alias: Some(TableAlias { - name: Ident::new("b"), - columns: vec![], - }), - args: None, - with_hints: vec![], - }), + Some(vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("basket")]), + alias: Some(TableAlias { + name: Ident::new("b"), + columns: vec![], + }), + args: None, + with_hints: vec![], + }, + joins: vec![], + }]), using ); assert_eq!( From 3b1076c194d40f473b9eb3d1194d90202465d284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alja=C5=BE=20Mur=20Er=C5=BEen?= Date: Thu, 27 Apr 2023 21:34:54 +0200 Subject: [PATCH 189/806] Support `DISTINCT ON (...)` (#852) * Support "DISTINCT ON (...)" * a test * fix the merge --- src/ast/mod.rs | 8 ++++---- src/ast/query.rs | 32 ++++++++++++++++++++++++++++--- src/parser.rs | 33 +++++++++++++++++++++++--------- tests/sqlparser_clickhouse.rs | 2 +- tests/sqlparser_common.rs | 36 +++++++++++++++++++++++++++++------ tests/sqlparser_mysql.rs | 12 ++++++------ tests/sqlparser_postgres.rs | 4 ++-- 7 files changed, 96 insertions(+), 31 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 271325a50..02121acc9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -34,10 +34,10 @@ pub use self::ddl::{ }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, ExceptSelectItem, ExcludeSelectItem, Fetch, IdentWithAlias, Join, JoinConstraint, - JoinOperator, LateralView, LockClause, LockType, NonBlock, Offset, OffsetRows, OrderByExpr, - Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, - SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, + Cte, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, IdentWithAlias, Join, + JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NonBlock, Offset, OffsetRows, + OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, + SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, TableWithJoins, Top, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index 6762730c5..a85c62a25 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -193,7 +193,7 @@ impl fmt::Display for Table { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Select { - pub distinct: bool, + pub distinct: Option, /// MSSQL syntax: `TOP () [ PERCENT ] [ WITH TIES ]` pub top: Option, /// projection expressions @@ -222,7 +222,10 @@ pub struct Select { impl fmt::Display for Select { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "SELECT{}", if self.distinct { " DISTINCT" } else { "" })?; + write!(f, "SELECT")?; + if let Some(ref distinct) = self.distinct { + write!(f, " {distinct}")?; + } if let Some(ref top) = self.top { write!(f, " {top}")?; } @@ -1079,6 +1082,29 @@ impl fmt::Display for NonBlock { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Distinct { + /// DISTINCT + Distinct, + + /// DISTINCT ON({column names}) + On(Vec), +} + +impl fmt::Display for Distinct { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Distinct::Distinct => write!(f, "DISTINCT"), + Distinct::On(col_names) => { + let col_names = display_comma_separated(col_names); + write!(f, "DISTINCT ON ({col_names})") + } + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -1105,7 +1131,7 @@ impl fmt::Display for Top { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Values { - /// Was there an explict ROWs keyword (MySQL)? + /// Was there an explicit ROWs keyword (MySQL)? /// pub explicit_row: bool, pub rows: Vec>, diff --git a/src/parser.rs b/src/parser.rs index 003369de4..c468e9be8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -879,7 +879,7 @@ impl<'a> Parser<'a> { pub fn parse_function(&mut self, name: ObjectName) -> Result { self.expect_token(&Token::LParen)?; - let distinct = self.parse_all_or_distinct()?; + let distinct = self.parse_all_or_distinct()?.is_some(); let args = self.parse_optional_args()?; let over = if self.parse_keyword(Keyword::OVER) { // TBD: support window names (`OVER mywin`) in place of inline specification @@ -1302,7 +1302,7 @@ impl<'a> Parser<'a> { /// Parse a SQL LISTAGG expression, e.g. `LISTAGG(...) WITHIN GROUP (ORDER BY ...)`. pub fn parse_listagg_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; - let distinct = self.parse_all_or_distinct()?; + let distinct = self.parse_all_or_distinct()?.is_some(); let expr = Box::new(self.parse_expr()?); // While ANSI SQL would would require the separator, Redshift makes this optional. Here we // choose to make the separator optional as this provides the more general implementation. @@ -2300,16 +2300,31 @@ impl<'a> Parser<'a> { } } - /// Parse either `ALL` or `DISTINCT`. Returns `true` if `DISTINCT` is parsed and results in a - /// `ParserError` if both `ALL` and `DISTINCT` are fround. - pub fn parse_all_or_distinct(&mut self) -> Result { + /// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns `None` if `ALL` is parsed + /// and results in a `ParserError` if both `ALL` and `DISTINCT` are found. + pub fn parse_all_or_distinct(&mut self) -> Result, ParserError> { let all = self.parse_keyword(Keyword::ALL); let distinct = self.parse_keyword(Keyword::DISTINCT); - if all && distinct { - parser_err!("Cannot specify both ALL and DISTINCT".to_string()) - } else { - Ok(distinct) + if !distinct { + return Ok(None); } + if all { + return parser_err!("Cannot specify both ALL and DISTINCT".to_string()); + } + let on = self.parse_keyword(Keyword::ON); + if !on { + return Ok(Some(Distinct::Distinct)); + } + + self.expect_token(&Token::LParen)?; + let col_names = if self.consume_token(&Token::RParen) { + self.prev_token(); + Vec::new() + } else { + self.parse_comma_separated(Parser::parse_expr)? + }; + self.expect_token(&Token::RParen)?; + Ok(Some(Distinct::On(col_names))) } /// Parse a SQL CREATE statement diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 3ee3fbc03..8af8dd839 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -32,7 +32,7 @@ fn parse_map_access_expr() { let select = clickhouse().verified_only_select(sql); assert_eq!( Select { - distinct: false, + distinct: None, top: None, projection: vec![UnnamedExpr(MapAccess { column: Box::new(Identifier(Ident { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index aee9efd70..27a642dbe 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -225,7 +225,7 @@ fn parse_update_set_from() { subquery: Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![ SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), @@ -597,7 +597,7 @@ fn parse_top_level() { fn parse_simple_select() { let sql = "SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT 5"; let select = verified_only_select(sql); - assert!(!select.distinct); + 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); @@ -622,7 +622,7 @@ fn parse_limit_is_not_an_alias() { fn parse_select_distinct() { let sql = "SELECT DISTINCT name FROM customer"; let select = verified_only_select(sql); - assert!(select.distinct); + assert!(select.distinct.is_some()); assert_eq!( &SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), only(&select.projection) @@ -633,7 +633,7 @@ fn parse_select_distinct() { fn parse_select_distinct_two_fields() { let sql = "SELECT DISTINCT name, id FROM customer"; let select = verified_only_select(sql); - assert!(select.distinct); + assert!(select.distinct.is_some()); assert_eq!( &SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), &select.projection[0] @@ -657,6 +657,30 @@ fn parse_select_distinct_tuple() { ); } +#[test] +fn parse_select_distinct_on() { + let sql = "SELECT DISTINCT ON (album_id) name FROM track ORDER BY album_id, milliseconds"; + let select = verified_only_select(sql); + assert_eq!( + &Some(Distinct::On(vec![Expr::Identifier(Ident::new("album_id"))])), + &select.distinct + ); + + let sql = "SELECT DISTINCT ON () name FROM track ORDER BY milliseconds"; + let select = verified_only_select(sql); + assert_eq!(&Some(Distinct::On(vec![])), &select.distinct); + + let sql = "SELECT DISTINCT ON (album_id, milliseconds) name FROM track"; + let select = verified_only_select(sql); + assert_eq!( + &Some(Distinct::On(vec![ + Expr::Identifier(Ident::new("album_id")), + Expr::Identifier(Ident::new("milliseconds")), + ])), + &select.distinct + ); +} + #[test] fn parse_select_distinct_missing_paren() { let result = parse_sql_statements("SELECT DISTINCT (name, id FROM customer"); @@ -3517,7 +3541,7 @@ fn parse_interval_and_or_xor() { let expected_ast = vec![Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![UnnamedExpr(Expr::Identifier(Ident { value: "col".to_string(), @@ -5834,7 +5858,7 @@ fn parse_merge() { subquery: Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![SelectItem::Wildcard( WildcardAdditionalOptions::default() diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 2b312dc5e..1bdce0009 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -445,7 +445,7 @@ fn parse_quote_identifiers_2() { Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { value: "quoted ` identifier".into(), @@ -479,7 +479,7 @@ fn parse_quote_identifiers_3() { Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { value: "`quoted identifier`".into(), @@ -857,7 +857,7 @@ fn parse_select_with_numeric_prefix_column_name() { assert_eq!( q.body, Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new( "123col_$@123abc" @@ -896,7 +896,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { assert_eq!( q.body, Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![ SelectItem::UnnamedExpr(Expr::Value(Value::Number( @@ -1075,7 +1075,7 @@ fn parse_substring_in_select() { Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { - distinct: true, + distinct: Some(Distinct::Distinct), top: None, projection: vec![SelectItem::UnnamedExpr(Expr::Substring { expr: Box::new(Expr::Identifier(Ident { @@ -1372,7 +1372,7 @@ fn parse_hex_string_introducer() { Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![SelectItem::UnnamedExpr(Expr::IntroducedString { introducer: "_latin1".to_string(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 471421215..af9783f35 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1694,7 +1694,7 @@ fn parse_array_subquery_expr() { op: SetOperator::Union, set_quantifier: SetQuantifier::None, left: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![SelectItem::UnnamedExpr(Expr::Value(Value::Number( #[cfg(not(feature = "bigdecimal"))] @@ -1715,7 +1715,7 @@ fn parse_array_subquery_expr() { qualify: None, }))), right: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![SelectItem::UnnamedExpr(Expr::Value(Value::Number( #[cfg(not(feature = "bigdecimal"))] From 0113bbd924d486fad49990341c2b7c915dcb6d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alja=C5=BE=20Mur=20Er=C5=BEen?= Date: Mon, 1 May 2023 14:31:17 +0200 Subject: [PATCH 190/806] Test trailing commas (#859) * test: add tests for trailing commas * tweaks --- src/parser.rs | 8 ++++++- src/test_utils.rs | 21 ++++++++++++++--- tests/sqlparser_bigquery.rs | 2 ++ tests/sqlparser_clickhouse.rs | 1 + tests/sqlparser_common.rs | 43 ++++++++++++++++++++++++++++++++++- tests/sqlparser_hive.rs | 2 ++ tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_mysql.rs | 2 ++ tests/sqlparser_postgres.rs | 2 ++ tests/sqlparser_redshift.rs | 1 + tests/sqlparser_snowflake.rs | 2 ++ tests/sqlparser_sqlite.rs | 2 ++ 12 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index c468e9be8..352be3b63 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -195,7 +195,7 @@ impl std::error::Error for ParserError {} // By default, allow expressions up to this deep before erroring const DEFAULT_REMAINING_DEPTH: usize = 50; -#[derive(Default)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct ParserOptions { pub trailing_commas: bool, } @@ -6958,6 +6958,7 @@ mod tests { // Character string types: let dialect = TestedDialects { dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + options: None, }; test_parse_data_type!(dialect, "CHARACTER", DataType::Character(None)); @@ -7087,6 +7088,7 @@ mod tests { // Character large object types: let dialect = TestedDialects { dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + options: None, }; test_parse_data_type!( @@ -7119,6 +7121,7 @@ mod tests { fn test_parse_custom_types() { let dialect = TestedDialects { dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + options: None, }; test_parse_data_type!( dialect, @@ -7150,6 +7153,7 @@ mod tests { // Exact numeric types: let dialect = TestedDialects { dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + options: None, }; test_parse_data_type!(dialect, "NUMERIC", DataType::Numeric(ExactNumberInfo::None)); @@ -7200,6 +7204,7 @@ mod tests { // Datetime types: let dialect = TestedDialects { dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + options: None, }; test_parse_data_type!(dialect, "DATE", DataType::Date); @@ -7311,6 +7316,7 @@ mod tests { let dialect = TestedDialects { dialects: vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})], + options: None, }; test_parse_table_constraint!( diff --git a/src/test_utils.rs b/src/test_utils.rs index 422ecc77d..d01bbbab9 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -26,17 +26,27 @@ use alloc::{ }; use core::fmt::Debug; -use crate::ast::*; use crate::dialect::*; use crate::parser::{Parser, ParserError}; +use crate::{ast::*, parser::ParserOptions}; /// Tests use the methods on this struct to invoke the parser on one or /// multiple dialects. pub struct TestedDialects { pub dialects: Vec>, + pub options: Option, } impl TestedDialects { + fn new_parser<'a>(&self, dialect: &'a dyn Dialect) -> Parser<'a> { + let parser = Parser::new(dialect); + if let Some(options) = &self.options { + parser.with_options(options.clone()) + } else { + parser + } + } + /// Run the given function for all of `self.dialects`, assert that they /// return the same result, and return that result. pub fn one_of_identical_results(&self, f: F) -> T @@ -63,7 +73,7 @@ impl TestedDialects { F: Fn(&mut Parser) -> T, { self.one_of_identical_results(|dialect| { - let mut parser = Parser::new(dialect).try_with_sql(sql).unwrap(); + let mut parser = self.new_parser(dialect).try_with_sql(sql).unwrap(); f(&mut parser) }) } @@ -71,7 +81,11 @@ impl TestedDialects { /// Parses a single SQL string into multiple statements, ensuring /// the result is the same for all tested dialects. pub fn parse_sql_statements(&self, sql: &str) -> Result, ParserError> { - self.one_of_identical_results(|dialect| Parser::parse_sql(dialect, sql)) + self.one_of_identical_results(|dialect| { + self.new_parser(dialect) + .try_with_sql(sql)? + .parse_statements() + }) // To fail the `ensure_multiple_dialects_are_tested` test: // Parser::parse_sql(&**self.dialects.first().unwrap(), sql) } @@ -155,6 +169,7 @@ pub fn all_dialects() -> TestedDialects { Box::new(BigQueryDialect {}), Box::new(SQLiteDialect {}), ], + options: None, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 11a8c6e5c..0850bd4bf 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -406,12 +406,14 @@ fn test_select_wildcard_with_replace() { fn bigquery() -> TestedDialects { TestedDialects { dialects: vec![Box::new(BigQueryDialect {})], + options: None, } } fn bigquery_and_generic() -> TestedDialects { TestedDialects { dialects: vec![Box::new(BigQueryDialect {}), Box::new(GenericDialect {})], + options: None, } } diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 8af8dd839..bb0ec48fa 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -329,5 +329,6 @@ fn parse_create_table() { fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], + options: None, } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 27a642dbe..16fd623dd 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -28,7 +28,7 @@ use sqlparser::dialect::{ MySqlDialect, PostgreSqlDialect, RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, }; use sqlparser::keywords::ALL_KEYWORDS; -use sqlparser::parser::{Parser, ParserError}; +use sqlparser::parser::{Parser, ParserError, ParserOptions}; use test_utils::{ all_dialects, assert_eq_vec, expr_from_projection, join, number, only, table, table_alias, TestedDialects, @@ -201,6 +201,7 @@ fn parse_update_set_from() { Box::new(RedshiftSqlDialect {}), Box::new(MsSqlDialect {}), ], + options: None, }; let stmt = dialects.verified_stmt(sql); assert_eq!( @@ -949,6 +950,7 @@ fn parse_exponent_in_select() -> Result<(), ParserError> { Box::new(SnowflakeDialect {}), Box::new(SQLiteDialect {}), ], + options: None, }; let sql = "SELECT 10e-20, 1e3, 1e+3, 1e3a, 1e, 0.5e2"; let mut select = dialects.parse_sql_statements(sql)?; @@ -1386,6 +1388,7 @@ pub fn all_dialects_but_pg() -> TestedDialects { .into_iter() .filter(|x| !x.is::()) .collect(), + options: None, } } @@ -2055,6 +2058,7 @@ fn parse_array_agg_func() { Box::new(AnsiDialect {}), Box::new(HiveDialect {}), ], + options: None, }; for sql in [ @@ -2254,6 +2258,7 @@ fn parse_create_table_hive_array() { // Parsing [] type arrays does not work in MsSql since [ is used in is_delimited_identifier_start let dialects = TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(HiveDialect {})], + options: None, }; let sql = "CREATE TABLE IF NOT EXISTS something (name int, val array)"; match dialects.one_statement_parses_to( @@ -2296,6 +2301,7 @@ fn parse_create_table_hive_array() { Box::new(HiveDialect {}), Box::new(MySqlDialect {}), ], + options: None, }; let sql = "CREATE TABLE IF NOT EXISTS something (name int, val array TestedDialects { TestedDialects { dialects: vec![Box::new(HiveDialect {})], + options: None, } } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 41b0803e4..1f708b5e7 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -305,10 +305,12 @@ fn parse_similar_to() { fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], + options: None, } } fn ms_and_generic() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})], + options: None, } } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 1bdce0009..1c479bb18 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1350,12 +1350,14 @@ fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name fn mysql() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})], + options: None, } } fn mysql_and_generic() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})], + options: None, } } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index af9783f35..09db64293 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2052,12 +2052,14 @@ fn parse_on_commit() { fn pg() -> TestedDialects { TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {})], + options: None, } } fn pg_and_generic() -> TestedDialects { TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(GenericDialect {})], + options: None, } } diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 7597ee981..d3a676939 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -260,6 +260,7 @@ fn parse_similar_to() { fn redshift() -> TestedDialects { TestedDialects { dialects: vec![Box::new(RedshiftSqlDialect {})], + options: None, } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 8bcbb7b5a..d1a63dc86 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -396,12 +396,14 @@ fn test_array_agg_func() { fn snowflake() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SnowflakeDialect {})], + options: None, } } fn snowflake_and_generic() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SnowflakeDialect {}), Box::new(GenericDialect {})], + options: None, } } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 8fc3a8d63..31d2dd97f 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -245,6 +245,7 @@ fn parse_similar_to() { fn sqlite() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SQLiteDialect {})], + options: None, } } @@ -252,5 +253,6 @@ fn sqlite_and_generic() -> TestedDialects { TestedDialects { // we don't have a separate SQLite dialect, so test only the generic dialect for now dialects: vec![Box::new(SQLiteDialect {}), Box::new(GenericDialect {})], + options: None, } } From 0ff863b2c7bf357d95154eb052ee29583e260643 Mon Sep 17 00:00:00 2001 From: Armin Primadi Date: Tue, 2 May 2023 02:39:18 +0700 Subject: [PATCH 191/806] Add support for query source in COPY .. TO statement (#858) * Add support for query source in COPY .. TO statement * Fix compile error --- src/ast/mod.rs | 41 ++++++++--- src/parser.rs | 26 +++++-- tests/sqlparser_postgres.rs | 139 +++++++++++++++++++++++++++++------- 3 files changed, 165 insertions(+), 41 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 02121acc9..7b460c62a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1173,14 +1173,11 @@ pub enum Statement { source: Box, }, Copy { - /// TABLE - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - /// COLUMNS - columns: Vec, + /// The source of 'COPY TO', or the target of 'COPY FROM' + source: CopySource, /// If true, is a 'COPY TO' statement. If false is a 'COPY FROM' to: bool, - /// The source of 'COPY FROM', or the target of 'COPY TO' + /// The target of 'COPY TO', or the source of 'COPY FROM' target: CopyTarget, /// WITH options (from PostgreSQL version 9.0) options: Vec, @@ -1926,17 +1923,25 @@ impl fmt::Display for Statement { } Statement::Copy { - table_name, - columns, + source, to, target, options, legacy_options, values, } => { - write!(f, "COPY {table_name}")?; - if !columns.is_empty() { - write!(f, " ({})", display_comma_separated(columns))?; + write!(f, "COPY")?; + match source { + CopySource::Query(query) => write!(f, " ({query})")?, + CopySource::Table { + table_name, + columns, + } => { + write!(f, " {table_name}")?; + if !columns.is_empty() { + write!(f, " ({})", display_comma_separated(columns))?; + } + } } write!(f, " {} {}", if *to { "TO" } else { "FROM" }, target)?; if !options.is_empty() { @@ -3750,6 +3755,20 @@ impl fmt::Display for SqliteOnConflict { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CopySource { + Table { + /// The name of the table to copy from. + table_name: ObjectName, + /// A list of column names to copy. Empty list means that all columns + /// are copied. + columns: Vec, + }, + Query(Box), +} + #[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.rs b/src/parser.rs index 352be3b63..82cbe9d12 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4015,13 +4015,32 @@ impl<'a> Parser<'a> { /// Parse a copy statement pub fn parse_copy(&mut self) -> Result { - let table_name = self.parse_object_name()?; - let columns = self.parse_parenthesized_column_list(Optional, false)?; + let source; + if self.consume_token(&Token::LParen) { + source = CopySource::Query(Box::new(self.parse_query()?)); + self.expect_token(&Token::RParen)?; + } else { + let table_name = self.parse_object_name()?; + let columns = self.parse_parenthesized_column_list(Optional, false)?; + source = CopySource::Table { + table_name, + columns, + }; + } let to = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::TO]) { Some(Keyword::FROM) => false, Some(Keyword::TO) => true, _ => self.expected("FROM or TO", self.peek_token())?, }; + if !to { + // Use a separate if statement to prevent Rust compiler from complaining about + // "if statement in this position is unstable: https://github.com/rust-lang/rust/issues/53667" + if let CopySource::Query(_) = source { + return Err(ParserError::ParserError( + "COPY ... FROM does not support query as a source".to_string(), + )); + } + } let target = if self.parse_keyword(Keyword::STDIN) { CopyTarget::Stdin } else if self.parse_keyword(Keyword::STDOUT) { @@ -4052,8 +4071,7 @@ impl<'a> Parser<'a> { vec![] }; Ok(Statement::Copy { - table_name, - columns, + source, to, target, options, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 09db64293..ff7b27048 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -691,8 +691,10 @@ fn test_copy_from() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: false, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -707,8 +709,10 @@ fn test_copy_from() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: false, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -723,8 +727,10 @@ fn test_copy_from() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: false, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -745,8 +751,10 @@ fn test_copy_to() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: true, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -761,8 +769,10 @@ fn test_copy_to() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: true, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -777,8 +787,10 @@ fn test_copy_to() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: true, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -816,8 +828,10 @@ fn parse_copy_from() { assert_eq!( pg_and_generic().one_statement_parses_to(sql, ""), Statement::Copy { - table_name: ObjectName(vec!["table".into()]), - columns: vec!["a".into(), "b".into()], + source: CopySource::Table { + table_name: ObjectName(vec!["table".into()]), + columns: vec!["a".into(), "b".into()], + }, to: false, target: CopyTarget::File { filename: "file.csv".into() @@ -845,14 +859,25 @@ fn parse_copy_from() { ); } +#[test] +fn parse_copy_from_error() { + let res = pg().parse_sql_statements("COPY (SELECT 42 AS a, 'hello' AS b) FROM 'query.csv'"); + assert_eq!( + ParserError::ParserError("COPY ... FROM does not support query as a source".to_string()), + res.unwrap_err() + ); +} + #[test] fn parse_copy_to() { let stmt = pg().verified_stmt("COPY users TO 'data.csv'"); assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: true, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -867,8 +892,10 @@ fn parse_copy_to() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["country".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["country".into()]), + columns: vec![], + }, to: true, target: CopyTarget::Stdout, options: vec![CopyOption::Delimiter('|')], @@ -882,8 +909,10 @@ fn parse_copy_to() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["country".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["country".into()]), + columns: vec![], + }, to: true, target: CopyTarget::Program { command: "gzip > /usr1/proj/bray/sql/country_data.gz".into(), @@ -893,6 +922,58 @@ fn parse_copy_to() { values: vec![], } ); + + let stmt = pg().verified_stmt("COPY (SELECT 42 AS a, 'hello' AS b) TO 'query.csv'"); + assert_eq!( + stmt, + Statement::Copy { + source: CopySource::Query(Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: false, + top: None, + projection: vec![ + SelectItem::ExprWithAlias { + expr: Expr::Value(number("42")), + alias: Ident { + value: "a".into(), + quote_style: None, + }, + }, + SelectItem::ExprWithAlias { + expr: Expr::Value(Value::SingleQuotedString("hello".into())), + alias: Ident { + value: "b".into(), + quote_style: None, + }, + } + ], + into: None, + from: vec![], + lateral_views: vec![], + selection: None, + group_by: vec![], + having: None, + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + qualify: None, + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + locks: vec![], + })), + to: true, + target: CopyTarget::File { + filename: "query.csv".into(), + }, + options: vec![], + legacy_options: vec![], + values: vec![], + } + ) } #[test] @@ -901,8 +982,10 @@ fn parse_copy_from_before_v9_0() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: false, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -928,8 +1011,10 @@ fn parse_copy_from_before_v9_0() { assert_eq!( pg_and_generic().one_statement_parses_to(sql, ""), Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: false, target: CopyTarget::File { filename: "data.csv".to_string(), @@ -954,8 +1039,10 @@ fn parse_copy_to_before_v9_0() { assert_eq!( stmt, Statement::Copy { - table_name: ObjectName(vec!["users".into()]), - columns: vec![], + source: CopySource::Table { + table_name: ObjectName(vec!["users".into()]), + columns: vec![], + }, to: true, target: CopyTarget::File { filename: "data.csv".to_string(), From be85f54ca3bf9325786d23326d65f23b41a2859b Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 2 May 2023 07:07:56 -0400 Subject: [PATCH 192/806] Fix logical merge conflict (#865) --- tests/sqlparser_postgres.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ff7b27048..9de8e0eb4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -930,7 +930,7 @@ fn parse_copy_to() { source: CopySource::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { - distinct: false, + distinct: None, top: None, projection: vec![ SelectItem::ExprWithAlias { From b29b551fa111bbd50c40568f6aea6c49eafc0b9c Mon Sep 17 00:00:00 2001 From: Okue Date: Tue, 2 May 2023 20:08:38 +0900 Subject: [PATCH 193/806] Fix tiny typo in custom_sql_parser.md (#864) --- docs/custom_sql_parser.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/custom_sql_parser.md b/docs/custom_sql_parser.md index c13041af4..055ddbaa5 100644 --- a/docs/custom_sql_parser.md +++ b/docs/custom_sql_parser.md @@ -6,5 +6,5 @@ The concept is simply to write a new parser that delegates to the ANSI parser so I also plan on building in specific support for custom data types, where a lambda function can be passed to the parser to parse data types. -For an example of this, see the [DataFusion](https://github.com/apache/arrow-datafusion) project and its [query planner] (https://github.com/apache/arrow-datafusion/tree/master/datafusion/sql). +For an example of this, see the [DataFusion](https://github.com/apache/arrow-datafusion) project and its [query planner](https://github.com/apache/arrow-datafusion/tree/master/datafusion/sql). From f15da8772e4613b02245f4508a80441de65f377f Mon Sep 17 00:00:00 2001 From: Armin Primadi Date: Wed, 10 May 2023 07:42:03 +0700 Subject: [PATCH 194/806] Make Expr::Interval its own struct (#872) * Make Expr::Interval its own struct * Add test interval display * Fix cargo fmt --- src/ast/mod.rs | 149 ++++++++++++++++++++++++-------------- src/parser.rs | 4 +- tests/sqlparser_common.rs | 40 +++++----- 3 files changed, 116 insertions(+), 77 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7b460c62a..8bb662d8c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -194,6 +194,70 @@ impl fmt::Display for Array { } } +/// Represents an INTERVAL expression, roughly in the following format: +/// `INTERVAL '' [ [ () ] ] +/// [ TO [ () ] ]`, +/// e.g. `INTERVAL '123:45.67' MINUTE(3) TO SECOND(2)`. +/// +/// The parser does not validate the ``, nor does it ensure +/// that the `` units >= the units in ``, +/// so the user will have to reject intervals like `HOUR TO YEAR`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Interval { + pub value: Box, + pub leading_field: Option, + pub leading_precision: Option, + pub last_field: Option, + /// The seconds precision can be specified in SQL source as + /// `INTERVAL '__' SECOND(_, x)` (in which case the `leading_field` + /// will be `Second` and the `last_field` will be `None`), + /// or as `__ TO SECOND(x)`. + pub fractional_seconds_precision: Option, +} + +impl fmt::Display for Interval { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = self.value.as_ref(); + match ( + self.leading_field, + self.leading_precision, + self.fractional_seconds_precision, + ) { + ( + Some(DateTimeField::Second), + Some(leading_precision), + Some(fractional_seconds_precision), + ) => { + // When the leading field is SECOND, the parser guarantees that + // the last field is None. + assert!(self.last_field.is_none()); + write!( + f, + "INTERVAL {value} SECOND ({leading_precision}, {fractional_seconds_precision})" + ) + } + _ => { + write!(f, "INTERVAL {value}")?; + if let Some(leading_field) = self.leading_field { + write!(f, " {leading_field}")?; + } + if let Some(leading_precision) = self.leading_precision { + write!(f, " ({leading_precision})")?; + } + if let Some(last_field) = self.last_field { + write!(f, " TO {last_field}")?; + } + if let Some(fractional_seconds_precision) = self.fractional_seconds_precision { + write!(f, " ({fractional_seconds_precision})")?; + } + Ok(()) + } + } + } +} + /// JsonOperator #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -491,25 +555,8 @@ pub enum Expr { ArrayIndex { obj: Box, indexes: Vec }, /// An array expression e.g. `ARRAY[1, 2]` Array(Array), - /// INTERVAL literals, roughly in the following format: - /// `INTERVAL '' [ [ () ] ] - /// [ TO [ () ] ]`, - /// e.g. `INTERVAL '123:45.67' MINUTE(3) TO SECOND(2)`. - /// - /// The parser does not validate the ``, nor does it ensure - /// that the `` units >= the units in ``, - /// so the user will have to reject intervals like `HOUR TO YEAR`. - Interval { - value: Box, - leading_field: Option, - leading_precision: Option, - last_field: Option, - /// The seconds precision can be specified in SQL source as - /// `INTERVAL '__' SECOND(_, x)` (in which case the `leading_field` - /// will be `Second` and the `last_field` will be `None`), - /// or as `__ TO SECOND(x)`. - fractional_seconds_precision: Option, - }, + /// An interval expression e.g. `INTERVAL '1' YEAR` + Interval(Interval), /// `MySQL` specific text search function [(1)]. /// /// Syntax: @@ -861,42 +908,8 @@ impl fmt::Display for Expr { } => { write!(f, "{timestamp} AT TIME ZONE '{time_zone}'") } - Expr::Interval { - value, - leading_field: Some(DateTimeField::Second), - leading_precision: Some(leading_precision), - last_field, - fractional_seconds_precision: Some(fractional_seconds_precision), - } => { - // When the leading field is SECOND, the parser guarantees that - // the last field is None. - assert!(last_field.is_none()); - write!( - f, - "INTERVAL {value} SECOND ({leading_precision}, {fractional_seconds_precision})" - ) - } - Expr::Interval { - value, - leading_field, - leading_precision, - last_field, - fractional_seconds_precision, - } => { - write!(f, "INTERVAL {value}")?; - if let Some(leading_field) = leading_field { - write!(f, " {leading_field}")?; - } - if let Some(leading_precision) = leading_precision { - write!(f, " ({leading_precision})")?; - } - if let Some(last_field) = last_field { - write!(f, " TO {last_field}")?; - } - if let Some(fractional_seconds_precision) = fractional_seconds_precision { - write!(f, " ({fractional_seconds_precision})")?; - } - Ok(()) + Expr::Interval(interval) => { + write!(f, "{interval}") } Expr::MatchAgainst { columns, @@ -4410,4 +4423,30 @@ mod tests { ]); assert_eq!("CUBE (a, (b, c), d)", format!("{cube}")); } + + #[test] + fn test_interval_display() { + let interval = Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( + "123:45.67", + )))), + leading_field: Some(DateTimeField::Minute), + leading_precision: Some(10), + last_field: Some(DateTimeField::Second), + fractional_seconds_precision: Some(9), + }); + assert_eq!( + "INTERVAL '123:45.67' MINUTE (10) TO SECOND (9)", + format!("{interval}"), + ); + + let interval = Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("5")))), + leading_field: Some(DateTimeField::Second), + leading_precision: Some(1), + last_field: None, + fractional_seconds_precision: Some(3), + }); + assert_eq!("INTERVAL '5' SECOND (1, 3)", format!("{interval}")); + } } diff --git a/src/parser.rs b/src/parser.rs index 82cbe9d12..7299a5c5d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1615,13 +1615,13 @@ impl<'a> Parser<'a> { } }; - Ok(Expr::Interval { + Ok(Expr::Interval(Interval { value: Box::new(value), leading_field, leading_precision, last_field, fractional_seconds_precision: fsec_precision, - }) + })) } /// Parse an operator following an expression diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 16fd623dd..5d28118fb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3395,20 +3395,20 @@ fn parse_interval() { let sql = "SELECT INTERVAL '1-1' YEAR TO MONTH"; let select = verified_only_select(sql); assert_eq!( - &Expr::Interval { + &Expr::Interval(Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("1-1")))), leading_field: Some(DateTimeField::Year), leading_precision: None, last_field: Some(DateTimeField::Month), fractional_seconds_precision: None, - }, + }), expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '01:01.01' MINUTE (5) TO SECOND (5)"; let select = verified_only_select(sql); assert_eq!( - &Expr::Interval { + &Expr::Interval(Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( "01:01.01" )))), @@ -3416,53 +3416,53 @@ fn parse_interval() { leading_precision: Some(5), last_field: Some(DateTimeField::Second), fractional_seconds_precision: Some(5), - }, + }), expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '1' SECOND (5, 4)"; let select = verified_only_select(sql); assert_eq!( - &Expr::Interval { + &Expr::Interval(Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("1")))), leading_field: Some(DateTimeField::Second), leading_precision: Some(5), last_field: None, fractional_seconds_precision: Some(4), - }, + }), expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '10' HOUR"; let select = verified_only_select(sql); assert_eq!( - &Expr::Interval { + &Expr::Interval(Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("10")))), leading_field: Some(DateTimeField::Hour), leading_precision: None, last_field: None, fractional_seconds_precision: None, - }, + }), expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL 5 DAY"; let select = verified_only_select(sql); assert_eq!( - &Expr::Interval { + &Expr::Interval(Interval { value: Box::new(Expr::Value(number("5"))), leading_field: Some(DateTimeField::Day), leading_precision: None, last_field: None, fractional_seconds_precision: None, - }, + }), expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL 1 + 1 DAY"; let select = verified_only_select(sql); assert_eq!( - &Expr::Interval { + &Expr::Interval(Interval { value: Box::new(Expr::BinaryOp { left: Box::new(Expr::Value(number("1"))), op: BinaryOperator::Plus, @@ -3472,27 +3472,27 @@ fn parse_interval() { leading_precision: None, last_field: None, fractional_seconds_precision: None, - }, + }), expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '10' HOUR (1)"; let select = verified_only_select(sql); assert_eq!( - &Expr::Interval { + &Expr::Interval(Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("10")))), leading_field: Some(DateTimeField::Hour), leading_precision: Some(1), last_field: None, fractional_seconds_precision: None, - }, + }), expr_from_projection(only(&select.projection)), ); let sql = "SELECT INTERVAL '1 DAY'"; let select = verified_only_select(sql); assert_eq!( - &Expr::Interval { + &Expr::Interval(Interval { value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( "1 DAY" )))), @@ -3500,7 +3500,7 @@ fn parse_interval() { leading_precision: None, last_field: None, fractional_seconds_precision: None, - }, + }), expr_from_projection(only(&select.projection)), ); @@ -3581,7 +3581,7 @@ fn parse_interval_and_or_xor() { quote_style: None, })), op: BinaryOperator::Plus, - right: Box::new(Expr::Interval { + right: Box::new(Expr::Interval(Interval { value: Box::new(Expr::Value(Value::SingleQuotedString( "5 days".to_string(), ))), @@ -3589,7 +3589,7 @@ fn parse_interval_and_or_xor() { leading_precision: None, last_field: None, fractional_seconds_precision: None, - }), + })), }), }), op: BinaryOperator::And, @@ -3605,7 +3605,7 @@ fn parse_interval_and_or_xor() { quote_style: None, })), op: BinaryOperator::Plus, - right: Box::new(Expr::Interval { + right: Box::new(Expr::Interval(Interval { value: Box::new(Expr::Value(Value::SingleQuotedString( "3 days".to_string(), ))), @@ -3613,7 +3613,7 @@ fn parse_interval_and_or_xor() { leading_precision: None, last_field: None, fractional_seconds_precision: None, - }), + })), }), }), }), From ae3b5844c839072c235965fe0d1bddc473dced87 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 9 May 2023 17:48:57 -0700 Subject: [PATCH 195/806] Include license file in published crate (#871) --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index a1126d278..3b3af073f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ license = "Apache-2.0" include = [ "src/**/*.rs", "Cargo.toml", + "LICENSE.TXT", ] edition = "2021" From 482a3ad4174f482b144f5b6e62bf648d70228fc3 Mon Sep 17 00:00:00 2001 From: Mustafa Akur <106137913+mustafasrepo@users.noreply.github.com> Date: Wed, 17 May 2023 20:04:33 +0300 Subject: [PATCH 196/806] Add support for multiple expressions, order by in aggregations (#879) * Add support for multiple expressions, order by in aggregations * Fix formatting errors * Resolve linter errors --- src/ast/mod.rs | 10 +++++++--- src/parser.rs | 12 +++++++----- tests/sqlparser_common.rs | 2 ++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8bb662d8c..e26cb0dfc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3518,7 +3518,7 @@ impl fmt::Display for ListAggOnOverflow { pub struct ArrayAgg { pub distinct: bool, pub expr: Box, - pub order_by: Option>, + pub order_by: Option>, pub limit: Option>, pub within_group: bool, // order by is used inside a within group or not } @@ -3533,7 +3533,7 @@ impl fmt::Display for ArrayAgg { )?; if !self.within_group { if let Some(order_by) = &self.order_by { - write!(f, " ORDER BY {order_by}")?; + write!(f, " ORDER BY {}", display_comma_separated(order_by))?; } if let Some(limit) = &self.limit { write!(f, " LIMIT {limit}")?; @@ -3542,7 +3542,11 @@ impl fmt::Display for ArrayAgg { write!(f, ")")?; if self.within_group { if let Some(order_by) = &self.order_by { - write!(f, " WITHIN GROUP (ORDER BY {order_by})")?; + write!( + f, + " WITHIN GROUP (ORDER BY {})", + display_comma_separated(order_by) + )?; } } Ok(()) diff --git a/src/parser.rs b/src/parser.rs index 7299a5c5d..cb3660498 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1369,8 +1369,7 @@ impl<'a> Parser<'a> { // ANSI SQL and BigQuery define ORDER BY inside function. if !self.dialect.supports_within_after_array_aggregation() { let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { - let order_by_expr = self.parse_order_by_expr()?; - Some(Box::new(order_by_expr)) + Some(self.parse_comma_separated(Parser::parse_order_by_expr)?) } else { None }; @@ -1393,10 +1392,13 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; let within_group = if self.parse_keywords(&[Keyword::WITHIN, Keyword::GROUP]) { self.expect_token(&Token::LParen)?; - self.expect_keywords(&[Keyword::ORDER, Keyword::BY])?; - let order_by_expr = self.parse_order_by_expr()?; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + Some(self.parse_comma_separated(Parser::parse_order_by_expr)?) + } else { + None + }; self.expect_token(&Token::RParen)?; - Some(Box::new(order_by_expr)) + order_by } else { None }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5d28118fb..ac5c18c0e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2065,6 +2065,8 @@ fn parse_array_agg_func() { "SELECT ARRAY_AGG(x ORDER BY x) AS a FROM T", "SELECT ARRAY_AGG(x ORDER BY x LIMIT 2) FROM tbl", "SELECT ARRAY_AGG(DISTINCT x ORDER BY x LIMIT 2) FROM tbl", + "SELECT ARRAY_AGG(x ORDER BY x, y) AS a FROM T", + "SELECT ARRAY_AGG(x ORDER BY x ASC, y DESC) AS a FROM T", ] { supported_dialects.verified_stmt(sql); } From 4559d87a824a6a268a350d02e63c51b77e41bd34 Mon Sep 17 00:00:00 2001 From: Jeffrey <22608443+Jefffrey@users.noreply.github.com> Date: Thu, 18 May 2023 03:04:57 +1000 Subject: [PATCH 197/806] Add parse_multipart_identifier function to parser (#860) * Add parse_multipart_identifier function to parser * Update doc for parse_multipart_identifier * Fix conflict --- src/parser.rs | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/src/parser.rs b/src/parser.rs index cb3660498..45639eefc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4707,6 +4707,92 @@ impl<'a> Parser<'a> { Ok(idents) } + /// Parse identifiers of form ident1[.identN]* + /// + /// Similar in functionality to [parse_identifiers], with difference + /// being this function is much more strict about parsing a valid multipart identifier, not + /// allowing extraneous tokens to be parsed, otherwise it fails. + /// + /// For example: + /// + /// ```rust + /// use sqlparser::ast::Ident; + /// use sqlparser::dialect::GenericDialect; + /// use sqlparser::parser::Parser; + /// + /// let dialect = GenericDialect {}; + /// let expected = vec![Ident::new("one"), Ident::new("two")]; + /// + /// // expected usage + /// let sql = "one.two"; + /// let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); + /// let actual = parser.parse_multipart_identifier().unwrap(); + /// assert_eq!(&actual, &expected); + /// + /// // parse_identifiers is more loose on what it allows, parsing successfully + /// let sql = "one + two"; + /// let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); + /// let actual = parser.parse_identifiers().unwrap(); + /// assert_eq!(&actual, &expected); + /// + /// // expected to strictly fail due to + separator + /// let sql = "one + two"; + /// let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); + /// let actual = parser.parse_multipart_identifier().unwrap_err(); + /// assert_eq!( + /// actual.to_string(), + /// "sql parser error: Unexpected token in identifier: +" + /// ); + /// ``` + /// + /// [parse_identifiers]: Parser::parse_identifiers + pub fn parse_multipart_identifier(&mut self) -> Result, ParserError> { + let mut idents = vec![]; + + // expecting at least one word for identifier + match self.next_token().token { + Token::Word(w) => idents.push(w.to_ident()), + Token::EOF => { + return Err(ParserError::ParserError( + "Empty input when parsing identifier".to_string(), + ))? + } + token => { + return Err(ParserError::ParserError(format!( + "Unexpected token in identifier: {token}" + )))? + } + }; + + // parse optional next parts if exist + loop { + match self.next_token().token { + // ensure that optional period is succeeded by another identifier + Token::Period => match self.next_token().token { + Token::Word(w) => idents.push(w.to_ident()), + Token::EOF => { + return Err(ParserError::ParserError( + "Trailing period in identifier".to_string(), + ))? + } + token => { + return Err(ParserError::ParserError(format!( + "Unexpected token following period in identifier: {token}" + )))? + } + }, + Token::EOF => break, + token => { + return Err(ParserError::ParserError(format!( + "Unexpected token in identifier: {token}" + )))? + } + } + } + + Ok(idents) + } + /// Parse a simple one-word identifier (possibly quoted, possibly a keyword) pub fn parse_identifier(&mut self) -> Result { let next_token = self.next_token(); @@ -7455,4 +7541,85 @@ mod tests { )) ); } + + #[test] + fn test_parse_multipart_identifier_positive() { + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + options: None, + }; + + // parse multipart with quotes + let expected = vec![ + Ident { + value: "CATALOG".to_string(), + quote_style: None, + }, + Ident { + value: "F(o)o. \"bar".to_string(), + quote_style: Some('"'), + }, + Ident { + value: "table".to_string(), + quote_style: None, + }, + ]; + dialect.run_parser_method(r#"CATALOG."F(o)o. ""bar".table"#, |parser| { + let actual = parser.parse_multipart_identifier().unwrap(); + assert_eq!(expected, actual); + }); + + // allow whitespace between ident parts + let expected = vec![ + Ident { + value: "CATALOG".to_string(), + quote_style: None, + }, + Ident { + value: "table".to_string(), + quote_style: None, + }, + ]; + dialect.run_parser_method("CATALOG . table", |parser| { + let actual = parser.parse_multipart_identifier().unwrap(); + assert_eq!(expected, actual); + }); + } + + #[test] + fn test_parse_multipart_identifier_negative() { + macro_rules! test_parse_multipart_identifier_error { + ($input:expr, $expected_err:expr $(,)?) => {{ + all_dialects().run_parser_method(&*$input, |parser| { + let actual_err = parser.parse_multipart_identifier().unwrap_err(); + assert_eq!(actual_err.to_string(), $expected_err); + }); + }}; + } + + test_parse_multipart_identifier_error!( + "", + "sql parser error: Empty input when parsing identifier", + ); + + test_parse_multipart_identifier_error!( + "*schema.table", + "sql parser error: Unexpected token in identifier: *", + ); + + test_parse_multipart_identifier_error!( + "schema.table*", + "sql parser error: Unexpected token in identifier: *", + ); + + test_parse_multipart_identifier_error!( + "schema.table.", + "sql parser error: Trailing period in identifier", + ); + + test_parse_multipart_identifier_error!( + "schema.*", + "sql parser error: Unexpected token following period in identifier: *", + ); + } } From feaa13c9a908077b06569ae7a0a9ea1403918a5d Mon Sep 17 00:00:00 2001 From: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> Date: Wed, 17 May 2023 10:12:50 -0700 Subject: [PATCH 198/806] feat: Add custom operator (#868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add custom operator From #863 - It doesn't parse anything — I'm not sure how to parse ` SELECT 'a' REGEXP '^[a-d]';` with `REGEXP` as the operator... (but fine for my narrow purpose) - If we need tests, where would I add them? * Update src/ast/operator.rs --------- Co-authored-by: Andrew Lamb --- src/ast/operator.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 75877c949..14d91362c 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -85,6 +85,8 @@ pub enum BinaryOperator { BitwiseOr, BitwiseAnd, BitwiseXor, + /// Support for custom operators (built by parsers outside this crate) + Custom(String), PGBitwiseXor, PGBitwiseShiftLeft, PGBitwiseShiftRight, @@ -122,6 +124,7 @@ impl fmt::Display for BinaryOperator { BinaryOperator::BitwiseOr => f.write_str("|"), BinaryOperator::BitwiseAnd => f.write_str("&"), BinaryOperator::BitwiseXor => f.write_str("^"), + BinaryOperator::Custom(s) => f.write_str(s), BinaryOperator::PGBitwiseXor => f.write_str("#"), BinaryOperator::PGBitwiseShiftLeft => f.write_str("<<"), BinaryOperator::PGBitwiseShiftRight => f.write_str(">>"), From 097e7ad56ed09e5757fbf0719d07194aa93521e8 Mon Sep 17 00:00:00 2001 From: eitsupi <50911393+eitsupi@users.noreply.github.com> Date: Thu, 18 May 2023 02:26:14 +0900 Subject: [PATCH 199/806] feat: Support MySQL's `DIV` operator (#876) * feat: MySQL's DIV operator * fix: do not use `_` prefix for used variable --------- Co-authored-by: Andrew Lamb --- src/ast/operator.rs | 3 +++ src/dialect/mysql.rs | 27 ++++++++++++++++++++++++++- src/keywords.rs | 1 + src/parser.rs | 3 +++ tests/sqlparser_mysql.rs | 5 +++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 14d91362c..0ab9c66a4 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -85,6 +85,8 @@ pub enum BinaryOperator { BitwiseOr, BitwiseAnd, BitwiseXor, + /// MySQL [`DIV`](https://dev.mysql.com/doc/refman/8.0/en/arithmetic-functions.html) integer division + MyIntegerDivide, /// Support for custom operators (built by parsers outside this crate) Custom(String), PGBitwiseXor, @@ -124,6 +126,7 @@ impl fmt::Display for BinaryOperator { BinaryOperator::BitwiseOr => f.write_str("|"), BinaryOperator::BitwiseAnd => f.write_str("&"), BinaryOperator::BitwiseXor => f.write_str("^"), + BinaryOperator::MyIntegerDivide => f.write_str("DIV"), BinaryOperator::Custom(s) => f.write_str(s), BinaryOperator::PGBitwiseXor => f.write_str("#"), BinaryOperator::PGBitwiseShiftLeft => f.write_str("<<"), diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index ceab34810..0f914ed02 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -10,7 +10,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dialect::Dialect; +#[cfg(not(feature = "std"))] +use alloc::boxed::Box; + +use crate::{ + ast::{BinaryOperator, Expr}, + dialect::Dialect, + keywords::Keyword, +}; /// [MySQL](https://www.mysql.com/) #[derive(Debug)] @@ -35,4 +42,22 @@ impl Dialect for MySqlDialect { fn is_delimited_identifier_start(&self, ch: char) -> bool { ch == '`' } + + fn parse_infix( + &self, + parser: &mut crate::parser::Parser, + expr: &crate::ast::Expr, + _precedence: u8, + ) -> Option> { + // Parse DIV as an operator + if parser.parse_keyword(Keyword::DIV) { + Some(Ok(Expr::BinaryOp { + left: Box::new(expr.clone()), + op: BinaryOperator::MyIntegerDivide, + right: Box::new(parser.parse_expr().unwrap()), + })) + } else { + None + } + } } diff --git a/src/keywords.rs b/src/keywords.rs index a0c5b68cb..6132b31f0 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -211,6 +211,7 @@ define_keywords!( DISCONNECT, DISTINCT, DISTRIBUTE, + DIV, DO, DOUBLE, DOW, diff --git a/src/parser.rs b/src/parser.rs index 45639eefc..af577ce5d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1982,6 +1982,8 @@ impl<'a> Parser<'a> { const AND_PREC: u8 = 10; const OR_PREC: u8 = 5; + const DIV_OP_PREC: u8 = 40; + /// Get the precedence of the next token pub fn get_next_precedence(&self) -> Result { // allow the dialect to override precedence logic @@ -2031,6 +2033,7 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(Self::BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::DIV => Ok(Self::DIV_OP_PREC), Token::Eq | Token::Lt | Token::LtEq diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 1c479bb18..c70f2cb95 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1407,3 +1407,8 @@ fn parse_string_introducers() { mysql().one_statement_parses_to("SELECT _utf8mb4'abc'", "SELECT _utf8mb4 'abc'"); mysql().verified_stmt("SELECT _binary 'abc', _utf8mb4 'abc'"); } + +#[test] +fn parse_div_infix() { + mysql().verified_stmt(r#"SELECT 5 DIV 2"#); +} From 3be19c7666d94070d28f3007f3fd4d54410a9bb2 Mon Sep 17 00:00:00 2001 From: Maciej Obuchowski Date: Thu, 18 May 2023 20:55:02 +0200 Subject: [PATCH 200/806] truncate: table as optional keyword (#883) Signed-off-by: Maciej Obuchowski --- src/ast/mod.rs | 6 +++++- src/parser.rs | 3 ++- tests/sqlparser_postgres.rs | 13 +++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e26cb0dfc..e7006859e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1142,6 +1142,8 @@ pub enum Statement { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, partitions: Option>, + /// TABLE - optional keyword; + table: bool, }, /// Msck (Hive) Msck { @@ -1844,8 +1846,10 @@ impl fmt::Display for Statement { Statement::Truncate { table_name, partitions, + table, } => { - write!(f, "TRUNCATE TABLE {table_name}")?; + let table = if *table { "TABLE " } else { "" }; + write!(f, "TRUNCATE {table}{table_name}")?; if let Some(ref parts) = partitions { if !parts.is_empty() { write!(f, " PARTITION ({})", display_comma_separated(parts))?; diff --git a/src/parser.rs b/src/parser.rs index af577ce5d..74dfea725 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -473,7 +473,7 @@ impl<'a> Parser<'a> { } pub fn parse_truncate(&mut self) -> Result { - self.expect_keyword(Keyword::TABLE)?; + let table = self.parse_keyword(Keyword::TABLE); let table_name = self.parse_object_name()?; let mut partitions = None; if self.parse_keyword(Keyword::PARTITION) { @@ -484,6 +484,7 @@ impl<'a> Parser<'a> { Ok(Statement::Truncate { table_name, partitions, + table, }) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9de8e0eb4..5abf17233 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2950,3 +2950,16 @@ fn parse_select_group_by_cube() { select.group_by ); } + +#[test] +fn parse_truncate() { + let truncate = pg_and_generic().verified_stmt("TRUNCATE db.table_name"); + assert_eq!( + Statement::Truncate { + table_name: ObjectName(vec![Ident::new("db"), Ident::new("table_name")]), + partitions: None, + table: false + }, + truncate + ); +} From 33b12acce7a44f216ccf85ba81d2b28eb89e691f Mon Sep 17 00:00:00 2001 From: eitsupi <50911393+eitsupi@users.noreply.github.com> Date: Fri, 19 May 2023 03:57:29 +0900 Subject: [PATCH 201/806] feat: add DuckDB dialect (#878) * feat: add DuckDB dialect * formatting * fix conflict * support // in GenericDialect * add DucDbDialect to all_dialects * add comment from suggestion Co-authored-by: Andrew Lamb * fix: support // in GenericDialect --------- Co-authored-by: Andrew Lamb --- examples/cli.rs | 1 + src/ast/operator.rs | 3 ++ src/dialect/duckdb.rs | 31 +++++++++++++++++ src/dialect/mod.rs | 5 +++ src/parser.rs | 20 +++++++---- src/test_utils.rs | 1 + src/tokenizer.rs | 8 ++++- tests/sqlparser_common.rs | 11 ++++-- tests/sqlparser_duckdb.rs | 70 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 src/dialect/duckdb.rs create mode 100644 tests/sqlparser_duckdb.rs diff --git a/examples/cli.rs b/examples/cli.rs index a320a00bc..8af6246a0 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -46,6 +46,7 @@ $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] "--hive" => Box::new(HiveDialect {}), "--redshift" => Box::new(RedshiftSqlDialect {}), "--clickhouse" => Box::new(ClickHouseDialect {}), + "--duckdb" => Box::new(DuckDbDialect {}), "--generic" | "" => Box::new(GenericDialect {}), s => panic!("Unexpected parameter: {s}"), }; diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 0ab9c66a4..b988265ba 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -85,6 +85,8 @@ pub enum BinaryOperator { BitwiseOr, BitwiseAnd, BitwiseXor, + /// Integer division operator `//` in DuckDB + DuckIntegerDivide, /// MySQL [`DIV`](https://dev.mysql.com/doc/refman/8.0/en/arithmetic-functions.html) integer division MyIntegerDivide, /// Support for custom operators (built by parsers outside this crate) @@ -126,6 +128,7 @@ impl fmt::Display for BinaryOperator { BinaryOperator::BitwiseOr => f.write_str("|"), BinaryOperator::BitwiseAnd => f.write_str("&"), BinaryOperator::BitwiseXor => f.write_str("^"), + BinaryOperator::DuckIntegerDivide => f.write_str("//"), BinaryOperator::MyIntegerDivide => f.write_str("DIV"), BinaryOperator::Custom(s) => f.write_str(s), BinaryOperator::PGBitwiseXor => f.write_str("#"), diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs new file mode 100644 index 000000000..55f258e53 --- /dev/null +++ b/src/dialect/duckdb.rs @@ -0,0 +1,31 @@ +// Licensed 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 crate::dialect::Dialect; + +#[derive(Debug, Default)] +pub struct DuckDbDialect; + +// In most cases the redshift dialect is identical to [`PostgresSqlDialect`]. +impl Dialect for DuckDbDialect { + fn is_identifier_start(&self, ch: char) -> bool { + ch.is_alphabetic() || ch == '_' + } + + fn is_identifier_part(&self, ch: char) -> bool { + ch.is_alphabetic() || ch.is_ascii_digit() || ch == '$' || ch == '_' + } + + fn supports_filter_during_aggregation(&self) -> bool { + true + } +} diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 5744ae65e..48357501c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -13,6 +13,7 @@ mod ansi; mod bigquery; mod clickhouse; +mod duckdb; mod generic; mod hive; mod mssql; @@ -31,6 +32,7 @@ use core::str::Chars; pub use self::ansi::AnsiDialect; pub use self::bigquery::BigQueryDialect; pub use self::clickhouse::ClickHouseDialect; +pub use self::duckdb::DuckDbDialect; pub use self::generic::GenericDialect; pub use self::hive::HiveDialect; pub use self::mssql::MsSqlDialect; @@ -163,6 +165,7 @@ pub fn dialect_from_str(dialect_name: impl AsRef) -> Option Some(Box::new(ClickHouseDialect {})), "bigquery" => Some(Box::new(BigQueryDialect)), "ansi" => Some(Box::new(AnsiDialect {})), + "duckdb" => Some(Box::new(DuckDbDialect {})), _ => None, } } @@ -214,6 +217,8 @@ mod tests { assert!(parse_dialect("BigQuery").is::()); assert!(parse_dialect("ansi").is::()); assert!(parse_dialect("ANSI").is::()); + assert!(parse_dialect("duckdb").is::()); + assert!(parse_dialect("DuckDb").is::()); // error cases assert!(dialect_from_str("Unknown").is_none()); diff --git a/src/parser.rs b/src/parser.rs index 74dfea725..610ac745b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -992,7 +992,7 @@ impl<'a> Parser<'a> { /// parse a group by expr. a group by expr can be one of group sets, roll up, cube, or simple /// expr. fn parse_group_by_expr(&mut self) -> Result { - if dialect_of!(self is PostgreSqlDialect | GenericDialect) { + if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) { if self.parse_keywords(&[Keyword::GROUPING, Keyword::SETS]) { self.expect_token(&Token::LParen)?; let result = self.parse_comma_separated(|p| p.parse_tuple(false, true))?; @@ -1662,10 +1662,13 @@ impl<'a> Parser<'a> { } Token::Ampersand => Some(BinaryOperator::BitwiseAnd), Token::Div => Some(BinaryOperator::Divide), - Token::ShiftLeft if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { + Token::DuckIntDiv if dialect_of!(self is DuckDbDialect | GenericDialect) => { + Some(BinaryOperator::DuckIntegerDivide) + } + Token::ShiftLeft if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { Some(BinaryOperator::PGBitwiseShiftLeft) } - Token::ShiftRight if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { + Token::ShiftRight if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { Some(BinaryOperator::PGBitwiseShiftRight) } Token::Sharp if dialect_of!(self is PostgreSqlDialect) => { @@ -2051,7 +2054,9 @@ impl<'a> Parser<'a> { Token::Caret | Token::Sharp | Token::ShiftRight | Token::ShiftLeft => Ok(22), Token::Ampersand => Ok(23), Token::Plus | Token::Minus => Ok(Self::PLUS_MINUS_PREC), - Token::Mul | Token::Div | Token::Mod | Token::StringConcat => Ok(40), + Token::Mul | Token::Div | Token::DuckIntDiv | Token::Mod | Token::StringConcat => { + Ok(40) + } Token::DoubleColon => Ok(50), Token::Colon => Ok(50), Token::ExclamationMark => Ok(50), @@ -3842,7 +3847,7 @@ impl<'a> Parser<'a> { } else { let column_keyword = self.parse_keyword(Keyword::COLUMN); - let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | GenericDialect) + let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | DuckDbDialect | GenericDialect) { self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) || if_not_exists @@ -6315,7 +6320,7 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::SET)?; let assignments = self.parse_comma_separated(Parser::parse_assignment)?; let from = if self.parse_keyword(Keyword::FROM) - && dialect_of!(self is GenericDialect | PostgreSqlDialect | BigQueryDialect | SnowflakeDialect | RedshiftSqlDialect | MsSqlDialect) + && dialect_of!(self is GenericDialect | PostgreSqlDialect | DuckDbDialect | BigQueryDialect | SnowflakeDialect | RedshiftSqlDialect | MsSqlDialect) { Some(self.parse_table_and_joins()?) } else { @@ -6415,7 +6420,8 @@ impl<'a> Parser<'a> { pub fn parse_wildcard_additional_options( &mut self, ) -> Result { - let opt_exclude = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + let opt_exclude = if dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) + { self.parse_optional_select_item_exclude()? } else { None diff --git a/src/test_utils.rs b/src/test_utils.rs index d01bbbab9..57b21e1c9 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -168,6 +168,7 @@ pub fn all_dialects() -> TestedDialects { Box::new(MySqlDialect {}), Box::new(BigQueryDialect {}), Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), ], options: None, } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index a550c4f5d..ffa1a96f2 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -35,7 +35,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::DollarQuotedString; -use crate::dialect::{BigQueryDialect, GenericDialect, SnowflakeDialect}; +use crate::dialect::{BigQueryDialect, DuckDbDialect, GenericDialect, SnowflakeDialect}; use crate::dialect::{Dialect, MySqlDialect}; use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX}; @@ -98,6 +98,8 @@ pub enum Token { Mul, /// Division operator `/` Div, + /// Integer division operator `//` in DuckDB + DuckIntDiv, /// Modulo Operator `%` Mod, /// String concatenation `||` @@ -212,6 +214,7 @@ impl fmt::Display for Token { Token::Minus => f.write_str("-"), Token::Mul => f.write_str("*"), Token::Div => f.write_str("/"), + Token::DuckIntDiv => f.write_str("//"), Token::StringConcat => f.write_str("||"), Token::Mod => f.write_str("%"), Token::LParen => f.write_str("("), @@ -768,6 +771,9 @@ impl<'a> Tokenizer<'a> { comment, }))) } + Some('/') if dialect_of!(self is DuckDbDialect | GenericDialect) => { + self.consume_and_return(chars, Token::DuckIntDiv) + } // a regular '/' operator _ => Ok(Some(Token::Div)), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ac5c18c0e..448100eb8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -24,8 +24,9 @@ use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Pivot; use sqlparser::ast::*; use sqlparser::dialect::{ - AnsiDialect, BigQueryDialect, ClickHouseDialect, GenericDialect, HiveDialect, MsSqlDialect, - MySqlDialect, PostgreSqlDialect, RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, + AnsiDialect, BigQueryDialect, ClickHouseDialect, DuckDbDialect, GenericDialect, HiveDialect, + MsSqlDialect, MySqlDialect, PostgreSqlDialect, RedshiftSqlDialect, SQLiteDialect, + SnowflakeDialect, }; use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError, ParserOptions}; @@ -195,6 +196,7 @@ fn parse_update_set_from() { let dialects = TestedDialects { dialects: vec![ Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), Box::new(PostgreSqlDialect {}), Box::new(BigQueryDialect {}), Box::new(SnowflakeDialect {}), @@ -941,6 +943,7 @@ fn parse_exponent_in_select() -> Result<(), ParserError> { Box::new(AnsiDialect {}), Box::new(BigQueryDialect {}), Box::new(ClickHouseDialect {}), + Box::new(DuckDbDialect {}), Box::new(GenericDialect {}), // Box::new(HiveDialect {}), Box::new(MsSqlDialect {}), @@ -2053,6 +2056,7 @@ fn parse_array_agg_func() { let supported_dialects = TestedDialects { dialects: vec![ Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), Box::new(PostgreSqlDialect {}), Box::new(MsSqlDialect {}), Box::new(AnsiDialect {}), @@ -2848,6 +2852,7 @@ fn parse_alter_table_add_column_if_not_exists() { Box::new(PostgreSqlDialect {}), Box::new(BigQueryDialect {}), Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), ], options: None, }; @@ -6139,6 +6144,7 @@ fn test_placeholder() { let dialects = TestedDialects { dialects: vec![ Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), Box::new(PostgreSqlDialect {}), Box::new(MsSqlDialect {}), Box::new(AnsiDialect {}), @@ -6873,6 +6879,7 @@ fn parse_non_latin_identifiers() { let supported_dialects = TestedDialects { dialects: vec![ Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), Box::new(PostgreSqlDialect {}), Box::new(MsSqlDialect {}), Box::new(RedshiftSqlDialect {}), diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs new file mode 100644 index 000000000..1a4f04c33 --- /dev/null +++ b/tests/sqlparser_duckdb.rs @@ -0,0 +1,70 @@ +// Licensed 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. + +#[macro_use] +mod test_utils; + +use test_utils::*; + +use sqlparser::ast::*; +use sqlparser::dialect::{DuckDbDialect, GenericDialect}; + +fn duckdb() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(DuckDbDialect {})], + options: None, + } +} + +fn duckdb_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(DuckDbDialect {}), Box::new(GenericDialect {})], + options: None, + } +} + +#[test] +fn test_select_wildcard_with_exclude() { + let select = duckdb().verified_only_select("SELECT * EXCLUDE (col_a) FROM data"); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(ExcludeSelectItem::Multiple(vec![Ident::new("col_a")])), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); + + let select = + duckdb().verified_only_select("SELECT name.* EXCLUDE department_id FROM employee_table"); + let expected = SelectItem::QualifiedWildcard( + ObjectName(vec![Ident::new("name")]), + WildcardAdditionalOptions { + opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), + ..Default::default() + }, + ); + assert_eq!(expected, select.projection[0]); + + let select = duckdb() + .verified_only_select("SELECT * EXCLUDE (department_id, employee_id) FROM employee_table"); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: Some(ExcludeSelectItem::Multiple(vec![ + Ident::new("department_id"), + Ident::new("employee_id"), + ])), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); +} + +#[test] +fn parse_div_infix() { + duckdb_and_generic().verified_stmt(r#"SELECT 5 // 2"#); +} From 1b86abebe20c382c166d13d464f4e60979741e46 Mon Sep 17 00:00:00 2001 From: Mustafa Akur <106137913+mustafasrepo@users.noreply.github.com> Date: Thu, 18 May 2023 21:59:14 +0300 Subject: [PATCH 202/806] Add support for first, last aggregate function parsing (#882) * Add order by parsing to functions * Fix doc error * minor changes --- src/ast/mod.rs | 10 ++++++++- src/ast/visitor.rs | 2 +- src/parser.rs | 28 ++++++++++++++++++++---- tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_clickhouse.rs | 4 ++++ tests/sqlparser_common.rs | 40 +++++++++++++++++++++++++++++++++++ tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 1 + tests/sqlparser_mysql.rs | 6 ++++++ tests/sqlparser_postgres.rs | 8 ++++++- tests/sqlparser_redshift.rs | 1 + tests/sqlparser_snowflake.rs | 1 + 12 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e7006859e..14cf04f5c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3366,6 +3366,8 @@ pub struct Function { // Some functions must be called without trailing parentheses, for example Postgres // do it for current_catalog, current_schema, etc. This flags is used for formatting. pub special: bool, + // Required ordering for the function (if empty, there is no requirement). + pub order_by: Vec, } #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -3392,12 +3394,18 @@ impl fmt::Display for Function { if self.special { write!(f, "{}", self.name)?; } else { + let order_by = if !self.order_by.is_empty() { + " ORDER BY " + } else { + "" + }; write!( f, - "{}({}{})", + "{}({}{}{order_by}{})", self.name, if self.distinct { "DISTINCT " } else { "" }, display_comma_separated(&self.args), + display_comma_separated(&self.order_by), )?; if let Some(o) = &self.over { diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 6787bfd68..81343220a 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -480,7 +480,7 @@ where /// *expr = Expr::Function(Function { /// name: ObjectName(vec![Ident::new("f")]), /// args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(old_expr))], -/// over: None, distinct: false, special: false, +/// over: None, distinct: false, special: false, order_by: vec![], /// }); /// } /// ControlFlow::<()>::Continue(()) diff --git a/src/parser.rs b/src/parser.rs index 610ac745b..9e593eb8a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -706,6 +706,7 @@ impl<'a> Parser<'a> { over: None, distinct: false, special: true, + order_by: vec![], })) } Keyword::CURRENT_TIMESTAMP @@ -881,7 +882,7 @@ impl<'a> Parser<'a> { pub fn parse_function(&mut self, name: ObjectName) -> Result { self.expect_token(&Token::LParen)?; let distinct = self.parse_all_or_distinct()?.is_some(); - let args = self.parse_optional_args()?; + let (args, order_by) = self.parse_optional_args_with_orderby()?; let over = if self.parse_keyword(Keyword::OVER) { // TBD: support window names (`OVER mywin`) in place of inline specification self.expect_token(&Token::LParen)?; @@ -918,14 +919,15 @@ impl<'a> Parser<'a> { over, distinct, special: false, + order_by, })) } pub fn parse_time_functions(&mut self, name: ObjectName) -> Result { - let args = if self.consume_token(&Token::LParen) { - self.parse_optional_args()? + let (args, order_by) = if self.consume_token(&Token::LParen) { + self.parse_optional_args_with_orderby()? } else { - vec![] + (vec![], vec![]) }; Ok(Expr::Function(Function { name, @@ -933,6 +935,7 @@ impl<'a> Parser<'a> { over: None, distinct: false, special: false, + order_by, })) } @@ -6376,6 +6379,23 @@ impl<'a> Parser<'a> { } } + pub fn parse_optional_args_with_orderby( + &mut self, + ) -> Result<(Vec, Vec), ParserError> { + if self.consume_token(&Token::RParen) { + Ok((vec![], vec![])) + } else { + let args = self.parse_comma_separated(Parser::parse_function_args)?; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_order_by_expr)? + } else { + vec![] + }; + self.expect_token(&Token::RParen)?; + Ok((args, order_by)) + } + } + /// Parse a comma-delimited list of projections after SELECT pub fn parse_select_item(&mut self) -> Result { match self.parse_wildcard_expr()? { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 0850bd4bf..11998ae6d 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -437,6 +437,7 @@ fn parse_map_access_offset() { over: None, distinct: false, special: false, + order_by: vec![], })], }) ); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index bb0ec48fa..23a399608 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -52,6 +52,7 @@ fn parse_map_access_expr() { over: None, distinct: false, special: false, + order_by: vec![], })], })], into: None, @@ -88,6 +89,7 @@ fn parse_map_access_expr() { over: None, distinct: false, special: false, + order_by: vec![], })] }), op: BinaryOperator::NotEq, @@ -135,6 +137,7 @@ fn parse_array_fn() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(only(&select.projection)) ); @@ -189,6 +192,7 @@ fn parse_delimited_identifiers() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[1]), ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 448100eb8..fadefcf9d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -840,6 +840,7 @@ fn parse_select_count_wildcard() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(only(&select.projection)) ); @@ -859,6 +860,7 @@ fn parse_select_count_distinct() { over: None, distinct: true, special: false, + order_by: vec![], }), expr_from_projection(only(&select.projection)) ); @@ -1696,6 +1698,7 @@ fn parse_select_having() { over: None, distinct: false, special: false, + order_by: vec![], })), op: BinaryOperator::Gt, right: Box::new(Expr::Value(number("1"))), @@ -1729,6 +1732,7 @@ fn parse_select_qualify() { }), distinct: false, special: false, + order_by: vec![], })), op: BinaryOperator::Eq, right: Box::new(Expr::Value(number("1"))), @@ -2076,6 +2080,29 @@ fn parse_array_agg_func() { } } +#[test] +fn parse_agg_with_order_by() { + let supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(HiveDialect {}), + ], + options: None, + }; + + for sql in [ + "SELECT FIRST_VALUE(x ORDER BY x) AS a FROM T", + "SELECT FIRST_VALUE(x ORDER BY x) FROM tbl", + "SELECT LAST_VALUE(x ORDER BY x, y) AS a FROM T", + "SELECT LAST_VALUE(x ORDER BY x ASC, y DESC) AS a FROM T", + ] { + supported_dialects.verified_stmt(sql); + } +} + #[test] fn parse_create_table() { let sql = "CREATE TABLE uk_cities (\ @@ -3121,6 +3148,7 @@ fn parse_scalar_function_in_projection() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(only(&select.projection)) ); @@ -3239,6 +3267,7 @@ fn parse_named_argument_function() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(only(&select.projection)) ); @@ -3277,6 +3306,7 @@ fn parse_window_functions() { }), distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[0]) ); @@ -3675,6 +3705,7 @@ fn parse_at_timezone() { over: None, distinct: false, special: false, + order_by: vec![], })), time_zone: "UTC-06:00".to_string(), }, @@ -3701,6 +3732,7 @@ fn parse_at_timezone() { over: None, distinct: false, special: false, + order_by: vec![], },)), time_zone: "UTC-06:00".to_string(), },),), @@ -3711,6 +3743,7 @@ fn parse_at_timezone() { over: None, distinct: false, special: false, + order_by: vec![], },), alias: Ident { value: "hour".to_string(), @@ -3868,6 +3901,7 @@ fn parse_table_function() { over: None, distinct: false, special: false, + order_by: vec![], }); assert_eq!(expr, expected_expr); assert_eq!(alias, table_alias("a")) @@ -6292,6 +6326,7 @@ fn parse_time_functions() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[0]) ); @@ -6308,6 +6343,7 @@ fn parse_time_functions() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[0]) ); @@ -6324,6 +6360,7 @@ fn parse_time_functions() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[0]) ); @@ -6340,6 +6377,7 @@ fn parse_time_functions() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[0]) ); @@ -6356,6 +6394,7 @@ fn parse_time_functions() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[0]) ); @@ -6820,6 +6859,7 @@ fn parse_pivot_table() { over: None, distinct: false, special: false, + order_by: vec![], }), value_column: vec![Ident::new("a"), Ident::new("MONTH")], pivot_values: vec![ diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 87c2660a8..ddc5a8ccf 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -346,6 +346,7 @@ fn parse_delimited_identifiers() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[1]), ); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 1f708b5e7..53db33230 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -177,6 +177,7 @@ fn parse_delimited_identifiers() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[1]), ); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index c70f2cb95..ba6f2b5f7 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -791,6 +791,7 @@ fn parse_insert_with_on_duplicate_update() { over: None, distinct: false, special: false, + order_by: vec![], }) }, Assignment { @@ -803,6 +804,7 @@ fn parse_insert_with_on_duplicate_update() { over: None, distinct: false, special: false, + order_by: vec![], }) }, Assignment { @@ -815,6 +817,7 @@ fn parse_insert_with_on_duplicate_update() { over: None, distinct: false, special: false, + order_by: vec![], }) }, Assignment { @@ -827,6 +830,7 @@ fn parse_insert_with_on_duplicate_update() { over: None, distinct: false, special: false, + order_by: vec![], }) }, Assignment { @@ -839,6 +843,7 @@ fn parse_insert_with_on_duplicate_update() { over: None, distinct: false, special: false, + order_by: vec![], }) }, ])), @@ -1182,6 +1187,7 @@ fn parse_table_colum_option_on_update() { over: None, distinct: false, special: false, + order_by: vec![], })), },], }], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5abf17233..94e5d23bf 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2057,7 +2057,8 @@ fn test_composite_value() { )))], over: None, distinct: false, - special: false + special: false, + order_by: vec![], })))) }), select.projection[0] @@ -2219,6 +2220,7 @@ fn parse_current_functions() { over: None, distinct: false, special: true, + order_by: vec![], }), expr_from_projection(&select.projection[0]) ); @@ -2229,6 +2231,7 @@ fn parse_current_functions() { over: None, distinct: false, special: true, + order_by: vec![], }), expr_from_projection(&select.projection[1]) ); @@ -2239,6 +2242,7 @@ fn parse_current_functions() { over: None, distinct: false, special: true, + order_by: vec![], }), expr_from_projection(&select.projection[2]) ); @@ -2249,6 +2253,7 @@ fn parse_current_functions() { over: None, distinct: false, special: true, + order_by: vec![], }), expr_from_projection(&select.projection[3]) ); @@ -2503,6 +2508,7 @@ fn parse_delimited_identifiers() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[1]), ); diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index d3a676939..c44f6dee4 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -132,6 +132,7 @@ fn parse_delimited_identifiers() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[1]), ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index d1a63dc86..9a54c89cf 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -249,6 +249,7 @@ fn parse_delimited_identifiers() { over: None, distinct: false, special: false, + order_by: vec![], }), expr_from_projection(&select.projection[1]), ); From ef46cd37528184e7fd7536f97e065b0d91610b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20=C5=9Eahin?= <124376117+berkaysynnada@users.noreply.github.com> Date: Thu, 18 May 2023 22:00:24 +0300 Subject: [PATCH 203/806] Named window frames (#881) * after over clause, named window can be parsed with window ... as after having clause * Lint errors are fixed * Support for multiple windows * fix lint errors * simplifications * rename function * Rewrite named window search in functional style * Test added and some minor changes * Minor changes on tests and namings, and semantic check is removed --------- Co-authored-by: Mustafa Akur Co-authored-by: Mehmet Ozan Kabak --- src/ast/mod.rs | 31 ++++++-- src/ast/query.rs | 16 ++++ src/keywords.rs | 1 + src/parser.rs | 70 +++++++++++------- tests/sqlparser_clickhouse.rs | 1 + tests/sqlparser_common.rs | 134 ++++++++++++++++++++++++++++++++-- tests/sqlparser_mysql.rs | 6 ++ tests/sqlparser_postgres.rs | 3 + 8 files changed, 225 insertions(+), 37 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 14cf04f5c..ee3366a28 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -17,7 +17,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use core::fmt; +use core::fmt::{self, Display}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -35,10 +35,10 @@ pub use self::ddl::{ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ Cte, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, IdentWithAlias, Join, - JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NonBlock, Offset, OffsetRows, - OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, - TableWithJoins, Top, Values, WildcardAdditionalOptions, With, + JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NamedWindowDefinition, + NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, + TableAlias, TableFactor, TableWithJoins, Top, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, @@ -930,6 +930,23 @@ impl fmt::Display for Expr { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum WindowType { + WindowSpec(WindowSpec), + NamedWindow(Ident), +} + +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), + } + } +} + /// A window specification (i.e. `OVER (PARTITION BY .. ORDER BY .. etc.)`) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -3360,7 +3377,7 @@ impl fmt::Display for CloseCursor { pub struct Function { pub name: ObjectName, pub args: Vec, - pub over: Option, + pub over: Option, // aggregate functions may specify eg `COUNT(DISTINCT x)` pub distinct: bool, // Some functions must be called without trailing parentheses, for example Postgres @@ -3409,7 +3426,7 @@ impl fmt::Display for Function { )?; if let Some(o) = &self.over { - write!(f, " OVER ({o})")?; + write!(f, " OVER {o}")?; } } diff --git a/src/ast/query.rs b/src/ast/query.rs index a85c62a25..a709e101f 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -216,6 +216,8 @@ pub struct Select { pub sort_by: Vec, /// HAVING pub having: Option, + /// WINDOW AS + pub named_window: Vec, /// QUALIFY (Snowflake) pub qualify: Option, } @@ -269,6 +271,9 @@ impl fmt::Display for Select { if let Some(ref having) = self.having { write!(f, " HAVING {having}")?; } + if !self.named_window.is_empty() { + write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?; + } if let Some(ref qualify) = self.qualify { write!(f, " QUALIFY {qualify}")?; } @@ -311,6 +316,17 @@ impl fmt::Display for LateralView { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct NamedWindowDefinition(pub Ident, pub WindowSpec); + +impl fmt::Display for NamedWindowDefinition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} AS ({})", self.0, self.1) + } +} + #[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/keywords.rs b/src/keywords.rs index 6132b31f0..e73b89a98 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -688,6 +688,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::OUTER, Keyword::SET, Keyword::QUALIFY, + Keyword::WINDOW, ]; /// Can't be used as a column alias, so that `SELECT alias` diff --git a/src/parser.rs b/src/parser.rs index 9e593eb8a..bab581452 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -884,32 +884,12 @@ impl<'a> Parser<'a> { let distinct = self.parse_all_or_distinct()?.is_some(); let (args, order_by) = self.parse_optional_args_with_orderby()?; let over = if self.parse_keyword(Keyword::OVER) { - // TBD: support window names (`OVER mywin`) in place of inline specification - self.expect_token(&Token::LParen)?; - let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { - // a list of possibly-qualified column names - self.parse_comma_separated(Parser::parse_expr)? - } else { - vec![] - }; - let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { - self.parse_comma_separated(Parser::parse_order_by_expr)? - } else { - vec![] - }; - let window_frame = if !self.consume_token(&Token::RParen) { - let window_frame = self.parse_window_frame()?; - self.expect_token(&Token::RParen)?; - Some(window_frame) + if self.consume_token(&Token::LParen) { + let window_spec = self.parse_window_spec()?; + Some(WindowType::WindowSpec(window_spec)) } else { - None - }; - - Some(WindowSpec { - partition_by, - order_by, - window_frame, - }) + Some(WindowType::NamedWindow(self.parse_identifier()?)) + } } else { None }; @@ -5367,6 +5347,12 @@ impl<'a> Parser<'a> { None }; + let named_windows = if self.parse_keyword(Keyword::WINDOW) { + self.parse_comma_separated(Parser::parse_named_window)? + } else { + vec![] + }; + let qualify = if self.parse_keyword(Keyword::QUALIFY) { Some(self.parse_expr()?) } else { @@ -5386,6 +5372,7 @@ impl<'a> Parser<'a> { distribute_by, sort_by, having, + named_window: named_windows, qualify, }) } @@ -7033,6 +7020,39 @@ impl<'a> Parser<'a> { pub fn index(&self) -> usize { self.index } + + pub fn parse_named_window(&mut self) -> Result { + let ident = self.parse_identifier()?; + self.expect_keyword(Keyword::AS)?; + self.expect_token(&Token::LParen)?; + let window_spec = self.parse_window_spec()?; + Ok(NamedWindowDefinition(ident, window_spec)) + } + + pub fn parse_window_spec(&mut self) -> Result { + let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_expr)? + } else { + vec![] + }; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_order_by_expr)? + } else { + vec![] + }; + let window_frame = if !self.consume_token(&Token::RParen) { + let window_frame = self.parse_window_frame()?; + self.expect_token(&Token::RParen)?; + Some(window_frame) + } else { + None + }; + Ok(WindowSpec { + partition_by, + order_by, + window_frame, + }) + } } impl Word { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 23a399608..24c641561 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -101,6 +101,7 @@ fn parse_map_access_expr() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }, select diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fadefcf9d..a5458cac2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -19,7 +19,6 @@ //! dialect-specific parsing rules). use matches::assert_matches; - use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Pivot; use sqlparser::ast::*; @@ -251,6 +250,7 @@ fn parse_update_set_from() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }))), order_by: vec![], @@ -1721,7 +1721,7 @@ fn parse_select_qualify() { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("ROW_NUMBER")]), args: vec![], - over: Some(WindowSpec { + over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![Expr::Identifier(Ident::new("p"))], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), @@ -1729,7 +1729,7 @@ fn parse_select_qualify() { nulls_first: None, }], window_frame: None, - }), + })), distinct: false, special: false, order_by: vec![], @@ -3295,7 +3295,7 @@ fn parse_window_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), args: vec![], - over: Some(WindowSpec { + over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("dt")), @@ -3303,7 +3303,7 @@ fn parse_window_functions() { nulls_first: None, }], window_frame: None, - }), + })), distinct: false, special: false, order_by: vec![], @@ -3312,6 +3312,128 @@ fn parse_window_functions() { ); } +#[test] +fn test_parse_named_window() { + let sql = "SELECT \ + MIN(c12) OVER window1 AS min1, \ + MAX(c12) OVER window2 AS max1 \ + FROM aggregate_test_100 \ + WINDOW window1 AS (ORDER BY C12), \ + window2 AS (PARTITION BY C11) \ + ORDER BY C3"; + let actual_select_only = verified_only_select(sql); + let expected = Select { + distinct: None, + top: None, + projection: vec![ + SelectItem::ExprWithAlias { + expr: Expr::Function(Function { + name: ObjectName(vec![Ident { + value: "MIN".to_string(), + quote_style: None, + }]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident { + value: "c12".to_string(), + quote_style: None, + }), + ))], + over: Some(WindowType::NamedWindow(Ident { + value: "window1".to_string(), + quote_style: None, + })), + distinct: false, + special: false, + }), + alias: Ident { + value: "min1".to_string(), + quote_style: None, + }, + }, + SelectItem::ExprWithAlias { + expr: Expr::Function(Function { + name: ObjectName(vec![Ident { + value: "MAX".to_string(), + quote_style: None, + }]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident { + value: "c12".to_string(), + quote_style: None, + }), + ))], + over: Some(WindowType::NamedWindow(Ident { + value: "window2".to_string(), + quote_style: None, + })), + distinct: false, + special: false, + }), + alias: Ident { + value: "max1".to_string(), + quote_style: None, + }, + }, + ], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "aggregate_test_100".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![ + NamedWindowDefinition( + Ident { + value: "window1".to_string(), + quote_style: None, + }, + WindowSpec { + partition_by: vec![], + order_by: vec![OrderByExpr { + expr: Expr::Identifier(Ident { + value: "C12".to_string(), + quote_style: None, + }), + asc: None, + nulls_first: None, + }], + window_frame: None, + }, + ), + NamedWindowDefinition( + Ident { + value: "window2".to_string(), + quote_style: None, + }, + WindowSpec { + partition_by: vec![Expr::Identifier(Ident { + value: "C11".to_string(), + quote_style: None, + })], + order_by: vec![], + window_frame: None, + }, + ), + ], + qualify: None, + }; + assert_eq!(actual_select_only, expected); +} + #[test] fn parse_aggregate_with_group_by() { let sql = "SELECT a, COUNT(1), MIN(b), MAX(b) FROM foo GROUP BY a"; @@ -3659,6 +3781,7 @@ fn parse_interval_and_or_xor() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))), order_by: vec![], @@ -5929,6 +6052,7 @@ fn parse_merge() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))), order_by: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ba6f2b5f7..3e5c810ef 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -460,6 +460,7 @@ fn parse_quote_identifiers_2() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }))), order_by: vec![], @@ -494,6 +495,7 @@ fn parse_quote_identifiers_3() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }))), order_by: vec![], @@ -884,6 +886,7 @@ fn parse_select_with_numeric_prefix_column_name() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))) ); @@ -927,6 +930,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))) ); @@ -1116,6 +1120,7 @@ fn parse_substring_in_select() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }))), order_by: vec![], @@ -1394,6 +1399,7 @@ fn parse_hex_string_introducer() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, into: None }))), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 94e5d23bf..80a4261ee 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -954,6 +954,7 @@ fn parse_copy_to() { selection: None, group_by: vec![], having: None, + named_window: vec![], cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1799,6 +1800,7 @@ fn parse_array_subquery_expr() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))), right: Box::new(SetExpr::Select(Box::new(Select { @@ -1820,6 +1822,7 @@ fn parse_array_subquery_expr() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))), }), From f740d528da33f90c986f0bd887becf27ffc51f96 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 18 May 2023 15:28:11 -0400 Subject: [PATCH 204/806] Fix merge conflict (#885) --- tests/sqlparser_common.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a5458cac2..a066126db 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3344,6 +3344,7 @@ fn test_parse_named_window() { })), distinct: false, special: false, + order_by: vec![], }), alias: Ident { value: "min1".to_string(), @@ -3368,6 +3369,7 @@ fn test_parse_named_window() { })), distinct: false, special: false, + order_by: vec![], }), alias: Ident { value: "max1".to_string(), From adfa37d565f180454be1e3ec35b515334f81c5ac Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 19 May 2023 09:45:23 -0400 Subject: [PATCH 205/806] Update CHANGELOG for `0.34.0` release (#884) --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afcd9ad2e..b4bd16e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,37 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.34.0] 2023-05-19 + +### Added + +* Support named window frames (#881) - Thanks @berkaysynnada, @mustafasrepo, and @ozankabak +* Support for `ORDER BY` clauses in aggregate functions (#882) - Thanks @mustafasrepo +* Support `DuckDB` dialect (#878) - Thanks @eitsupi +* Support optional `TABLE` keyword for `TRUNCATE TABLE` (#883) - Thanks @mobuchowski +* Support MySQL's `DIV` operator (#876) - Thanks @eitsupi +* Support Custom operators (#868) - Thanks @max-sixty +* Add `Parser::parse_multipart_identifier` (#860) - Thanks @Jefffrey +* Support for multiple expressions, order by in `ARRAY_AGG` (#879) - Thanks @mustafasrepo +* Support for query source in `COPY .. TO` statement (#858) - Thanks @aprimadi +* Support `DISTINCT ON (...)` (#852) - Thanks @aljazerzen +* Support multiple-table `DELETE` syntax (#855) - Thanks @AviRaboah +* Support `COPY INTO` in `SnowflakeDialect` (#841) - Thanks @pawel-big-lebowski +* Support identifiers beginning with digits in MySQL (#856) - Thanks @AviRaboah + +### Changed +* Include license file in published crate (#871) - Thanks @ankane +* Make `Expr::Interval` its own struct (#872) - Thanks @aprimadi +* Add dialect_from_str and improve Dialect documentation (#848) - Thanks @alamb +* Add clickhouse to example (#849) - Thanks @anglinb + +### Fixed +* Fix merge conflict (#885) - Thanks @alamb +* Fix tiny typo in custom_sql_parser.md (#864) - Thanks @okue +* Fix logical merge conflict (#865) - Thanks @alamb +* Test trailing commas (#859) - Thanks @aljazerzen + + ## [0.33.0] 2023-04-10 ### Added From 4607addf4d26a6e10c6b0732e0b1ef04d899d68e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 19 May 2023 09:47:43 -0400 Subject: [PATCH 206/806] chore: Release sqlparser version 0.34.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3b3af073f..4a4c8ce4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.33.0" +version = "0.34.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From e8cad6ab6574a64875efd4472f87d177d532f8ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 17:41:59 -0400 Subject: [PATCH 207/806] Update criterion requirement from 0.4 to 0.5 in /sqlparser_bench (#890) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sqlparser_bench/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlparser_bench/Cargo.toml b/sqlparser_bench/Cargo.toml index f5dc85e4a..f2cd93288 100644 --- a/sqlparser_bench/Cargo.toml +++ b/sqlparser_bench/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" sqlparser = { path = "../" } [dev-dependencies] -criterion = "0.4" +criterion = "0.5" [[bench]] name = "sqlparser_bench" From 2b37e4ae6ecbf2f22593b593ad1e4638ca76a8b2 Mon Sep 17 00:00:00 2001 From: Sam Rijs Date: Thu, 8 Jun 2023 12:56:39 +0200 Subject: [PATCH 208/806] Add support for CREATE TYPE (AS) statements (#888) Co-authored-by: Andrew Lamb --- src/ast/ddl.rs | 40 +++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 12 +++++++++++ src/parser.rs | 42 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 26 ++++++++++++++++++++++++ 4 files changed, 120 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index b515c261e..3e428e21a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -715,3 +715,43 @@ impl fmt::Display for ReferentialAction { }) } } + +/// SQL user defined type definition +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum UserDefinedTypeRepresentation { + Composite { + attributes: Vec, + }, +} + +impl fmt::Display for UserDefinedTypeRepresentation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + UserDefinedTypeRepresentation::Composite { attributes } => { + write!(f, "({})", display_comma_separated(attributes)) + } + } + } +} + +/// SQL user defined type attribute definition +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct UserDefinedTypeCompositeAttributeDef { + pub name: Ident, + pub data_type: DataType, + pub collation: Option, +} + +impl fmt::Display for UserDefinedTypeCompositeAttributeDef { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.name, self.data_type)?; + if let Some(collation) = &self.collation { + write!(f, " COLLATE {collation}")?; + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ee3366a28..7f3b4742f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -31,6 +31,7 @@ pub use self::data_type::{ pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, ReferentialAction, TableConstraint, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ @@ -1711,6 +1712,11 @@ pub enum Statement { sequence_options: Vec, owned_by: Option, }, + /// CREATE TYPE `` + CreateType { + name: ObjectName, + representation: UserDefinedTypeRepresentation, + }, } impl fmt::Display for Statement { @@ -2921,6 +2927,12 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateType { + name, + representation, + } => { + write!(f, "CREATE TYPE {name} AS {representation}") + } } } } diff --git a/src/parser.rs b/src/parser.rs index bab581452..b89077fb5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2365,6 +2365,8 @@ impl<'a> Parser<'a> { self.parse_create_role() } else if self.parse_keyword(Keyword::SEQUENCE) { self.parse_create_sequence(temporary) + } else if self.parse_keyword(Keyword::TYPE) { + self.parse_create_type() } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -7053,6 +7055,46 @@ impl<'a> Parser<'a> { window_frame, }) } + + pub fn parse_create_type(&mut self) -> Result { + let name = self.parse_object_name()?; + self.expect_keyword(Keyword::AS)?; + + let mut attributes = vec![]; + if !self.consume_token(&Token::LParen) || self.consume_token(&Token::RParen) { + return Ok(Statement::CreateType { + name, + representation: UserDefinedTypeRepresentation::Composite { attributes }, + }); + } + + loop { + let attr_name = self.parse_identifier()?; + let attr_data_type = self.parse_data_type()?; + let attr_collation = if self.parse_keyword(Keyword::COLLATE) { + Some(self.parse_object_name()?) + } else { + None + }; + attributes.push(UserDefinedTypeCompositeAttributeDef { + name: attr_name, + data_type: attr_data_type, + collation: attr_collation, + }); + let comma = self.consume_token(&Token::Comma); + if self.consume_token(&Token::RParen) { + // allow a trailing comma + break; + } else if !comma { + return self.expected("',' or ')' after attribute definition", self.peek_token()); + } + } + + Ok(Statement::CreateType { + name, + representation: UserDefinedTypeRepresentation::Composite { attributes }, + }) + } } impl Word { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a066126db..ad3507058 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7092,3 +7092,29 @@ fn parse_trailing_comma() { trailing_commas.verified_stmt("SELECT DISTINCT ON (album_id) name FROM track"); } + +#[test] +fn parse_create_type() { + let create_type = + verified_stmt("CREATE TYPE db.type_name AS (foo INT, bar TEXT COLLATE \"de_DE\")"); + assert_eq!( + Statement::CreateType { + name: ObjectName(vec![Ident::new("db"), Ident::new("type_name")]), + representation: UserDefinedTypeRepresentation::Composite { + attributes: vec![ + UserDefinedTypeCompositeAttributeDef { + name: Ident::new("foo"), + data_type: DataType::Int(None), + collation: None, + }, + UserDefinedTypeCompositeAttributeDef { + name: Ident::new("bar"), + data_type: DataType::Text, + collation: Some(ObjectName(vec![Ident::with_quote('\"', "de_DE")])), + } + ] + } + }, + create_type + ); +} From 2296de2bc432282a814d083f67dcb6503dd4ecc0 Mon Sep 17 00:00:00 2001 From: Jeremy Dyer Date: Thu, 15 Jun 2023 09:10:56 -0400 Subject: [PATCH 209/806] Add fn support_group_by_expr to Dialect trait (#896) --- src/dialect/duckdb.rs | 4 ++++ src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 4 ++++ src/dialect/postgresql.rs | 4 ++++ src/parser.rs | 2 +- 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 55f258e53..4e6e9d9a4 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -28,4 +28,8 @@ impl Dialect for DuckDbDialect { fn supports_filter_during_aggregation(&self) -> bool { true } + + fn supports_group_by_expr(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8d6ccb5e6..8310954cd 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -28,4 +28,8 @@ impl Dialect for GenericDialect { || ch == '#' || ch == '_' } + + fn supports_group_by_expr(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 48357501c..8b3a58888 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -113,6 +113,10 @@ pub trait Dialect: Debug + Any { fn supports_within_after_array_aggregation(&self) -> bool { false } + /// Returns true if the dialects supports `group sets, roll up, or cube` expressions. + fn supports_group_by_expr(&self) -> bool { + false + } /// Dialect-specific prefix parser override fn parse_prefix(&self, _parser: &mut Parser) -> Option> { // return None to fall back to the default behavior diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 4ba6229eb..d131ff9c6 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -42,6 +42,10 @@ impl Dialect for PostgreSqlDialect { fn supports_filter_during_aggregation(&self) -> bool { true } + + fn supports_group_by_expr(&self) -> bool { + true + } } pub fn parse_comment(parser: &mut Parser) -> Result { diff --git a/src/parser.rs b/src/parser.rs index b89077fb5..2ba95ad6f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -975,7 +975,7 @@ impl<'a> Parser<'a> { /// parse a group by expr. a group by expr can be one of group sets, roll up, cube, or simple /// expr. fn parse_group_by_expr(&mut self) -> Result { - if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) { + if self.dialect.supports_group_by_expr() { if self.parse_keywords(&[Keyword::GROUPING, Keyword::SETS]) { self.expect_token(&Token::LParen)?; let result = self.parse_comma_separated(|p| p.parse_tuple(false, true))?; From 75f18ecfda9e345e2940ea50d2c15e207baf0e29 Mon Sep 17 00:00:00 2001 From: dawg Date: Wed, 21 Jun 2023 21:12:58 +0200 Subject: [PATCH 210/806] Add support for DuckDB's CREATE MACRO statements (#897) --- src/ast/mod.rs | 85 +++++++++++++++++++++++++++++++++++++++ src/keywords.rs | 1 + src/parser.rs | 51 +++++++++++++++++++++++ src/tokenizer.rs | 4 ++ tests/sqlparser_duckdb.rs | 67 ++++++++++++++++++++++++++++++ 5 files changed, 208 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7f3b4742f..7afb576bc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1580,6 +1580,19 @@ pub enum Statement { params: CreateFunctionBody, }, /// ```sql + /// CREATE MACRO + /// ``` + /// + /// Supported variants: + /// 1. [DuckDB](https://duckdb.org/docs/sql/statements/create_macro) + CreateMacro { + or_replace: bool, + temporary: bool, + name: ObjectName, + args: Option>, + definition: MacroDefinition, + }, + /// ```sql /// CREATE STAGE /// ``` /// See @@ -2098,6 +2111,28 @@ impl fmt::Display for Statement { write!(f, "{params}")?; Ok(()) } + Statement::CreateMacro { + or_replace, + temporary, + name, + args, + definition, + } => { + write!( + f, + "CREATE {or_replace}{temp}MACRO {name}", + temp = if *temporary { "TEMPORARY " } else { "" }, + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + )?; + if let Some(args) = args { + write!(f, "({})", display_comma_separated(args))?; + } + match definition { + MacroDefinition::Expr(expr) => write!(f, " AS {expr}")?, + MacroDefinition::Table(query) => write!(f, " AS TABLE {query}")?, + } + Ok(()) + } Statement::CreateView { name, or_replace, @@ -4304,6 +4339,56 @@ impl fmt::Display for CreateFunctionUsing { } } +/// `NAME = ` arguments for DuckDB macros +/// +/// See [Create Macro - DuckDB](https://duckdb.org/docs/sql/statements/create_macro) +/// for more details +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MacroArg { + pub name: Ident, + pub default_expr: Option, +} + +impl MacroArg { + /// Returns an argument with name. + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + default_expr: None, + } + } +} + +impl fmt::Display for MacroArg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name)?; + if let Some(default_expr) = &self.default_expr { + write!(f, " := {default_expr}")?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MacroDefinition { + Expr(Expr), + Table(Query), +} + +impl fmt::Display for MacroDefinition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MacroDefinition::Expr(expr) => write!(f, "{expr}")?, + MacroDefinition::Table(query) => write!(f, "{query}")?, + } + Ok(()) + } +} + /// Schema possible naming variants ([1]). /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#schema-definition diff --git a/src/keywords.rs b/src/keywords.rs index e73b89a98..663818c7e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -347,6 +347,7 @@ define_keywords!( LOCKED, LOGIN, LOWER, + MACRO, MANAGEDLOCATION, MATCH, MATCHED, diff --git a/src/parser.rs b/src/parser.rs index 2ba95ad6f..bdf9fda33 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2346,6 +2346,8 @@ impl<'a> Parser<'a> { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { self.parse_create_function(or_replace, temporary) + } else if self.parse_keyword(Keyword::MACRO) { + self.parse_create_macro(or_replace, temporary) } else if or_replace { self.expected( "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE", @@ -2624,6 +2626,8 @@ impl<'a> Parser<'a> { return_type, params, }) + } else if dialect_of!(self is DuckDbDialect) { + self.parse_create_macro(or_replace, temporary) } else { self.prev_token(); self.expected("an object type after CREATE", self.peek_token()) @@ -2699,6 +2703,53 @@ impl<'a> Parser<'a> { } } + pub fn parse_create_macro( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { + if dialect_of!(self is DuckDbDialect | GenericDialect) { + let name = self.parse_object_name()?; + self.expect_token(&Token::LParen)?; + let args = if self.consume_token(&Token::RParen) { + self.prev_token(); + None + } else { + Some(self.parse_comma_separated(Parser::parse_macro_arg)?) + }; + + self.expect_token(&Token::RParen)?; + self.expect_keyword(Keyword::AS)?; + + Ok(Statement::CreateMacro { + or_replace, + temporary, + name, + args, + definition: if self.parse_keyword(Keyword::TABLE) { + MacroDefinition::Table(self.parse_query()?) + } else { + MacroDefinition::Expr(self.parse_expr()?) + }, + }) + } else { + self.prev_token(); + self.expected("an object type after CREATE", self.peek_token()) + } + } + + fn parse_macro_arg(&mut self) -> Result { + let name = self.parse_identifier()?; + + let default_expr = + if self.consume_token(&Token::DuckAssignment) || self.consume_token(&Token::RArrow) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(MacroArg { name, default_expr }) + } + pub fn parse_create_external_table( &mut self, or_replace: bool, diff --git a/src/tokenizer.rs b/src/tokenizer.rs index ffa1a96f2..257a3517e 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -114,6 +114,8 @@ pub enum Token { Colon, /// DoubleColon `::` (used for casting in postgresql) DoubleColon, + /// Assignment `:=` (used for keyword argument in DuckDB macros) + DuckAssignment, /// SemiColon `;` used as separator for COPY and payload SemiColon, /// Backslash `\` used in terminating the COPY payload with `\.` @@ -222,6 +224,7 @@ impl fmt::Display for Token { Token::Period => f.write_str("."), Token::Colon => f.write_str(":"), Token::DoubleColon => f.write_str("::"), + Token::DuckAssignment => f.write_str(":="), Token::SemiColon => f.write_str(";"), Token::Backslash => f.write_str("\\"), Token::LBracket => f.write_str("["), @@ -847,6 +850,7 @@ impl<'a> Tokenizer<'a> { chars.next(); match chars.peek() { Some(':') => self.consume_and_return(chars, Token::DoubleColon), + Some('=') => self.consume_and_return(chars, Token::DuckAssignment), _ => Ok(Some(Token::Colon)), } } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 1a4f04c33..bb60235a6 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -68,3 +68,70 @@ fn test_select_wildcard_with_exclude() { fn parse_div_infix() { duckdb_and_generic().verified_stmt(r#"SELECT 5 // 2"#); } + +#[test] +fn test_create_macro() { + let macro_ = duckdb().verified_stmt("CREATE MACRO schema.add(a, b) AS a + b"); + let expected = Statement::CreateMacro { + or_replace: false, + temporary: false, + name: ObjectName(vec![Ident::new("schema"), Ident::new("add")]), + args: Some(vec![MacroArg::new("a"), MacroArg::new("b")]), + definition: MacroDefinition::Expr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }), + }; + assert_eq!(expected, macro_); +} + +#[test] +fn test_create_macro_default_args() { + let macro_ = duckdb().verified_stmt("CREATE MACRO add_default(a, b := 5) AS a + b"); + let expected = Statement::CreateMacro { + or_replace: false, + temporary: false, + name: ObjectName(vec![Ident::new("add_default")]), + args: Some(vec![ + MacroArg::new("a"), + MacroArg { + name: Ident::new("b"), + default_expr: Some(Expr::Value(Value::Number( + #[cfg(not(feature = "bigdecimal"))] + 5.to_string(), + #[cfg(feature = "bigdecimal")] + bigdecimal::BigDecimal::from(5), + false, + ))), + }, + ]), + definition: MacroDefinition::Expr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }), + }; + assert_eq!(expected, macro_); +} + +#[test] +fn test_create_table_macro() { + let query = "SELECT col1_value AS column1, col2_value AS column2 UNION ALL SELECT 'Hello' AS col1_value, 456 AS col2_value"; + let macro_ = duckdb().verified_stmt( + &("CREATE OR REPLACE TEMPORARY MACRO dynamic_table(col1_value, col2_value) AS TABLE " + .to_string() + + query), + ); + let expected = Statement::CreateMacro { + or_replace: true, + temporary: true, + name: ObjectName(vec![Ident::new("dynamic_table")]), + args: Some(vec![ + MacroArg::new("col1_value"), + MacroArg::new("col2_value"), + ]), + definition: MacroDefinition::Table(duckdb().verified_query(query)), + }; + assert_eq!(expected, macro_); +} From f72b5a5d9b9b6ae1650051f2d8fee91876279693 Mon Sep 17 00:00:00 2001 From: delsehi Date: Thu, 22 Jun 2023 17:09:14 +0200 Subject: [PATCH 211/806] Support basic CREATE PROCEDURE of MSSQL (#900) Co-authored-by: Andrew Lamb --- src/ast/ddl.rs | 13 +++++++ src/ast/mod.rs | 37 +++++++++++++++++- src/keywords.rs | 2 + src/parser.rs | 55 ++++++++++++++++++++++++++- tests/sqlparser_mssql.rs | 82 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 185 insertions(+), 4 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3e428e21a..a4640d557 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -489,6 +489,19 @@ impl fmt::Display for IndexType { } } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ProcedureParam { + pub name: Ident, + pub data_type: DataType, +} + +impl fmt::Display for ProcedureParam { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.name, self.data_type) + } +} /// SQL column definition #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7afb576bc..9c55c5d6d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -30,8 +30,8 @@ pub use self::data_type::{ }; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, - ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, ReferentialAction, TableConstraint, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, ProcedureParam, ReferentialAction, + TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ @@ -1580,6 +1580,15 @@ pub enum Statement { params: CreateFunctionBody, }, /// ```sql + /// CREATE PROCEDURE + /// ``` + CreateProcedure { + or_alter: bool, + name: ObjectName, + params: Option>, + body: Vec, + }, + /// ```sql /// CREATE MACRO /// ``` /// @@ -2111,6 +2120,30 @@ impl fmt::Display for Statement { write!(f, "{params}")?; Ok(()) } + Statement::CreateProcedure { + name, + or_alter, + params, + body, + } => { + write!( + f, + "CREATE {or_alter}PROCEDURE {name}", + or_alter = if *or_alter { "OR ALTER " } else { "" }, + name = name + )?; + + if let Some(p) = params { + if !p.is_empty() { + write!(f, " ({})", display_comma_separated(p))?; + } + } + write!( + f, + " AS BEGIN {body} END", + body = display_separated(body, "; ") + ) + } Statement::CreateMacro { or_replace, temporary, diff --git a/src/keywords.rs b/src/keywords.rs index 663818c7e..a76a9c953 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -690,6 +690,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::SET, Keyword::QUALIFY, Keyword::WINDOW, + Keyword::END, ]; /// Can't be used as a column alias, so that `SELECT alias` @@ -719,4 +720,5 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ // Reserved only as a column alias in the `SELECT` clause Keyword::FROM, Keyword::INTO, + Keyword::END, ]; diff --git a/src/parser.rs b/src/parser.rs index bdf9fda33..89e47b154 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -346,9 +346,14 @@ impl<'a> Parser<'a> { expecting_statement_delimiter = false; } - if self.peek_token() == Token::EOF { - break; + match self.peek_token().token { + Token::EOF => break, + + // end of statement + Token::Word(word) if word.keyword == Keyword::END => break, + _ => {} } + if expecting_statement_delimiter { return self.expected("end of statement", self.peek_token()); } @@ -2324,6 +2329,7 @@ impl<'a> Parser<'a> { /// Parse a SQL CREATE statement pub fn parse_create(&mut self) -> Result { let or_replace = self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); + let or_alter = self.parse_keywords(&[Keyword::OR, Keyword::ALTER]); let local = self.parse_one_of_keywords(&[Keyword::LOCAL]).is_some(); let global = self.parse_one_of_keywords(&[Keyword::GLOBAL]).is_some(); let transient = self.parse_one_of_keywords(&[Keyword::TRANSIENT]).is_some(); @@ -2369,6 +2375,8 @@ impl<'a> Parser<'a> { self.parse_create_sequence(temporary) } else if self.parse_keyword(Keyword::TYPE) { self.parse_create_type() + } else if self.parse_keyword(Keyword::PROCEDURE) { + self.parse_create_procedure(or_alter) } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -3503,6 +3511,28 @@ impl<'a> Parser<'a> { .build()) } + pub fn parse_optional_procedure_parameters( + &mut self, + ) -> Result>, ParserError> { + let mut params = vec![]; + if !self.consume_token(&Token::LParen) || self.consume_token(&Token::RParen) { + return Ok(Some(params)); + } + loop { + if let Token::Word(_) = self.peek_token().token { + params.push(self.parse_procedure_param()?) + } + let comma = self.consume_token(&Token::Comma); + if self.consume_token(&Token::RParen) { + // allow a trailing comma, even though it's not in standard + break; + } else if !comma { + return self.expected("',' or ')' after parameter definition", self.peek_token()); + } + } + Ok(Some(params)) + } + pub fn parse_columns(&mut self) -> Result<(Vec, Vec), ParserError> { let mut columns = vec![]; let mut constraints = vec![]; @@ -3530,6 +3560,12 @@ impl<'a> Parser<'a> { Ok((columns, constraints)) } + pub fn parse_procedure_param(&mut self) -> Result { + let name = self.parse_identifier()?; + let data_type = self.parse_data_type()?; + Ok(ProcedureParam { name, data_type }) + } + pub fn parse_column_def(&mut self) -> Result { let name = self.parse_identifier()?; let data_type = self.parse_data_type()?; @@ -7082,6 +7118,21 @@ impl<'a> Parser<'a> { Ok(NamedWindowDefinition(ident, window_spec)) } + pub fn parse_create_procedure(&mut self, or_alter: bool) -> Result { + let name = self.parse_object_name()?; + let params = self.parse_optional_procedure_parameters()?; + self.expect_keyword(Keyword::AS)?; + self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statements()?; + self.expect_keyword(Keyword::END)?; + Ok(Statement::CreateProcedure { + name, + or_alter, + params, + body: statements, + }) + } + pub fn parse_window_spec(&mut self) -> Result { let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { self.parse_comma_separated(Parser::parse_expr)? diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 53db33230..8fc3dcd59 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -55,6 +55,88 @@ 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"; + + #[cfg(feature = "bigdecimal")] + let one = Value::Number(bigdecimal::BigDecimal::from(1), false); + + #[cfg(not(feature = "bigdecimal"))] + let one = Value::Number("1".to_string(), false); + + assert_eq!( + ms().verified_stmt(sql), + Statement::CreateProcedure { + or_alter: true, + body: vec![Statement::Query(Box::new(Query { + with: None, + limit: None, + offset: None, + fetch: None, + locks: vec![], + order_by: vec![], + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::Value(one))], + into: None, + from: vec![], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None + }))) + }))], + params: Some(vec![ + ProcedureParam { + name: Ident { + value: "@foo".into(), + quote_style: None + }, + data_type: DataType::Int(None) + }, + ProcedureParam { + name: Ident { + value: "@bar".into(), + quote_style: None + }, + data_type: DataType::Varchar(Some(CharacterLength { + length: 256, + unit: None + })) + } + ]), + name: ObjectName(vec![Ident { + value: "test".into(), + quote_style: None + }]) + } + ) +} + +#[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().verified_stmt( + "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", + ); + 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"); + // Multiple statements + 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_mssql_apply_join() { let _ = ms_and_generic().verified_only_select( From 8877cbafa6d928dffd7677b45e3ff4a500c45f15 Mon Sep 17 00:00:00 2001 From: Igor Izvekov Date: Thu, 22 Jun 2023 18:15:31 +0300 Subject: [PATCH 212/806] fix: unary negation operator with operators: `Mul`, `Div` and `Mod` (#902) --- src/parser.rs | 9 ++++----- tests/sqlparser_common.rs | 22 +++++++++++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 89e47b154..a3f06047f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -799,7 +799,7 @@ impl<'a> Parser<'a> { }; Ok(Expr::UnaryOp { op, - expr: Box::new(self.parse_subexpr(Self::PLUS_MINUS_PREC)?), + expr: Box::new(self.parse_subexpr(Self::MUL_DIV_MOD_OP_PREC)?), }) } tok @ Token::DoubleExclamationMark @@ -1964,6 +1964,7 @@ impl<'a> Parser<'a> { } // use https://www.postgresql.org/docs/7.0/operators.htm#AEN2026 as a reference + const MUL_DIV_MOD_OP_PREC: u8 = 40; const PLUS_MINUS_PREC: u8 = 30; const XOR_PREC: u8 = 24; const TIME_ZONE_PREC: u8 = 20; @@ -1974,8 +1975,6 @@ impl<'a> Parser<'a> { const AND_PREC: u8 = 10; const OR_PREC: u8 = 5; - const DIV_OP_PREC: u8 = 40; - /// Get the precedence of the next token pub fn get_next_precedence(&self) -> Result { // allow the dialect to override precedence logic @@ -2025,7 +2024,7 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::DIV => Ok(Self::DIV_OP_PREC), + Token::Word(w) if w.keyword == Keyword::DIV => Ok(Self::MUL_DIV_MOD_OP_PREC), Token::Eq | Token::Lt | Token::LtEq @@ -2043,7 +2042,7 @@ impl<'a> Parser<'a> { Token::Ampersand => Ok(23), Token::Plus | Token::Minus => Ok(Self::PLUS_MINUS_PREC), Token::Mul | Token::Div | Token::DuckIntDiv | Token::Mod | Token::StringConcat => { - Ok(40) + Ok(Self::MUL_DIV_MOD_OP_PREC) } Token::DoubleColon => Ok(50), Token::Colon => Ok(50), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ad3507058..917a89d8d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1074,7 +1074,7 @@ fn parse_compound_expr_2() { } #[test] -fn parse_unary_math() { +fn parse_unary_math_with_plus() { use self::Expr::*; let sql = "-a + -b"; assert_eq!( @@ -1093,6 +1093,26 @@ fn parse_unary_math() { ); } +#[test] +fn parse_unary_math_with_multiply() { + use self::Expr::*; + let sql = "-a * -b"; + assert_eq!( + BinaryOp { + left: Box::new(UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Identifier(Ident::new("a"))), + }), + op: BinaryOperator::Multiply, + right: Box::new(UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Identifier(Ident::new("b"))), + }), + }, + verified_expr(sql) + ); +} + #[test] fn parse_is_null() { use self::Expr::*; From 04c9fbaead8773409e27ed89f9198bcbe97abd07 Mon Sep 17 00:00:00 2001 From: parkma99 <84610851+parkma99@users.noreply.github.com> Date: Fri, 23 Jun 2023 22:48:04 +0800 Subject: [PATCH 213/806] update parse STRICT tables (#903) Co-authored-by: Andrew Lamb --- src/ast/helpers/stmt_create_table.rs | 10 ++++++++++ src/ast/mod.rs | 9 ++++++++- src/keywords.rs | 1 + src/parser.rs | 2 ++ tests/sqlparser_sqlite.rs | 9 +++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index cb1ad45e7..2998935d9 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -70,6 +70,7 @@ pub struct CreateTableBuilder { pub on_commit: Option, pub on_cluster: Option, pub order_by: Option>, + pub strict: bool, } impl CreateTableBuilder { @@ -100,6 +101,7 @@ impl CreateTableBuilder { on_commit: None, on_cluster: None, order_by: None, + strict: false, } } pub fn or_replace(mut self, or_replace: bool) -> Self { @@ -220,6 +222,11 @@ impl CreateTableBuilder { self } + pub fn strict(mut self, strict: bool) -> Self { + self.strict = strict; + self + } + pub fn build(self) -> Statement { Statement::CreateTable { or_replace: self.or_replace, @@ -247,6 +254,7 @@ impl CreateTableBuilder { on_commit: self.on_commit, on_cluster: self.on_cluster, order_by: self.order_by, + strict: self.strict, } } } @@ -284,6 +292,7 @@ impl TryFrom for CreateTableBuilder { on_commit, on_cluster, order_by, + strict, } => Ok(Self { or_replace, temporary, @@ -310,6 +319,7 @@ impl TryFrom for CreateTableBuilder { on_commit, on_cluster, order_by, + strict, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9c55c5d6d..5652cce19 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1316,6 +1316,10 @@ pub enum Statement { /// than empty (represented as ()), the latter meaning "no sorting". /// order_by: 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. + strict: bool, }, /// SQLite's `CREATE VIRTUAL TABLE .. USING ()` CreateVirtualTable { @@ -2219,6 +2223,7 @@ impl fmt::Display for Statement { on_commit, on_cluster, order_by, + strict, } => { // We want to allow the following options // Empty column list, allowed by PostgreSQL: @@ -2387,7 +2392,9 @@ impl fmt::Display for Statement { }; write!(f, " {on_commit}")?; } - + if *strict { + write!(f, " STRICT")?; + } Ok(()) } Statement::CreateVirtualTable { diff --git a/src/keywords.rs b/src/keywords.rs index a76a9c953..463899804 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -551,6 +551,7 @@ define_keywords!( STDOUT, STORAGE_INTEGRATION, STORED, + STRICT, STRING, SUBMULTISET, SUBSTRING, diff --git a/src/parser.rs b/src/parser.rs index a3f06047f..0b1421463 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3485,6 +3485,7 @@ impl<'a> Parser<'a> { None }; + let strict = self.parse_keyword(Keyword::STRICT); Ok(CreateTableBuilder::new(table_name) .temporary(temporary) .columns(columns) @@ -3507,6 +3508,7 @@ impl<'a> Parser<'a> { .collation(collation) .on_commit(on_commit) .on_cluster(on_cluster) + .strict(strict) .build()) } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 31d2dd97f..8f6cc7572 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -242,6 +242,15 @@ fn parse_similar_to() { chk(true); } +#[test] +fn parse_create_table_with_strict() { + let sql = "CREATE TABLE Fruits (id TEXT NOT NULL PRIMARY KEY) STRICT"; + if let Statement::CreateTable { name, strict, .. } = sqlite().verified_stmt(sql) { + assert_eq!(name.to_string(), "Fruits"); + assert!(strict); + } +} + fn sqlite() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SQLiteDialect {})], From 631eddad78d102da21e7dfc690974137398e031f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 23 Jun 2023 10:53:18 -0400 Subject: [PATCH 214/806] Update CHANGELOG.md for version `0.35.0` (#904) --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bd16e84..7f52f7fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,22 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. + +## [0.35.0] 2023-06-23 + +### Added +* Support `CREATE PROCEDURE` of MSSQL (#900) - Thanks @delsehi +* Support DuckDB's `CREATE MACRO` statements (#897) - Thanks @MartinNowak +* Support for `CREATE TYPE (AS)` statements (#888) - Thanks @srijs +* Support `STRICT` tables of sqlite (#903) - Thanks @parkma99 + +### Fixed +* Fixed precedence of unary negation operator with operators: Mul, Div and Mod (#902) - Thanks @izveigor + +### Changed +* Add `support_group_by_expr` to `Dialect` trait (#896) - Thanks @jdye64 +* Update criterion requirement from `0.4` to `0.5` in `/sqlparser_bench` (#890) - Thanks @dependabot (!!) + ## [0.34.0] 2023-05-19 ### Added From efd8cb7fd1c4e6654b101868f7a76d80f66da74b Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 23 Jun 2023 10:54:52 -0400 Subject: [PATCH 215/806] chore: Release sqlparser version 0.35.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4a4c8ce4d..eea1fe78f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.34.0" +version = "0.35.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 9effeba0d836e9659425d8de026e15cca97e3cd0 Mon Sep 17 00:00:00 2001 From: Robert Pack <42610831+roeap@users.noreply.github.com> Date: Thu, 29 Jun 2023 19:30:21 +0200 Subject: [PATCH 216/806] feat: add deltalake keywords (#906) --- src/keywords.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/keywords.rs b/src/keywords.rs index 463899804..32556c6d9 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -110,6 +110,7 @@ define_keywords!( BIGNUMERIC, BINARY, BLOB, + BLOOMFILTER, BOOLEAN, BOTH, BTREE, @@ -201,10 +202,12 @@ define_keywords!( DELETE, DELIMITED, DELIMITER, + DELTA, DENSE_RANK, DEREF, DESC, DESCRIBE, + DETAIL, DETERMINISTIC, DIRECTORY, DISCARD, @@ -217,6 +220,7 @@ define_keywords!( DOW, DOY, DROP, + DRY, DUPLICATE, DYNAMIC, EACH, @@ -273,11 +277,13 @@ define_keywords!( FREE, FREEZE, FROM, + FSCK, FULL, FULLTEXT, FUNCTION, FUNCTIONS, FUSION, + GENERATE, GENERATED, GET, GLOBAL, @@ -290,9 +296,11 @@ define_keywords!( HASH, HAVING, HEADER, + HISTORY, HIVEVAR, HOLD, HOUR, + HOURS, IDENTITY, IF, IGNORE, @@ -416,6 +424,7 @@ define_keywords!( ONLY, OPEN, OPERATOR, + OPTIMIZE, OPTION, OPTIONS, OR, @@ -487,12 +496,14 @@ define_keywords!( RELATIVE, RELEASE, RENAME, + REORG, REPAIR, REPEATABLE, REPLACE, REPLICATION, RESTRICT, RESULT, + RETAIN, RETURN, RETURNING, RETURNS, @@ -505,6 +516,7 @@ define_keywords!( ROWID, ROWS, ROW_NUMBER, + RUN, SAFE_CAST, SAVEPOINT, SCHEMA, @@ -619,6 +631,7 @@ define_keywords!( USER, USING, UUID, + VACUUM, VALID, VALIDATION_MODE, VALUE, @@ -648,7 +661,8 @@ define_keywords!( WRITE, XOR, YEAR, - ZONE + ZONE, + ZORDER ); /// These keywords can't be used as a table alias, so that `FROM table_name alias` From f05f71e20dc54f14cac80a638e2ed252b6fca91c Mon Sep 17 00:00:00 2001 From: liadgiladi <51291468+liadgiladi@users.noreply.github.com> Date: Thu, 29 Jun 2023 20:33:51 +0300 Subject: [PATCH 217/806] Support `ALTER VIEW`, MySQL syntax (#907) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 24 +++++++++++++++ src/parser.rs | 21 +++++++++++++- tests/sqlparser_common.rs | 61 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5652cce19..1cf5a7688 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1375,6 +1375,15 @@ pub enum Statement { name: ObjectName, operation: AlterIndexOperation, }, + /// ALTER VIEW + AlterView { + /// View name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + name: ObjectName, + columns: Vec, + query: Box, + with_options: Vec, + }, /// DROP Drop { /// The type of the object to drop: TABLE, VIEW, etc. @@ -2534,6 +2543,21 @@ impl fmt::Display for Statement { Statement::AlterIndex { name, operation } => { write!(f, "ALTER INDEX {name} {operation}") } + Statement::AlterView { + name, + columns, + query, + with_options, + } => { + write!(f, "ALTER VIEW {name}")?; + if !with_options.is_empty() { + write!(f, " WITH ({})", display_comma_separated(with_options))?; + } + if !columns.is_empty() { + write!(f, " ({})", display_comma_separated(columns))?; + } + write!(f, " AS {query}") + } Statement::Drop { object_type, if_exists, diff --git a/src/parser.rs b/src/parser.rs index 0b1421463..ff6c5c543 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3898,8 +3898,10 @@ impl<'a> Parser<'a> { } pub fn parse_alter(&mut self) -> Result { - let object_type = self.expect_one_of_keywords(&[Keyword::TABLE, Keyword::INDEX])?; + let object_type = + self.expect_one_of_keywords(&[Keyword::VIEW, Keyword::TABLE, Keyword::INDEX])?; match object_type { + Keyword::VIEW => self.parse_alter_view(), Keyword::TABLE => { let _ = self.parse_keyword(Keyword::ONLY); // [ ONLY ] let table_name = self.parse_object_name()?; @@ -4097,6 +4099,23 @@ impl<'a> Parser<'a> { } } + pub fn parse_alter_view(&mut self) -> Result { + let name = self.parse_object_name()?; + let columns = self.parse_parenthesized_column_list(Optional, false)?; + + let with_options = self.parse_options(Keyword::WITH)?; + + self.expect_keyword(Keyword::AS)?; + let query = Box::new(self.parse_query()?); + + Ok(Statement::AlterView { + name, + columns, + query, + with_options, + }) + } + /// Parse a copy statement pub fn parse_copy(&mut self) -> Result { let source; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 917a89d8d..fa2574abc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2869,6 +2869,67 @@ fn parse_alter_index() { }; } +#[test] +fn parse_alter_view() { + let sql = "ALTER VIEW myschema.myview AS SELECT foo FROM bar"; + match verified_stmt(sql) { + Statement::AlterView { + name, + columns, + query, + with_options, + } => { + assert_eq!("myschema.myview", name.to_string()); + assert_eq!(Vec::::new(), columns); + assert_eq!("SELECT foo FROM bar", query.to_string()); + assert_eq!(with_options, vec![]); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_view_with_options() { + let sql = "ALTER VIEW v WITH (foo = 'bar', a = 123) AS SELECT 1"; + match verified_stmt(sql) { + Statement::AlterView { with_options, .. } => { + assert_eq!( + vec![ + SqlOption { + name: "foo".into(), + value: Value::SingleQuotedString("bar".into()), + }, + SqlOption { + name: "a".into(), + value: number("123"), + }, + ], + with_options + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_view_with_columns() { + let sql = "ALTER VIEW v (has, cols) AS SELECT 1, 2"; + match verified_stmt(sql) { + Statement::AlterView { + name, + columns, + query, + with_options, + } => { + assert_eq!("v", name.to_string()); + assert_eq!(columns, vec![Ident::new("has"), Ident::new("cols")]); + assert_eq!("SELECT 1, 2", query.to_string()); + assert_eq!(with_options, vec![]); + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table_add_column() { match verified_stmt("ALTER TABLE tab ADD foo TEXT") { From 20ac38b4da23d53249caf438c87f826bb58ff487 Mon Sep 17 00:00:00 2001 From: Jay Zhan Date: Sat, 1 Jul 2023 04:50:46 +0800 Subject: [PATCH 218/806] Support multi args for unnest (#909) Signed-off-by: jayzhan211 Co-authored-by: Andrew Lamb --- src/ast/query.rs | 7 +-- src/parser.rs | 4 +- src/test_utils.rs | 1 + tests/sqlparser_bigquery.rs | 4 +- tests/sqlparser_common.rs | 93 +++++++++++++++++++++++++++++++++++-- tests/sqlparser_postgres.rs | 3 +- 6 files changed, 98 insertions(+), 14 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index a709e101f..fe650e6bf 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -677,7 +677,7 @@ pub enum TableFactor { /// ``` UNNEST { alias: Option, - array_expr: Box, + array_exprs: Vec, with_offset: bool, with_offset_alias: Option, }, @@ -749,11 +749,12 @@ impl fmt::Display for TableFactor { } TableFactor::UNNEST { alias, - array_expr, + array_exprs, with_offset, with_offset_alias, } => { - write!(f, "UNNEST({array_expr})")?; + write!(f, "UNNEST({})", display_comma_separated(array_exprs))?; + if let Some(alias) = alias { write!(f, " AS {alias}")?; } diff --git a/src/parser.rs b/src/parser.rs index ff6c5c543..d907a6110 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5999,7 +5999,7 @@ impl<'a> Parser<'a> { && self.parse_keyword(Keyword::UNNEST) { self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; + let array_exprs = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; let alias = match self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS) { @@ -6025,7 +6025,7 @@ impl<'a> Parser<'a> { Ok(TableFactor::UNNEST { alias, - array_expr: Box::new(expr), + array_exprs, with_offset, with_offset_alias, }) diff --git a/src/test_utils.rs b/src/test_utils.rs index 57b21e1c9..e825db979 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -197,6 +197,7 @@ pub fn expr_from_projection(item: &SelectItem) -> &Expr { } } +/// Creates a `Value::Number`, panic'ing if n is not a number pub fn number(n: &'static str) -> Value { Value::Number(n.parse().unwrap(), false) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 11998ae6d..4eb381133 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -160,10 +160,10 @@ fn parse_join_constraint_unnest_alias() { vec![Join { relation: TableFactor::UNNEST { alias: table_alias("f"), - array_expr: Box::new(Expr::CompoundIdentifier(vec![ + array_exprs: vec![Expr::CompoundIdentifier(vec![ Ident::new("t1"), Ident::new("a") - ])), + ])], with_offset: false, with_offset_alias: None }, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fa2574abc..f025216a7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4130,7 +4130,16 @@ fn parse_table_function() { #[test] fn parse_unnest() { + let sql = "SELECT UNNEST(make_array(1, 2, 3))"; + one_statement_parses_to(sql, sql); + let sql = "SELECT UNNEST(make_array(1, 2, 3), make_array(4, 5))"; + one_statement_parses_to(sql, sql); +} + +#[test] +fn parse_unnest_in_from_clause() { fn chk( + array_exprs: &str, alias: bool, with_offset: bool, with_offset_alias: bool, @@ -4138,7 +4147,8 @@ fn parse_unnest() { want: Vec, ) { let sql = &format!( - "SELECT * FROM UNNEST(expr){}{}{}", + "SELECT * FROM UNNEST({}){}{}{}", + array_exprs, if alias { " AS numbers" } else { "" }, if with_offset { " WITH OFFSET" } else { "" }, if with_offset_alias { @@ -4156,6 +4166,7 @@ fn parse_unnest() { }; // 1. both Alias and WITH OFFSET clauses. chk( + "expr", true, true, false, @@ -4166,7 +4177,7 @@ fn parse_unnest() { name: Ident::new("numbers"), columns: vec![], }), - array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), + array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: true, with_offset_alias: None, }, @@ -4175,6 +4186,7 @@ fn parse_unnest() { ); // 2. neither Alias nor WITH OFFSET clause. chk( + "expr", false, false, false, @@ -4182,7 +4194,7 @@ fn parse_unnest() { vec![TableWithJoins { relation: TableFactor::UNNEST { alias: None, - array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), + array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: false, with_offset_alias: None, }, @@ -4191,6 +4203,7 @@ fn parse_unnest() { ); // 3. Alias but no WITH OFFSET clause. chk( + "expr", false, true, false, @@ -4198,7 +4211,7 @@ fn parse_unnest() { vec![TableWithJoins { relation: TableFactor::UNNEST { alias: None, - array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), + array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: true, with_offset_alias: None, }, @@ -4207,6 +4220,7 @@ fn parse_unnest() { ); // 4. WITH OFFSET but no Alias. chk( + "expr", true, false, false, @@ -4217,13 +4231,82 @@ fn parse_unnest() { name: Ident::new("numbers"), columns: vec![], }), - array_expr: Box::new(Expr::Identifier(Ident::new("expr"))), + array_exprs: vec![Expr::Identifier(Ident::new("expr"))], + with_offset: false, + with_offset_alias: None, + }, + joins: vec![], + }], + ); + // 5. Simple array + chk( + "make_array(1, 2, 3)", + false, + false, + false, + &dialects, + vec![TableWithJoins { + relation: TableFactor::UNNEST { + alias: None, + array_exprs: vec![Expr::Function(Function { + name: ObjectName(vec![Ident::new("make_array")]), + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("3")))), + ], + over: None, + distinct: false, + special: false, + order_by: vec![], + })], with_offset: false, with_offset_alias: None, }, joins: vec![], }], ); + // 6. Multiple arrays + chk( + "make_array(1, 2, 3), make_array(5, 6)", + false, + false, + false, + &dialects, + vec![TableWithJoins { + relation: TableFactor::UNNEST { + alias: None, + array_exprs: vec![ + Expr::Function(Function { + name: ObjectName(vec![Ident::new("make_array")]), + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("3")))), + ], + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + Expr::Function(Function { + name: ObjectName(vec![Ident::new("make_array")]), + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("5")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("6")))), + ], + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + ], + with_offset: false, + with_offset_alias: None, + }, + joins: vec![], + }], + ) } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 80a4261ee..6b8bc64d9 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2407,7 +2407,7 @@ fn parse_create_role() { in_role, in_group, role, - user, + user: _, admin, authorization_owner, }], @@ -2435,7 +2435,6 @@ fn parse_create_role() { assert_eq_vec(&["role1", "role2"], in_role); assert!(in_group.is_empty()); assert_eq_vec(&["role3"], role); - assert!(user.is_empty()); assert_eq_vec(&["role4", "role5"], admin); assert_eq!(*authorization_owner, None); } From a50671d95d34688529fbc3d4452bec164ff21497 Mon Sep 17 00:00:00 2001 From: Igor Izvekov Date: Thu, 6 Jul 2023 16:27:18 +0300 Subject: [PATCH 219/806] feat: support PGOverlap operator (#912) --- src/ast/operator.rs | 2 ++ src/parser.rs | 4 ++++ src/tokenizer.rs | 14 ++++++++++++-- tests/sqlparser_postgres.rs | 1 + 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index b988265ba..b60b5135d 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -95,6 +95,7 @@ pub enum BinaryOperator { PGBitwiseShiftLeft, PGBitwiseShiftRight, PGExp, + PGOverlap, PGRegexMatch, PGRegexIMatch, PGRegexNotMatch, @@ -135,6 +136,7 @@ impl fmt::Display for BinaryOperator { BinaryOperator::PGBitwiseShiftLeft => f.write_str("<<"), BinaryOperator::PGBitwiseShiftRight => f.write_str(">>"), BinaryOperator::PGExp => f.write_str("^"), + BinaryOperator::PGOverlap => f.write_str("&&"), BinaryOperator::PGRegexMatch => f.write_str("~"), BinaryOperator::PGRegexIMatch => f.write_str("~*"), BinaryOperator::PGRegexNotMatch => f.write_str("!~"), diff --git a/src/parser.rs b/src/parser.rs index d907a6110..d9b85fcc3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1662,6 +1662,9 @@ impl<'a> Parser<'a> { Token::Sharp if dialect_of!(self is PostgreSqlDialect) => { Some(BinaryOperator::PGBitwiseXor) } + Token::Overlap if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { + Some(BinaryOperator::PGOverlap) + } Token::Tilde => Some(BinaryOperator::PGRegexMatch), Token::TildeAsterisk => Some(BinaryOperator::PGRegexIMatch), Token::ExclamationMarkTilde => Some(BinaryOperator::PGRegexNotMatch), @@ -2050,6 +2053,7 @@ impl<'a> Parser<'a> { Token::LBracket | Token::LongArrow | Token::Arrow + | Token::Overlap | Token::HashArrow | Token::HashLongArrow | Token::AtArrow diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 257a3517e..f8e6793fe 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -150,6 +150,8 @@ pub enum Token { ShiftLeft, /// `>>`, a bitwise shift right operator in PostgreSQL ShiftRight, + /// '&&', an overlap operator in PostgreSQL + Overlap, /// Exclamation Mark `!` used for PostgreSQL factorial operator ExclamationMark, /// Double Exclamation Mark `!!` used for PostgreSQL prefix factorial operator @@ -158,7 +160,7 @@ pub enum Token { AtSign, /// `|/`, a square root math operator in PostgreSQL PGSquareRoot, - /// `||/` , a cube root math operator in PostgreSQL + /// `||/`, a cube root math operator in PostgreSQL PGCubeRoot, /// `?` or `$` , a prepared statement arg placeholder Placeholder(String), @@ -245,6 +247,7 @@ impl fmt::Display for Token { Token::AtSign => f.write_str("@"), Token::ShiftLeft => f.write_str("<<"), Token::ShiftRight => f.write_str(">>"), + Token::Overlap => f.write_str("&&"), Token::PGSquareRoot => f.write_str("|/"), Token::PGCubeRoot => f.write_str("||/"), Token::Placeholder(ref s) => write!(f, "{s}"), @@ -858,7 +861,14 @@ impl<'a> Tokenizer<'a> { '\\' => self.consume_and_return(chars, Token::Backslash), '[' => self.consume_and_return(chars, Token::LBracket), ']' => self.consume_and_return(chars, Token::RBracket), - '&' => self.consume_and_return(chars, Token::Ampersand), + '&' => { + chars.next(); // consume the '&' + match chars.peek() { + Some('&') => self.consume_and_return(chars, Token::Overlap), + // Bitshift '&' operator + _ => Ok(Some(Token::Ampersand)), + } + } '^' => self.consume_and_return(chars, Token::Caret), '{' => self.consume_and_return(chars, Token::LBrace), '}' => self.consume_and_return(chars, Token::RBrace), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6b8bc64d9..b29081ab6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1613,6 +1613,7 @@ fn parse_pg_binary_ops() { ("^", BinaryOperator::PGExp, pg()), (">>", BinaryOperator::PGBitwiseShiftRight, pg_and_generic()), ("<<", BinaryOperator::PGBitwiseShiftLeft, pg_and_generic()), + ("&&", BinaryOperator::PGOverlap, pg()), ]; for (str_op, op, dialects) in binary_ops { From 4efe55dd8ac440ac370752e0e7095b3e62d73d2c Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 17 Jul 2023 14:19:51 -0400 Subject: [PATCH 220/806] Remove most instances of `#[cfg(feature(bigdecimal))]` in tests (#910) --- src/ast/value.rs | 3 +++ src/test_utils.rs | 2 +- tests/sqlparser_bigquery.rs | 21 +++----------------- tests/sqlparser_common.rs | 1 - tests/sqlparser_duckdb.rs | 8 +------- tests/sqlparser_mssql.rs | 8 +------- tests/sqlparser_mysql.rs | 38 ++++++++++++++----------------------- tests/sqlparser_postgres.rs | 37 +++++------------------------------- 8 files changed, 28 insertions(+), 90 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index 95ea978d0..491553cac 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -32,6 +32,9 @@ pub enum Value { #[cfg(not(feature = "bigdecimal"))] Number(String, bool), #[cfg(feature = "bigdecimal")] + // HINT: use `test_utils::number` to make an instance of + // Value::Number This might help if you your tests pass locally + // but fail on CI with the `--all-features` flag enabled Number(BigDecimal, bool), /// 'string value' SingleQuotedString(String), diff --git a/src/test_utils.rs b/src/test_utils.rs index e825db979..47fb00d5d 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -198,7 +198,7 @@ pub fn expr_from_projection(item: &SelectItem) -> &Expr { } /// Creates a `Value::Number`, panic'ing if n is not a number -pub fn number(n: &'static str) -> Value { +pub fn number(n: &str) -> Value { Value::Number(n.parse().unwrap(), false) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 4eb381133..bbe1a6e9f 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -17,11 +17,6 @@ use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; use test_utils::*; -#[cfg(feature = "bigdecimal")] -use bigdecimal::*; -#[cfg(feature = "bigdecimal")] -use std::str::FromStr; - #[test] fn parse_literal_string() { let sql = r#"SELECT 'single', "double""#; @@ -377,22 +372,13 @@ fn test_select_wildcard_with_replace() { expr: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("quantity"))), op: BinaryOperator::Divide, - #[cfg(not(feature = "bigdecimal"))] - right: Box::new(Expr::Value(Value::Number("2".to_string(), false))), - #[cfg(feature = "bigdecimal")] - right: Box::new(Expr::Value(Value::Number( - BigDecimal::from_str("2").unwrap(), - false, - ))), + right: Box::new(Expr::Value(number("2"))), }, column_name: Ident::new("quantity"), as_keyword: true, }), Box::new(ReplaceSelectElement { - #[cfg(not(feature = "bigdecimal"))] - expr: Expr::Value(Value::Number("3".to_string(), false)), - #[cfg(feature = "bigdecimal")] - expr: Expr::Value(Value::Number(BigDecimal::from_str("3").unwrap(), false)), + expr: Expr::Value(number("3")), column_name: Ident::new("order_id"), as_keyword: true, }), @@ -421,7 +407,6 @@ fn bigquery_and_generic() -> TestedDialects { fn parse_map_access_offset() { let sql = "SELECT d[offset(0)]"; let _select = bigquery().verified_only_select(sql); - #[cfg(not(feature = "bigdecimal"))] assert_eq!( _select.projection[0], SelectItem::UnnamedExpr(Expr::MapAccess { @@ -432,7 +417,7 @@ fn parse_map_access_offset() { keys: vec![Expr::Function(Function { name: ObjectName(vec!["offset".into()]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::Number("0".into(), false) + number("0") ))),], over: None, distinct: false, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f025216a7..4fe1a57b4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1731,7 +1731,6 @@ fn parse_select_having() { assert!(select.having.is_some()); } -#[cfg(feature = "bigdecimal")] #[test] fn parse_select_qualify() { let sql = "SELECT i, p, o FROM qt QUALIFY ROW_NUMBER() OVER (PARTITION BY p ORDER BY o) = 1"; diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index bb60235a6..fabb9790a 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -97,13 +97,7 @@ fn test_create_macro_default_args() { MacroArg::new("a"), MacroArg { name: Ident::new("b"), - default_expr: Some(Expr::Value(Value::Number( - #[cfg(not(feature = "bigdecimal"))] - 5.to_string(), - #[cfg(feature = "bigdecimal")] - bigdecimal::BigDecimal::from(5), - false, - ))), + default_expr: Some(Expr::Value(number("5"))), }, ]), definition: MacroDefinition::Expr(Expr::BinaryOp { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 8fc3dcd59..b46a0c6c9 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -59,12 +59,6 @@ fn parse_mssql_delimited_identifiers() { fn parse_create_procedure() { let sql = "CREATE OR ALTER PROCEDURE test (@foo INT, @bar VARCHAR(256)) AS BEGIN SELECT 1 END"; - #[cfg(feature = "bigdecimal")] - let one = Value::Number(bigdecimal::BigDecimal::from(1), false); - - #[cfg(not(feature = "bigdecimal"))] - let one = Value::Number("1".to_string(), false); - assert_eq!( ms().verified_stmt(sql), Statement::CreateProcedure { @@ -79,7 +73,7 @@ fn parse_create_procedure() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, - projection: vec![SelectItem::UnnamedExpr(Expr::Value(one))], + projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))], into: None, from: vec![], lateral_views: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 3e5c810ef..12d2cc733 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -259,13 +259,7 @@ fn parse_set_variables() { local: true, hivevar: false, variable: ObjectName(vec!["autocommit".into()]), - value: vec![Expr::Value(Value::Number( - #[cfg(not(feature = "bigdecimal"))] - "1".to_string(), - #[cfg(feature = "bigdecimal")] - bigdecimal::BigDecimal::from(1), - false - ))], + value: vec![Expr::Value(number("1"))], } ); } @@ -643,7 +637,6 @@ fn parse_create_table_unsigned() { } #[test] -#[cfg(not(feature = "bigdecimal"))] fn parse_simple_insert() { let sql = r"INSERT INTO tasks (title, priority) VALUES ('Test Some Inserts', 1), ('Test Entry 2', 2), ('Test Entry 3', 3)"; @@ -668,15 +661,15 @@ fn parse_simple_insert() { Expr::Value(Value::SingleQuotedString( "Test Some Inserts".to_string() )), - Expr::Value(Value::Number("1".to_string(), false)) + Expr::Value(number("1")) ], vec![ Expr::Value(Value::SingleQuotedString("Test Entry 2".to_string())), - Expr::Value(Value::Number("2".to_string(), false)) + Expr::Value(number("2")) ], vec![ Expr::Value(Value::SingleQuotedString("Test Entry 3".to_string())), - Expr::Value(Value::Number("3".to_string(), false)) + Expr::Value(number("3")) ] ] })), @@ -895,6 +888,13 @@ fn parse_select_with_numeric_prefix_column_name() { } } +// Don't run with bigdecimal as it fails like this on rust beta: +// +// 'parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column' +// panicked at 'assertion failed: `(left == right)` +// +// left: `"SELECT 123e4, 123col_$@123abc FROM \"table\""`, +// right: `"SELECT 1230000, 123col_$@123abc FROM \"table\""`', src/test_utils.rs:114:13 #[cfg(not(feature = "bigdecimal"))] #[test] fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { @@ -907,10 +907,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { distinct: None, top: None, projection: vec![ - SelectItem::UnnamedExpr(Expr::Value(Value::Number( - "123e4".to_string(), - false - ))), + SelectItem::UnnamedExpr(Expr::Value(number("123e4"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("123col_$@123abc"))) ], into: None, @@ -1072,7 +1069,6 @@ fn parse_alter_table_change_column() { } #[test] -#[cfg(not(feature = "bigdecimal"))] fn parse_substring_in_select() { let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; match mysql().one_statement_parses_to( @@ -1091,14 +1087,8 @@ fn parse_substring_in_select() { value: "description".to_string(), quote_style: None })), - substring_from: Some(Box::new(Expr::Value(Value::Number( - "0".to_string(), - false - )))), - substring_for: Some(Box::new(Expr::Value(Value::Number( - "1".to_string(), - false - )))) + substring_from: Some(Box::new(Expr::Value(number("0")))), + substring_for: Some(Box::new(Expr::Value(number("1")))) })], into: None, from: vec![TableWithJoins { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b29081ab6..024ead6a3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1099,13 +1099,7 @@ fn parse_set() { local: false, hivevar: false, variable: ObjectName(vec![Ident::new("a")]), - value: vec![Expr::Value(Value::Number( - #[cfg(not(feature = "bigdecimal"))] - "0".to_string(), - #[cfg(feature = "bigdecimal")] - bigdecimal::BigDecimal::from(0), - false, - ))], + value: vec![Expr::Value(number("0"))], } ); @@ -1691,13 +1685,8 @@ fn parse_pg_regex_match_ops() { #[test] fn parse_array_index_expr() { - #[cfg(feature = "bigdecimal")] let num: Vec = (0..=10) - .map(|s| Expr::Value(Value::Number(bigdecimal::BigDecimal::from(s), false))) - .collect(); - #[cfg(not(feature = "bigdecimal"))] - let num: Vec = (0..=10) - .map(|s| Expr::Value(Value::Number(s.to_string(), false))) + .map(|s| Expr::Value(number(&s.to_string()))) .collect(); let sql = "SELECT foo[0] FROM foos"; @@ -1785,13 +1774,7 @@ fn parse_array_subquery_expr() { left: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, - projection: vec![SelectItem::UnnamedExpr(Expr::Value(Value::Number( - #[cfg(not(feature = "bigdecimal"))] - "1".to_string(), - #[cfg(feature = "bigdecimal")] - bigdecimal::BigDecimal::from(1), - false, - )))], + projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))], into: None, from: vec![], lateral_views: vec![], @@ -1807,13 +1790,7 @@ fn parse_array_subquery_expr() { right: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, - projection: vec![SelectItem::UnnamedExpr(Expr::Value(Value::Number( - #[cfg(not(feature = "bigdecimal"))] - "2".to_string(), - #[cfg(feature = "bigdecimal")] - bigdecimal::BigDecimal::from(2), - false, - )))], + projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("2")))], into: None, from: vec![], lateral_views: vec![], @@ -2021,10 +1998,6 @@ fn test_composite_value() { select.projection[0] ); - #[cfg(feature = "bigdecimal")] - let num: Expr = Expr::Value(Value::Number(bigdecimal::BigDecimal::from(9), false)); - #[cfg(not(feature = "bigdecimal"))] - let num: Expr = Expr::Value(Value::Number("9".to_string(), false)); assert_eq!( select.selection, Some(Expr::BinaryOp { @@ -2036,7 +2009,7 @@ fn test_composite_value() { ])))) }), op: BinaryOperator::Gt, - right: Box::new(num) + right: Box::new(Expr::Value(number("9"))) }) ); From 653346c4d6e67001e492a41920f3c8903267a547 Mon Sep 17 00:00:00 2001 From: JIN-YONG LEE <74762475+jinlee0@users.noreply.github.com> Date: Tue, 18 Jul 2023 03:21:45 +0900 Subject: [PATCH 221/806] Upgrade bigdecimal to 0.4.1 (#921) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eea1fe78f..03a08f66a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ json_example = ["serde_json", "serde"] visitor = ["sqlparser_derive"] [dependencies] -bigdecimal = { version = "0.3", features = ["serde"], optional = true } +bigdecimal = { version = "0.4.1", features = ["serde"], optional = true } log = "0.4" serde = { version = "1.0", features = ["derive"], optional = true } # serde_json is only used in examples/cli, but we have to put it outside From d6ebb58b969e0eb62fd34ed228ff3d51013e5ea5 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 17 Jul 2023 14:34:54 -0400 Subject: [PATCH 222/806] Fix dependabot by removing rust-toolchain toml (#922) --- rust-toolchain | 1 - 1 file changed, 1 deletion(-) delete mode 100644 rust-toolchain diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 870bbe4e5..000000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -stable \ No newline at end of file From c8b6e7f2c7b10698d7d6fb6704ed7933e7ab9d7d Mon Sep 17 00:00:00 2001 From: Igor Izvekov Date: Mon, 17 Jul 2023 21:42:28 +0300 Subject: [PATCH 223/806] feat: comments for all operators (#917) --- src/ast/operator.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index b60b5135d..9fb1bf022 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -28,8 +28,11 @@ use super::display_separated; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum UnaryOperator { + /// Plus, e.g. `+9` Plus, + /// Minus, e.g. `-9` Minus, + /// Not, e.g. `NOT(true)` Not, /// Bitwise Not, e.g. `~9` (PostgreSQL-specific) PGBitwiseNot, @@ -66,24 +69,43 @@ impl fmt::Display for UnaryOperator { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum BinaryOperator { + /// Plus, e.g. `a + b` Plus, + /// Minus, e.g. `a - b` Minus, + /// Multiply, e.g. `a * b` Multiply, + /// Divide, e.g. `a / b` Divide, + /// Modulo, e.g. `a % b` Modulo, + /// String/Array Concat operator, e.g. `a || b` StringConcat, + /// Greater than, e.g. `a > b` Gt, + /// Less than, e.g. `a < b` Lt, + /// Greater equal, e.g. `a >= b` GtEq, + /// Less equal, e.g. `a <= b` LtEq, + /// Spaceship, e.g. `a <=> b` Spaceship, + /// Equal, e.g. `a = b` Eq, + /// Not equal, e.g. `a <> b` NotEq, + /// And, e.g. `a AND b` And, + /// Or, e.g. `a OR b` Or, + /// XOR, e.g. `a XOR b` Xor, + /// Bitwise or, e.g. `a | b` BitwiseOr, + /// Bitwise and, e.g. `a & b` BitwiseAnd, + /// Bitwise XOR, e.g. `a ^ b` BitwiseXor, /// Integer division operator `//` in DuckDB DuckIntegerDivide, @@ -91,14 +113,23 @@ pub enum BinaryOperator { MyIntegerDivide, /// Support for custom operators (built by parsers outside this crate) Custom(String), + /// Bitwise XOR, e.g. `a # b` (PostgreSQL-specific) PGBitwiseXor, + /// Bitwise shift left, e.g. `a << b` (PostgreSQL-specific) PGBitwiseShiftLeft, + /// Bitwise shift right, e.g. `a >> b` (PostgreSQL-specific) PGBitwiseShiftRight, + /// Exponent, e.g. `a ^ b` (PostgreSQL-specific) PGExp, + /// Overlap operator, e.g. `a && b` (PostgreSQL-specific) PGOverlap, + /// String matches regular expression (case sensitively), e.g. `a ~ b` (PostgreSQL-specific) PGRegexMatch, + /// String matches regular expression (case insensitively), e.g. `a ~* b` (PostgreSQL-specific) PGRegexIMatch, + /// String does not match regular expression (case sensitively), e.g. `a !~ b` (PostgreSQL-specific) PGRegexNotMatch, + /// String does not match regular expression (case insensitively), e.g. `a !~* b` (PostgreSQL-specific) PGRegexNotIMatch, /// PostgreSQL-specific custom operator. /// From df45db13754311a597777cf3bf53d923cd7e3869 Mon Sep 17 00:00:00 2001 From: Igor Izvekov Date: Mon, 17 Jul 2023 22:03:48 +0300 Subject: [PATCH 224/806] fix: parsing `JsonOperator` (#913) --- src/tokenizer.rs | 111 ++++++++++++++++++++++++++------------ tests/sqlparser_common.rs | 35 ++++++++++++ 2 files changed, 112 insertions(+), 34 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index f8e6793fe..7a1813544 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -35,7 +35,9 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::DollarQuotedString; -use crate::dialect::{BigQueryDialect, DuckDbDialect, GenericDialect, SnowflakeDialect}; +use crate::dialect::{ + BigQueryDialect, DuckDbDialect, GenericDialect, HiveDialect, SnowflakeDialect, +}; use crate::dialect::{Dialect, MySqlDialect}; use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX}; @@ -495,9 +497,32 @@ impl<'a> Tokenizer<'a> { Ok(tokens) } + fn tokenize_identifier_or_keyword( + &self, + ch: String, + chars: &mut State, + ) -> Result, TokenizerError> { + chars.next(); // consume the first char + let word = self.tokenize_word(ch, chars); + + // TODO: implement parsing of exponent here + if word.chars().all(|x| x.is_ascii_digit() || x == '.') { + let mut inner_state = State { + peekable: word.chars().peekable(), + line: 0, + col: 0, + }; + let mut s = peeking_take_while(&mut inner_state, |ch| matches!(ch, '0'..='9' | '.')); + let s2 = peeking_take_while(chars, |ch| matches!(ch, '0'..='9' | '.')); + s += s2.as_str(); + return Ok(Some(Token::Number(s, false))); + } + + Ok(Some(Token::make_word(&word, None))) + } + /// Get the next token or return None fn next_token(&self, chars: &mut State) -> Result, TokenizerError> { - //println!("next_token: {:?}", chars.peek()); match chars.peek() { Some(&ch) => match ch { ' ' => self.consume_and_return(chars, Token::Whitespace(Whitespace::Space)), @@ -525,7 +550,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "b" or "B" - let s = self.tokenize_word(b, chars); + let s = self.tokenize_word(b.to_string(), chars); Ok(Some(Token::make_word(&s, None))) } } @@ -544,7 +569,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "r" or "R" - let s = self.tokenize_word(b, chars); + let s = self.tokenize_word(b.to_string(), chars); Ok(Some(Token::make_word(&s, None))) } } @@ -560,7 +585,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "N" - let s = self.tokenize_word(n, chars); + let s = self.tokenize_word(n.to_string(), chars); Ok(Some(Token::make_word(&s, None))) } } @@ -577,7 +602,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "E" or "e" - let s = self.tokenize_word(x, chars); + let s = self.tokenize_word(x.to_string(), chars); Ok(Some(Token::make_word(&s, None))) } } @@ -594,33 +619,11 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "X" - let s = self.tokenize_word(x, chars); + let s = self.tokenize_word(x.to_string(), chars); Ok(Some(Token::make_word(&s, None))) } } } - // identifier or keyword - ch if self.dialect.is_identifier_start(ch) => { - chars.next(); // consume the first char - let word = self.tokenize_word(ch, chars); - - // TODO: implement parsing of exponent here - if word.chars().all(|x| x.is_ascii_digit() || x == '.') { - let mut inner_state = State { - peekable: word.chars().peekable(), - line: 0, - col: 0, - }; - let mut s = peeking_take_while(&mut inner_state, |ch| { - matches!(ch, '0'..='9' | '.') - }); - let s2 = peeking_take_while(chars, |ch| matches!(ch, '0'..='9' | '.')); - s += s2.as_str(); - return Ok(Some(Token::Number(s, false))); - } - - Ok(Some(Token::make_word(&word, None))) - } // single quoted string '\'' => { let s = self.tokenize_quoted_string(chars, '\'')?; @@ -714,7 +717,7 @@ impl<'a> Tokenizer<'a> { // mysql dialect supports identifiers that start with a numeric prefix, // as long as they aren't an exponent number. - if dialect_of!(self is MySqlDialect) && exponent_part.is_empty() { + if dialect_of!(self is MySqlDialect | HiveDialect) && exponent_part.is_empty() { let word = peeking_take_while(chars, |ch| self.dialect.is_identifier_part(ch)); @@ -786,7 +789,18 @@ impl<'a> Tokenizer<'a> { } '+' => self.consume_and_return(chars, Token::Plus), '*' => self.consume_and_return(chars, Token::Mul), - '%' => self.consume_and_return(chars, Token::Mod), + '%' => { + chars.next(); + match chars.peek() { + Some(' ') => self.consume_and_return(chars, Token::Mod), + Some(sch) if self.dialect.is_identifier_start('%') => { + let mut s = ch.to_string(); + s.push_str(&sch.to_string()); + self.tokenize_identifier_or_keyword(s, chars) + } + _ => self.consume_and_return(chars, Token::Mod), + } + } '|' => { chars.next(); // consume the '|' match chars.peek() { @@ -901,6 +915,12 @@ impl<'a> Tokenizer<'a> { _ => Ok(Some(Token::HashArrow)), } } + Some(' ') => Ok(Some(Token::Sharp)), + Some(sch) if self.dialect.is_identifier_start('#') => { + let mut s = ch.to_string(); + s.push_str(&sch.to_string()); + self.tokenize_identifier_or_keyword(s, chars) + } _ => Ok(Some(Token::Sharp)), } } @@ -909,7 +929,25 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('>') => self.consume_and_return(chars, Token::AtArrow), Some('?') => self.consume_and_return(chars, Token::AtQuestion), - Some('@') => self.consume_and_return(chars, Token::AtAt), + Some('@') => { + chars.next(); + match chars.peek() { + Some(' ') => Ok(Some(Token::AtAt)), + Some(tch) if self.dialect.is_identifier_start('@') => { + let mut s = ch.to_string(); + s.push('@'); + s.push_str(&tch.to_string()); + self.tokenize_identifier_or_keyword(s, chars) + } + _ => Ok(Some(Token::AtAt)), + } + } + Some(' ') => Ok(Some(Token::AtSign)), + Some(sch) if self.dialect.is_identifier_start('@') => { + let mut s = ch.to_string(); + s.push_str(&sch.to_string()); + self.tokenize_identifier_or_keyword(s, chars) + } _ => Ok(Some(Token::AtSign)), } } @@ -918,6 +956,11 @@ impl<'a> Tokenizer<'a> { let s = peeking_take_while(chars, |ch| ch.is_numeric()); Ok(Some(Token::Placeholder(String::from("?") + &s))) } + + // identifier or keyword + ch if self.dialect.is_identifier_start(ch) => { + self.tokenize_identifier_or_keyword(ch.to_string(), chars) + } '$' => Ok(Some(self.tokenize_dollar_preceded_value(chars)?)), //whitespace check (including unicode chars) should be last as it covers some of the chars above @@ -1043,8 +1086,8 @@ impl<'a> Tokenizer<'a> { } /// Tokenize an identifier or keyword, after the first char is already consumed. - fn tokenize_word(&self, first_char: char, chars: &mut State) -> String { - let mut s = first_char.to_string(); + fn tokenize_word(&self, first_chars: String, chars: &mut State) -> String { + let mut s = first_chars; s.push_str(&peeking_take_while(chars, |ch| { self.dialect.is_identifier_part(ch) })); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4fe1a57b4..08451ac27 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1113,6 +1113,41 @@ fn parse_unary_math_with_multiply() { ); } +fn pg_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(GenericDialect {})], + options: None, + } +} + +#[test] +fn parse_json_ops_without_colon() { + use self::JsonOperator; + let binary_ops = &[ + ("->", JsonOperator::Arrow, all_dialects()), + ("->>", JsonOperator::LongArrow, all_dialects()), + ("#>", JsonOperator::HashArrow, pg_and_generic()), + ("#>>", JsonOperator::HashLongArrow, pg_and_generic()), + ("@>", JsonOperator::AtArrow, all_dialects()), + ("<@", JsonOperator::ArrowAt, all_dialects()), + ("#-", JsonOperator::HashMinus, pg_and_generic()), + ("@?", JsonOperator::AtQuestion, all_dialects()), + ("@@", JsonOperator::AtAt, all_dialects()), + ]; + + for (str_op, op, dialects) in binary_ops { + let select = dialects.verified_only_select(&format!("SELECT a {} b", &str_op)); + assert_eq!( + SelectItem::UnnamedExpr(Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::new("a"))), + operator: *op, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }), + select.projection[0] + ); + } +} + #[test] fn parse_is_null() { use self::Expr::*; From c45451850c94a0d7d05a90035463799a301e7d13 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 17 Jul 2023 16:09:55 -0400 Subject: [PATCH 225/806] Clean up JSON operator tokenizing code (#923) --- src/tokenizer.rs | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 7a1813544..6a135b439 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -497,12 +497,14 @@ impl<'a> Tokenizer<'a> { Ok(tokens) } + // Tokenize the identifer or keywords in `ch` fn tokenize_identifier_or_keyword( &self, - ch: String, + ch: impl IntoIterator, chars: &mut State, ) -> Result, TokenizerError> { chars.next(); // consume the first char + let ch: String = ch.into_iter().collect(); let word = self.tokenize_word(ch, chars); // TODO: implement parsing of exponent here @@ -550,7 +552,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "b" or "B" - let s = self.tokenize_word(b.to_string(), chars); + let s = self.tokenize_word(b, chars); Ok(Some(Token::make_word(&s, None))) } } @@ -569,7 +571,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "r" or "R" - let s = self.tokenize_word(b.to_string(), chars); + let s = self.tokenize_word(b, chars); Ok(Some(Token::make_word(&s, None))) } } @@ -585,7 +587,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "N" - let s = self.tokenize_word(n.to_string(), chars); + let s = self.tokenize_word(n, chars); Ok(Some(Token::make_word(&s, None))) } } @@ -602,7 +604,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "E" or "e" - let s = self.tokenize_word(x.to_string(), chars); + let s = self.tokenize_word(x, chars); Ok(Some(Token::make_word(&s, None))) } } @@ -619,7 +621,7 @@ impl<'a> Tokenizer<'a> { } _ => { // regular identifier starting with an "X" - let s = self.tokenize_word(x.to_string(), chars); + let s = self.tokenize_word(x, chars); Ok(Some(Token::make_word(&s, None))) } } @@ -794,9 +796,7 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some(' ') => self.consume_and_return(chars, Token::Mod), Some(sch) if self.dialect.is_identifier_start('%') => { - let mut s = ch.to_string(); - s.push_str(&sch.to_string()); - self.tokenize_identifier_or_keyword(s, chars) + self.tokenize_identifier_or_keyword([ch, *sch], chars) } _ => self.consume_and_return(chars, Token::Mod), } @@ -917,9 +917,7 @@ impl<'a> Tokenizer<'a> { } Some(' ') => Ok(Some(Token::Sharp)), Some(sch) if self.dialect.is_identifier_start('#') => { - let mut s = ch.to_string(); - s.push_str(&sch.to_string()); - self.tokenize_identifier_or_keyword(s, chars) + self.tokenize_identifier_or_keyword([ch, *sch], chars) } _ => Ok(Some(Token::Sharp)), } @@ -934,19 +932,14 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some(' ') => Ok(Some(Token::AtAt)), Some(tch) if self.dialect.is_identifier_start('@') => { - let mut s = ch.to_string(); - s.push('@'); - s.push_str(&tch.to_string()); - self.tokenize_identifier_or_keyword(s, chars) + self.tokenize_identifier_or_keyword([ch, '@', *tch], chars) } _ => Ok(Some(Token::AtAt)), } } Some(' ') => Ok(Some(Token::AtSign)), Some(sch) if self.dialect.is_identifier_start('@') => { - let mut s = ch.to_string(); - s.push_str(&sch.to_string()); - self.tokenize_identifier_or_keyword(s, chars) + self.tokenize_identifier_or_keyword([ch, *sch], chars) } _ => Ok(Some(Token::AtSign)), } @@ -959,7 +952,7 @@ impl<'a> Tokenizer<'a> { // identifier or keyword ch if self.dialect.is_identifier_start(ch) => { - self.tokenize_identifier_or_keyword(ch.to_string(), chars) + self.tokenize_identifier_or_keyword([ch], chars) } '$' => Ok(Some(self.tokenize_dollar_preceded_value(chars)?)), @@ -1086,8 +1079,8 @@ impl<'a> Tokenizer<'a> { } /// Tokenize an identifier or keyword, after the first char is already consumed. - fn tokenize_word(&self, first_chars: String, chars: &mut State) -> String { - let mut s = first_chars; + fn tokenize_word(&self, first_chars: impl Into, chars: &mut State) -> String { + let mut s = first_chars.into(); s.push_str(&peeking_take_while(chars, |ch| { self.dialect.is_identifier_part(ch) })); From eb288487a6a938b45004deda850e550a74fcf935 Mon Sep 17 00:00:00 2001 From: parkma99 <84610851+parkma99@users.noreply.github.com> Date: Wed, 19 Jul 2023 05:15:05 +0800 Subject: [PATCH 226/806] Support UNION (ALL) BY NAME syntax (#915) --- src/ast/query.rs | 9 ++- src/keywords.rs | 1 + src/parser.rs | 10 ++- tests/sqlparser_duckdb.rs | 147 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 3 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index fe650e6bf..5f4c289dc 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -110,7 +110,10 @@ impl fmt::Display for SetExpr { } => { write!(f, "{left} {op}")?; match set_quantifier { - SetQuantifier::All | SetQuantifier::Distinct => write!(f, " {set_quantifier}")?, + SetQuantifier::All + | SetQuantifier::Distinct + | SetQuantifier::ByName + | SetQuantifier::AllByName => write!(f, " {set_quantifier}")?, SetQuantifier::None => write!(f, "{set_quantifier}")?, } write!(f, " {right}")?; @@ -148,6 +151,8 @@ impl fmt::Display for SetOperator { pub enum SetQuantifier { All, Distinct, + ByName, + AllByName, None, } @@ -156,6 +161,8 @@ impl fmt::Display for SetQuantifier { match self { SetQuantifier::All => write!(f, "ALL"), SetQuantifier::Distinct => write!(f, "DISTINCT"), + SetQuantifier::ByName => write!(f, "BY NAME"), + SetQuantifier::AllByName => write!(f, "ALL BY NAME"), SetQuantifier::None => write!(f, ""), } } diff --git a/src/keywords.rs b/src/keywords.rs index 32556c6d9..80c605dd7 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -384,6 +384,7 @@ define_keywords!( MSCK, MULTISET, MUTATION, + NAME, NANOSECOND, NANOSECONDS, NATIONAL, diff --git a/src/parser.rs b/src/parser.rs index d9b85fcc3..4d331ce07 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5331,8 +5331,14 @@ impl<'a> Parser<'a> { pub fn parse_set_quantifier(&mut self, op: &Option) -> SetQuantifier { match op { Some(SetOperator::Union) => { - if self.parse_keyword(Keyword::ALL) { - SetQuantifier::All + if self.parse_keywords(&[Keyword::BY, Keyword::NAME]) { + SetQuantifier::ByName + } else if self.parse_keyword(Keyword::ALL) { + if self.parse_keywords(&[Keyword::BY, Keyword::NAME]) { + SetQuantifier::AllByName + } else { + SetQuantifier::All + } } else if self.parse_keyword(Keyword::DISTINCT) { SetQuantifier::Distinct } else { diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index fabb9790a..83b1e537c 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -129,3 +129,150 @@ fn test_create_table_macro() { }; assert_eq!(expected, macro_); } + +#[test] +fn test_select_union_by_name() { + let ast = duckdb().verified_query("SELECT * FROM capitals UNION BY NAME SELECT * FROM weather"); + let expected = Box::::new(SetExpr::SetOperation { + op: SetOperator::Union, + set_quantifier: SetQuantifier::ByName, + left: Box::::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: None, + opt_except: None, + opt_rename: None, + opt_replace: None, + })], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "capitals".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + }))), + right: Box::::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: None, + opt_except: None, + opt_rename: None, + opt_replace: None, + })], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "weather".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + }))), + }); + + assert_eq!(ast.body, expected); + + let ast = + duckdb().verified_query("SELECT * FROM capitals UNION ALL BY NAME SELECT * FROM weather"); + let expected = Box::::new(SetExpr::SetOperation { + op: SetOperator::Union, + set_quantifier: SetQuantifier::AllByName, + left: Box::::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: None, + opt_except: None, + opt_rename: None, + opt_replace: None, + })], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "capitals".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + }))), + right: Box::::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: None, + opt_except: None, + opt_rename: None, + opt_replace: None, + })], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "weather".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + }))), + }); + assert_eq!(ast.body, expected); +} From f98a2f9dca1c3b6c54f1c4db367c669648ab9678 Mon Sep 17 00:00:00 2001 From: canalun Date: Thu, 20 Jul 2023 05:36:52 +0900 Subject: [PATCH 227/806] feat: mysql no-escape mode (#870) Co-authored-by: Andrew Lamb --- src/ast/value.rs | 53 +++++++- src/ast/visitor.rs | 3 +- src/parser.rs | 68 ++++++++-- src/tokenizer.rs | 245 ++++++++++++++++++++++------------- tests/sqlparser_common.rs | 36 ++++- tests/sqlparser_mysql.rs | 195 +++++++++++++++++++++++++++- tests/sqlparser_snowflake.rs | 6 +- 7 files changed, 485 insertions(+), 121 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index 491553cac..9c18a325c 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -71,7 +71,7 @@ impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Value::Number(v, l) => write!(f, "{}{long}", v, long = if *l { "L" } else { "" }), - Value::DoubleQuotedString(v) => write!(f, "\"{v}\""), + Value::DoubleQuotedString(v) => write!(f, "\"{}\"", escape_double_quote_string(v)), Value::SingleQuotedString(v) => write!(f, "'{}'", escape_single_quote_string(v)), Value::DollarQuotedString(v) => write!(f, "{v}"), Value::EscapedStringLiteral(v) => write!(f, "E'{}'", escape_escaped_string(v)), @@ -187,12 +187,49 @@ pub struct EscapeQuotedString<'a> { impl<'a> fmt::Display for EscapeQuotedString<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for c in self.string.chars() { - if c == self.quote { - write!(f, "{q}{q}", q = self.quote)?; - } else { - write!(f, "{c}")?; + // EscapeQuotedString doesn't know which mode of escape was + // chosen by the user. So this code must to correctly display + // strings without knowing if the strings are already escaped + // or not. + // + // If the quote symbol in the string is repeated twice, OR, if + // the quote symbol is after backslash, display all the chars + // without any escape. However, if the quote symbol is used + // just between usual chars, `fmt()` should display it twice." + // + // The following table has examples + // + // | original query | mode | AST Node | serialized | + // | ------------- | --------- | -------------------------------------------------- | ------------ | + // | `"A""B""A"` | no-escape | `DoubleQuotedString(String::from("A\"\"B\"\"A"))` | `"A""B""A"` | + // | `"A""B""A"` | default | `DoubleQuotedString(String::from("A\"B\"A"))` | `"A""B""A"` | + // | `"A\"B\"A"` | no-escape | `DoubleQuotedString(String::from("A\\\"B\\\"A"))` | `"A\"B\"A"` | + // | `"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() { + match ch { + char if char == quote => { + if previous_char == '\\' { + write!(f, "{char}")?; + 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}")?; + } + } + _ => { + write!(f, "{ch}")?; + peekable_chars.next(); + } } + previous_char = ch; } Ok(()) } @@ -206,6 +243,10 @@ pub fn escape_single_quote_string(s: &str) -> EscapeQuotedString<'_> { escape_quoted_string(s, '\'') } +pub fn escape_double_quote_string(s: &str) -> EscapeQuotedString<'_> { + escape_quoted_string(s, '\"') +} + pub struct EscapeEscapedStringLiteral<'a>(&'a str); impl<'a> fmt::Display for EscapeEscapedStringLiteral<'a> { diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 81343220a..8aa038db9 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -632,8 +632,7 @@ mod tests { fn do_visit(sql: &str) -> Vec { let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let s = Parser::new(&dialect) .with_tokens(tokens) .parse_statement() diff --git a/src/parser.rs b/src/parser.rs index 4d331ce07..790ba8fbb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -195,9 +195,52 @@ impl std::error::Error for ParserError {} // By default, allow expressions up to this deep before erroring const DEFAULT_REMAINING_DEPTH: usize = 50; -#[derive(Debug, Default, Clone, PartialEq, Eq)] +/// Options that control how the [`Parser`] parses SQL text +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ParserOptions { pub trailing_commas: bool, + /// Controls how literal values are unescaped. See + /// [`Tokenizer::with_unescape`] for more details. + pub unescape: bool, +} + +impl Default for ParserOptions { + fn default() -> Self { + Self { + trailing_commas: false, + unescape: true, + } + } +} + +impl ParserOptions { + /// Create a new [`ParserOptions`] + pub fn new() -> Self { + Default::default() + } + + /// Set if trailing commas are allowed. + /// + /// If this option is `false` (the default), the following SQL will + /// not parse. If the option is `true`, the SQL will parse. + /// + /// ```sql + /// SELECT + /// foo, + /// bar, + /// FROM baz + /// ``` + pub fn with_trailing_commas(mut self, trailing_commas: bool) -> Self { + self.trailing_commas = trailing_commas; + self + } + + /// Set if literal values are unescaped. Defaults to true. See + /// [`Tokenizer::with_unescape`] for more details. + pub fn with_unescape(mut self, unescape: bool) -> Self { + self.unescape = unescape; + self + } } pub struct Parser<'a> { @@ -206,8 +249,9 @@ pub struct Parser<'a> { index: usize, /// The current dialect to use dialect: &'a dyn Dialect, - /// Additional options that allow you to mix & match behavior otherwise - /// constrained to certain dialects (e.g. trailing commas) + /// Additional options that allow you to mix & match behavior + /// otherwise constrained to certain dialects (e.g. trailing + /// commas) and/or format of parse (e.g. unescaping) options: ParserOptions, /// ensure the stack does not overflow by limiting recursion depth recursion_counter: RecursionCounter, @@ -267,17 +311,20 @@ impl<'a> Parser<'a> { /// Specify additional parser options /// /// - /// [`Parser`] supports additional options ([`ParserOptions`]) that allow you to - /// mix & match behavior otherwise constrained to certain dialects (e.g. trailing - /// commas). + /// [`Parser`] supports additional options ([`ParserOptions`]) + /// that allow you to mix & match behavior otherwise constrained + /// to certain dialects (e.g. trailing commas). /// /// Example: /// ``` /// # use sqlparser::{parser::{Parser, ParserError, ParserOptions}, dialect::GenericDialect}; /// # fn main() -> Result<(), ParserError> { /// let dialect = GenericDialect{}; + /// let options = ParserOptions::new() + /// .with_trailing_commas(true) + /// .with_unescape(false); /// let result = Parser::new(&dialect) - /// .with_options(ParserOptions { trailing_commas: true }) + /// .with_options(options) /// .try_with_sql("SELECT a, b, COUNT(*), FROM foo GROUP BY a, b,")? /// .parse_statements(); /// assert!(matches!(result, Ok(_))); @@ -317,8 +364,9 @@ 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); - let mut tokenizer = Tokenizer::new(self.dialect, sql); - let tokens = tokenizer.tokenize()?; + let tokens = Tokenizer::new(self.dialect, sql) + .with_unescape(self.options.unescape) + .tokenize()?; Ok(self.with_tokens(tokens)) } @@ -3654,7 +3702,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; Ok(Some(ColumnOption::Check(expr))) } else if self.parse_keyword(Keyword::AUTO_INCREMENT) - && dialect_of!(self is MySqlDialect | GenericDialect) + && dialect_of!(self is MySqlDialect | GenericDialect) { // Support AUTO_INCREMENT for MySQL Ok(Some(ColumnOption::DialectSpecific(vec![ diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 6a135b439..83e9f317e 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -455,12 +455,69 @@ impl<'a> State<'a> { pub struct Tokenizer<'a> { dialect: &'a dyn Dialect, query: &'a str, + /// If true (the default), the tokenizer will un-escape literal + /// SQL strings See [`Tokenizer::with_unescape`] for more details. + unescape: bool, } impl<'a> Tokenizer<'a> { /// Create a new SQL tokenizer for the specified SQL statement + /// + /// ``` + /// # use sqlparser::tokenizer::{Token, Whitespace, Tokenizer}; + /// # use sqlparser::dialect::GenericDialect; + /// # let dialect = GenericDialect{}; + /// let query = r#"SELECT 'foo'"#; + /// + /// // Parsing the query + /// let tokens = Tokenizer::new(&dialect, &query).tokenize().unwrap(); + /// + /// assert_eq!(tokens, vec![ + /// Token::make_word("SELECT", None), + /// Token::Whitespace(Whitespace::Space), + /// Token::SingleQuotedString("foo".to_string()), + /// ]); pub fn new(dialect: &'a dyn Dialect, query: &'a str) -> Self { - Self { dialect, query } + Self { + dialect, + query, + unescape: true, + } + } + + /// Set unescape mode + /// + /// When true (default) the tokenizer unescapes literal values + /// (for example, `""` in SQL is unescaped to the literal `"`). + /// + /// When false, the tokenizer provides the raw strings as provided + /// in the query. This can be helpful for programs that wish to + /// recover the *exact* original query text without normalizing + /// the escaping + /// + /// # Example + /// + /// ``` + /// # use sqlparser::tokenizer::{Token, Tokenizer}; + /// # use sqlparser::dialect::GenericDialect; + /// # let dialect = GenericDialect{}; + /// let query = r#""Foo "" Bar""#; + /// let unescaped = Token::make_word(r#"Foo " Bar"#, Some('"')); + /// let original = Token::make_word(r#"Foo "" Bar"#, Some('"')); + /// + /// // Parsing with unescaping (default) + /// let tokens = Tokenizer::new(&dialect, &query).tokenize().unwrap(); + /// assert_eq!(tokens, vec![unescaped]); + /// + /// // Parsing with unescape = false + /// let tokens = Tokenizer::new(&dialect, &query) + /// .with_unescape(false) + /// .tokenize().unwrap(); + /// assert_eq!(tokens, vec![original]); + /// ``` + pub fn with_unescape(mut self, unescape: bool) -> Self { + self.unescape = unescape; + self } /// Tokenize the statement and produce a vector of tokens @@ -650,7 +707,7 @@ impl<'a> Tokenizer<'a> { let error_loc = chars.location(); chars.next(); // consume the opening quote let quote_end = Word::matching_end_quote(quote_start); - let (s, last_char) = parse_quoted_ident(chars, quote_end); + let (s, last_char) = self.parse_quoted_ident(chars, quote_end); if last_char == Some(quote_end) { Ok(Some(Token::make_word(&s, Some(quote_start)))) @@ -1168,6 +1225,10 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume if chars.peek().map(|c| *c == quote_style).unwrap_or(false) { s.push(ch); + if !self.unescape { + // In no-escape mode, the given query has to be saved completely + s.push(ch); + } chars.next(); } else { return Ok(s); @@ -1176,22 +1237,29 @@ impl<'a> Tokenizer<'a> { '\\' => { // consume chars.next(); - // slash escaping is specific to MySQL dialect + // slash escaping is specific to MySQL dialect. if dialect_of!(self is MySqlDialect) { if let Some(next) = chars.peek() { - // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences - let n = match next { - '\'' | '\"' | '\\' | '%' | '_' => *next, - '0' => '\0', - 'b' => '\u{8}', - 'n' => '\n', - 'r' => '\r', - 't' => '\t', - 'Z' => '\u{1a}', - _ => *next, - }; - s.push(n); - chars.next(); // consume next + if !self.unescape { + // In no-escape mode, the given query has to be saved completely including backslashes. + s.push(ch); + s.push(*next); + chars.next(); // consume next + } else { + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences + let n = match next { + '\'' | '\"' | '\\' | '%' | '_' => *next, + '0' => '\0', + 'b' => '\u{8}', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'Z' => '\u{1a}', + _ => *next, + }; + s.push(n); + chars.next(); // consume next + } } } else { s.push(ch); @@ -1239,6 +1307,29 @@ impl<'a> Tokenizer<'a> { } } + fn parse_quoted_ident(&self, chars: &mut State, quote_end: char) -> (String, Option) { + let mut last_char = None; + let mut s = String::new(); + while let Some(ch) = chars.next() { + if ch == quote_end { + if chars.peek() == Some("e_end) { + chars.next(); + s.push(ch); + if !self.unescape { + // In no-escape mode, the given query has to be saved completely + s.push(ch); + } + } else { + last_char = Some(quote_end); + break; + } + } else { + s.push(ch); + } + } + (s, last_char) + } + #[allow(clippy::unnecessary_wraps)] fn consume_and_return( &self, @@ -1266,25 +1357,6 @@ fn peeking_take_while(chars: &mut State, mut predicate: impl FnMut(char) -> bool s } -fn parse_quoted_ident(chars: &mut State, quote_end: char) -> (String, Option) { - let mut last_char = None; - let mut s = String::new(); - while let Some(ch) = chars.next() { - if ch == quote_end { - if chars.peek() == Some("e_end) { - chars.next(); - s.push(ch); - } else { - last_char = Some(quote_end); - break; - } - } else { - s.push(ch); - } - } - (s, last_char) -} - #[cfg(test)] mod tests { use super::*; @@ -1309,8 +1381,7 @@ mod tests { fn tokenize_select_1() { let sql = String::from("SELECT 1"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1325,8 +1396,7 @@ mod tests { fn tokenize_select_float() { let sql = String::from("SELECT .1"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1341,8 +1411,7 @@ mod tests { fn tokenize_select_exponent() { let sql = String::from("SELECT 1e10, 1e-10, 1e+10, 1ea, 1e-10a, 1e-10-10"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1376,8 +1445,7 @@ mod tests { fn tokenize_scalar_function() { let sql = String::from("SELECT sqrt(1)"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1395,8 +1463,7 @@ mod tests { fn tokenize_string_string_concat() { let sql = String::from("SELECT 'a' || 'b'"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1414,8 +1481,7 @@ mod tests { fn tokenize_bitwise_op() { let sql = String::from("SELECT one | two ^ three"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1438,8 +1504,7 @@ mod tests { let sql = String::from("SELECT true XOR true, false XOR false, true XOR false, false XOR true"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1478,8 +1543,7 @@ mod tests { fn tokenize_simple_select() { let sql = String::from("SELECT * FROM customer WHERE id = 1 LIMIT 5"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1510,8 +1574,7 @@ mod tests { fn tokenize_explain_select() { let sql = String::from("EXPLAIN SELECT * FROM customer WHERE id = 1"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("EXPLAIN"), @@ -1540,8 +1603,7 @@ mod tests { fn tokenize_explain_analyze_select() { let sql = String::from("EXPLAIN ANALYZE SELECT * FROM customer WHERE id = 1"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("EXPLAIN"), @@ -1572,8 +1634,7 @@ mod tests { fn tokenize_string_predicate() { let sql = String::from("SELECT * FROM customer WHERE salary != 'Not Provided'"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), @@ -1601,8 +1662,7 @@ mod tests { let sql = String::from("\n💝مصطفىh"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); // println!("tokens: {:#?}", tokens); let expected = vec![ Token::Whitespace(Whitespace::Newline), @@ -1617,8 +1677,7 @@ mod tests { let sql = String::from("'foo\r\nbar\nbaz'"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![Token::SingleQuotedString("foo\r\nbar\nbaz".to_string())]; compare(expected, tokens); } @@ -1660,8 +1719,7 @@ mod tests { let sql = String::from("\n\nSELECT * FROM table\t💝مصطفىh"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); // println!("tokens: {:#?}", tokens); let expected = vec![ Token::Whitespace(Whitespace::Newline), @@ -1684,8 +1742,7 @@ mod tests { fn tokenize_right_arrow() { let sql = String::from("FUNCTION(key=>value)"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_word("FUNCTION", None), Token::LParen, @@ -1701,8 +1758,7 @@ mod tests { fn tokenize_is_null() { let sql = String::from("a IS NULL"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_word("a", None), @@ -1720,8 +1776,7 @@ mod tests { let sql = String::from("0--this is a comment\n1"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::Number("0".to_string(), false), Token::Whitespace(Whitespace::SingleLineComment { @@ -1738,8 +1793,7 @@ mod tests { let sql = String::from("--this is a comment"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![Token::Whitespace(Whitespace::SingleLineComment { prefix: "--".to_string(), comment: "this is a comment".to_string(), @@ -1752,8 +1806,7 @@ mod tests { let sql = String::from("0/*multi-line\n* /comment*/1"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::Number("0".to_string(), false), Token::Whitespace(Whitespace::MultiLineComment( @@ -1769,8 +1822,7 @@ mod tests { let sql = String::from("0/*multi-line\n* \n/* comment \n /*comment*/*/ */ /comment*/1"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::Number("0".to_string(), false), Token::Whitespace(Whitespace::MultiLineComment( @@ -1786,8 +1838,7 @@ mod tests { let sql = String::from("\n/** Comment **/\n"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::Whitespace(Whitespace::Newline), Token::Whitespace(Whitespace::MultiLineComment("* Comment *".to_string())), @@ -1801,8 +1852,7 @@ mod tests { let sql = String::from(" \u{2003}\n"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::Whitespace(Whitespace::Space), Token::Whitespace(Whitespace::Space), @@ -1832,8 +1882,7 @@ mod tests { let sql = String::from("line1\nline2\rline3\r\nline4\r"); let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ Token::make_word("line1", None), Token::Whitespace(Whitespace::Newline), @@ -1851,8 +1900,7 @@ mod tests { fn tokenize_mssql_top() { let sql = "SELECT TOP 5 [bar] FROM foo"; let dialect = MsSqlDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), Token::Whitespace(Whitespace::Space), @@ -1873,8 +1921,7 @@ mod tests { fn tokenize_pg_regex_match() { let sql = "SELECT col ~ '^a', col ~* '^a', col !~ '^a', col !~* '^a'"; let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("SELECT"), Token::Whitespace(Whitespace::Space), @@ -1912,8 +1959,7 @@ mod tests { fn tokenize_quoted_identifier() { let sql = r#" "a "" b" "a """ "c """"" "#; let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let expected = vec![ Token::Whitespace(Whitespace::Space), Token::make_word(r#"a " b"#, Some('"')), @@ -1926,12 +1972,33 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_quoted_identifier_with_no_escape() { + let sql = r#" "a "" b" "a """ "c """"" "#; + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, sql) + .with_unescape(false) + .tokenize() + .unwrap(); + let expected = vec![ + Token::Whitespace(Whitespace::Space), + Token::make_word(r#"a "" b"#, Some('"')), + Token::Whitespace(Whitespace::Space), + Token::make_word(r#"a """#, Some('"')), + Token::Whitespace(Whitespace::Space), + Token::make_word(r#"c """""#, Some('"')), + Token::Whitespace(Whitespace::Space), + ]; + compare(expected, tokens); + } + #[test] fn tokenize_with_location() { let sql = "SELECT a,\n b"; let dialect = GenericDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, sql); - let tokens = tokenizer.tokenize_with_location().unwrap(); + let tokens = Tokenizer::new(&dialect, sql) + .tokenize_with_location() + .unwrap(); let expected = vec![ TokenWithLocation::new(Token::make_keyword("SELECT"), 1, 1), TokenWithLocation::new(Token::Whitespace(Whitespace::Space), 1, 7), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 08451ac27..356926e13 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1004,11 +1004,13 @@ fn parse_select_with_date_column_name() { } #[test] -fn parse_escaped_single_quote_string_predicate() { +fn parse_escaped_single_quote_string_predicate_with_escape() { use self::BinaryOperator::*; let sql = "SELECT id, fname, lname FROM customer \ WHERE salary <> 'Jim''s salary'"; + let ast = verified_only_select(sql); + assert_eq!( Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("salary"))), @@ -1021,6 +1023,34 @@ fn parse_escaped_single_quote_string_predicate() { ); } +#[test] +fn parse_escaped_single_quote_string_predicate_with_no_escape() { + use self::BinaryOperator::*; + let sql = "SELECT id, fname, lname FROM customer \ + WHERE salary <> 'Jim''s salary'"; + + let ast = TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: Some( + ParserOptions::new() + .with_trailing_commas(true) + .with_unescape(false), + ), + } + .verified_only_select(sql); + + assert_eq!( + Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("salary"))), + op: NotEq, + right: Box::new(Expr::Value(Value::SingleQuotedString( + "Jim''s salary".to_string() + ))), + }), + ast.selection, + ); +} + #[test] fn parse_number() { let expr = verified_expr("1.0"); @@ -7264,9 +7294,7 @@ fn parse_non_latin_identifiers() { fn parse_trailing_comma() { let trailing_commas = TestedDialects { dialects: vec![Box::new(GenericDialect {})], - options: Some(ParserOptions { - trailing_commas: true, - }), + options: Some(ParserOptions::new().with_trailing_commas(true)), }; trailing_commas.one_statement_parses_to( diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 12d2cc733..ae95de2ea 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -18,6 +18,7 @@ use sqlparser::ast::Expr; use sqlparser::ast::Value; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; +use sqlparser::parser::ParserOptions; use sqlparser::tokenizer::Token; use test_utils::*; @@ -432,10 +433,14 @@ fn parse_quote_identifiers() { } #[test] -fn parse_quote_identifiers_2() { +fn parse_escaped_quote_identifiers_with_escape() { let sql = "SELECT `quoted `` identifier`"; assert_eq!( - mysql().verified_stmt(sql), + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: None, + } + .verified_stmt(sql), Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { @@ -467,10 +472,56 @@ fn parse_quote_identifiers_2() { } #[test] -fn parse_quote_identifiers_3() { +fn parse_escaped_quote_identifiers_with_no_escape() { + let sql = "SELECT `quoted `` identifier`"; + assert_eq!( + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: Some(ParserOptions { + trailing_commas: false, + unescape: false, + }), + } + .verified_stmt(sql), + Statement::Query(Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { + value: "quoted `` identifier".into(), + quote_style: Some('`'), + }))], + into: None, + from: vec![], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + locks: vec![], + })) + ); +} + +#[test] +fn parse_escaped_backticks_with_escape() { let sql = "SELECT ```quoted identifier```"; assert_eq!( - mysql().verified_stmt(sql), + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: None, + } + .verified_stmt(sql), Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { @@ -501,6 +552,45 @@ fn parse_quote_identifiers_3() { ); } +#[test] +fn parse_escaped_backticks_with_no_escape() { + let sql = "SELECT ```quoted identifier```"; + assert_eq!( + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: Some(ParserOptions::new().with_unescape(false)), + } + .verified_stmt(sql), + Statement::Query(Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { + value: "``quoted identifier``".into(), + quote_style: Some('`'), + }))], + into: None, + from: vec![], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + locks: vec![], + })) + ); +} + #[test] fn parse_unterminated_escape() { let sql = r#"SELECT 'I\'m not fine\'"#; @@ -513,9 +603,13 @@ fn parse_unterminated_escape() { } #[test] -fn parse_escaped_string() { +fn parse_escaped_string_with_escape() { fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = mysql().one_statement_parses_to(sql, ""); + let stmt = TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: None, + } + .one_statement_parses_to(sql, ""); match stmt { Statement::Query(query) => match *query.body { @@ -544,6 +638,95 @@ fn parse_escaped_string() { assert_mysql_query_value(sql, "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} a "); } +#[test] +fn parse_escaped_string_with_no_escape() { + fn assert_mysql_query_value(sql: &str, quoted: &str) { + let stmt = TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: Some(ParserOptions::new().with_unescape(false)), + } + .one_statement_parses_to(sql, ""); + + match stmt { + Statement::Query(query) => match *query.body { + SetExpr::Select(value) => { + let expr = expr_from_projection(only(&value.projection)); + assert_eq!( + *expr, + Expr::Value(Value::SingleQuotedString(quoted.to_string())) + ); + } + _ => unreachable!(), + }, + _ => unreachable!(), + }; + } + let sql = r#"SELECT 'I\'m fine'"#; + assert_mysql_query_value(sql, r#"I\'m fine"#); + + let sql = r#"SELECT 'I''m fine'"#; + assert_mysql_query_value(sql, r#"I''m fine"#); + + let sql = r#"SELECT 'I\"m fine'"#; + assert_mysql_query_value(sql, r#"I\"m fine"#); + + let sql = r#"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"#; + assert_mysql_query_value(sql, r#"Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ "#); +} + +#[test] +fn check_roundtrip_of_escaped_string() { + let options = Some(ParserOptions::new().with_unescape(false)); + + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: options.clone(), + } + .verified_stmt(r#"SELECT 'I\'m fine'"#); + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: options.clone(), + } + .verified_stmt(r#"SELECT 'I''m fine'"#); + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: options.clone(), + } + .verified_stmt(r#"SELECT 'I\\\'m fine'"#); + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: options.clone(), + } + .verified_stmt(r#"SELECT 'I\\\'m fine'"#); + + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: options.clone(), + } + .verified_stmt(r#"SELECT "I\"m fine""#); + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: options.clone(), + } + .verified_stmt(r#"SELECT "I""m fine""#); + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: options.clone(), + } + .verified_stmt(r#"SELECT "I\\\"m fine""#); + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options: options.clone(), + } + .verified_stmt(r#"SELECT "I\\\"m fine""#); + + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + options, + } + .verified_stmt(r#"SELECT "I'm ''fine''""#); +} + #[test] fn parse_create_table_with_minimum_display_width() { let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3), bar_smallint SMALLINT(5), bar_mediumint MEDIUMINT(6), bar_int INT(11), bar_bigint BIGINT(20))"; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 9a54c89cf..43ebb8b11 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -55,8 +55,7 @@ fn test_snowflake_create_transient_table() { fn test_snowflake_single_line_tokenize() { let sql = "CREATE TABLE# this is a comment \ntable_1"; let dialect = SnowflakeDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("CREATE"), @@ -72,8 +71,7 @@ fn test_snowflake_single_line_tokenize() { assert_eq!(expected, tokens); let sql = "CREATE TABLE // this is a comment \ntable_1"; - let mut tokenizer = Tokenizer::new(&dialect, sql); - let tokens = tokenizer.tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let expected = vec![ Token::make_keyword("CREATE"), From a4520541116a8dc9fe21b6a4b3fbd947504263c7 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 19 Jul 2023 17:29:21 -0400 Subject: [PATCH 228/806] CHANGELOG for 0.36.0 (#924) --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f52f7fac..b5b713e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,25 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.36.0] 2023-07-19 + +### Added +* Support toggling "unescape" mode to retain original escaping (#870) - Thanks @canalun +* Support UNION (ALL) BY NAME syntax (#915) - Thanks @parkma99 +* Add doc comment for all operators (#917) - Thanks @izveigor +* Support `PGOverlap` operator (#912) - Thanks @izveigor +* Support multi args for unnest (#909) - Thanks @jayzhan211 +* Support `ALTER VIEW`, MySQL syntax (#907) - Thanks @liadgiladi +* Add DeltaLake keywords (#906) - Thanks @roeap + +### Fixed +* Parse JsonOperators correctly (#913) - Thanks @izveigor +* Fix dependabot by removing rust-toolchain toml (#922) - Thanks @alamb + +### Changed +* Clean up JSON operator tokenizing code (#923) - Thanks @alamb +* Upgrade bigdecimal to 0.4.1 (#921) - Thanks @jinlee0 +* Remove most instances of #[cfg(feature(bigdecimal))] in tests (#910) - Thanks @alamb ## [0.35.0] 2023-06-23 From e36b34d8ccc63bb1a8d5061dab8baa4af6e82da7 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 19 Jul 2023 17:31:43 -0400 Subject: [PATCH 229/806] chore: Release sqlparser version 0.36.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 03a08f66a..6a2869540 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.35.0" +version = "0.36.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 3a412152b97678406729d4e8134dd79a78dc3a43 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 21 Jul 2023 05:55:41 -0400 Subject: [PATCH 230/806] fix parsing of identifiers after `%` symbol (#927) --- src/test_utils.rs | 16 +++++++++++----- src/tokenizer.rs | 8 +++++--- tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 47fb00d5d..0ec595095 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -116,6 +116,16 @@ impl TestedDialects { only_statement } + /// 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 { + let ast = self + .run_parser_method(sql, |parser| parser.parse_expr()) + .unwrap(); + assert_eq!(canonical, &ast.to_string()); + ast + } + /// Ensures that `sql` parses as a single [Statement], and that /// re-serializing the parse result produces the same `sql` /// string (is not modified after a serialization round-trip). @@ -147,11 +157,7 @@ impl TestedDialects { /// re-serializing the parse result produces the same `sql` /// string (is not modified after a serialization round-trip). pub fn verified_expr(&self, sql: &str) -> Expr { - let ast = self - .run_parser_method(sql, |parser| parser.parse_expr()) - .unwrap(); - assert_eq!(sql, &ast.to_string(), "round-tripping without changes"); - ast + self.expr_parses_to(sql, sql) } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 83e9f317e..f20e01b71 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -424,6 +424,7 @@ struct State<'a> { } impl<'a> State<'a> { + /// return the next character and advance the stream pub fn next(&mut self) -> Option { match self.peekable.next() { None => None, @@ -439,6 +440,7 @@ impl<'a> State<'a> { } } + /// return the next character but do not advance the stream pub fn peek(&mut self) -> Option<&char> { self.peekable.peek() } @@ -849,13 +851,13 @@ impl<'a> Tokenizer<'a> { '+' => self.consume_and_return(chars, Token::Plus), '*' => self.consume_and_return(chars, Token::Mul), '%' => { - chars.next(); + chars.next(); // advance past '%' match chars.peek() { - Some(' ') => self.consume_and_return(chars, Token::Mod), + Some(' ') => Ok(Some(Token::Mod)), Some(sch) if self.dialect.is_identifier_start('%') => { self.tokenize_identifier_or_keyword([ch, *sch], chars) } - _ => self.consume_and_return(chars, Token::Mod), + _ => Ok(Some(Token::Mod)), } } '|' => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 356926e13..a9fd419ea 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1143,6 +1143,20 @@ fn parse_unary_math_with_multiply() { ); } +#[test] +fn parse_mod() { + use self::Expr::*; + let sql = "a % b"; + assert_eq!( + BinaryOp { + left: Box::new(Identifier(Ident::new("a"))), + op: BinaryOperator::Modulo, + right: Box::new(Identifier(Ident::new("b"))), + }, + verified_expr(sql) + ); +} + fn pg_and_generic() -> TestedDialects { TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(GenericDialect {})], @@ -1178,6 +1192,24 @@ fn parse_json_ops_without_colon() { } } +#[test] +fn parse_mod_no_spaces() { + use self::Expr::*; + let canonical = "a1 % b1"; + let sqls = ["a1 % b1", "a1% b1", "a1 %b1", "a1%b1"]; + for sql in sqls { + println!("Parsing {sql}"); + assert_eq!( + BinaryOp { + left: Box::new(Identifier(Ident::new("a1"))), + op: BinaryOperator::Modulo, + right: Box::new(Identifier(Ident::new("b1"))), + }, + pg_and_generic().expr_parses_to(sql, canonical) + ); + } +} + #[test] fn parse_is_null() { use self::Expr::*; From 91ef061254b9b962ae9b07d7bff365a729bd9ab2 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 21 Jul 2023 06:00:48 -0400 Subject: [PATCH 231/806] Changelog for 0.36.1 (#928) --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5b713e6f..8669f0584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.36.1] 2023-07-19 + +### Fixed +* Fix parsing of identifiers after '%' symbol (#927) - Thanks @alamb + ## [0.36.0] 2023-07-19 ### Added From f60a6f758ce12aa84fa3801a38c9501f55f245ef Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 21 Jul 2023 05:16:48 -0500 Subject: [PATCH 232/806] chore: Release sqlparser version 0.36.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6a2869540..777e9f4ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.36.0" +version = "0.36.1" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 53593f1982b343f04943c2be85a947a1c041b786 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Wed, 26 Jul 2023 22:47:02 +0300 Subject: [PATCH 233/806] Fix parsing of datetime functions without parenthesis (#930) --- src/parser.rs | 9 ++-- tests/sqlparser_common.rs | 101 +++++++++----------------------------- 2 files changed, 29 insertions(+), 81 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 790ba8fbb..e0c82c5e6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -957,17 +957,18 @@ impl<'a> Parser<'a> { } pub fn parse_time_functions(&mut self, name: ObjectName) -> Result { - let (args, order_by) = if self.consume_token(&Token::LParen) { - self.parse_optional_args_with_orderby()? + let (args, order_by, special) = if self.consume_token(&Token::LParen) { + let (args, order_by) = self.parse_optional_args_with_orderby()?; + (args, order_by, false) } else { - (vec![], vec![]) + (vec![], vec![], true) }; Ok(Expr::Function(Function { name, args, over: None, distinct: false, - special: false, + special, order_by, })) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a9fd419ea..31f252204 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6703,90 +6703,37 @@ fn parse_offset_and_limit() { #[test] fn parse_time_functions() { - let sql = "SELECT CURRENT_TIMESTAMP()"; - let select = verified_only_select(sql); - assert_eq!( - &Expr::Function(Function { - name: ObjectName(vec![Ident::new("CURRENT_TIMESTAMP")]), - args: vec![], - over: None, - distinct: false, - special: false, - order_by: vec![], - }), - expr_from_projection(&select.projection[0]) - ); - - // Validating Parenthesis - one_statement_parses_to("SELECT CURRENT_TIMESTAMP", sql); - - let sql = "SELECT CURRENT_TIME()"; - let select = verified_only_select(sql); - assert_eq!( - &Expr::Function(Function { - name: ObjectName(vec![Ident::new("CURRENT_TIME")]), - args: vec![], - over: None, - distinct: false, - special: false, - order_by: vec![], - }), - expr_from_projection(&select.projection[0]) - ); - - // Validating Parenthesis - one_statement_parses_to("SELECT CURRENT_TIME", sql); - - let sql = "SELECT CURRENT_DATE()"; - let select = verified_only_select(sql); - assert_eq!( - &Expr::Function(Function { - name: ObjectName(vec![Ident::new("CURRENT_DATE")]), - args: vec![], - over: None, - distinct: false, - special: false, - order_by: vec![], - }), - expr_from_projection(&select.projection[0]) - ); - - // Validating Parenthesis - one_statement_parses_to("SELECT CURRENT_DATE", sql); - - let sql = "SELECT LOCALTIME()"; - let select = verified_only_select(sql); - assert_eq!( - &Expr::Function(Function { - name: ObjectName(vec![Ident::new("LOCALTIME")]), + fn test_time_function(func_name: &'static str) { + let sql = format!("SELECT {}()", func_name); + let select = verified_only_select(&sql); + let select_localtime_func_call_ast = Function { + name: ObjectName(vec![Ident::new(func_name)]), args: vec![], over: None, distinct: false, special: false, order_by: vec![], - }), - expr_from_projection(&select.projection[0]) - ); - - // Validating Parenthesis - one_statement_parses_to("SELECT LOCALTIME", sql); + }; + assert_eq!( + &Expr::Function(select_localtime_func_call_ast.clone()), + expr_from_projection(&select.projection[0]) + ); - let sql = "SELECT LOCALTIMESTAMP()"; - let select = verified_only_select(sql); - assert_eq!( - &Expr::Function(Function { - name: ObjectName(vec![Ident::new("LOCALTIMESTAMP")]), - args: vec![], - over: None, - distinct: false, - special: false, - order_by: vec![], - }), - expr_from_projection(&select.projection[0]) - ); + // Validating Parenthesis + let sql_without_parens = format!("SELECT {}", func_name); + let mut ast_without_parens = select_localtime_func_call_ast.clone(); + ast_without_parens.special = true; + assert_eq!( + &Expr::Function(ast_without_parens.clone()), + expr_from_projection(&verified_only_select(&sql_without_parens).projection[0]) + ); + } - // Validating Parenthesis - one_statement_parses_to("SELECT LOCALTIMESTAMP", sql); + test_time_function("CURRENT_TIMESTAMP"); + test_time_function("CURRENT_TIME"); + test_time_function("CURRENT_DATE"); + test_time_function("LOCALTIME"); + test_time_function("LOCALTIMESTAMP"); } #[test] From 0ddb85341034f95eb8dff22b385b7d0c21006de2 Mon Sep 17 00:00:00 2001 From: Kikkon <19528375+Kikkon@users.noreply.github.com> Date: Thu, 27 Jul 2023 18:20:24 +0800 Subject: [PATCH 234/806] feat: support pg type alias (#933) --- src/ast/data_type.rs | 61 +++++++++++++++++++++++++++++-- src/keywords.rs | 6 ++++ src/parser.rs | 27 ++++++++++++++ tests/sqlparser_postgres.rs | 71 +++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 647e7b35e..bc4ecd4f5 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -97,6 +97,14 @@ pub enum DataType { TinyInt(Option), /// Unsigned tiny integer with optional display width e.g. TINYINT UNSIGNED or TINYINT(3) UNSIGNED UnsignedTinyInt(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) + /// + /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + Int2(Option), + /// Unsigned Int2 with optional display width e.g. INT2 Unsigned or INT2(5) Unsigned + UnsignedInt2(Option), /// 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 @@ -109,20 +117,44 @@ pub enum DataType { /// /// [1]: https://dev.mysql.com/doc/refman/8.0/en/integer-types.html UnsignedMediumInt(Option), - /// Integer 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) + /// + /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + Int4(Option), /// Integer with optional display width e.g. INTEGER or INTEGER(11) Integer(Option), - /// Unsigned integer 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 UnsignedInt(Option), + /// Unsigned int4 with optional display width e.g. INT4 UNSIGNED or INT4(11) UNSIGNED + UnsignedInt4(Option), /// Unsigned integer with optional display width e.g. INTGER UNSIGNED or INTEGER(11) UNSIGNED UnsignedInteger(Option), /// 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 UnsignedBigInt(Option), + /// Int8 as alias for Bigint in [postgresql] + /// Note: Int8 mean 8 bytes in postgres (not 8 bits) + /// Int8 with optional display width e.g. INT8 or INT8(11) + /// + /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + Int8(Option), + /// Unsigned Int8 with optional display width e.g. INT8 UNSIGNED or INT8(11) UNSIGNED + UnsignedInt8(Option), + /// FLOAT4 as alias for Real in [postgresql] + /// + /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + FLOAT4, /// Floating point e.g. REAL Real, + /// FLOAT8 as alias for Double in [postgresql] + /// + /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + FLOAT8, /// Double Double, /// Double PRECISION e.g. [standard], [postgresql] @@ -130,6 +162,10 @@ pub enum DataType { /// [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] + /// + /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html + Bool, /// Boolean Boolean, /// Date @@ -213,6 +249,12 @@ impl fmt::Display for DataType { DataType::UnsignedTinyInt(zerofill) => { format_type_with_optional_length(f, "TINYINT", zerofill, true) } + DataType::Int2(zerofill) => { + format_type_with_optional_length(f, "INT2", zerofill, false) + } + DataType::UnsignedInt2(zerofill) => { + format_type_with_optional_length(f, "INT2", zerofill, true) + } DataType::SmallInt(zerofill) => { format_type_with_optional_length(f, "SMALLINT", zerofill, false) } @@ -229,6 +271,12 @@ impl fmt::Display for DataType { DataType::UnsignedInt(zerofill) => { format_type_with_optional_length(f, "INT", zerofill, true) } + DataType::Int4(zerofill) => { + format_type_with_optional_length(f, "INT4", zerofill, false) + } + DataType::UnsignedInt4(zerofill) => { + format_type_with_optional_length(f, "INT4", zerofill, true) + } DataType::Integer(zerofill) => { format_type_with_optional_length(f, "INTEGER", zerofill, false) } @@ -241,9 +289,18 @@ impl fmt::Display for DataType { DataType::UnsignedBigInt(zerofill) => { format_type_with_optional_length(f, "BIGINT", zerofill, true) } + DataType::Int8(zerofill) => { + format_type_with_optional_length(f, "INT8", zerofill, false) + } + DataType::UnsignedInt8(zerofill) => { + format_type_with_optional_length(f, "INT8", zerofill, true) + } DataType::Real => write!(f, "REAL"), + DataType::FLOAT4 => write!(f, "FLOAT4"), DataType::Double => write!(f, "DOUBLE"), + DataType::FLOAT8 => write!(f, "FLOAT8"), DataType::DoublePrecision => write!(f, "DOUBLE PRECISION"), + DataType::Bool => write!(f, "BOOL"), DataType::Boolean => write!(f, "BOOLEAN"), DataType::Date => write!(f, "DATE"), DataType::Time(precision, timezone_info) => { diff --git a/src/keywords.rs b/src/keywords.rs index 80c605dd7..98e039414 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -111,6 +111,7 @@ define_keywords!( BINARY, BLOB, BLOOMFILTER, + BOOL, BOOLEAN, BOTH, BTREE, @@ -263,6 +264,8 @@ define_keywords!( FIRST, FIRST_VALUE, FLOAT, + FLOAT4, + FLOAT8, FLOOR, FOLLOWING, FOR, @@ -317,6 +320,9 @@ define_keywords!( INSENSITIVE, INSERT, INT, + INT2, + INT4, + INT8, INTEGER, INTERSECT, INTERSECTION, diff --git a/src/parser.rs b/src/parser.rs index e0c82c5e6..b8f36332e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4560,8 +4560,11 @@ impl<'a> Parser<'a> { let mut data = match next_token.token { Token::Word(w) => match w.keyword { Keyword::BOOLEAN => Ok(DataType::Boolean), + Keyword::BOOL => Ok(DataType::Bool), Keyword::FLOAT => Ok(DataType::Float(self.parse_optional_precision()?)), Keyword::REAL => Ok(DataType::Real), + Keyword::FLOAT4 => Ok(DataType::FLOAT4), + Keyword::FLOAT8 => Ok(DataType::FLOAT8), Keyword::DOUBLE => { if self.parse_keyword(Keyword::PRECISION) { Ok(DataType::DoublePrecision) @@ -4577,6 +4580,14 @@ impl<'a> Parser<'a> { Ok(DataType::TinyInt(optional_precision?)) } } + Keyword::INT2 => { + let optional_precision = self.parse_optional_precision(); + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::UnsignedInt2(optional_precision?)) + } else { + Ok(DataType::Int2(optional_precision?)) + } + } Keyword::SMALLINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { @@ -4601,6 +4612,14 @@ impl<'a> Parser<'a> { Ok(DataType::Int(optional_precision?)) } } + Keyword::INT4 => { + let optional_precision = self.parse_optional_precision(); + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::UnsignedInt4(optional_precision?)) + } else { + Ok(DataType::Int4(optional_precision?)) + } + } Keyword::INTEGER => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { @@ -4617,6 +4636,14 @@ impl<'a> Parser<'a> { Ok(DataType::BigInt(optional_precision?)) } } + Keyword::INT8 => { + let optional_precision = self.parse_optional_precision(); + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::UnsignedInt8(optional_precision?)) + } else { + Ok(DataType::Int8(optional_precision?)) + } + } Keyword::VARCHAR => Ok(DataType::Varchar(self.parse_optional_character_length()?)), Keyword::NVARCHAR => Ok(DataType::Nvarchar(self.parse_optional_precision()?)), Keyword::CHARACTER => { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 024ead6a3..fe55b7415 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2945,3 +2945,74 @@ fn parse_truncate() { truncate ); } + +#[test] +fn parse_create_table_with_alias() { + let sql = "CREATE TABLE public.datatype_aliases + ( + int8_col INT8, + int4_col INT4, + int2_col INT2, + float8_col FLOAT8, + float4_col FLOAT4, + bool_col BOOL, + );"; + match pg_and_generic().one_statement_parses_to(sql, "") { + Statement::CreateTable { + name, + columns, + constraints, + with_options: _with_options, + if_not_exists: false, + external: false, + file_format: None, + location: None, + .. + } => { + assert_eq!("public.datatype_aliases", name.to_string()); + assert_eq!( + columns, + vec![ + ColumnDef { + name: "int8_col".into(), + data_type: DataType::Int8(None), + collation: None, + options: vec![] + }, + ColumnDef { + name: "int4_col".into(), + data_type: DataType::Int4(None), + collation: None, + options: vec![] + }, + ColumnDef { + name: "int2_col".into(), + data_type: DataType::Int2(None), + collation: None, + options: vec![] + }, + ColumnDef { + name: "float8_col".into(), + data_type: DataType::FLOAT8, + collation: None, + options: vec![] + }, + ColumnDef { + name: "float4_col".into(), + data_type: DataType::FLOAT4, + collation: None, + options: vec![] + }, + ColumnDef { + name: "bool_col".into(), + data_type: DataType::Bool, + collation: None, + options: vec![] + }, + ] + ); + assert!(constraints.is_empty()); + } + _ => unreachable!(), + } +} From 10a6ec56371e32f5fa145074e797f8ec74f6d812 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Thu, 27 Jul 2023 14:32:55 +0300 Subject: [PATCH 235/806] Fix "BEGIN TRANSACTION" being serialized as "START TRANSACTION" (#935) --- src/ast/mod.rs | 22 ++++++++++++++++++---- src/parser.rs | 2 ++ tests/sqlparser_common.rs | 10 +++++----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1cf5a7688..d08ef0e2b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1540,8 +1540,15 @@ pub enum Statement { /// /// Note: This is a MySQL-specific statement. Use { db_name: Ident }, - /// `{ BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...` - StartTransaction { modes: Vec }, + /// `START [ TRANSACTION | WORK ] | START TRANSACTION } ...` + /// If `begin` is false. + /// + /// `BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...` + /// If `begin` is true + StartTransaction { + modes: Vec, + begin: bool, + }, /// `SET TRANSACTION ...` SetTransaction { modes: Vec, @@ -2720,8 +2727,15 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::StartTransaction { modes } => { - write!(f, "START TRANSACTION")?; + Statement::StartTransaction { + modes, + begin: syntax_begin, + } => { + if *syntax_begin { + write!(f, "BEGIN TRANSACTION")?; + } else { + write!(f, "START TRANSACTION")?; + } if !modes.is_empty() { write!(f, " {}", display_comma_separated(modes))?; } diff --git a/src/parser.rs b/src/parser.rs index b8f36332e..7dd07d050 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6905,6 +6905,7 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::TRANSACTION)?; Ok(Statement::StartTransaction { modes: self.parse_transaction_modes()?, + begin: false, }) } @@ -6912,6 +6913,7 @@ impl<'a> Parser<'a> { let _ = self.parse_one_of_keywords(&[Keyword::TRANSACTION, Keyword::WORK]); Ok(Statement::StartTransaction { modes: self.parse_transaction_modes()?, + begin: true, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 31f252204..444e6d346 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5732,7 +5732,7 @@ fn lateral_derived() { #[test] fn parse_start_transaction() { match verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { - Statement::StartTransaction { modes } => assert_eq!( + Statement::StartTransaction { modes, .. } => assert_eq!( modes, vec![ TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), @@ -5749,7 +5749,7 @@ fn parse_start_transaction() { "START TRANSACTION READ ONLY READ WRITE ISOLATION LEVEL SERIALIZABLE", "START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE", ) { - Statement::StartTransaction { modes } => assert_eq!( + Statement::StartTransaction { modes, .. } => assert_eq!( modes, vec![ TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), @@ -5761,9 +5761,9 @@ fn parse_start_transaction() { } verified_stmt("START TRANSACTION"); - one_statement_parses_to("BEGIN", "START TRANSACTION"); - one_statement_parses_to("BEGIN WORK", "START TRANSACTION"); - one_statement_parses_to("BEGIN TRANSACTION", "START TRANSACTION"); + one_statement_parses_to("BEGIN", "BEGIN TRANSACTION"); + one_statement_parses_to("BEGIN WORK", "BEGIN TRANSACTION"); + one_statement_parses_to("BEGIN TRANSACTION", "BEGIN TRANSACTION"); verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED"); From eb4be989806f3f325ffadcdf5821b81f325ec22d Mon Sep 17 00:00:00 2001 From: liadgiladi <51291468+liadgiladi@users.noreply.github.com> Date: Mon, 7 Aug 2023 17:54:24 +0300 Subject: [PATCH 236/806] Support `DROP TEMPORARY TABLE`, MySQL syntax (#916) --- src/ast/mod.rs | 6 +++++- src/parser.rs | 5 +++++ tests/sqlparser_common.rs | 4 ++++ tests/sqlparser_mysql.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d08ef0e2b..ad8c76298 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1401,6 +1401,8 @@ pub enum Statement { /// Hive allows you specify whether the table's stored data will be /// deleted along with the dropped table purge: bool, + /// MySQL-specific "TEMPORARY" keyword + temporary: bool, }, /// DROP Function DropFunction { @@ -2572,9 +2574,11 @@ impl fmt::Display for Statement { cascade, restrict, purge, + temporary, } => write!( f, - "DROP {}{} {}{}{}{}", + "DROP {}{}{} {}{}{}{}", + if *temporary { "TEMPORARY " } else { "" }, object_type, if *if_exists { " IF EXISTS" } else { "" }, display_comma_separated(names), diff --git a/src/parser.rs b/src/parser.rs index 7dd07d050..91e21ec38 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3127,6 +3127,10 @@ impl<'a> Parser<'a> { } pub fn parse_drop(&mut self) -> Result { + // MySQL dialect supports `TEMPORARY` + let temporary = dialect_of!(self is MySqlDialect | GenericDialect) + && self.parse_keyword(Keyword::TEMPORARY); + let object_type = if self.parse_keyword(Keyword::TABLE) { ObjectType::Table } else if self.parse_keyword(Keyword::VIEW) { @@ -3169,6 +3173,7 @@ impl<'a> Parser<'a> { cascade, restrict, purge, + temporary, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 444e6d346..9ec182f21 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5432,6 +5432,7 @@ fn parse_drop_table() { names, cascade, purge: _, + temporary, .. } => { assert!(!if_exists); @@ -5441,6 +5442,7 @@ fn parse_drop_table() { names.iter().map(ToString::to_string).collect::>() ); assert!(!cascade); + assert!(!temporary); } _ => unreachable!(), } @@ -5453,6 +5455,7 @@ fn parse_drop_table() { names, cascade, purge: _, + temporary, .. } => { assert!(if_exists); @@ -5462,6 +5465,7 @@ fn parse_drop_table() { names.iter().map(ToString::to_string).collect::>() ); assert!(cascade); + assert!(!temporary); } _ => unreachable!(), } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ae95de2ea..07337ec70 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1597,3 +1597,29 @@ fn parse_string_introducers() { fn parse_div_infix() { mysql().verified_stmt(r#"SELECT 5 DIV 2"#); } + +#[test] +fn parse_drop_temporary_table() { + let sql = "DROP TEMPORARY TABLE foo"; + match mysql().verified_stmt(sql) { + Statement::Drop { + object_type, + if_exists, + names, + cascade, + purge: _, + temporary, + .. + } => { + assert!(!if_exists); + assert_eq!(ObjectType::Table, object_type); + assert_eq!( + vec!["foo"], + names.iter().map(ToString::to_string).collect::>() + ); + assert!(!cascade); + assert!(temporary); + } + _ => unreachable!(), + } +} From 173a6db8189a0f1bb9896117c86374c4e691f0f3 Mon Sep 17 00:00:00 2001 From: Kikkon <19528375+Kikkon@users.noreply.github.com> Date: Mon, 7 Aug 2023 22:55:42 +0800 Subject: [PATCH 237/806] Fix: use Rust idiomatic capitalization for newly added `DataType` enums (#939) --- src/ast/data_type.rs | 12 ++++++------ src/parser.rs | 4 ++-- tests/sqlparser_postgres.rs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index bc4ecd4f5..2a6a004f4 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -145,16 +145,16 @@ pub enum DataType { Int8(Option), /// Unsigned Int8 with optional display width e.g. INT8 UNSIGNED or INT8(11) UNSIGNED UnsignedInt8(Option), - /// FLOAT4 as alias for Real in [postgresql] + /// Float4 as alias for Real in [postgresql] /// /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html - FLOAT4, + Float4, /// Floating point e.g. REAL Real, - /// FLOAT8 as alias for Double in [postgresql] + /// Float8 as alias for Double in [postgresql] /// /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html - FLOAT8, + Float8, /// Double Double, /// Double PRECISION e.g. [standard], [postgresql] @@ -296,9 +296,9 @@ impl fmt::Display for DataType { format_type_with_optional_length(f, "INT8", zerofill, true) } DataType::Real => write!(f, "REAL"), - DataType::FLOAT4 => write!(f, "FLOAT4"), + DataType::Float4 => write!(f, "FLOAT4"), DataType::Double => write!(f, "DOUBLE"), - DataType::FLOAT8 => write!(f, "FLOAT8"), + DataType::Float8 => write!(f, "FLOAT8"), DataType::DoublePrecision => write!(f, "DOUBLE PRECISION"), DataType::Bool => write!(f, "BOOL"), DataType::Boolean => write!(f, "BOOLEAN"), diff --git a/src/parser.rs b/src/parser.rs index 91e21ec38..20e587c74 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4568,8 +4568,8 @@ impl<'a> Parser<'a> { Keyword::BOOL => Ok(DataType::Bool), Keyword::FLOAT => Ok(DataType::Float(self.parse_optional_precision()?)), Keyword::REAL => Ok(DataType::Real), - Keyword::FLOAT4 => Ok(DataType::FLOAT4), - Keyword::FLOAT8 => Ok(DataType::FLOAT8), + Keyword::FLOAT4 => Ok(DataType::Float4), + Keyword::FLOAT8 => Ok(DataType::Float8), Keyword::DOUBLE => { if self.parse_keyword(Keyword::PRECISION) { Ok(DataType::DoublePrecision) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fe55b7415..f6bb80d7e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2993,13 +2993,13 @@ fn parse_create_table_with_alias() { }, ColumnDef { name: "float8_col".into(), - data_type: DataType::FLOAT8, + data_type: DataType::Float8, collation: None, options: vec![] }, ColumnDef { name: "float4_col".into(), - data_type: DataType::FLOAT4, + data_type: DataType::Float4, collation: None, options: vec![] }, From 8bbb85356ceb50e2d45545da0e8a15da3680353e Mon Sep 17 00:00:00 2001 From: Jeremy Maness Date: Thu, 17 Aug 2023 06:17:57 -0400 Subject: [PATCH 238/806] Fix SUBSTRING from/to argument construction for mssql (#947) --- src/ast/mod.rs | 17 +++++++++-- src/dialect/mod.rs | 4 +++ src/dialect/mssql.rs | 4 +++ src/parser.rs | 56 ++++++++++++++++++++++++++----------- tests/sqlparser_common.rs | 34 +++++++++++++++++++--- tests/sqlparser_mssql.rs | 59 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_mysql.rs | 3 +- 7 files changed, 153 insertions(+), 24 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ad8c76298..0a366f632 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -477,6 +477,10 @@ pub enum Expr { expr: Box, substring_from: Option>, substring_for: Option>, + + // Some dialects use `SUBSTRING(expr [FROM start] [FOR len])` syntax while others omit FROM, + // FOR keywords (e.g. Microsoft SQL Server). This flags is used for formatting. + special: bool, }, /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) @@ -830,13 +834,22 @@ impl fmt::Display for Expr { expr, substring_from, substring_for, + special, } => { write!(f, "SUBSTRING({expr}")?; if let Some(from_part) = substring_from { - write!(f, " FROM {from_part}")?; + if *special { + write!(f, ", {from_part}")?; + } else { + write!(f, " FROM {from_part}")?; + } } if let Some(for_part) = substring_for { - write!(f, " FOR {for_part}")?; + if *special { + write!(f, ", {for_part}")?; + } else { + write!(f, " FOR {for_part}")?; + } } write!(f, ")") diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 8b3a58888..e174528b0 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -117,6 +117,10 @@ pub trait Dialect: Debug + Any { fn supports_group_by_expr(&self) -> bool { false } + /// Returns true if the dialect supports `SUBSTRING(expr [FROM start] [FOR len])` expressions + fn supports_substring_from_for_expr(&self) -> bool { + true + } /// Dialect-specific prefix parser override fn parse_prefix(&self, _parser: &mut Parser) -> Option> { // return None to fall back to the default behavior diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 6d1f49cd7..f04398100 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -34,4 +34,8 @@ impl Dialect for MsSqlDialect { || ch == '#' || ch == '_' } + + fn supports_substring_from_for_expr(&self) -> bool { + false + } } diff --git a/src/parser.rs b/src/parser.rs index 20e587c74..5d44ce9ce 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1223,25 +1223,47 @@ impl<'a> Parser<'a> { } pub fn parse_substring_expr(&mut self) -> Result { - // PARSE SUBSTRING (EXPR [FROM 1] [FOR 3]) - self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; - let mut from_expr = None; - if self.parse_keyword(Keyword::FROM) || self.consume_token(&Token::Comma) { - from_expr = Some(self.parse_expr()?); - } + if self.dialect.supports_substring_from_for_expr() { + // PARSE SUBSTRING (EXPR [FROM 1] [FOR 3]) + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + let mut from_expr = None; + if self.parse_keyword(Keyword::FROM) || self.consume_token(&Token::Comma) { + from_expr = Some(self.parse_expr()?); + } - let mut to_expr = None; - if self.parse_keyword(Keyword::FOR) || self.consume_token(&Token::Comma) { - to_expr = Some(self.parse_expr()?); - } - self.expect_token(&Token::RParen)?; + let mut to_expr = None; + if self.parse_keyword(Keyword::FOR) || self.consume_token(&Token::Comma) { + to_expr = Some(self.parse_expr()?); + } + self.expect_token(&Token::RParen)?; - Ok(Expr::Substring { - expr: Box::new(expr), - substring_from: from_expr.map(Box::new), - substring_for: to_expr.map(Box::new), - }) + Ok(Expr::Substring { + expr: Box::new(expr), + substring_from: from_expr.map(Box::new), + substring_for: to_expr.map(Box::new), + special: !self.dialect.supports_substring_from_for_expr(), + }) + } else { + // PARSE SUBSTRING(EXPR, start, length) + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + + self.expect_token(&Token::Comma)?; + let from_expr = Some(self.parse_expr()?); + + self.expect_token(&Token::Comma)?; + let to_expr = Some(self.parse_expr()?); + + self.expect_token(&Token::RParen)?; + + Ok(Expr::Substring { + expr: Box::new(expr), + substring_from: from_expr.map(Box::new), + substring_for: to_expr.map(Box::new), + special: !self.dialect.supports_substring_from_for_expr(), + }) + } } pub fn parse_overlay_expr(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9ec182f21..ca30e9516 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5084,19 +5084,45 @@ fn parse_scalar_subqueries() { #[test] fn parse_substring() { - one_statement_parses_to("SELECT SUBSTRING('1')", "SELECT SUBSTRING('1')"); + let from_for_supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(HiveDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), + ], + options: None, + }; - one_statement_parses_to( + let from_for_unsupported_dialects = TestedDialects { + dialects: vec![Box::new(MsSqlDialect {})], + options: None, + }; + + from_for_supported_dialects + .one_statement_parses_to("SELECT SUBSTRING('1')", "SELECT SUBSTRING('1')"); + + from_for_supported_dialects.one_statement_parses_to( "SELECT SUBSTRING('1' FROM 1)", "SELECT SUBSTRING('1' FROM 1)", ); - one_statement_parses_to( + from_for_supported_dialects.one_statement_parses_to( "SELECT SUBSTRING('1' FROM 1 FOR 3)", "SELECT SUBSTRING('1' FROM 1 FOR 3)", ); - one_statement_parses_to("SELECT SUBSTRING('1' FOR 3)", "SELECT SUBSTRING('1' FOR 3)"); + from_for_unsupported_dialects + .one_statement_parses_to("SELECT SUBSTRING('1', 1, 3)", "SELECT SUBSTRING('1', 1, 3)"); + + from_for_supported_dialects + .one_statement_parses_to("SELECT SUBSTRING('1' FOR 3)", "SELECT SUBSTRING('1' FOR 3)"); } #[test] diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b46a0c6c9..d4e093ad1 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -379,6 +379,65 @@ fn parse_similar_to() { chk(true); } +#[test] +fn parse_substring_in_select() { + let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; + match ms().one_statement_parses_to( + sql, + "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test", + ) { + Statement::Query(query) => { + assert_eq!( + Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: Some(Distinct::Distinct), + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::Substring { + expr: Box::new(Expr::Identifier(Ident { + value: "description".to_string(), + quote_style: None + })), + substring_from: Some(Box::new(Expr::Value(number("0")))), + substring_for: Some(Box::new(Expr::Value(number("1")))), + special: true, + })], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "test".to_string(), + quote_style: None + }]), + alias: None, + args: None, + with_hints: vec![] + }, + joins: vec![] + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None + }))), + order_by: vec![], + limit: None, + offset: None, + fetch: None, + locks: vec![], + }), + query + ); + } + _ => unreachable!(), + } +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 07337ec70..9618786d3 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1271,7 +1271,8 @@ fn parse_substring_in_select() { quote_style: None })), substring_from: Some(Box::new(Expr::Value(number("0")))), - substring_for: Some(Box::new(Expr::Value(number("1")))) + substring_for: Some(Box::new(Expr::Value(number("1")))), + special: false, })], into: None, from: vec![TableWithJoins { From 83e30677b071e7dd8b595c414b98f90995a2a7cf Mon Sep 17 00:00:00 2001 From: ehoeve Date: Thu, 17 Aug 2023 12:44:55 +0200 Subject: [PATCH 239/806] Add support for table-level comments (#946) Co-authored-by: Andrew Lamb --- src/ast/helpers/stmt_create_table.rs | 10 ++++++++++ src/ast/mod.rs | 5 +++++ src/parser.rs | 12 ++++++++++++ tests/sqlparser_mysql.rs | 16 ++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 2998935d9..a00555fd8 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -65,6 +65,7 @@ pub struct CreateTableBuilder { pub like: Option, pub clone: Option, pub engine: Option, + pub comment: Option, pub default_charset: Option, pub collation: Option, pub on_commit: Option, @@ -96,6 +97,7 @@ impl CreateTableBuilder { like: None, clone: None, engine: None, + comment: None, default_charset: None, collation: None, on_commit: None, @@ -197,6 +199,11 @@ impl CreateTableBuilder { self } + pub fn comment(mut self, comment: Option) -> Self { + self.comment = comment; + self + } + pub fn default_charset(mut self, default_charset: Option) -> Self { self.default_charset = default_charset; self @@ -249,6 +256,7 @@ impl CreateTableBuilder { like: self.like, clone: self.clone, engine: self.engine, + comment: self.comment, default_charset: self.default_charset, collation: self.collation, on_commit: self.on_commit, @@ -287,6 +295,7 @@ impl TryFrom for CreateTableBuilder { like, clone, engine, + comment, default_charset, collation, on_commit, @@ -314,6 +323,7 @@ impl TryFrom for CreateTableBuilder { like, clone, engine, + comment, default_charset, collation, on_commit, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0a366f632..41a5530a3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1319,6 +1319,7 @@ pub enum Statement { like: Option, clone: Option, engine: Option, + comment: Option, default_charset: Option, collation: Option, on_commit: Option, @@ -2250,6 +2251,7 @@ impl fmt::Display for Statement { clone, default_charset, engine, + comment, collation, on_commit, on_cluster, @@ -2401,6 +2403,9 @@ impl fmt::Display for Statement { if let Some(engine) = engine { write!(f, " ENGINE={engine}")?; } + if let Some(comment) = comment { + write!(f, " COMMENT '{comment}'")?; + } if let Some(order_by) = order_by { write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; } diff --git a/src/parser.rs b/src/parser.rs index 5d44ce9ce..34456e098 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3503,6 +3503,17 @@ impl<'a> Parser<'a> { None }; + let comment = if self.parse_keyword(Keyword::COMMENT) { + let _ = self.consume_token(&Token::Eq); + let next_token = self.next_token(); + match next_token.token { + Token::SingleQuotedString(str) => Some(str), + _ => self.expected("comment", next_token)?, + } + } else { + None + }; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { if self.consume_token(&Token::LParen) { let columns = if self.peek_token() != Token::RParen { @@ -3583,6 +3594,7 @@ impl<'a> Parser<'a> { .like(like) .clone_clause(clone) .engine(engine) + .comment(comment) .order_by(order_by) .default_charset(default_charset) .collation(collation) diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 9618786d3..c0a51edab 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -296,6 +296,22 @@ fn parse_create_table_auto_increment() { } } +#[test] +fn parse_create_table_comment() { + let canonical = "CREATE TABLE foo (bar INT) COMMENT 'baz'"; + let with_equal = "CREATE TABLE foo (bar INT) COMMENT = 'baz'"; + + for sql in [canonical, with_equal] { + match mysql().one_statement_parses_to(sql, canonical) { + Statement::CreateTable { name, comment, .. } => { + assert_eq!(name.to_string(), "foo"); + assert_eq!(comment.expect("Should exist").to_string(), "baz"); + } + _ => unreachable!(), + } + } +} + #[test] fn parse_create_table_set_enum() { let sql = "CREATE TABLE foo (bar SET('a', 'b'), baz ENUM('a', 'b'))"; From a7d28582e557b6d7c177d5e98809de1090581fc6 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 17 Aug 2023 06:45:18 -0400 Subject: [PATCH 240/806] Minor: clarify the value of the special flag (#948) --- src/parser.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 34456e098..fcac8f235 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1242,7 +1242,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), substring_from: from_expr.map(Box::new), substring_for: to_expr.map(Box::new), - special: !self.dialect.supports_substring_from_for_expr(), + special: false, }) } else { // PARSE SUBSTRING(EXPR, start, length) @@ -1261,7 +1261,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), substring_from: from_expr.map(Box::new), substring_for: to_expr.map(Box::new), - special: !self.dialect.supports_substring_from_for_expr(), + special: true, }) } } From a49ea1908d3862f3471e3e1304c15926314eda48 Mon Sep 17 00:00:00 2001 From: "r.4ntix" Date: Thu, 17 Aug 2023 20:05:54 +0800 Subject: [PATCH 241/806] feat: add `ALTER ROLE` syntax of PostgreSQL and MS SQL Server (#942) --- src/ast/dcl.rs | 195 +++++++++++++++++++++++++++++ src/ast/mod.rs | 10 ++ src/keywords.rs | 1 + src/parser/alter.rs | 204 +++++++++++++++++++++++++++++++ src/{parser.rs => parser/mod.rs} | 11 +- tests/sqlparser_mssql.rs | 54 ++++++++ tests/sqlparser_postgres.rs | 193 +++++++++++++++++++++++++++++ 7 files changed, 666 insertions(+), 2 deletions(-) create mode 100644 src/ast/dcl.rs create mode 100644 src/parser/alter.rs rename src/{parser.rs => parser/mod.rs} (99%) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs new file mode 100644 index 000000000..f90de34d4 --- /dev/null +++ b/src/ast/dcl.rs @@ -0,0 +1,195 @@ +// Licensed 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. + +//! AST types specific to GRANT/REVOKE/ROLE variants of [`Statement`](crate::ast::Statement) +//! (commonly referred to as Data Control Language, or DCL) + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use core::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +use super::{Expr, Ident, Password}; +use crate::ast::{display_separated, ObjectName}; + +/// An option in `ROLE` 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 RoleOption { + BypassRLS(bool), + ConnectionLimit(Expr), + CreateDB(bool), + CreateRole(bool), + Inherit(bool), + Login(bool), + Password(Password), + Replication(bool), + SuperUser(bool), + ValidUntil(Expr), +} + +impl fmt::Display for RoleOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RoleOption::BypassRLS(value) => { + write!(f, "{}", if *value { "BYPASSRLS" } else { "NOBYPASSRLS" }) + } + RoleOption::ConnectionLimit(expr) => { + write!(f, "CONNECTION LIMIT {expr}") + } + RoleOption::CreateDB(value) => { + write!(f, "{}", if *value { "CREATEDB" } else { "NOCREATEDB" }) + } + RoleOption::CreateRole(value) => { + write!(f, "{}", if *value { "CREATEROLE" } else { "NOCREATEROLE" }) + } + RoleOption::Inherit(value) => { + write!(f, "{}", if *value { "INHERIT" } else { "NOINHERIT" }) + } + RoleOption::Login(value) => { + write!(f, "{}", if *value { "LOGIN" } else { "NOLOGIN" }) + } + RoleOption::Password(password) => match password { + Password::Password(expr) => write!(f, "PASSWORD {expr}"), + Password::NullPassword => write!(f, "PASSWORD NULL"), + }, + RoleOption::Replication(value) => { + write!( + f, + "{}", + if *value { + "REPLICATION" + } else { + "NOREPLICATION" + } + ) + } + RoleOption::SuperUser(value) => { + write!(f, "{}", if *value { "SUPERUSER" } else { "NOSUPERUSER" }) + } + RoleOption::ValidUntil(expr) => { + write!(f, "VALID UNTIL {expr}") + } + } + } +} + +/// SET config value option: +/// * SET `configuration_parameter` { TO | = } { `value` | DEFAULT } +/// * SET `configuration_parameter` FROM CURRENT +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SetConfigValue { + Default, + FromCurrent, + Value(Expr), +} + +/// RESET config option: +/// * RESET `configuration_parameter` +/// * RESET ALL +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ResetConfig { + ALL, + ConfigName(ObjectName), +} + +/// An `ALTER ROLE` (`Statement::AlterRole`) operation +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterRoleOperation { + /// Generic + RenameRole { + role_name: Ident, + }, + /// MS SQL Server + /// + AddMember { + member_name: Ident, + }, + DropMember { + member_name: Ident, + }, + /// PostgreSQL + /// + WithOptions { + options: Vec, + }, + Set { + config_name: ObjectName, + config_value: SetConfigValue, + in_database: Option, + }, + Reset { + config_name: ResetConfig, + in_database: Option, + }, +} + +impl fmt::Display for AlterRoleOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterRoleOperation::RenameRole { role_name } => { + write!(f, "RENAME TO {role_name}") + } + AlterRoleOperation::AddMember { member_name } => { + write!(f, "ADD MEMBER {member_name}") + } + AlterRoleOperation::DropMember { member_name } => { + write!(f, "DROP MEMBER {member_name}") + } + AlterRoleOperation::WithOptions { options } => { + write!(f, "WITH {}", display_separated(options, " ")) + } + AlterRoleOperation::Set { + config_name, + config_value, + in_database, + } => { + if let Some(database_name) = in_database { + write!(f, "IN DATABASE {} ", database_name)?; + } + + match config_value { + SetConfigValue::Default => write!(f, "SET {config_name} TO DEFAULT"), + SetConfigValue::FromCurrent => write!(f, "SET {config_name} FROM CURRENT"), + SetConfigValue::Value(expr) => write!(f, "SET {config_name} TO {expr}"), + } + } + AlterRoleOperation::Reset { + config_name, + in_database, + } => { + if let Some(database_name) = in_database { + write!(f, "IN DATABASE {} ", database_name)?; + } + + match config_name { + ResetConfig::ALL => write!(f, "RESET ALL"), + ResetConfig::ConfigName(name) => write!(f, "RESET {name}"), + } + } + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 41a5530a3..edab3c63a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -28,6 +28,7 @@ use sqlparser_derive::{Visit, VisitMut}; pub use self::data_type::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, }; +pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue}; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, ProcedureParam, ReferentialAction, @@ -52,6 +53,7 @@ use crate::ast::helpers::stmt_data_loading::{ pub use visitor::*; mod data_type; +mod dcl; mod ddl; pub mod helpers; mod operator; @@ -1398,6 +1400,11 @@ pub enum Statement { query: Box, with_options: Vec, }, + /// ALTER ROLE + AlterRole { + name: Ident, + operation: AlterRoleOperation, + }, /// DROP Drop { /// The type of the object to drop: TABLE, VIEW, etc. @@ -2585,6 +2592,9 @@ impl fmt::Display for Statement { } write!(f, " AS {query}") } + Statement::AlterRole { name, operation } => { + write!(f, "ALTER ROLE {name} {operation}") + } Statement::Drop { object_type, if_exists, diff --git a/src/keywords.rs b/src/keywords.rs index 98e039414..5f3e44022 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -508,6 +508,7 @@ define_keywords!( REPEATABLE, REPLACE, REPLICATION, + RESET, RESTRICT, RESULT, RETAIN, diff --git a/src/parser/alter.rs b/src/parser/alter.rs new file mode 100644 index 000000000..838b64899 --- /dev/null +++ b/src/parser/alter.rs @@ -0,0 +1,204 @@ +// Licensed 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. + +//! SQL Parser for ALTER + +#[cfg(not(feature = "std"))] +use alloc::vec; + +use super::{Parser, ParserError}; +use crate::{ + ast::{AlterRoleOperation, Expr, Password, ResetConfig, RoleOption, SetConfigValue, Statement}, + dialect::{MsSqlDialect, PostgreSqlDialect}, + keywords::Keyword, + tokenizer::Token, +}; + +impl<'a> Parser<'a> { + pub fn parse_alter_role(&mut self) -> Result { + if dialect_of!(self is PostgreSqlDialect) { + return self.parse_pg_alter_role(); + } else if dialect_of!(self is MsSqlDialect) { + return self.parse_mssql_alter_role(); + } + + Err(ParserError::ParserError( + "ALTER ROLE is only support for PostgreSqlDialect, MsSqlDialect".into(), + )) + } + + fn parse_mssql_alter_role(&mut self) -> Result { + let role_name = self.parse_identifier()?; + + let operation = if self.parse_keywords(&[Keyword::ADD, Keyword::MEMBER]) { + let member_name = self.parse_identifier()?; + AlterRoleOperation::AddMember { member_name } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::MEMBER]) { + let member_name = self.parse_identifier()?; + AlterRoleOperation::DropMember { member_name } + } else if self.parse_keywords(&[Keyword::WITH, Keyword::NAME]) { + if self.consume_token(&Token::Eq) { + let role_name = self.parse_identifier()?; + AlterRoleOperation::RenameRole { role_name } + } else { + return self.expected("= after WITH NAME ", self.peek_token()); + } + } else { + return self.expected("'ADD' or 'DROP' or 'WITH NAME'", self.peek_token()); + }; + + Ok(Statement::AlterRole { + name: role_name, + operation, + }) + } + + fn parse_pg_alter_role(&mut self) -> Result { + let role_name = self.parse_identifier()?; + + // [ IN DATABASE _`database_name`_ ] + let in_database = if self.parse_keywords(&[Keyword::IN, Keyword::DATABASE]) { + self.parse_object_name().ok() + } else { + None + }; + + let operation = if self.parse_keyword(Keyword::RENAME) { + if self.parse_keyword(Keyword::TO) { + let role_name = self.parse_identifier()?; + AlterRoleOperation::RenameRole { role_name } + } else { + return self.expected("TO after RENAME", self.peek_token()); + } + // SET + } else if self.parse_keyword(Keyword::SET) { + let config_name = self.parse_object_name()?; + // FROM CURRENT + if self.parse_keywords(&[Keyword::FROM, Keyword::CURRENT]) { + AlterRoleOperation::Set { + config_name, + config_value: SetConfigValue::FromCurrent, + in_database, + } + // { TO | = } { value | DEFAULT } + } else if self.consume_token(&Token::Eq) || self.parse_keyword(Keyword::TO) { + if self.parse_keyword(Keyword::DEFAULT) { + AlterRoleOperation::Set { + config_name, + config_value: SetConfigValue::Default, + in_database, + } + } else if let Ok(expr) = self.parse_expr() { + AlterRoleOperation::Set { + config_name, + config_value: SetConfigValue::Value(expr), + in_database, + } + } else { + self.expected("config value", self.peek_token())? + } + } else { + self.expected("'TO' or '=' or 'FROM CURRENT'", self.peek_token())? + } + // RESET + } else if self.parse_keyword(Keyword::RESET) { + if self.parse_keyword(Keyword::ALL) { + AlterRoleOperation::Reset { + config_name: ResetConfig::ALL, + in_database, + } + } else { + let config_name = self.parse_object_name()?; + AlterRoleOperation::Reset { + config_name: ResetConfig::ConfigName(config_name), + in_database, + } + } + // option + } else { + // [ WITH ] + let _ = self.parse_keyword(Keyword::WITH); + // option + let mut options = vec![]; + while let Some(opt) = self.maybe_parse(|parser| parser.parse_pg_role_option()) { + options.push(opt); + } + // check option + if options.is_empty() { + return self.expected("option", self.peek_token())?; + } + + AlterRoleOperation::WithOptions { options } + }; + + Ok(Statement::AlterRole { + name: role_name, + operation, + }) + } + + fn parse_pg_role_option(&mut self) -> Result { + let option = match self.parse_one_of_keywords(&[ + Keyword::BYPASSRLS, + Keyword::NOBYPASSRLS, + Keyword::CONNECTION, + Keyword::CREATEDB, + Keyword::NOCREATEDB, + Keyword::CREATEROLE, + Keyword::NOCREATEROLE, + Keyword::INHERIT, + Keyword::NOINHERIT, + Keyword::LOGIN, + Keyword::NOLOGIN, + Keyword::PASSWORD, + Keyword::REPLICATION, + Keyword::NOREPLICATION, + Keyword::SUPERUSER, + Keyword::NOSUPERUSER, + Keyword::VALID, + ]) { + Some(Keyword::BYPASSRLS) => RoleOption::BypassRLS(true), + Some(Keyword::NOBYPASSRLS) => RoleOption::BypassRLS(false), + Some(Keyword::CONNECTION) => { + self.expect_keyword(Keyword::LIMIT)?; + RoleOption::ConnectionLimit(Expr::Value(self.parse_number_value()?)) + } + Some(Keyword::CREATEDB) => RoleOption::CreateDB(true), + Some(Keyword::NOCREATEDB) => RoleOption::CreateDB(false), + Some(Keyword::CREATEROLE) => RoleOption::CreateRole(true), + Some(Keyword::NOCREATEROLE) => RoleOption::CreateRole(false), + Some(Keyword::INHERIT) => RoleOption::Inherit(true), + Some(Keyword::NOINHERIT) => RoleOption::Inherit(false), + Some(Keyword::LOGIN) => RoleOption::Login(true), + Some(Keyword::NOLOGIN) => RoleOption::Login(false), + Some(Keyword::PASSWORD) => { + let password = if self.parse_keyword(Keyword::NULL) { + Password::NullPassword + } else { + Password::Password(Expr::Value(self.parse_value()?)) + }; + RoleOption::Password(password) + } + Some(Keyword::REPLICATION) => RoleOption::Replication(true), + Some(Keyword::NOREPLICATION) => RoleOption::Replication(false), + Some(Keyword::SUPERUSER) => RoleOption::SuperUser(true), + Some(Keyword::NOSUPERUSER) => RoleOption::SuperUser(false), + Some(Keyword::VALID) => { + self.expect_keyword(Keyword::UNTIL)?; + RoleOption::ValidUntil(Expr::Value(self.parse_value()?)) + } + _ => self.expected("option", self.peek_token())?, + }; + + Ok(option) + } +} diff --git a/src/parser.rs b/src/parser/mod.rs similarity index 99% rename from src/parser.rs rename to src/parser/mod.rs index fcac8f235..d6f45359e 100644 --- a/src/parser.rs +++ b/src/parser/mod.rs @@ -33,6 +33,8 @@ use crate::dialect::*; use crate::keywords::{self, Keyword}; use crate::tokenizer::*; +mod alter; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ParserError { TokenizerError(String), @@ -3990,8 +3992,12 @@ impl<'a> Parser<'a> { } pub fn parse_alter(&mut self) -> Result { - let object_type = - self.expect_one_of_keywords(&[Keyword::VIEW, Keyword::TABLE, Keyword::INDEX])?; + let object_type = self.expect_one_of_keywords(&[ + Keyword::VIEW, + Keyword::TABLE, + Keyword::INDEX, + Keyword::ROLE, + ])?; match object_type { Keyword::VIEW => self.parse_alter_view(), Keyword::TABLE => { @@ -4186,6 +4192,7 @@ impl<'a> Parser<'a> { operation, }) } + Keyword::ROLE => self.parse_alter_role(), // unreachable because expect_one_of_keywords used above _ => unreachable!(), } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index d4e093ad1..56fbd576e 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -216,6 +216,60 @@ fn parse_mssql_create_role() { } } +#[test] +fn parse_alter_role() { + let sql = "ALTER ROLE old_name WITH NAME = new_name"; + assert_eq!( + ms().parse_sql_statements(sql).unwrap(), + [Statement::AlterRole { + name: Ident { + value: "old_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::RenameRole { + role_name: Ident { + value: "new_name".into(), + quote_style: None + } + }, + }] + ); + + let sql = "ALTER ROLE role_name ADD MEMBER new_member"; + assert_eq!( + ms().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::AddMember { + member_name: Ident { + value: "new_member".into(), + quote_style: None + } + }, + } + ); + + let sql = "ALTER ROLE role_name DROP MEMBER old_member"; + assert_eq!( + ms().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::DropMember { + member_name: Ident { + value: "old_member".into(), + quote_style: None + } + }, + } + ); +} + #[test] fn parse_delimited_identifiers() { // check that quoted identifiers in any position remain quoted after serialization diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index f6bb80d7e..09196db46 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2447,6 +2447,199 @@ fn parse_create_role() { } } +#[test] +fn parse_alter_role() { + let sql = "ALTER ROLE old_name RENAME TO new_name"; + assert_eq!( + pg().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "old_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::RenameRole { + role_name: Ident { + value: "new_name".into(), + quote_style: None + } + }, + } + ); + + let sql = "ALTER ROLE role_name WITH SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN REPLICATION BYPASSRLS CONNECTION LIMIT 100 PASSWORD 'abcdef' VALID UNTIL '2025-01-01'"; + assert_eq!( + pg().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::WithOptions { + options: vec![ + RoleOption::SuperUser(true), + RoleOption::CreateDB(true), + RoleOption::CreateRole(true), + RoleOption::Inherit(true), + RoleOption::Login(true), + RoleOption::Replication(true), + RoleOption::BypassRLS(true), + RoleOption::ConnectionLimit(Expr::Value(number("100"))), + RoleOption::Password({ + Password::Password(Expr::Value(Value::SingleQuotedString("abcdef".into()))) + }), + RoleOption::ValidUntil(Expr::Value(Value::SingleQuotedString( + "2025-01-01".into(), + ))) + ] + }, + } + ); + + let sql = "ALTER ROLE role_name WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOLOGIN NOREPLICATION NOBYPASSRLS PASSWORD NULL"; + assert_eq!( + pg().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::WithOptions { + options: vec![ + RoleOption::SuperUser(false), + RoleOption::CreateDB(false), + RoleOption::CreateRole(false), + RoleOption::Inherit(false), + RoleOption::Login(false), + RoleOption::Replication(false), + RoleOption::BypassRLS(false), + RoleOption::Password(Password::NullPassword), + ] + }, + } + ); + + let sql = "ALTER ROLE role_name SET maintenance_work_mem FROM CURRENT"; + assert_eq!( + pg().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::Set { + config_name: ObjectName(vec![Ident { + value: "maintenance_work_mem".into(), + quote_style: None + }]), + config_value: SetConfigValue::FromCurrent, + in_database: None + }, + } + ); + + let sql = "ALTER ROLE role_name IN DATABASE database_name SET maintenance_work_mem = 100000"; + assert_eq!( + pg().parse_sql_statements(sql).unwrap(), + [Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::Set { + config_name: ObjectName(vec![Ident { + value: "maintenance_work_mem".into(), + quote_style: None + }]), + config_value: SetConfigValue::Value(Expr::Value(number("100000"))), + in_database: Some(ObjectName(vec![Ident { + value: "database_name".into(), + quote_style: None + }])) + }, + }] + ); + + let sql = "ALTER ROLE role_name IN DATABASE database_name SET maintenance_work_mem TO 100000"; + assert_eq!( + pg().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::Set { + config_name: ObjectName(vec![Ident { + value: "maintenance_work_mem".into(), + quote_style: None + }]), + config_value: SetConfigValue::Value(Expr::Value(number("100000"))), + in_database: Some(ObjectName(vec![Ident { + value: "database_name".into(), + quote_style: None + }])) + }, + } + ); + + let sql = "ALTER ROLE role_name IN DATABASE database_name SET maintenance_work_mem TO DEFAULT"; + assert_eq!( + pg().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::Set { + config_name: ObjectName(vec![Ident { + value: "maintenance_work_mem".into(), + quote_style: None + }]), + config_value: SetConfigValue::Default, + in_database: Some(ObjectName(vec![Ident { + value: "database_name".into(), + quote_style: None + }])) + }, + } + ); + + let sql = "ALTER ROLE role_name RESET ALL"; + assert_eq!( + pg().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::Reset { + config_name: ResetConfig::ALL, + in_database: None + }, + } + ); + + let sql = "ALTER ROLE role_name IN DATABASE database_name RESET maintenance_work_mem"; + assert_eq!( + pg().verified_stmt(sql), + Statement::AlterRole { + name: Ident { + value: "role_name".into(), + quote_style: None + }, + operation: AlterRoleOperation::Reset { + config_name: ResetConfig::ConfigName(ObjectName(vec![Ident { + value: "maintenance_work_mem".into(), + quote_style: None + }])), + in_database: Some(ObjectName(vec![Ident { + value: "database_name".into(), + quote_style: None + }])) + }, + } + ); +} + #[test] fn parse_delimited_identifiers() { // check that quoted identifiers in any position remain quoted after serialization From 9a39afbe07b6eecd8819fe1f7d9af2b4aad3a6d7 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Thu, 17 Aug 2023 16:47:11 +0100 Subject: [PATCH 242/806] feat: support more Postgres index syntax (#943) --- src/ast/mod.rs | 35 ++++++- src/keywords.rs | 2 + src/parser/mod.rs | 38 ++++++- tests/sqlparser_common.rs | 12 ++- tests/sqlparser_postgres.rs | 202 ++++++++++++++++++++++++++++++++++++ 5 files changed, 279 insertions(+), 10 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index edab3c63a..47419a893 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1348,13 +1348,17 @@ pub enum Statement { /// CREATE INDEX CreateIndex { /// index name - name: ObjectName, + name: Option, #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, using: Option, columns: Vec, unique: bool, + concurrently: bool, if_not_exists: bool, + include: Vec, + nulls_distinct: Option, + predicate: Option, }, /// CREATE ROLE /// See [postgres](https://www.postgresql.org/docs/current/sql-createrole.html) @@ -2464,20 +2468,41 @@ impl fmt::Display for Statement { using, columns, unique, + concurrently, if_not_exists, + include, + nulls_distinct, + predicate, } => { write!( f, - "CREATE {unique}INDEX {if_not_exists}{name} ON {table_name}", + "CREATE {unique}INDEX {concurrently}{if_not_exists}", unique = if *unique { "UNIQUE " } else { "" }, + concurrently = if *concurrently { "CONCURRENTLY " } else { "" }, if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - name = name, - table_name = table_name )?; + if let Some(value) = name { + write!(f, "{value} ")?; + } + write!(f, "ON {table_name}")?; if let Some(value) = using { write!(f, " USING {value} ")?; } - write!(f, "({})", display_separated(columns, ",")) + write!(f, "({})", display_separated(columns, ","))?; + if !include.is_empty() { + write!(f, " INCLUDE ({})", display_separated(include, ","))?; + } + if let Some(value) = nulls_distinct { + if *value { + write!(f, " NULLS DISTINCT")?; + } else { + write!(f, " NULLS NOT DISTINCT")?; + } + } + if let Some(predicate) = predicate { + write!(f, " WHERE {predicate}")?; + } + Ok(()) } Statement::CreateRole { names, diff --git a/src/keywords.rs b/src/keywords.rs index 5f3e44022..c73535fca 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -153,6 +153,7 @@ define_keywords!( COMMITTED, COMPRESSION, COMPUTE, + CONCURRENTLY, CONDITION, CONFLICT, CONNECT, @@ -310,6 +311,7 @@ define_keywords!( ILIKE, IMMUTABLE, IN, + INCLUDE, INCREMENT, INDEX, INDICATOR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d6f45359e..94814627d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3370,9 +3370,15 @@ impl<'a> Parser<'a> { } pub fn parse_create_index(&mut self, unique: bool) -> Result { + let concurrently = self.parse_keyword(Keyword::CONCURRENTLY); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let index_name = self.parse_object_name()?; - self.expect_keyword(Keyword::ON)?; + let index_name = if if_not_exists || !self.parse_keyword(Keyword::ON) { + let index_name = self.parse_object_name()?; + self.expect_keyword(Keyword::ON)?; + Some(index_name) + } else { + None + }; let table_name = self.parse_object_name()?; let using = if self.parse_keyword(Keyword::USING) { Some(self.parse_identifier()?) @@ -3382,13 +3388,41 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let columns = self.parse_comma_separated(Parser::parse_order_by_expr)?; self.expect_token(&Token::RParen)?; + + let include = if self.parse_keyword(Keyword::INCLUDE) { + self.expect_token(&Token::LParen)?; + let columns = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + columns + } else { + vec![] + }; + + let nulls_distinct = if self.parse_keyword(Keyword::NULLS) { + let not = self.parse_keyword(Keyword::NOT); + self.expect_keyword(Keyword::DISTINCT)?; + Some(!not) + } else { + None + }; + + let predicate = if self.parse_keyword(Keyword::WHERE) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(Statement::CreateIndex { name: index_name, table_name, using, columns, unique, + concurrently, if_not_exists, + include, + nulls_distinct, + predicate, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ca30e9516..8dfcc6e7f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5981,7 +5981,7 @@ fn parse_create_index() { ]; match verified_stmt(sql) { Statement::CreateIndex { - name, + name: Some(name), table_name, columns, unique, @@ -6015,19 +6015,25 @@ fn test_create_index_with_using_function() { ]; match verified_stmt(sql) { Statement::CreateIndex { - name, + name: Some(name), table_name, using, columns, unique, + concurrently, if_not_exists, + include, + nulls_distinct: None, + predicate: None, } => { assert_eq!("idx_name", name.to_string()); assert_eq!("test", table_name.to_string()); assert_eq!("btree", using.unwrap().to_string()); assert_eq!(indexed_columns, columns); assert!(unique); - assert!(if_not_exists) + assert!(!concurrently); + assert!(if_not_exists); + assert!(include.is_empty()); } _ => unreachable!(), } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 09196db46..a62c41e42 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1761,6 +1761,208 @@ fn parse_array_index_expr() { ); } +#[test] +fn parse_create_index() { + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using, + columns, + unique, + concurrently, + if_not_exists, + nulls_distinct: None, + include, + predicate: None, + } => { + assert_eq_vec(&["my_index"], &name); + assert_eq_vec(&["my_table"], &table_name); + assert_eq!(None, using); + assert!(!unique); + assert!(!concurrently); + assert!(if_not_exists); + assert_eq_vec(&["col1", "col2"], &columns); + assert!(include.is_empty()); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_anonymous_index() { + let sql = "CREATE INDEX ON my_table(col1,col2)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex { + name, + table_name: ObjectName(table_name), + using, + columns, + unique, + concurrently, + if_not_exists, + include, + nulls_distinct: None, + predicate: None, + } => { + assert_eq!(None, name); + assert_eq_vec(&["my_table"], &table_name); + assert_eq!(None, using); + assert!(!unique); + assert!(!concurrently); + assert!(!if_not_exists); + assert_eq_vec(&["col1", "col2"], &columns); + assert!(include.is_empty()); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_index_concurrently() { + let sql = "CREATE INDEX CONCURRENTLY IF NOT EXISTS my_index ON my_table(col1,col2)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using, + columns, + unique, + concurrently, + if_not_exists, + include, + nulls_distinct: None, + predicate: None, + } => { + assert_eq_vec(&["my_index"], &name); + assert_eq_vec(&["my_table"], &table_name); + assert_eq!(None, using); + assert!(!unique); + assert!(concurrently); + assert!(if_not_exists); + assert_eq_vec(&["col1", "col2"], &columns); + assert!(include.is_empty()); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_index_with_predicate() { + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) WHERE col3 IS NULL"; + match pg().verified_stmt(sql) { + Statement::CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using, + columns, + unique, + concurrently, + if_not_exists, + include, + nulls_distinct: None, + predicate: Some(_), + } => { + assert_eq_vec(&["my_index"], &name); + assert_eq_vec(&["my_table"], &table_name); + assert_eq!(None, using); + assert!(!unique); + assert!(!concurrently); + assert!(if_not_exists); + assert_eq_vec(&["col1", "col2"], &columns); + assert!(include.is_empty()); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_index_with_include() { + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) INCLUDE (col3)"; + match pg().verified_stmt(sql) { + Statement::CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using, + columns, + unique, + concurrently, + if_not_exists, + include, + nulls_distinct: None, + predicate: None, + } => { + assert_eq_vec(&["my_index"], &name); + assert_eq_vec(&["my_table"], &table_name); + assert_eq!(None, using); + assert!(!unique); + assert!(!concurrently); + assert!(if_not_exists); + assert_eq_vec(&["col1", "col2"], &columns); + assert_eq_vec(&["col3"], &include); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_index_with_nulls_distinct() { + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) NULLS NOT DISTINCT"; + match pg().verified_stmt(sql) { + Statement::CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using, + columns, + unique, + concurrently, + if_not_exists, + include, + nulls_distinct: Some(nulls_distinct), + predicate: None, + } => { + assert_eq_vec(&["my_index"], &name); + assert_eq_vec(&["my_table"], &table_name); + assert_eq!(None, using); + assert!(!unique); + assert!(!concurrently); + assert!(if_not_exists); + assert_eq_vec(&["col1", "col2"], &columns); + assert!(include.is_empty()); + assert!(!nulls_distinct); + } + _ => unreachable!(), + } + + let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) NULLS DISTINCT"; + match pg().verified_stmt(sql) { + Statement::CreateIndex { + name: Some(ObjectName(name)), + table_name: ObjectName(table_name), + using, + columns, + unique, + concurrently, + if_not_exists, + include, + nulls_distinct: Some(nulls_distinct), + predicate: None, + } => { + assert_eq_vec(&["my_index"], &name); + assert_eq_vec(&["my_table"], &table_name); + assert_eq!(None, using); + assert!(!unique); + assert!(!concurrently); + assert!(if_not_exists); + assert_eq_vec(&["col1", "col2"], &columns); + assert!(include.is_empty()); + assert!(nulls_distinct); + } + _ => unreachable!(), + } +} + #[test] fn parse_array_subquery_expr() { let sql = "SELECT ARRAY(SELECT 1 UNION SELECT 2)"; From 41e47cc0136c705a3335b1504d50ae8b22711b86 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 21 Aug 2023 19:21:45 +0200 Subject: [PATCH 243/806] add a test for mssql table name in square brackets (#952) --- tests/sqlparser_mssql.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 56fbd576e..0bb2ba3de 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -324,6 +324,26 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } +#[test] +fn parse_table_name_in_square_brackets() { + let select = ms().verified_only_select(r#"SELECT [a column] FROM [a schema].[a table]"#); + if let TableFactor::Table { name, .. } = only(select.from).relation { + assert_eq!( + vec![ + Ident::with_quote('[', "a schema"), + Ident::with_quote('[', "a table") + ], + name.0 + ); + } else { + panic!("Expecting TableFactor::Table"); + } + assert_eq!( + &Expr::Identifier(Ident::with_quote('[', "a column")), + expr_from_projection(&select.projection[0]), + ); +} + #[test] fn parse_like() { fn chk(negated: bool) { From 9500649c3519f638ba54c407a4a3ed834606abe0 Mon Sep 17 00:00:00 2001 From: ehoeve Date: Mon, 21 Aug 2023 22:25:32 +0200 Subject: [PATCH 244/806] Add support for MySQL auto_increment offset (#950) --- src/ast/helpers/stmt_create_table.rs | 10 ++++++++++ src/ast/mod.rs | 5 +++++ src/parser/mod.rs | 12 ++++++++++++ tests/sqlparser_mysql.rs | 25 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index a00555fd8..17327e7f8 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -66,6 +66,7 @@ pub struct CreateTableBuilder { 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, @@ -98,6 +99,7 @@ impl CreateTableBuilder { clone: None, engine: None, comment: None, + auto_increment_offset: None, default_charset: None, collation: None, on_commit: None, @@ -204,6 +206,11 @@ impl CreateTableBuilder { 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 @@ -257,6 +264,7 @@ impl CreateTableBuilder { 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, @@ -296,6 +304,7 @@ impl TryFrom for CreateTableBuilder { clone, engine, comment, + auto_increment_offset, default_charset, collation, on_commit, @@ -324,6 +333,7 @@ impl TryFrom for CreateTableBuilder { clone, engine, comment, + auto_increment_offset, default_charset, collation, on_commit, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 47419a893..50c11ba59 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1322,6 +1322,7 @@ pub enum Statement { clone: Option, engine: Option, comment: Option, + auto_increment_offset: Option, default_charset: Option, collation: Option, on_commit: Option, @@ -2263,6 +2264,7 @@ impl fmt::Display for Statement { default_charset, engine, comment, + auto_increment_offset, collation, on_commit, on_cluster, @@ -2417,6 +2419,9 @@ impl fmt::Display for Statement { if let Some(comment) = comment { write!(f, " COMMENT '{comment}'")?; } + if let Some(auto_increment_offset) = auto_increment_offset { + write!(f, " AUTO_INCREMENT {auto_increment_offset}")?; + } if let Some(order_by) = order_by { write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 94814627d..1b11237ff 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3550,6 +3550,17 @@ impl<'a> Parser<'a> { 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(s.parse::().expect("literal int")), + _ => self.expected("literal int", next_token)?, + } + } else { + None + }; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { if self.consume_token(&Token::LParen) { let columns = if self.peek_token() != Token::RParen { @@ -3631,6 +3642,7 @@ impl<'a> Parser<'a> { .clone_clause(clone) .engine(engine) .comment(comment) + .auto_increment_offset(auto_increment_offset) .order_by(order_by) .default_charset(default_charset) .collation(collation) diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index c0a51edab..209143b18 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -312,6 +312,31 @@ 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"; + + for sql in [canonical, with_equal] { + match mysql().one_statement_parses_to(sql, canonical) { + Statement::CreateTable { + name, + auto_increment_offset, + .. + } => { + assert_eq!(name.to_string(), "foo"); + assert_eq!( + auto_increment_offset.expect("Should exist").to_string(), + "123" + ); + } + _ => unreachable!(), + } + } +} + #[test] fn parse_create_table_set_enum() { let sql = "CREATE TABLE foo (bar SET('a', 'b'), baz ENUM('a', 'b'))"; From 1ea88585759f5610a8665bfc280400dce9e8be3c Mon Sep 17 00:00:00 2001 From: Marko Grujic Date: Tue, 22 Aug 2023 12:06:32 +0200 Subject: [PATCH 245/806] Table time travel clause support, add `visit_table_factor` to Visitor (#951) --- src/ast/mod.rs | 3 +- src/ast/query.rs | 24 ++++++++++ src/ast/visitor.rs | 82 +++++++++++++++++++++++++++++++---- src/parser/mod.rs | 18 ++++++++ src/test_utils.rs | 1 + tests/sqlparser_bigquery.rs | 26 +++++++++++ tests/sqlparser_clickhouse.rs | 3 ++ tests/sqlparser_common.rs | 29 +++++++++++++ tests/sqlparser_duckdb.rs | 4 ++ tests/sqlparser_hive.rs | 2 + tests/sqlparser_mssql.rs | 30 ++++++++++++- tests/sqlparser_mysql.rs | 7 ++- tests/sqlparser_postgres.rs | 2 + tests/sqlparser_redshift.rs | 4 ++ tests/sqlparser_snowflake.rs | 2 + 15 files changed, 225 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 50c11ba59..a241f9509 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,7 +40,8 @@ pub use self::query::{ JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, - TableAlias, TableFactor, TableWithJoins, Top, Values, WildcardAdditionalOptions, With, + TableAlias, TableFactor, TableVersion, TableWithJoins, Top, Values, WildcardAdditionalOptions, + With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, diff --git a/src/ast/query.rs b/src/ast/query.rs index 5f4c289dc..b70017654 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -646,6 +646,7 @@ impl fmt::Display for TableWithJoins { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "visitor", visit(with = "visit_table_factor"))] pub enum TableFactor { Table { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] @@ -661,6 +662,9 @@ pub enum TableFactor { args: Option>, /// MSSQL-specific `WITH (...)` hints such as NOLOCK. with_hints: Vec, + /// Optional version qualifier to facilitate table time-travel, as + /// supported by BigQuery and MSSQL. + version: Option, }, Derived { lateral: bool, @@ -720,6 +724,7 @@ impl fmt::Display for TableFactor { alias, args, with_hints, + version, } => { write!(f, "{name}")?; if let Some(args) = args { @@ -731,6 +736,9 @@ impl fmt::Display for TableFactor { if !with_hints.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_hints))?; } + if let Some(version) = version { + write!(f, "{version}")?; + } Ok(()) } TableFactor::Derived { @@ -835,6 +843,22 @@ impl fmt::Display for TableAlias { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableVersion { + ForSystemTimeAsOf(Expr), +} + +impl Display for TableVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TableVersion::ForSystemTimeAsOf(e) => write!(f, " FOR SYSTEM_TIME AS OF {e}")?, + } + 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/visitor.rs b/src/ast/visitor.rs index 8aa038db9..bb7c19678 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -12,7 +12,7 @@ //! Recursive visitors for ast Nodes. See [`Visitor`] for more details. -use crate::ast::{Expr, ObjectName, Statement}; +use crate::ast::{Expr, ObjectName, Statement, TableFactor}; use core::ops::ControlFlow; /// A type that can be visited by a [`Visitor`]. See [`Visitor`] for @@ -115,8 +115,8 @@ visit_noop!(bigdecimal::BigDecimal); /// A visitor that can be used to walk an AST tree. /// -/// `previst_` methods are invoked before visiting all children of the -/// node and `postvisit_` methods are invoked after visiting all +/// `pre_visit_` methods are invoked before visiting all children of the +/// node and `post_visit_` methods are invoked after visiting all /// children of the node. /// /// # See also @@ -139,7 +139,7 @@ visit_noop!(bigdecimal::BigDecimal); /// } /// /// // Visit relations and exprs before children are visited (depth first walk) -/// // Note you can also visit statements and visit exprs after children have been visitoed +/// // Note you can also visit statements and visit exprs after children have been visited /// impl Visitor for V { /// type Break = (); /// @@ -189,6 +189,16 @@ pub trait Visitor { ControlFlow::Continue(()) } + /// Invoked for any table factors that appear in the AST before visiting children + fn pre_visit_table_factor(&mut self, _table_factor: &TableFactor) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any table factors that appear in the AST after visiting children + fn post_visit_table_factor(&mut self, _table_factor: &TableFactor) -> ControlFlow { + ControlFlow::Continue(()) + } + /// Invoked for any expressions that appear in the AST before visiting children fn pre_visit_expr(&mut self, _expr: &Expr) -> ControlFlow { ControlFlow::Continue(()) @@ -212,8 +222,8 @@ pub trait Visitor { /// A visitor that can be used to mutate an AST tree. /// -/// `previst_` methods are invoked before visiting all children of the -/// node and `postvisit_` methods are invoked after visiting all +/// `pre_visit_` methods are invoked before visiting all children of the +/// node and `post_visit_` methods are invoked after visiting all /// children of the node. /// /// # See also @@ -267,6 +277,22 @@ pub trait VisitorMut { ControlFlow::Continue(()) } + /// Invoked for any table factors that appear in the AST before visiting children + fn pre_visit_table_factor( + &mut self, + _table_factor: &mut TableFactor, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any table factors that appear in the AST after visiting children + fn post_visit_table_factor( + &mut self, + _table_factor: &mut TableFactor, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + /// Invoked for any expressions that appear in the AST before visiting children fn pre_visit_expr(&mut self, _expr: &mut Expr) -> ControlFlow { ControlFlow::Continue(()) @@ -609,6 +635,24 @@ mod tests { ControlFlow::Continue(()) } + fn pre_visit_table_factor( + &mut self, + table_factor: &TableFactor, + ) -> ControlFlow { + self.visited + .push(format!("PRE: TABLE FACTOR: {table_factor}")); + ControlFlow::Continue(()) + } + + fn post_visit_table_factor( + &mut self, + table_factor: &TableFactor, + ) -> ControlFlow { + self.visited + .push(format!("POST: TABLE FACTOR: {table_factor}")); + ControlFlow::Continue(()) + } + fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow { self.visited.push(format!("PRE: EXPR: {expr}")); ControlFlow::Continue(()) @@ -647,22 +691,28 @@ mod tests { fn test_sql() { let tests = vec![ ( - "SELECT * from table_name", + "SELECT * from table_name as my_table", vec![ - "PRE: STATEMENT: SELECT * FROM table_name", + "PRE: STATEMENT: SELECT * FROM table_name AS my_table", + "PRE: TABLE FACTOR: table_name AS my_table", "PRE: RELATION: table_name", "POST: RELATION: table_name", - "POST: STATEMENT: SELECT * FROM table_name", + "POST: TABLE FACTOR: table_name AS my_table", + "POST: STATEMENT: SELECT * FROM table_name AS my_table", ], ), ( "SELECT * from t1 join t2 on t1.id = t2.t1_id", vec![ "PRE: STATEMENT: SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id", + "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", + "POST: TABLE FACTOR: t1", + "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", + "POST: TABLE FACTOR: t2", "PRE: EXPR: t1.id = t2.t1_id", "PRE: EXPR: t1.id", "POST: EXPR: t1.id", @@ -676,13 +726,17 @@ mod tests { "SELECT * from t1 where EXISTS(SELECT column from t2)", vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", + "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", + "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: EXPR: column", "POST: EXPR: column", + "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", + "POST: TABLE FACTOR: t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", ], @@ -691,13 +745,17 @@ mod tests { "SELECT * from t1 where EXISTS(SELECT column from t2)", vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", + "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", + "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: EXPR: column", "POST: EXPR: column", + "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", + "POST: TABLE FACTOR: t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", ], @@ -706,16 +764,22 @@ mod tests { "SELECT * from t1 where EXISTS(SELECT column from t2) UNION SELECT * from t3", vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2) UNION SELECT * FROM t3", + "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", + "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: EXPR: column", "POST: EXPR: column", + "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", + "POST: TABLE FACTOR: t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", + "PRE: TABLE FACTOR: t3", "PRE: RELATION: t3", "POST: RELATION: t3", + "POST: TABLE FACTOR: t3", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2) UNION SELECT * FROM t3", ], ), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1b11237ff..c2a33b42a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6210,6 +6210,9 @@ impl<'a> Parser<'a> { } else { let name = self.parse_object_name()?; + // Parse potential version qualifier + let version = self.parse_table_version()?; + // Postgres, MSSQL: table-valued functions: let args = if self.consume_token(&Token::LParen) { Some(self.parse_optional_args()?) @@ -6240,10 +6243,25 @@ impl<'a> Parser<'a> { alias, args, with_hints, + version, }) } } + /// Parse a given table version specifier. + /// + /// For now it only supports timestamp versioning for BigQuery and MSSQL dialects. + pub fn parse_table_version(&mut self) -> Result, ParserError> { + if dialect_of!(self is BigQueryDialect | MsSqlDialect) + && self.parse_keywords(&[Keyword::FOR, Keyword::SYSTEM_TIME, Keyword::AS, Keyword::OF]) + { + let expr = self.parse_expr()?; + Ok(Some(TableVersion::ForSystemTimeAsOf(expr))) + } else { + Ok(None) + } + } + pub fn parse_derived_table_factor( &mut self, lateral: IsLateral, diff --git a/src/test_utils.rs b/src/test_utils.rs index 0ec595095..91130fb51 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -221,6 +221,7 @@ pub fn table(name: impl Into) -> TableFactor { alias: None, args: None, with_hints: vec![], + version: None, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index bbe1a6e9f..ca711b26e 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -95,6 +95,7 @@ fn parse_table_identifiers() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![] },] @@ -143,6 +144,31 @@ fn parse_table_identifiers() { test_table_ident("abc5.GROUP", vec![Ident::new("abc5"), Ident::new("GROUP")]); } +#[test] +fn parse_table_time_travel() { + let version = "2023-08-18 23:08:18".to_string(); + let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); + let select = bigquery().verified_only_select(&sql); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("t1")]), + alias: None, + args: None, + with_hints: vec![], + version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( + Value::SingleQuotedString(version) + ))), + }, + joins: vec![] + },] + ); + + let sql = "SELECT 1 FROM t1 FOR SYSTEM TIME AS OF 'some_timestamp'".to_string(); + assert!(bigquery().parse_sql_statements(&sql).is_err()); +} + #[test] fn parse_join_constraint_unnest_alias() { assert_eq!( diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 24c641561..77b936d55 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -62,6 +62,7 @@ fn parse_map_access_expr() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![] }], @@ -169,11 +170,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8dfcc6e7f..96dac3da0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -214,6 +214,7 @@ fn parse_update_set_from() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }, @@ -240,6 +241,7 @@ fn parse_update_set_from() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -303,6 +305,7 @@ fn parse_update_with_table_alias() { }), args: None, with_hints: vec![], + version: None, }, joins: vec![], }, @@ -365,6 +368,7 @@ fn parse_select_with_table_alias() { }), args: None, with_hints: vec![], + version: None, }, joins: vec![], }] @@ -395,6 +399,7 @@ fn parse_delete_statement() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].relation ); @@ -422,6 +427,7 @@ fn parse_delete_statement_for_multi_tables() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].relation ); @@ -431,6 +437,7 @@ fn parse_delete_statement_for_multi_tables() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].joins[0].relation ); @@ -454,6 +461,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].relation ); @@ -463,6 +471,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { alias: None, args: None, with_hints: vec![], + version: None, }, from[1].relation ); @@ -472,6 +481,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { alias: None, args: None, with_hints: vec![], + version: None, }, using[0].relation ); @@ -481,6 +491,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { alias: None, args: None, with_hints: vec![], + version: None, }, using[0].joins[0].relation ); @@ -508,6 +519,7 @@ fn parse_where_delete_statement() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].relation, ); @@ -549,6 +561,7 @@ fn parse_where_delete_with_alias_statement() { }), args: None, with_hints: vec![], + version: None, }, from[0].relation, ); @@ -562,6 +575,7 @@ fn parse_where_delete_with_alias_statement() { }), args: None, with_hints: vec![], + version: None, }, joins: vec![], }]), @@ -3564,6 +3578,7 @@ fn test_parse_named_window() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -3902,6 +3917,7 @@ fn parse_interval_and_or_xor() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -4506,6 +4522,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }, @@ -4515,6 +4532,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }, @@ -4532,6 +4550,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -4539,6 +4558,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -4549,6 +4569,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -4556,6 +4577,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -4576,6 +4598,7 @@ fn parse_cross_join() { alias: None, args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::CrossJoin, }, @@ -4596,6 +4619,7 @@ fn parse_joins_on() { alias, args: None, with_hints: vec![], + version: None, }, join_operator: f(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -4665,6 +4689,7 @@ fn parse_joins_using() { alias, args: None, with_hints: vec![], + version: None, }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), } @@ -4726,6 +4751,7 @@ fn parse_natural_join() { alias, args: None, with_hints: vec![], + version: None, }, join_operator: f(JoinConstraint::Natural), } @@ -4990,6 +5016,7 @@ fn parse_derived_tables() { alias: None, args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -6317,6 +6344,7 @@ fn parse_merge() { }), args: None, with_hints: vec![], + version: None, } ); assert_eq!(table, table_no_into); @@ -6340,6 +6368,7 @@ fn parse_merge() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 83b1e537c..3587e8d90 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -155,6 +155,7 @@ fn test_select_union_by_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -187,6 +188,7 @@ fn test_select_union_by_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -228,6 +230,7 @@ fn test_select_union_by_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -260,6 +263,7 @@ fn test_select_union_by_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index ddc5a8ccf..8cdfe9248 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -322,11 +322,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 0bb2ba3de..c4e0f3274 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -42,6 +42,31 @@ fn parse_mssql_identifiers() { }; } +#[test] +fn parse_table_time_travel() { + let version = "2023-08-18 23:08:18".to_string(); + let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); + let select = ms().verified_only_select(&sql); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("t1")]), + alias: None, + args: None, + with_hints: vec![], + version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( + Value::SingleQuotedString(version) + ))), + }, + joins: vec![] + },] + ); + + let sql = "SELECT 1 FROM t1 FOR SYSTEM TIME AS OF 'some_timestamp'".to_string(); + assert!(ms().parse_sql_statements(&sql).is_err()); +} + #[test] fn parse_mssql_single_quoted_aliases() { let _ = ms_and_generic().one_statement_parses_to("SELECT foo 'alias'", "SELECT foo AS 'alias'"); @@ -283,11 +308,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } @@ -485,7 +512,8 @@ fn parse_substring_in_select() { }]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], + version: None, }, joins: vec![] }], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 209143b18..3a0177df4 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1093,6 +1093,7 @@ fn parse_select_with_numeric_prefix_column_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![] }], @@ -1141,6 +1142,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![] }], @@ -1200,6 +1202,7 @@ fn parse_update_with_joins() { }), args: None, with_hints: vec![], + version: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -1210,6 +1213,7 @@ fn parse_update_with_joins() { }), args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ @@ -1324,7 +1328,8 @@ fn parse_substring_in_select() { }]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], + version: None, }, joins: vec![] }], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a62c41e42..b3621a34b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2855,11 +2855,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index c44f6dee4..9f5f62f78 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -45,6 +45,7 @@ fn test_square_brackets_over_db_schema_table_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], } @@ -89,6 +90,7 @@ fn test_double_quotes_over_db_schema_table_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], } @@ -108,11 +110,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 43ebb8b11..200849896 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -223,11 +223,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } From a2533c20fe5cd1c0060f64190f6ef19fe893fdac Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 22 Aug 2023 08:23:43 -0400 Subject: [PATCH 246/806] Changelog for version 0.37.0 (#953) --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8669f0584..4c6c9dcc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,25 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.37.0] 2023-08-22 + +### Added +* Support `FOR SYSTEM_TIME AS OF` table time travel clause support, `visit_table_factor` to Visitor (#951) - Thanks @gruuya +* Support MySQL `auto_increment` offset in table definition (#950) - Thanks @ehoeve +* Test for mssql table name in square brackets (#952) - Thanks @lovasoa +* Support additional Postgres `CREATE INDEX` syntax (#943) - Thanks @ForbesLindesay +* Support `ALTER ROLE` syntax of PostgreSQL and MS SQL Server (#942) - Thanks @r4ntix +* Support table-level comments (#946) - Thanks @ehoeve +* Support `DROP TEMPORARY TABLE`, MySQL syntax (#916) - Thanks @liadgiladi +* Support posgres type alias (#933) - Thanks @Kikkon + +### Fixed +* Clarify the value of the special flag (#948) - Thanks @alamb +* Fix `SUBSTRING` from/to argument construction for mssql (#947) - Thanks @jmaness +* Fix: use Rust idiomatic capitalization for newly added DataType enums (#939) - Thanks @Kikkon +* Fix `BEGIN TRANSACTION` being serialized as `START TRANSACTION` (#935) - Thanks @lovasoa +* Fix parsing of datetime functions without parenthesis (#930) - Thanks @lovasoa + ## [0.36.1] 2023-07-19 ### Fixed From b8a58bbc1186503d523726f8f18f06b6e30a3313 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 22 Aug 2023 08:28:03 -0400 Subject: [PATCH 247/806] chore: Release sqlparser version 0.37.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 777e9f4ae..43d9f981d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.36.1" +version = "0.37.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 9c2e8bcdbc437e715031540981ff971ef715afdd Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 22 Aug 2023 08:32:14 -0400 Subject: [PATCH 248/806] Break test and coverage test into separate jobs (#949) --- .github/workflows/rust.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 836d55614..64c4d114a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -75,11 +75,25 @@ jobs: uses: actions/checkout@v2 - name: Test run: cargo test --all-features + + test-coverage: + runs-on: ubuntu-latest + steps: + - name: Setup Rust + uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + - name: Install Tarpaulin + uses: actions-rs/install@v0.1 + with: + crate: cargo-tarpaulin + version: 0.14.2 + use-tool-cache: true + - name: Checkout + uses: actions/checkout@v2 - name: Coverage - if: matrix.rust == 'stable' run: cargo tarpaulin -o Lcov --output-dir ./coverage - name: Coveralls - if: matrix.rust == 'stable' uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} From 4a2fa66b553cd2e4e6ff9e837e0f04e1e7687c04 Mon Sep 17 00:00:00 2001 From: David Dolphin <445312+ddol@users.noreply.github.com> Date: Fri, 25 Aug 2023 09:06:25 -0700 Subject: [PATCH 249/806] [cli] add --sqlite param (#956) --- examples/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/cli.rs b/examples/cli.rs index 8af6246a0..72f963b1e 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -47,6 +47,7 @@ $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] "--redshift" => Box::new(RedshiftSqlDialect {}), "--clickhouse" => Box::new(ClickHouseDialect {}), "--duckdb" => Box::new(DuckDbDialect {}), + "--sqlite" => Box::new(SQLiteDialect {}), "--generic" | "" => Box::new(GenericDialect {}), s => panic!("Unexpected parameter: {s}"), }; From 14da37d1822ab6b7e96304cec9e409fb96ffdf09 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 25 Aug 2023 12:21:31 -0400 Subject: [PATCH 250/806] Fix Rust 1.72 clippy lints (#957) --- src/parser/mod.rs | 2 +- tests/sqlparser_bigquery.rs | 6 +++--- tests/sqlparser_common.rs | 4 ++-- tests/sqlparser_mysql.rs | 22 +++++++++++----------- tests/sqlparser_postgres.rs | 7 +++---- tests/sqlparser_snowflake.rs | 2 +- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c2a33b42a..846215249 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5881,7 +5881,7 @@ impl<'a> Parser<'a> { Some(_) => { let db_name = vec![self.parse_identifier()?]; let ObjectName(table_name) = object_name; - let object_name = db_name.into_iter().chain(table_name.into_iter()).collect(); + let object_name = db_name.into_iter().chain(table_name).collect(); ObjectName(object_name) } None => object_name, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index ca711b26e..d6a28fd00 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -55,7 +55,7 @@ fn parse_raw_literal() { let sql = r#"SELECT R'abc', R"abc", R'f\(abc,(.*),def\)', R"f\(abc,(.*),def\)""#; let stmt = bigquery().one_statement_parses_to( sql, - r#"SELECT R'abc', R'abc', R'f\(abc,(.*),def\)', R'f\(abc,(.*),def\)'"#, + r"SELECT R'abc', R'abc', R'f\(abc,(.*),def\)', R'f\(abc,(.*),def\)'", ); if let Statement::Query(query) = stmt { if let SetExpr::Select(select) = *query.body { @@ -69,11 +69,11 @@ fn parse_raw_literal() { expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::RawStringLiteral(r#"f\(abc,(.*),def\)"#.to_string())), + &Expr::Value(Value::RawStringLiteral(r"f\(abc,(.*),def\)".to_string())), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::RawStringLiteral(r#"f\(abc,(.*),def\)"#.to_string())), + &Expr::Value(Value::RawStringLiteral(r"f\(abc,(.*),def\)".to_string())), expr_from_projection(&select.projection[3]) ); return; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 96dac3da0..3fdf3d211 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7212,7 +7212,7 @@ fn parse_with_recursion_limit() { .expect("tokenize to work") .parse_statements(); - assert!(matches!(res, Ok(_)), "{res:?}"); + assert!(res.is_ok(), "{res:?}"); // limit recursion to something smaller, expect parsing to fail let res = Parser::new(&dialect) @@ -7230,7 +7230,7 @@ fn parse_with_recursion_limit() { .with_recursion_limit(50) .parse_statements(); - assert!(matches!(res, Ok(_)), "{res:?}"); + assert!(res.is_ok(), "{res:?}"); } #[test] diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 3a0177df4..ab7997cdf 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -634,11 +634,11 @@ fn parse_escaped_backticks_with_no_escape() { #[test] fn parse_unterminated_escape() { - let sql = r#"SELECT 'I\'m not fine\'"#; + let sql = r"SELECT 'I\'m not fine\'"; let result = std::panic::catch_unwind(|| mysql().one_statement_parses_to(sql, "")); assert!(result.is_err()); - let sql = r#"SELECT 'I\\'m not fine'"#; + let sql = r"SELECT 'I\\'m not fine'"; let result = std::panic::catch_unwind(|| mysql().one_statement_parses_to(sql, "")); assert!(result.is_err()); } @@ -666,7 +666,7 @@ fn parse_escaped_string_with_escape() { _ => unreachable!(), }; } - let sql = r#"SELECT 'I\'m fine'"#; + let sql = r"SELECT 'I\'m fine'"; assert_mysql_query_value(sql, "I'm fine"); let sql = r#"SELECT 'I''m fine'"#; @@ -675,7 +675,7 @@ fn parse_escaped_string_with_escape() { let sql = r#"SELECT 'I\"m fine'"#; assert_mysql_query_value(sql, "I\"m fine"); - let sql = r#"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"#; + let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"; assert_mysql_query_value(sql, "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} a "); } @@ -702,8 +702,8 @@ fn parse_escaped_string_with_no_escape() { _ => unreachable!(), }; } - let sql = r#"SELECT 'I\'m fine'"#; - assert_mysql_query_value(sql, r#"I\'m fine"#); + let sql = r"SELECT 'I\'m fine'"; + assert_mysql_query_value(sql, r"I\'m fine"); let sql = r#"SELECT 'I''m fine'"#; assert_mysql_query_value(sql, r#"I''m fine"#); @@ -711,8 +711,8 @@ fn parse_escaped_string_with_no_escape() { let sql = r#"SELECT 'I\"m fine'"#; assert_mysql_query_value(sql, r#"I\"m fine"#); - let sql = r#"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"#; - assert_mysql_query_value(sql, r#"Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ "#); + let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"; + assert_mysql_query_value(sql, r"Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ "); } #[test] @@ -723,7 +723,7 @@ fn check_roundtrip_of_escaped_string() { dialects: vec![Box::new(MySqlDialect {})], options: options.clone(), } - .verified_stmt(r#"SELECT 'I\'m fine'"#); + .verified_stmt(r"SELECT 'I\'m fine'"); TestedDialects { dialects: vec![Box::new(MySqlDialect {})], options: options.clone(), @@ -733,12 +733,12 @@ fn check_roundtrip_of_escaped_string() { dialects: vec![Box::new(MySqlDialect {})], options: options.clone(), } - .verified_stmt(r#"SELECT 'I\\\'m fine'"#); + .verified_stmt(r"SELECT 'I\\\'m fine'"); TestedDialects { dialects: vec![Box::new(MySqlDialect {})], options: options.clone(), } - .verified_stmt(r#"SELECT 'I\\\'m fine'"#); + .verified_stmt(r"SELECT 'I\\\'m fine'"); TestedDialects { dialects: vec![Box::new(MySqlDialect {})], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b3621a34b..c34ba75a8 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2332,8 +2332,7 @@ fn pg_and_generic() -> TestedDialects { #[test] fn parse_escaped_literal_string() { - let sql = - r#"SELECT E's1 \n s1', E's2 \\n s2', E's3 \\\n s3', E's4 \\\\n s4', E'\'', E'foo \\'"#; + let sql = r"SELECT E's1 \n s1', E's2 \\n s2', E's3 \\\n s3', E's4 \\\\n s4', E'\'', E'foo \\'"; let select = pg_and_generic().verified_only_select(sql); assert_eq!(6, select.projection.len()); assert_eq!( @@ -2361,7 +2360,7 @@ fn parse_escaped_literal_string() { expr_from_projection(&select.projection[5]) ); - let sql = r#"SELECT E'\'"#; + let sql = r"SELECT E'\'"; assert_eq!( pg_and_generic() .parse_sql_statements(sql) @@ -2631,7 +2630,7 @@ fn parse_create_role() { err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } - let negatables = vec![ + let negatables = [ "BYPASSRLS", "CREATEDB", "CREATEROLE", diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 200849896..ecd608527 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1004,7 +1004,7 @@ fn test_copy_into_copy_options() { #[test] fn test_snowflake_stage_object_names() { - let allowed_formatted_names = vec![ + let allowed_formatted_names = [ "my_company.emp_basic", "@namespace.%table_name", "@namespace.%table_name/path", From 4c3a4ad5a87ef8b4dd24a6e8540243e14fbfa148 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 25 Aug 2023 12:21:40 -0400 Subject: [PATCH 251/806] Update release documentation (#954) --- docs/releasing.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index b71c97595..efe0ab3d2 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -45,13 +45,6 @@ $ cargo install cargo-release Note that credentials for authoring in this way are securely stored in the (GitHub) repo secrets as `CRATE_TOKEN`. - * Bump the crate version again (to something like `0.8.1-alpha.0`) to - indicate the start of new development cycle. - -3. Push the updates to the `main` branch upstream: - ``` - $ git push upstream - ``` 4. Check that the new version of the crate is available on crates.io: https://crates.io/crates/sqlparser From b02c3f87ec0b70b608ddbf6e92bf3b94ffc9d211 Mon Sep 17 00:00:00 2001 From: dawg Date: Thu, 7 Sep 2023 22:23:09 +0200 Subject: [PATCH 252/806] feat: show location info in parse errors (#958) --- src/parser/mod.rs | 101 +++++++++++++++++++++++++------------- src/test_utils.rs | 8 ++- src/tokenizer.rs | 48 +++++++++--------- tests/sqlparser_common.rs | 30 ++++++----- 4 files changed, 116 insertions(+), 71 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 846215249..6902fc565 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -44,8 +44,8 @@ pub enum ParserError { // Use `Parser::expected` instead, if possible macro_rules! parser_err { - ($MSG:expr) => { - Err(ParserError::ParserError($MSG.to_string())) + ($MSG:expr, $loc:expr) => { + Err(ParserError::ParserError(format!("{}{}", $MSG, $loc))) }; } @@ -368,8 +368,8 @@ impl<'a> Parser<'a> { debug!("Parsing sql '{}'...", sql); let tokens = Tokenizer::new(self.dialect, sql) .with_unescape(self.options.unescape) - .tokenize()?; - Ok(self.with_tokens(tokens)) + .tokenize_with_location()?; + Ok(self.with_tokens_with_locations(tokens)) } /// Parse potentially multiple statements @@ -724,6 +724,7 @@ impl<'a> Parser<'a> { // Note also that naively `SELECT date` looks like a syntax error because the `date` type // name is not followed by a string literal, but in fact in PostgreSQL it is a valid // expression that should parse as the column name "date". + let loc = self.peek_token().location; return_ok_if_some!(self.maybe_parse(|parser| { match parser.parse_data_type()? { DataType::Interval => parser.parse_interval(), @@ -734,7 +735,7 @@ impl<'a> Parser<'a> { // name, resulting in `NOT 'a'` being recognized as a `TypedString` instead of // an unary negation `NOT ('a' LIKE 'b')`. To solve this, we don't accept the // `type 'string'` syntax for the custom data types at all. - DataType::Custom(..) => parser_err!("dummy"), + DataType::Custom(..) => parser_err!("dummy", loc), data_type => Ok(Expr::TypedString { data_type, value: parser.parse_literal_string()?, @@ -909,7 +910,12 @@ impl<'a> Parser<'a> { let tok = self.next_token(); let key = match tok.token { Token::Word(word) => word.to_ident(), - _ => return parser_err!(format!("Expected identifier, found: {tok}")), + _ => { + return parser_err!( + format!("Expected identifier, found: {tok}"), + tok.location + ) + } }; Ok(Expr::CompositeAccess { expr: Box::new(expr), @@ -1220,7 +1226,10 @@ impl<'a> Parser<'a> { r#in: Box::new(from), }) } else { - parser_err!("Position function must include IN keyword".to_string()) + parser_err!( + "Position function must include IN keyword".to_string(), + self.peek_token().location + ) } } @@ -1884,7 +1893,10 @@ impl<'a> Parser<'a> { } } // Can only happen if `get_next_precedence` got out of sync with this function - _ => parser_err!(format!("No infix parser for token {:?}", tok.token)), + _ => parser_err!( + format!("No infix parser for token {:?}", tok.token), + tok.location + ), } } else if Token::DoubleColon == tok { self.parse_pg_cast(expr) @@ -1935,7 +1947,10 @@ impl<'a> Parser<'a> { }) } else { // Can only happen if `get_next_precedence` got out of sync with this function - parser_err!(format!("No infix parser for token {:?}", tok.token)) + parser_err!( + format!("No infix parser for token {:?}", tok.token), + tok.location + ) } } @@ -2213,7 +2228,10 @@ impl<'a> Parser<'a> { /// Report unexpected token pub fn expected(&self, expected: &str, found: TokenWithLocation) -> Result { - parser_err!(format!("Expected {expected}, found: {found}")) + parser_err!( + format!("Expected {expected}, found: {found}"), + found.location + ) } /// Look for an expected keyword and consume it if it exists @@ -2378,13 +2396,14 @@ impl<'a> Parser<'a> { /// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns `None` if `ALL` is parsed /// and results in a `ParserError` if both `ALL` and `DISTINCT` are found. pub fn parse_all_or_distinct(&mut self) -> Result, ParserError> { + let loc = self.peek_token().location; let all = self.parse_keyword(Keyword::ALL); let distinct = self.parse_keyword(Keyword::DISTINCT); if !distinct { return Ok(None); } if all { - return parser_err!("Cannot specify both ALL and DISTINCT".to_string()); + return parser_err!("Cannot specify both ALL and DISTINCT".to_string(), loc); } let on = self.parse_keyword(Keyword::ON); if !on { @@ -2986,10 +3005,14 @@ impl<'a> Parser<'a> { let mut admin = vec![]; while let Some(keyword) = self.parse_one_of_keywords(&optional_keywords) { + let loc = self + .tokens + .get(self.index - 1) + .map_or(Location { line: 0, column: 0 }, |t| t.location); match keyword { Keyword::AUTHORIZATION => { if authorization_owner.is_some() { - parser_err!("Found multiple AUTHORIZATION") + parser_err!("Found multiple AUTHORIZATION", loc) } else { authorization_owner = Some(self.parse_object_name()?); Ok(()) @@ -2997,7 +3020,7 @@ impl<'a> Parser<'a> { } Keyword::LOGIN | Keyword::NOLOGIN => { if login.is_some() { - parser_err!("Found multiple LOGIN or NOLOGIN") + parser_err!("Found multiple LOGIN or NOLOGIN", loc) } else { login = Some(keyword == Keyword::LOGIN); Ok(()) @@ -3005,7 +3028,7 @@ impl<'a> Parser<'a> { } Keyword::INHERIT | Keyword::NOINHERIT => { if inherit.is_some() { - parser_err!("Found multiple INHERIT or NOINHERIT") + parser_err!("Found multiple INHERIT or NOINHERIT", loc) } else { inherit = Some(keyword == Keyword::INHERIT); Ok(()) @@ -3013,7 +3036,7 @@ impl<'a> Parser<'a> { } Keyword::BYPASSRLS | Keyword::NOBYPASSRLS => { if bypassrls.is_some() { - parser_err!("Found multiple BYPASSRLS or NOBYPASSRLS") + parser_err!("Found multiple BYPASSRLS or NOBYPASSRLS", loc) } else { bypassrls = Some(keyword == Keyword::BYPASSRLS); Ok(()) @@ -3021,7 +3044,7 @@ impl<'a> Parser<'a> { } Keyword::CREATEDB | Keyword::NOCREATEDB => { if create_db.is_some() { - parser_err!("Found multiple CREATEDB or NOCREATEDB") + parser_err!("Found multiple CREATEDB or NOCREATEDB", loc) } else { create_db = Some(keyword == Keyword::CREATEDB); Ok(()) @@ -3029,7 +3052,7 @@ impl<'a> Parser<'a> { } Keyword::CREATEROLE | Keyword::NOCREATEROLE => { if create_role.is_some() { - parser_err!("Found multiple CREATEROLE or NOCREATEROLE") + parser_err!("Found multiple CREATEROLE or NOCREATEROLE", loc) } else { create_role = Some(keyword == Keyword::CREATEROLE); Ok(()) @@ -3037,7 +3060,7 @@ impl<'a> Parser<'a> { } Keyword::SUPERUSER | Keyword::NOSUPERUSER => { if superuser.is_some() { - parser_err!("Found multiple SUPERUSER or NOSUPERUSER") + parser_err!("Found multiple SUPERUSER or NOSUPERUSER", loc) } else { superuser = Some(keyword == Keyword::SUPERUSER); Ok(()) @@ -3045,7 +3068,7 @@ impl<'a> Parser<'a> { } Keyword::REPLICATION | Keyword::NOREPLICATION => { if replication.is_some() { - parser_err!("Found multiple REPLICATION or NOREPLICATION") + parser_err!("Found multiple REPLICATION or NOREPLICATION", loc) } else { replication = Some(keyword == Keyword::REPLICATION); Ok(()) @@ -3053,7 +3076,7 @@ impl<'a> Parser<'a> { } Keyword::PASSWORD => { if password.is_some() { - parser_err!("Found multiple PASSWORD") + parser_err!("Found multiple PASSWORD", loc) } else { password = if self.parse_keyword(Keyword::NULL) { Some(Password::NullPassword) @@ -3066,7 +3089,7 @@ impl<'a> Parser<'a> { Keyword::CONNECTION => { self.expect_keyword(Keyword::LIMIT)?; if connection_limit.is_some() { - parser_err!("Found multiple CONNECTION LIMIT") + parser_err!("Found multiple CONNECTION LIMIT", loc) } else { connection_limit = Some(Expr::Value(self.parse_number_value()?)); Ok(()) @@ -3075,7 +3098,7 @@ impl<'a> Parser<'a> { Keyword::VALID => { self.expect_keyword(Keyword::UNTIL)?; if valid_until.is_some() { - parser_err!("Found multiple VALID UNTIL") + parser_err!("Found multiple VALID UNTIL", loc) } else { valid_until = Some(Expr::Value(self.parse_value()?)); Ok(()) @@ -3084,14 +3107,14 @@ impl<'a> Parser<'a> { Keyword::IN => { if self.parse_keyword(Keyword::ROLE) { if !in_role.is_empty() { - parser_err!("Found multiple IN ROLE") + parser_err!("Found multiple IN ROLE", loc) } else { in_role = self.parse_comma_separated(Parser::parse_identifier)?; Ok(()) } } else if self.parse_keyword(Keyword::GROUP) { if !in_group.is_empty() { - parser_err!("Found multiple IN GROUP") + parser_err!("Found multiple IN GROUP", loc) } else { in_group = self.parse_comma_separated(Parser::parse_identifier)?; Ok(()) @@ -3102,7 +3125,7 @@ impl<'a> Parser<'a> { } Keyword::ROLE => { if !role.is_empty() { - parser_err!("Found multiple ROLE") + parser_err!("Found multiple ROLE", loc) } else { role = self.parse_comma_separated(Parser::parse_identifier)?; Ok(()) @@ -3110,7 +3133,7 @@ impl<'a> Parser<'a> { } Keyword::USER => { if !user.is_empty() { - parser_err!("Found multiple USER") + parser_err!("Found multiple USER", loc) } else { user = self.parse_comma_separated(Parser::parse_identifier)?; Ok(()) @@ -3118,7 +3141,7 @@ impl<'a> Parser<'a> { } Keyword::ADMIN => { if !admin.is_empty() { - parser_err!("Found multiple ADMIN") + parser_err!("Found multiple ADMIN", loc) } else { admin = self.parse_comma_separated(Parser::parse_identifier)?; Ok(()) @@ -3181,14 +3204,19 @@ impl<'a> Parser<'a> { // specifying multiple objects to delete in a single statement let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let names = self.parse_comma_separated(Parser::parse_object_name)?; + + let loc = self.peek_token().location; let cascade = self.parse_keyword(Keyword::CASCADE); let restrict = self.parse_keyword(Keyword::RESTRICT); let purge = self.parse_keyword(Keyword::PURGE); if cascade && restrict { - return parser_err!("Cannot specify both CASCADE and RESTRICT in DROP"); + return parser_err!("Cannot specify both CASCADE and RESTRICT in DROP", loc); } if object_type == ObjectType::Role && (cascade || restrict || purge) { - return parser_err!("Cannot specify CASCADE, RESTRICT, or PURGE in DROP ROLE"); + return parser_err!( + "Cannot specify CASCADE, RESTRICT, or PURGE in DROP ROLE", + loc + ); } Ok(Statement::Drop { object_type, @@ -4446,7 +4474,11 @@ impl<'a> Parser<'a> { fn parse_literal_char(&mut self) -> Result { let s = self.parse_literal_string()?; if s.len() != 1 { - return parser_err!(format!("Expect a char, found {s:?}")); + let loc = self + .tokens + .get(self.index - 1) + .map_or(Location { line: 0, column: 0 }, |t| t.location); + return parser_err!(format!("Expect a char, found {s:?}"), loc); } Ok(s.chars().next().unwrap()) } @@ -4525,7 +4557,7 @@ impl<'a> Parser<'a> { // (i.e., it returns the input string). Token::Number(ref n, l) => match n.parse() { Ok(n) => Ok(Value::Number(n, l)), - Err(e) => parser_err!(format!("Could not parse '{n}' as number: {e}")), + Err(e) => parser_err!(format!("Could not parse '{n}' as number: {e}"), location), }, Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), @@ -6465,10 +6497,11 @@ impl<'a> Parser<'a> { .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) .then(|| self.parse_identifier().unwrap()); + let loc = self.peek_token().location; let cascade = self.parse_keyword(Keyword::CASCADE); let restrict = self.parse_keyword(Keyword::RESTRICT); if cascade && restrict { - return parser_err!("Cannot specify both CASCADE and RESTRICT in REVOKE"); + return parser_err!("Cannot specify both CASCADE and RESTRICT in REVOKE", loc); } Ok(Statement::Revoke { @@ -7929,14 +7962,12 @@ mod tests { #[test] fn test_parser_error_loc() { - // TODO: Once we thread token locations through the parser, we should update this - // test to assert the locations of the referenced token let sql = "SELECT this is a syntax error"; let ast = Parser::parse_sql(&GenericDialect, sql); assert_eq!( ast, Err(ParserError::ParserError( - "Expected [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: a" + "Expected [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: a at Line: 1, Column 16" .to_string() )) ); diff --git a/src/test_utils.rs b/src/test_utils.rs index 91130fb51..a5e9e739d 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -28,6 +28,7 @@ use core::fmt::Debug; use crate::dialect::*; use crate::parser::{Parser, ParserError}; +use crate::tokenizer::Tokenizer; use crate::{ast::*, parser::ParserOptions}; /// Tests use the methods on this struct to invoke the parser on one or @@ -82,8 +83,13 @@ impl TestedDialects { /// the result is the same for all tested dialects. pub fn parse_sql_statements(&self, sql: &str) -> Result, ParserError> { self.one_of_identical_results(|dialect| { + let mut tokenizer = Tokenizer::new(dialect, sql); + if let Some(options) = &self.options { + tokenizer = tokenizer.with_unescape(options.unescape); + } + let tokens = tokenizer.tokenize()?; self.new_parser(dialect) - .try_with_sql(sql)? + .with_tokens(tokens) .parse_statements() }) // To fail the `ensure_multiple_dialects_are_tested` test: diff --git a/src/tokenizer.rs b/src/tokenizer.rs index f20e01b71..1b1b1e96c 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -350,7 +350,7 @@ impl fmt::Display for Whitespace { } /// Location in input string -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Eq, PartialEq, Clone, Copy)] pub struct Location { /// Line number, starting from 1 pub line: u64, @@ -358,6 +358,20 @@ pub struct Location { pub column: u64, } +impl fmt::Display for Location { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.line == 0 { + return Ok(()); + } + write!( + f, + // TODO: use standard compiler location syntax (::) + " at Line: {}, Column {}", + self.line, self.column, + ) + } +} + /// A [Token] with [Location] attached to it #[derive(Debug, Eq, PartialEq, Clone)] pub struct TokenWithLocation { @@ -400,17 +414,12 @@ impl fmt::Display for TokenWithLocation { #[derive(Debug, PartialEq, Eq)] pub struct TokenizerError { pub message: String, - pub line: u64, - pub col: u64, + pub location: Location, } impl fmt::Display for TokenizerError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} at Line: {}, Column {}", - self.message, self.line, self.col - ) + write!(f, "{}{}", self.message, self.location,) } } @@ -546,10 +555,7 @@ impl<'a> Tokenizer<'a> { let mut location = state.location(); while let Some(token) = self.next_token(&mut state)? { - tokens.push(TokenWithLocation { - token, - location: location.clone(), - }); + tokens.push(TokenWithLocation { token, location }); location = state.location(); } @@ -1122,8 +1128,7 @@ impl<'a> Tokenizer<'a> { ) -> Result { Err(TokenizerError { message: message.into(), - col: loc.column, - line: loc.line, + location: loc, }) } @@ -1368,8 +1373,7 @@ mod tests { fn tokenizer_error_impl() { let err = TokenizerError { message: "test".into(), - line: 1, - col: 1, + location: Location { line: 1, column: 1 }, }; #[cfg(feature = "std")] { @@ -1694,8 +1698,7 @@ mod tests { tokenizer.tokenize(), Err(TokenizerError { message: "Unterminated string literal".to_string(), - line: 1, - col: 8 + location: Location { line: 1, column: 8 }, }) ); } @@ -1710,8 +1713,10 @@ mod tests { tokenizer.tokenize(), Err(TokenizerError { message: "Unterminated string literal".to_string(), - line: 1, - col: 35 + location: Location { + line: 1, + column: 35 + } }) ); } @@ -1873,8 +1878,7 @@ mod tests { tokenizer.tokenize(), Err(TokenizerError { message: "Expected close delimiter '\"' before EOF.".to_string(), - line: 1, - col: 1 + location: Location { line: 1, column: 1 }, }) ); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3fdf3d211..450743360 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -836,7 +836,12 @@ fn test_eof_after_as() { #[test] fn test_no_infix_error() { - let res = Parser::parse_sql(&ClickHouseDialect {}, "ASSERT-URA<<"); + let dialects = TestedDialects { + dialects: vec![Box::new(ClickHouseDialect {})], + options: None, + }; + + let res = dialects.parse_sql_statements("ASSERT-URA<<"); assert_eq!( ParserError::ParserError("No infix parser for token ShiftLeft".to_string()), res.unwrap_err() @@ -3238,19 +3243,21 @@ fn parse_alter_table_alter_column_type() { _ => unreachable!(), } - let res = Parser::parse_sql( - &GenericDialect {}, - &format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT"), - ); + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + options: None, + }; + + 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, SET DATA TYPE after ALTER COLUMN, found: TYPE".to_string()), res.unwrap_err() ); - let res = Parser::parse_sql( - &GenericDialect {}, - &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 SET DATA TYPE TEXT USING 'text'" + )); assert_eq!( ParserError::ParserError("Expected end of statement, found: USING".to_string()), res.unwrap_err() @@ -3295,10 +3302,7 @@ fn parse_alter_table_drop_constraint() { _ => unreachable!(), } - let res = Parser::parse_sql( - &GenericDialect {}, - &format!("{alter_stmt} DROP CONSTRAINT is_active TEXT"), - ); + let res = parse_sql_statements(&format!("{alter_stmt} DROP CONSTRAINT is_active TEXT")); assert_eq!( ParserError::ParserError("Expected end of statement, found: TEXT".to_string()), res.unwrap_err() From e0afd4b179bd5209be6140c0aa02ef49c5a32707 Mon Sep 17 00:00:00 2001 From: SeanTroyUWO Date: Thu, 7 Sep 2023 14:32:50 -0600 Subject: [PATCH 253/806] `ANY` and `ALL` contains their operators (#963) --- src/ast/mod.rs | 28 ++++++++++++++++++++++------ src/parser/mod.rs | 32 ++++++++++++++++++++++++-------- tests/sqlparser_common.rs | 12 ++++++------ 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a241f9509..41d66166d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -419,10 +419,18 @@ pub enum Expr { pattern: Box, escape_char: Option, }, - /// Any operation e.g. `1 ANY (1)` or `foo > ANY(bar)`, It will be wrapped in the right side of BinaryExpr - AnyOp(Box), - /// ALL operation e.g. `1 ALL (1)` or `foo > ALL(bar)`, It will be wrapped in the right side of BinaryExpr - AllOp(Box), + /// Any operation e.g. `foo > ANY(bar)`, comparison operator is one of [=, >, <, =>, =<, !=] + AnyOp { + left: Box, + compare_op: BinaryOperator, + right: Box, + }, + /// ALL operation e.g. `foo > ALL(bar)`, comparison operator is one of [=, >, <, =>, =<, !=] + AllOp { + left: Box, + compare_op: BinaryOperator, + right: Box, + }, /// Unary operation e.g. `NOT foo` UnaryOp { op: UnaryOperator, expr: Box }, /// CAST an expression to a different data type e.g. `CAST(foo AS VARCHAR(123))` @@ -724,8 +732,16 @@ impl fmt::Display for Expr { pattern ), }, - Expr::AnyOp(expr) => write!(f, "ANY({expr})"), - Expr::AllOp(expr) => write!(f, "ALL({expr})"), + Expr::AnyOp { + left, + compare_op, + right, + } => write!(f, "{left} {compare_op} ANY({right})"), + Expr::AllOp { + left, + compare_op, + right, + } => write!(f, "{left} {compare_op} ALL({right})"), Expr::UnaryOp { op, expr } => { if op == &UnaryOperator::PGPostfixFactorial { write!(f, "{expr}{op}") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6902fc565..9de67a51b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1782,16 +1782,32 @@ impl<'a> Parser<'a> { let right = self.parse_subexpr(precedence)?; self.expect_token(&Token::RParen)?; - let right = match keyword { - Keyword::ALL => Box::new(Expr::AllOp(Box::new(right))), - Keyword::ANY => Box::new(Expr::AnyOp(Box::new(right))), - _ => unreachable!(), + if !matches!( + op, + BinaryOperator::Gt + | BinaryOperator::Lt + | BinaryOperator::GtEq + | BinaryOperator::LtEq + | BinaryOperator::Eq + | BinaryOperator::NotEq + ) { + return parser_err!(format!( + "Expected one of [=, >, <, =>, =<, !=] as comparison operator, found: {op}" + )); }; - Ok(Expr::BinaryOp { - left: Box::new(expr), - op, - right, + Ok(match keyword { + Keyword::ALL => Expr::AllOp { + left: Box::new(expr), + compare_op: op, + right: Box::new(right), + }, + Keyword::ANY => Expr::AnyOp { + left: Box::new(expr), + compare_op: op, + right: Box::new(right), + }, + _ => unreachable!(), }) } else { Ok(Expr::BinaryOp { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 450743360..a9c4130b4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1558,10 +1558,10 @@ fn parse_bitwise_ops() { fn parse_binary_any() { let select = verified_only_select("SELECT a = ANY(b)"); assert_eq!( - SelectItem::UnnamedExpr(Expr::BinaryOp { + SelectItem::UnnamedExpr(Expr::AnyOp { left: Box::new(Expr::Identifier(Ident::new("a"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::AnyOp(Box::new(Expr::Identifier(Ident::new("b"))))), + compare_op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("b"))), }), select.projection[0] ); @@ -1571,10 +1571,10 @@ fn parse_binary_any() { fn parse_binary_all() { let select = verified_only_select("SELECT a = ALL(b)"); assert_eq!( - SelectItem::UnnamedExpr(Expr::BinaryOp { + SelectItem::UnnamedExpr(Expr::AllOp { left: Box::new(Expr::Identifier(Ident::new("a"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::AllOp(Box::new(Expr::Identifier(Ident::new("b"))))), + compare_op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("b"))), }), select.projection[0] ); From 25e037c50fc46bc061126d583bc64c2da7c6cabf Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Thu, 7 Sep 2023 21:39:47 +0100 Subject: [PATCH 254/806] feat: allow multiple actions in one `ALTER TABLE` statement (#960) --- src/ast/mod.rs | 24 ++- src/parser/mod.rs | 342 ++++++++++++++++++----------------- src/test_utils.rs | 20 ++ tests/sqlparser_common.rs | 179 +++++++----------- tests/sqlparser_mysql.rs | 36 ++-- tests/sqlparser_postgres.rs | 66 +++++-- tests/sqlparser_snowflake.rs | 8 +- 7 files changed, 349 insertions(+), 326 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 41d66166d..79edea7d1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1407,7 +1407,9 @@ pub enum Statement { /// Table name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] name: ObjectName, - operation: AlterTableOperation, + if_exists: bool, + only: bool, + operations: Vec, }, AlterIndex { name: ObjectName, @@ -2618,8 +2620,24 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::AlterTable { name, operation } => { - write!(f, "ALTER TABLE {name} {operation}") + Statement::AlterTable { + name, + if_exists, + only, + operations, + } => { + write!(f, "ALTER TABLE ")?; + if *if_exists { + write!(f, "IF EXISTS ")?; + } + if *only { + write!(f, "ONLY ")?; + } + write!( + f, + "{name} {operations}", + operations = display_comma_separated(operations) + ) } Statement::AlterIndex { name, operation } => { write!(f, "ALTER INDEX {name} {operation}") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9de67a51b..b872bbfc8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4081,6 +4081,174 @@ impl<'a> Parser<'a> { Ok(SqlOption { name, value }) } + 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) + } else { + let if_not_exists = + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + if self.parse_keyword(Keyword::PARTITION) { + self.expect_token(&Token::LParen)?; + let partitions = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::AddPartitions { + if_not_exists, + new_partitions: partitions, + } + } else { + let column_keyword = self.parse_keyword(Keyword::COLUMN); + + let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | DuckDbDialect | GenericDialect) + { + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) + || if_not_exists + } else { + false + }; + + let column_def = self.parse_column_def()?; + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + } + } + } + } else if self.parse_keyword(Keyword::RENAME) { + if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::CONSTRAINT) { + let old_name = self.parse_identifier()?; + self.expect_keyword(Keyword::TO)?; + let new_name = self.parse_identifier()?; + AlterTableOperation::RenameConstraint { old_name, new_name } + } else if self.parse_keyword(Keyword::TO) { + let table_name = self.parse_object_name()?; + AlterTableOperation::RenameTable { table_name } + } else { + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] + let old_column_name = self.parse_identifier()?; + self.expect_keyword(Keyword::TO)?; + let new_column_name = self.parse_identifier()?; + AlterTableOperation::RenameColumn { + old_column_name, + new_column_name, + } + } + } else if self.parse_keyword(Keyword::DROP) { + if self.parse_keywords(&[Keyword::IF, Keyword::EXISTS, Keyword::PARTITION]) { + self.expect_token(&Token::LParen)?; + let partitions = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::DropPartitions { + partitions, + if_exists: true, + } + } else if self.parse_keyword(Keyword::PARTITION) { + self.expect_token(&Token::LParen)?; + let partitions = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::DropPartitions { + partitions, + if_exists: false, + } + } else if self.parse_keyword(Keyword::CONSTRAINT) { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier()?; + let cascade = self.parse_keyword(Keyword::CASCADE); + AlterTableOperation::DropConstraint { + if_exists, + name, + cascade, + } + } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) + && dialect_of!(self is MySqlDialect | GenericDialect) + { + AlterTableOperation::DropPrimaryKey + } else { + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let column_name = self.parse_identifier()?; + let cascade = self.parse_keyword(Keyword::CASCADE); + AlterTableOperation::DropColumn { + column_name, + if_exists, + cascade, + } + } + } else if self.parse_keyword(Keyword::PARTITION) { + self.expect_token(&Token::LParen)?; + let before = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + self.expect_keyword(Keyword::RENAME)?; + self.expect_keywords(&[Keyword::TO, Keyword::PARTITION])?; + self.expect_token(&Token::LParen)?; + let renames = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::RenamePartitions { + old_partitions: before, + new_partitions: renames, + } + } else if self.parse_keyword(Keyword::CHANGE) { + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] + let old_name = self.parse_identifier()?; + let new_name = self.parse_identifier()?; + let data_type = self.parse_data_type()?; + let mut options = vec![]; + while let Some(option) = self.parse_optional_column_option()? { + options.push(option); + } + + AlterTableOperation::ChangeColumn { + old_name, + new_name, + data_type, + options, + } + } else if self.parse_keyword(Keyword::ALTER) { + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] + let column_name = self.parse_identifier()?; + let is_postgresql = dialect_of!(self is PostgreSqlDialect); + + let op = if self.parse_keywords(&[Keyword::SET, Keyword::NOT, Keyword::NULL]) { + AlterColumnOperation::SetNotNull {} + } else if self.parse_keywords(&[Keyword::DROP, Keyword::NOT, Keyword::NULL]) { + AlterColumnOperation::DropNotNull {} + } else if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT]) { + AlterColumnOperation::SetDefault { + value: self.parse_expr()?, + } + } 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 { + return self.expected( + "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE after ALTER COLUMN", + self.peek_token(), + ); + }; + AlterTableOperation::AlterColumn { column_name, op } + } else if self.parse_keyword(Keyword::SWAP) { + self.expect_keyword(Keyword::WITH)?; + let table_name = self.parse_object_name()?; + AlterTableOperation::SwapWith { table_name } + } else { + return self.expected( + "ADD, RENAME, PARTITION, SWAP or DROP after ALTER TABLE", + self.peek_token(), + ); + }; + Ok(operation) + } + pub fn parse_alter(&mut self) -> Result { let object_type = self.expect_one_of_keywords(&[ Keyword::VIEW, @@ -4091,177 +4259,15 @@ impl<'a> Parser<'a> { match object_type { Keyword::VIEW => self.parse_alter_view(), Keyword::TABLE => { - let _ = self.parse_keyword(Keyword::ONLY); // [ ONLY ] + 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()?; - let operation = if self.parse_keyword(Keyword::ADD) { - if let Some(constraint) = self.parse_optional_table_constraint()? { - AlterTableOperation::AddConstraint(constraint) - } else { - let if_not_exists = - self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - if self.parse_keyword(Keyword::PARTITION) { - self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - AlterTableOperation::AddPartitions { - if_not_exists, - new_partitions: partitions, - } - } else { - let column_keyword = self.parse_keyword(Keyword::COLUMN); - - let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | DuckDbDialect | GenericDialect) - { - self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) - || if_not_exists - } else { - false - }; - - let column_def = self.parse_column_def()?; - AlterTableOperation::AddColumn { - column_keyword, - if_not_exists, - column_def, - } - } - } - } else if self.parse_keyword(Keyword::RENAME) { - if dialect_of!(self is PostgreSqlDialect) - && self.parse_keyword(Keyword::CONSTRAINT) - { - let old_name = self.parse_identifier()?; - self.expect_keyword(Keyword::TO)?; - let new_name = self.parse_identifier()?; - AlterTableOperation::RenameConstraint { old_name, new_name } - } else if self.parse_keyword(Keyword::TO) { - let table_name = self.parse_object_name()?; - AlterTableOperation::RenameTable { table_name } - } else { - let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] - let old_column_name = self.parse_identifier()?; - self.expect_keyword(Keyword::TO)?; - let new_column_name = self.parse_identifier()?; - AlterTableOperation::RenameColumn { - old_column_name, - new_column_name, - } - } - } else if self.parse_keyword(Keyword::DROP) { - if self.parse_keywords(&[Keyword::IF, Keyword::EXISTS, Keyword::PARTITION]) { - self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - AlterTableOperation::DropPartitions { - partitions, - if_exists: true, - } - } else if self.parse_keyword(Keyword::PARTITION) { - self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - AlterTableOperation::DropPartitions { - partitions, - if_exists: false, - } - } else if self.parse_keyword(Keyword::CONSTRAINT) { - let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let name = self.parse_identifier()?; - let cascade = self.parse_keyword(Keyword::CASCADE); - AlterTableOperation::DropConstraint { - if_exists, - name, - cascade, - } - } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) - && dialect_of!(self is MySqlDialect | GenericDialect) - { - AlterTableOperation::DropPrimaryKey - } else { - let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] - let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let column_name = self.parse_identifier()?; - let cascade = self.parse_keyword(Keyword::CASCADE); - AlterTableOperation::DropColumn { - column_name, - if_exists, - cascade, - } - } - } else if self.parse_keyword(Keyword::PARTITION) { - self.expect_token(&Token::LParen)?; - let before = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - self.expect_keyword(Keyword::RENAME)?; - self.expect_keywords(&[Keyword::TO, Keyword::PARTITION])?; - self.expect_token(&Token::LParen)?; - let renames = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - AlterTableOperation::RenamePartitions { - old_partitions: before, - new_partitions: renames, - } - } else if self.parse_keyword(Keyword::CHANGE) { - let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] - let old_name = self.parse_identifier()?; - let new_name = self.parse_identifier()?; - let data_type = self.parse_data_type()?; - let mut options = vec![]; - while let Some(option) = self.parse_optional_column_option()? { - options.push(option); - } - - AlterTableOperation::ChangeColumn { - old_name, - new_name, - data_type, - options, - } - } else if self.parse_keyword(Keyword::ALTER) { - let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] - let column_name = self.parse_identifier()?; - let is_postgresql = dialect_of!(self is PostgreSqlDialect); - - let op = if self.parse_keywords(&[Keyword::SET, Keyword::NOT, Keyword::NULL]) { - AlterColumnOperation::SetNotNull {} - } else if self.parse_keywords(&[Keyword::DROP, Keyword::NOT, Keyword::NULL]) { - AlterColumnOperation::DropNotNull {} - } else if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT]) { - AlterColumnOperation::SetDefault { - value: self.parse_expr()?, - } - } 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 { - return self.expected( - "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE after ALTER COLUMN", - self.peek_token(), - ); - }; - AlterTableOperation::AlterColumn { column_name, op } - } else if self.parse_keyword(Keyword::SWAP) { - self.expect_keyword(Keyword::WITH)?; - let table_name = self.parse_object_name()?; - AlterTableOperation::SwapWith { table_name } - } else { - return self.expected( - "ADD, RENAME, PARTITION, SWAP or DROP after ALTER TABLE", - self.peek_token(), - ); - }; + let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; Ok(Statement::AlterTable { name: table_name, - operation, + if_exists, + only, + operations, }) } Keyword::INDEX => { diff --git a/src/test_utils.rs b/src/test_utils.rs index a5e9e739d..0db1e7d24 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -209,6 +209,26 @@ pub fn expr_from_projection(item: &SelectItem) -> &Expr { } } +pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTableOperation { + match stmt { + Statement::AlterTable { + name, + if_exists, + only: is_only, + operations, + } => { + assert_eq!(name.to_string(), expected_name); + assert!(!if_exists); + assert!(!is_only); + only(operations) + } + _ => panic!("Expected ALTER TABLE statement"), + } +} +pub fn alter_table_op(stmt: Statement) -> AlterTableOperation { + alter_table_op_with_name(stmt, "tab") +} + /// Creates a `Value::Number`, panic'ing if n is not a number pub fn number(n: &str) -> Value { Value::Number(n.parse().unwrap(), false) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a9c4130b4..1bf108cb0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -30,8 +30,8 @@ use sqlparser::dialect::{ use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError, ParserOptions}; use test_utils::{ - all_dialects, assert_eq_vec, expr_from_projection, join, number, only, table, table_alias, - TestedDialects, + all_dialects, alter_table_op, assert_eq_vec, expr_from_projection, join, number, only, table, + table_alias, TestedDialects, }; #[macro_use] @@ -2920,19 +2920,17 @@ fn parse_create_external_table_lowercase() { #[test] fn parse_alter_table() { let add_column = "ALTER TABLE tab ADD COLUMN foo TEXT;"; - match one_statement_parses_to(add_column, "ALTER TABLE tab ADD COLUMN foo TEXT") { - Statement::AlterTable { - name, - operation: - AlterTableOperation::AddColumn { - column_keyword, - if_not_exists, - column_def, - }, + match alter_table_op(one_statement_parses_to( + add_column, + "ALTER TABLE tab ADD COLUMN foo TEXT", + )) { + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, } => { assert!(column_keyword); assert!(!if_not_exists); - assert_eq!("tab", name.to_string()); assert_eq!("foo", column_def.name.to_string()); assert_eq!("TEXT", column_def.data_type.to_string()); } @@ -2940,28 +2938,19 @@ fn parse_alter_table() { }; let rename_table = "ALTER TABLE tab RENAME TO new_tab"; - match verified_stmt(rename_table) { - Statement::AlterTable { - name, - operation: AlterTableOperation::RenameTable { table_name }, - } => { - assert_eq!("tab", name.to_string()); - assert_eq!("new_tab", table_name.to_string()) + match alter_table_op(verified_stmt(rename_table)) { + AlterTableOperation::RenameTable { table_name } => { + assert_eq!("new_tab", table_name.to_string()); } _ => unreachable!(), }; let rename_column = "ALTER TABLE tab RENAME COLUMN foo TO new_foo"; - match verified_stmt(rename_column) { - Statement::AlterTable { - name, - operation: - AlterTableOperation::RenameColumn { - old_column_name, - new_column_name, - }, + match alter_table_op(verified_stmt(rename_column)) { + AlterTableOperation::RenameColumn { + old_column_name, + new_column_name, } => { - assert_eq!("tab", name.to_string()); assert_eq!(old_column_name.to_string(), "foo"); assert_eq!(new_column_name.to_string(), "new_foo"); } @@ -3047,21 +3036,15 @@ fn parse_alter_view_with_columns() { #[test] fn parse_alter_table_add_column() { - match verified_stmt("ALTER TABLE tab ADD foo TEXT") { - Statement::AlterTable { - operation: AlterTableOperation::AddColumn { column_keyword, .. }, - .. - } => { + match alter_table_op(verified_stmt("ALTER TABLE tab ADD foo TEXT")) { + AlterTableOperation::AddColumn { column_keyword, .. } => { assert!(!column_keyword); } _ => unreachable!(), }; - match verified_stmt("ALTER TABLE tab ADD COLUMN foo TEXT") { - Statement::AlterTable { - operation: AlterTableOperation::AddColumn { column_keyword, .. }, - .. - } => { + match alter_table_op(verified_stmt("ALTER TABLE tab ADD COLUMN foo TEXT")) { + AlterTableOperation::AddColumn { column_keyword, .. } => { assert!(column_keyword); } _ => unreachable!(), @@ -3080,24 +3063,19 @@ fn parse_alter_table_add_column_if_not_exists() { options: None, }; - match dialects.verified_stmt("ALTER TABLE tab ADD IF NOT EXISTS foo TEXT") { - Statement::AlterTable { - operation: AlterTableOperation::AddColumn { if_not_exists, .. }, - .. - } => { + match alter_table_op(dialects.verified_stmt("ALTER TABLE tab ADD IF NOT EXISTS foo TEXT")) { + AlterTableOperation::AddColumn { if_not_exists, .. } => { assert!(if_not_exists); } _ => unreachable!(), }; - match dialects.verified_stmt("ALTER TABLE tab ADD COLUMN IF NOT EXISTS foo TEXT") { - Statement::AlterTable { - operation: - AlterTableOperation::AddColumn { - column_keyword, - if_not_exists, - .. - }, + match alter_table_op( + dialects.verified_stmt("ALTER TABLE tab ADD COLUMN IF NOT EXISTS foo TEXT"), + ) { + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, .. } => { assert!(column_keyword); @@ -3123,12 +3101,10 @@ fn parse_alter_table_constraints() { check_one("CHECK (end_date > start_date OR end_date IS NULL)"); fn check_one(constraint_text: &str) { - match verified_stmt(&format!("ALTER TABLE tab ADD {constraint_text}")) { - Statement::AlterTable { - name, - operation: AlterTableOperation::AddConstraint(constraint), - } => { - assert_eq!("tab", name.to_string()); + match alter_table_op(verified_stmt(&format!( + "ALTER TABLE tab ADD {constraint_text}" + ))) { + AlterTableOperation::AddConstraint(constraint) => { assert_eq!(constraint_text, constraint.to_string()); } _ => unreachable!(), @@ -3150,17 +3126,12 @@ fn parse_alter_table_drop_column() { ); fn check_one(constraint_text: &str) { - match verified_stmt(&format!("ALTER TABLE tab {constraint_text}")) { - Statement::AlterTable { - name, - operation: - AlterTableOperation::DropColumn { - column_name, - if_exists, - cascade, - }, + match alter_table_op(verified_stmt(&format!("ALTER TABLE tab {constraint_text}"))) { + AlterTableOperation::DropColumn { + column_name, + if_exists, + cascade, } => { - assert_eq!("tab", name.to_string()); assert_eq!("is_active", column_name.to_string()); assert!(if_exists); assert!(cascade); @@ -3173,12 +3144,10 @@ fn parse_alter_table_drop_column() { #[test] fn parse_alter_table_alter_column() { let alter_stmt = "ALTER TABLE tab"; - match verified_stmt(&format!("{alter_stmt} ALTER COLUMN is_active SET NOT NULL")) { - Statement::AlterTable { - name, - operation: AlterTableOperation::AlterColumn { column_name, op }, - } => { - assert_eq!("tab", name.to_string()); + match alter_table_op(verified_stmt(&format!( + "{alter_stmt} ALTER COLUMN is_active SET NOT NULL" + ))) { + AlterTableOperation::AlterColumn { column_name, op } => { assert_eq!("is_active", column_name.to_string()); assert_eq!(op, AlterColumnOperation::SetNotNull {}); } @@ -3190,14 +3159,10 @@ fn parse_alter_table_alter_column() { "ALTER TABLE tab ALTER COLUMN is_active DROP NOT NULL", ); - match verified_stmt(&format!( + match alter_table_op(verified_stmt(&format!( "{alter_stmt} ALTER COLUMN is_active SET DEFAULT false" - )) { - Statement::AlterTable { - name, - operation: AlterTableOperation::AlterColumn { column_name, op }, - } => { - assert_eq!("tab", name.to_string()); + ))) { + AlterTableOperation::AlterColumn { column_name, op } => { assert_eq!("is_active", column_name.to_string()); assert_eq!( op, @@ -3209,12 +3174,10 @@ fn parse_alter_table_alter_column() { _ => unreachable!(), } - match verified_stmt(&format!("{alter_stmt} ALTER COLUMN is_active DROP DEFAULT")) { - Statement::AlterTable { - name, - operation: AlterTableOperation::AlterColumn { column_name, op }, - } => { - assert_eq!("tab", name.to_string()); + match alter_table_op(verified_stmt(&format!( + "{alter_stmt} ALTER COLUMN is_active DROP DEFAULT" + ))) { + AlterTableOperation::AlterColumn { column_name, op } => { assert_eq!("is_active", column_name.to_string()); assert_eq!(op, AlterColumnOperation::DropDefault {}); } @@ -3225,12 +3188,10 @@ fn parse_alter_table_alter_column() { #[test] fn parse_alter_table_alter_column_type() { let alter_stmt = "ALTER TABLE tab"; - match verified_stmt("ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT") { - Statement::AlterTable { - name, - operation: AlterTableOperation::AlterColumn { column_name, op }, - } => { - assert_eq!("tab", name.to_string()); + match alter_table_op(verified_stmt( + "ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT", + )) { + AlterTableOperation::AlterColumn { column_name, op } => { assert_eq!("is_active", column_name.to_string()); assert_eq!( op, @@ -3267,34 +3228,28 @@ fn parse_alter_table_alter_column_type() { #[test] fn parse_alter_table_drop_constraint() { let alter_stmt = "ALTER TABLE tab"; - match verified_stmt("ALTER TABLE tab DROP CONSTRAINT constraint_name CASCADE") { - Statement::AlterTable { - name, - operation: - AlterTableOperation::DropConstraint { - name: constr_name, - if_exists, - cascade, - }, + match alter_table_op(verified_stmt( + "ALTER TABLE tab DROP CONSTRAINT constraint_name CASCADE", + )) { + AlterTableOperation::DropConstraint { + name: constr_name, + if_exists, + cascade, } => { - assert_eq!("tab", name.to_string()); assert_eq!("constraint_name", constr_name.to_string()); assert!(!if_exists); assert!(cascade); } _ => unreachable!(), } - match verified_stmt("ALTER TABLE tab DROP CONSTRAINT IF EXISTS constraint_name") { - Statement::AlterTable { - name, - operation: - AlterTableOperation::DropConstraint { - name: constr_name, - if_exists, - cascade, - }, + match alter_table_op(verified_stmt( + "ALTER TABLE tab DROP CONSTRAINT IF EXISTS constraint_name", + )) { + AlterTableOperation::DropConstraint { + name: constr_name, + if_exists, + cascade, } => { - assert_eq!("tab", name.to_string()); assert_eq!("constraint_name", constr_name.to_string()); assert!(if_exists); assert!(!cascade); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ab7997cdf..7346b9c0d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -14,6 +14,7 @@ //! Test SQL syntax specific to MySQL. The parser based on the generic dialect //! is also tested (on the inputs it can handle). +use matches::assert_matches; use sqlparser::ast::Expr; use sqlparser::ast::Value; use sqlparser::ast::*; @@ -1256,15 +1257,10 @@ fn parse_update_with_joins() { #[test] fn parse_alter_table_drop_primary_key() { - match mysql_and_generic().verified_stmt("ALTER TABLE tab DROP PRIMARY KEY") { - Statement::AlterTable { - name, - operation: AlterTableOperation::DropPrimaryKey, - } => { - assert_eq!("tab", name.to_string()); - } - _ => unreachable!(), - } + assert_matches!( + alter_table_op(mysql_and_generic().verified_stmt("ALTER TABLE tab DROP PRIMARY KEY")), + AlterTableOperation::DropPrimaryKey + ); } #[test] @@ -1278,22 +1274,16 @@ fn parse_alter_table_change_column() { }; let sql1 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL"; - match mysql().verified_stmt(sql1) { - Statement::AlterTable { name, operation } => { - assert_eq!(expected_name, name); - assert_eq!(expected_operation, operation); - } - _ => unreachable!(), - } + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql1), &expected_name.to_string()); + assert_eq!(expected_operation, operation); let sql2 = "ALTER TABLE orders CHANGE description desc TEXT NOT NULL"; - match mysql().one_statement_parses_to(sql2, sql1) { - Statement::AlterTable { name, operation } => { - assert_eq!(expected_name, name); - assert_eq!(expected_operation, operation); - } - _ => unreachable!(), - } + let operation = alter_table_op_with_name( + mysql().one_statement_parses_to(sql2, sql1), + &expected_name.to_string(), + ); + assert_eq!(expected_operation, operation); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c34ba75a8..093fac8c1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -546,12 +546,10 @@ fn parse_create_table_constraints_only() { #[test] fn parse_alter_table_constraints_rename() { - match pg().verified_stmt("ALTER TABLE tab RENAME CONSTRAINT old_name TO new_name") { - Statement::AlterTable { - name, - operation: AlterTableOperation::RenameConstraint { old_name, new_name }, - } => { - assert_eq!("tab", name.to_string()); + match alter_table_op( + pg().verified_stmt("ALTER TABLE tab RENAME CONSTRAINT old_name TO new_name"), + ) { + AlterTableOperation::RenameConstraint { old_name, new_name } => { assert_eq!(old_name.to_string(), "old_name"); assert_eq!(new_name.to_string(), "new_name"); } @@ -566,14 +564,12 @@ fn parse_alter_table_alter_column() { "ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'", ); - match pg() - .verified_stmt("ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'") - { - Statement::AlterTable { - name, - operation: AlterTableOperation::AlterColumn { column_name, op }, - } => { - assert_eq!("tab", name.to_string()); + match alter_table_op( + pg().verified_stmt( + "ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'", + ), + ) { + AlterTableOperation::AlterColumn { column_name, op } => { assert_eq!("is_active", column_name.to_string()); let using_expr = Expr::Value(Value::SingleQuotedString("text".to_string())); assert_eq!( @@ -588,6 +584,48 @@ fn parse_alter_table_alter_column() { } } +#[test] +fn parse_alter_table_add_columns() { + match pg().verified_stmt("ALTER TABLE IF EXISTS ONLY tab ADD COLUMN a TEXT, ADD COLUMN b INT") { + Statement::AlterTable { + name, + if_exists, + only, + operations, + } => { + assert_eq!(name.to_string(), "tab"); + assert!(if_exists); + assert!(only); + assert_eq!( + operations, + vec![ + AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "a".into(), + data_type: DataType::Text, + collation: None, + options: vec![], + }, + }, + AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "b".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + }, + ] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_if_not_exists() { let sql = "CREATE TABLE IF NOT EXISTS uk_cities ()"; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ecd608527..dabe66e1d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -502,12 +502,8 @@ fn test_select_wildcard_with_exclude_and_rename() { #[test] fn test_alter_table_swap_with() { let sql = "ALTER TABLE tab1 SWAP WITH tab2"; - match snowflake_and_generic().verified_stmt(sql) { - Statement::AlterTable { - name, - operation: AlterTableOperation::SwapWith { table_name }, - } => { - assert_eq!("tab1", name.to_string()); + match alter_table_op_with_name(snowflake_and_generic().verified_stmt(sql), "tab1") { + AlterTableOperation::SwapWith { table_name } => { assert_eq!("tab2", table_name.to_string()); } _ => unreachable!(), From 2593dcfb79bb8709a6014673d47ffefcf9a68e20 Mon Sep 17 00:00:00 2001 From: ding-young Date: Fri, 8 Sep 2023 19:12:44 +0900 Subject: [PATCH 255/806] Add missing token loc in parse err msg (#965) --- src/parser/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b872bbfc8..348267fd8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1791,9 +1791,12 @@ impl<'a> Parser<'a> { | BinaryOperator::Eq | BinaryOperator::NotEq ) { - return parser_err!(format!( + return parser_err!( + format!( "Expected one of [=, >, <, =>, =<, !=] as comparison operator, found: {op}" - )); + ), + tok.location + ); }; Ok(match keyword { From bb7b05e106496c204b96d5371ed91bfb1edef728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20=C5=9Eahin?= <124376117+berkaysynnada@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:47:56 +0300 Subject: [PATCH 256/806] feat: Group By All (#964) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 2 +- src/ast/query.rs | 37 ++++++++++++++++++++++++++++++++--- src/parser/mod.rs | 8 ++++++-- tests/sqlparser_clickhouse.rs | 2 +- tests/sqlparser_common.rs | 26 ++++++++++++++++++------ tests/sqlparser_duckdb.rs | 8 ++++---- tests/sqlparser_mssql.rs | 4 ++-- tests/sqlparser_mysql.rs | 16 +++++++-------- tests/sqlparser_postgres.rs | 18 ++++++++--------- 9 files changed, 85 insertions(+), 36 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 79edea7d1..eb8830bb1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -36,7 +36,7 @@ pub use self::ddl::{ }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, IdentWithAlias, Join, + Cte, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, GroupByExpr, IdentWithAlias, Join, JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, diff --git a/src/ast/query.rs b/src/ast/query.rs index b70017654..c9fcdecc9 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -214,7 +214,7 @@ pub struct Select { /// WHERE pub selection: Option, /// GROUP BY - pub group_by: Vec, + pub group_by: GroupByExpr, /// CLUSTER BY (Hive) pub cluster_by: Vec, /// DISTRIBUTE BY (Hive) @@ -255,8 +255,13 @@ impl fmt::Display for Select { if let Some(ref selection) = self.selection { write!(f, " WHERE {selection}")?; } - if !self.group_by.is_empty() { - write!(f, " GROUP BY {}", display_comma_separated(&self.group_by))?; + match &self.group_by { + GroupByExpr::All => write!(f, " GROUP BY ALL")?, + GroupByExpr::Expressions(exprs) => { + if !exprs.is_empty() { + write!(f, " GROUP BY {}", display_comma_separated(exprs))?; + } + } } if !self.cluster_by.is_empty() { write!( @@ -1218,3 +1223,29 @@ impl fmt::Display for SelectInto { write!(f, "INTO{}{}{} {}", temporary, unlogged, table, self.name) } } + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum GroupByExpr { + /// ALL syntax of [Snowflake], and [DuckDB] + /// + /// [Snowflake]: + /// [DuckDB]: + All, + + /// Expressions + Expressions(Vec), +} + +impl fmt::Display for GroupByExpr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GroupByExpr::All => write!(f, "GROUP BY ALL"), + GroupByExpr::Expressions(col_names) => { + let col_names = display_comma_separated(col_names); + write!(f, "GROUP BY ({col_names})") + } + } + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 348267fd8..6f81e1e4e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5662,9 +5662,13 @@ impl<'a> Parser<'a> { }; let group_by = if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { - self.parse_comma_separated(Parser::parse_group_by_expr)? + if self.parse_keyword(Keyword::ALL) { + GroupByExpr::All + } else { + GroupByExpr::Expressions(self.parse_comma_separated(Parser::parse_group_by_expr)?) + } } else { - vec![] + GroupByExpr::Expressions(vec![]) }; let cluster_by = if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 77b936d55..5241777e5 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -97,7 +97,7 @@ fn parse_map_access_expr() { right: Box::new(Expr::Value(Value::SingleQuotedString("foo".to_string()))) }) }), - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1bf108cb0..be10914dc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -247,7 +247,9 @@ fn parse_update_set_from() { }], lateral_views: vec![], selection: None, - group_by: vec![Expr::Identifier(Ident::new("id"))], + group_by: GroupByExpr::Expressions(vec![Expr::Identifier(Ident::new( + "id" + ))]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1808,10 +1810,10 @@ fn parse_select_group_by() { let sql = "SELECT id, fname, lname FROM customer GROUP BY lname, fname"; let select = verified_only_select(sql); assert_eq!( - vec![ + GroupByExpr::Expressions(vec![ Expr::Identifier(Ident::new("lname")), Expr::Identifier(Ident::new("fname")), - ], + ]), select.group_by ); @@ -1822,6 +1824,18 @@ fn parse_select_group_by() { ); } +#[test] +fn parse_select_group_by_all() { + let sql = "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL"; + let select = verified_only_select(sql); + assert_eq!(GroupByExpr::All, select.group_by); + + one_statement_parses_to( + "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL", + "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL", + ); +} + #[test] fn parse_select_having() { let sql = "SELECT foo FROM bar GROUP BY foo HAVING COUNT(*) > 1"; @@ -3543,7 +3557,7 @@ fn test_parse_named_window() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -3930,7 +3944,7 @@ fn parse_interval_and_or_xor() { }), }), }), - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -6333,7 +6347,7 @@ fn parse_merge() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 3587e8d90..0fe4bb0e7 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -161,7 +161,7 @@ fn test_select_union_by_name() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -194,7 +194,7 @@ fn test_select_union_by_name() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -236,7 +236,7 @@ fn test_select_union_by_name() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -269,7 +269,7 @@ fn test_select_union_by_name() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index c4e0f3274..9ed12ac21 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -103,7 +103,7 @@ fn parse_create_procedure() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -519,7 +519,7 @@ fn parse_substring_in_select() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 7346b9c0d..00901799f 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -496,7 +496,7 @@ fn parse_escaped_quote_identifiers_with_escape() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -538,7 +538,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -577,7 +577,7 @@ fn parse_escaped_backticks_with_escape() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -616,7 +616,7 @@ fn parse_escaped_backticks_with_no_escape() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1100,7 +1100,7 @@ fn parse_select_with_numeric_prefix_column_name() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1149,7 +1149,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1325,7 +1325,7 @@ fn parse_substring_in_select() { }], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1604,7 +1604,7 @@ fn parse_hex_string_introducer() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 093fac8c1..079e4695a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -990,7 +990,7 @@ fn parse_copy_to() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), having: None, named_window: vec![], cluster_by: vec![], @@ -2019,7 +2019,7 @@ fn parse_array_subquery_expr() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -2035,7 +2035,7 @@ fn parse_array_subquery_expr() { from: vec![], lateral_views: vec![], selection: None, - group_by: vec![], + group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -3321,14 +3321,14 @@ fn parse_select_group_by_grouping_sets() { "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, GROUPING SETS ((brand), (size), ())" ); assert_eq!( - vec![ + GroupByExpr::Expressions(vec![ Expr::Identifier(Ident::new("size")), Expr::GroupingSets(vec![ vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], vec![], ]), - ], + ]), select.group_by ); } @@ -3339,13 +3339,13 @@ fn parse_select_group_by_rollup() { "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, ROLLUP (brand, size)", ); assert_eq!( - vec![ + GroupByExpr::Expressions(vec![ Expr::Identifier(Ident::new("size")), Expr::Rollup(vec![ vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], ]), - ], + ]), select.group_by ); } @@ -3356,13 +3356,13 @@ fn parse_select_group_by_cube() { "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, CUBE (brand, size)", ); assert_eq!( - vec![ + GroupByExpr::Expressions(vec![ Expr::Identifier(Ident::new("size")), Expr::Cube(vec![ vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], ]), - ], + ]), select.group_by ); } From 0480ee9886e73affaebe330844cd5c26e0e8e1b0 Mon Sep 17 00:00:00 2001 From: artorias1024 <82564604+artorias1024@users.noreply.github.com> Date: Fri, 8 Sep 2023 18:58:31 +0800 Subject: [PATCH 257/806] feat: Add support for parsing the syntax of MySQL UNIQUE KEY. (#962) Co-authored-by: yukunpeng Co-authored-by: Andrew Lamb --- src/parser/mod.rs | 12 ++++++--- tests/sqlparser_mysql.rs | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6f81e1e4e..4d642919e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3941,9 +3941,15 @@ impl<'a> Parser<'a> { match next_token.token { Token::Word(w) if w.keyword == Keyword::PRIMARY || w.keyword == Keyword::UNIQUE => { let is_primary = w.keyword == Keyword::PRIMARY; - if is_primary { - self.expect_keyword(Keyword::KEY)?; - } + + // parse optional [KEY] + let _ = self.parse_keyword(Keyword::KEY); + + // optional constraint name + let name = self + .maybe_parse(|parser| parser.parse_identifier()) + .or(name); + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; Ok(Some(TableConstraint::Unique { name, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 00901799f..5aeffb0b6 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -297,6 +297,62 @@ fn parse_create_table_auto_increment() { } } +#[test] +fn parse_create_table_unique_key() { + let sql = "CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, UNIQUE KEY bar_key (bar))"; + let canonical = "CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, CONSTRAINT bar_key UNIQUE (bar))"; + match mysql().one_statement_parses_to(sql, canonical) { + Statement::CreateTable { + name, + columns, + constraints, + .. + } => { + assert_eq!(name.to_string(), "foo"); + assert_eq!( + vec![TableConstraint::Unique { + name: Some(Ident::new("bar_key")), + columns: vec![Ident::new("bar")], + is_primary: false + }], + constraints + ); + assert_eq!( + vec![ + ColumnDef { + name: Ident::new("id"), + data_type: DataType::Int(None), + collation: None, + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Unique { is_primary: true }, + }, + ColumnOptionDef { + name: None, + option: ColumnOption::DialectSpecific(vec![Token::make_keyword( + "AUTO_INCREMENT" + )]), + }, + ], + }, + ColumnDef { + name: Ident::new("bar"), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::NotNull, + },], + }, + ], + columns + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_comment() { let canonical = "CREATE TABLE foo (bar INT) COMMENT 'baz'"; From a16791d0199633333909044b2e56ca21d96ac83b Mon Sep 17 00:00:00 2001 From: William Date: Thu, 14 Sep 2023 13:56:49 -0400 Subject: [PATCH 258/806] Support `UNNEST` as a table factor for PostgreSQL (#968) --- src/parser/mod.rs | 2 +- tests/sqlparser_postgres.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4d642919e..11f967876 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6240,7 +6240,7 @@ impl<'a> Parser<'a> { // appearing alone in parentheses (e.g. `FROM (mytable)`) self.expected("joined table", self.peek_token()) } - } else if dialect_of!(self is BigQueryDialect | GenericDialect) + } else if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | GenericDialect) && self.parse_keyword(Keyword::UNNEST) { self.expect_token(&Token::LParen)?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 079e4695a..77e5ba45e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3450,3 +3450,30 @@ fn parse_create_table_with_alias() { _ => unreachable!(), } } + +#[test] +fn parse_join_constraint_unnest_alias() { + assert_eq!( + only( + pg().verified_only_select("SELECT * FROM t1 JOIN UNNEST(t1.a) AS f ON c1 = c2") + .from + ) + .joins, + vec![Join { + relation: TableFactor::UNNEST { + alias: table_alias("f"), + array_exprs: vec![Expr::CompoundIdentifier(vec![ + Ident::new("t1"), + Ident::new("a") + ])], + with_offset: false, + with_offset_alias: None + }, + join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + left: Box::new(Expr::Identifier("c1".into())), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier("c2".into())), + })), + }] + ); +} From f6e4be4c154e81cf457249a310004e95d16ecf6f Mon Sep 17 00:00:00 2001 From: "chunshao.rcs" Date: Fri, 15 Sep 2023 02:21:47 +0800 Subject: [PATCH 259/806] Support mysql `partition` to table selection (#959) Co-authored-by: Andrew Lamb --- src/ast/query.rs | 6 +++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 41 +++++++++++++++++++++++++++++++++++ src/test_utils.rs | 1 + tests/sqlparser_bigquery.rs | 2 ++ tests/sqlparser_clickhouse.rs | 16 ++++++++------ tests/sqlparser_common.rs | 29 +++++++++++++++++++++++++ tests/sqlparser_duckdb.rs | 4 ++++ tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 3 +++ tests/sqlparser_mysql.rs | 5 +++++ tests/sqlparser_postgres.rs | 1 + tests/sqlparser_redshift.rs | 3 +++ tests/sqlparser_snowflake.rs | 1 + 14 files changed, 108 insertions(+), 7 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index c9fcdecc9..af35c37a3 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -670,6 +670,8 @@ pub enum TableFactor { /// Optional version qualifier to facilitate table time-travel, as /// supported by BigQuery and MSSQL. version: Option, + /// [Partition selection](https://dev.mysql.com/doc/refman/8.0/en/partitioning-selection.html), supported by MySQL. + partitions: Vec, }, Derived { lateral: bool, @@ -730,8 +732,12 @@ impl fmt::Display for TableFactor { args, with_hints, version, + partitions, } => { write!(f, "{name}")?; + if !partitions.is_empty() { + write!(f, "PARTITION ({})", display_comma_separated(partitions))?; + } if let Some(args) = args { write!(f, "({})", display_comma_separated(args))?; } diff --git a/src/keywords.rs b/src/keywords.rs index c73535fca..ad0526ccd 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -716,6 +716,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::QUALIFY, Keyword::WINDOW, Keyword::END, + // for MYSQL PARTITION SELECTION + Keyword::PARTITION, ]; /// 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 11f967876..ba8f5784f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6277,6 +6277,14 @@ impl<'a> Parser<'a> { } else { let name = self.parse_object_name()?; + let partitions: Vec = if dialect_of!(self is MySqlDialect | GenericDialect) + && self.parse_keyword(Keyword::PARTITION) + { + self.parse_partitions()? + } else { + vec![] + }; + // Parse potential version qualifier let version = self.parse_table_version()?; @@ -6311,6 +6319,7 @@ impl<'a> Parser<'a> { args, with_hints, version, + partitions, }) } } @@ -7483,6 +7492,13 @@ impl<'a> Parser<'a> { representation: UserDefinedTypeRepresentation::Composite { attributes }, }) } + + fn parse_partitions(&mut self) -> Result, ParserError> { + self.expect_token(&Token::LParen)?; + let partitions = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + Ok(partitions) + } } impl Word { @@ -8100,4 +8116,29 @@ mod tests { "sql parser error: Unexpected token following period in identifier: *", ); } + + #[test] + fn test_mysql_partition_selection() { + let sql = "SELECT * FROM employees PARTITION (p0, p2)"; + let expected = vec!["p0", "p2"]; + + let ast: Vec = Parser::parse_sql(&MySqlDialect {}, sql).unwrap(); + assert_eq!(ast.len(), 1); + if let Statement::Query(v) = &ast[0] { + if let SetExpr::Select(select) = &*v.body { + assert_eq!(select.from.len(), 1); + let from: &TableWithJoins = &select.from[0]; + let table_factor = &from.relation; + if let TableFactor::Table { partitions, .. } = table_factor { + let actual: Vec<&str> = partitions + .iter() + .map(|ident| ident.value.as_str()) + .collect(); + assert_eq!(expected, actual); + } + } + } else { + panic!("fail to parse mysql partition selection"); + } + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 0db1e7d24..b81cd5f4e 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -248,6 +248,7 @@ pub fn table(name: impl Into) -> TableFactor { args: None, with_hints: vec![], version: None, + partitions: vec![], } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index d6a28fd00..3502d7dfa 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -96,6 +96,7 @@ fn parse_table_identifiers() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![] },] @@ -160,6 +161,7 @@ fn parse_table_time_travel() { version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( Value::SingleQuotedString(version) ))), + partitions: vec![], }, joins: vec![] },] diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 5241777e5..a14598b3d 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -63,15 +63,16 @@ fn parse_map_access_expr() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, - joins: vec![] + joins: vec![], }], lateral_views: vec![], selection: Some(BinaryOp { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString("test".to_string()))) + right: Box::new(Expr::Value(Value::SingleQuotedString("test".to_string()))), }), op: BinaryOperator::And, right: Box::new(BinaryOp { @@ -91,11 +92,11 @@ fn parse_map_access_expr() { distinct: false, special: false, order_by: vec![], - })] + })], }), op: BinaryOperator::NotEq, - right: Box::new(Expr::Value(Value::SingleQuotedString("foo".to_string()))) - }) + right: Box::new(Expr::Value(Value::SingleQuotedString("foo".to_string()))), + }), }), group_by: GroupByExpr::Expressions(vec![]), cluster_by: vec![], @@ -103,7 +104,7 @@ fn parse_map_access_expr() { sort_by: vec![], having: None, named_window: vec![], - qualify: None + qualify: None, }, select ); @@ -117,7 +118,7 @@ fn parse_array_expr() { &Expr::Array(Array { elem: vec![ Expr::Value(Value::SingleQuotedString("1".to_string())), - Expr::Value(Value::SingleQuotedString("2".to_string())) + Expr::Value(Value::SingleQuotedString("2".to_string())), ], named: false, }), @@ -171,6 +172,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index be10914dc..6f780de9e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -215,6 +215,7 @@ fn parse_update_set_from() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }, @@ -242,6 +243,7 @@ fn parse_update_set_from() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }], @@ -308,6 +310,7 @@ fn parse_update_with_table_alias() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }, @@ -371,6 +374,7 @@ fn parse_select_with_table_alias() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }] @@ -402,6 +406,7 @@ fn parse_delete_statement() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, from[0].relation ); @@ -430,6 +435,7 @@ fn parse_delete_statement_for_multi_tables() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, from[0].relation ); @@ -440,6 +446,7 @@ fn parse_delete_statement_for_multi_tables() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, from[0].joins[0].relation ); @@ -464,6 +471,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, from[0].relation ); @@ -474,6 +482,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, from[1].relation ); @@ -484,6 +493,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, using[0].relation ); @@ -494,6 +504,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, using[0].joins[0].relation ); @@ -522,6 +533,7 @@ fn parse_where_delete_statement() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, from[0].relation, ); @@ -564,6 +576,7 @@ fn parse_where_delete_with_alias_statement() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, from[0].relation, ); @@ -578,6 +591,7 @@ fn parse_where_delete_with_alias_statement() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }]), @@ -3552,6 +3566,7 @@ fn test_parse_named_window() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }], @@ -3891,6 +3906,7 @@ fn parse_interval_and_or_xor() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }], @@ -4496,6 +4512,7 @@ fn parse_implicit_join() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }, @@ -4506,6 +4523,7 @@ fn parse_implicit_join() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }, @@ -4524,6 +4542,7 @@ fn parse_implicit_join() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -4532,6 +4551,7 @@ fn parse_implicit_join() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -4543,6 +4563,7 @@ fn parse_implicit_join() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -4551,6 +4572,7 @@ fn parse_implicit_join() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -4572,6 +4594,7 @@ fn parse_cross_join() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, join_operator: JoinOperator::CrossJoin, }, @@ -4593,6 +4616,7 @@ fn parse_joins_on() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, join_operator: f(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -4663,6 +4687,7 @@ fn parse_joins_using() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), } @@ -4725,6 +4750,7 @@ fn parse_natural_join() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, join_operator: f(JoinConstraint::Natural), } @@ -4990,6 +5016,7 @@ fn parse_derived_tables() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -6318,6 +6345,7 @@ fn parse_merge() { args: None, with_hints: vec![], version: None, + partitions: vec![], } ); assert_eq!(table, table_no_into); @@ -6342,6 +6370,7 @@ fn parse_merge() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }], diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 0fe4bb0e7..b05cc0dd4 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -156,6 +156,7 @@ fn test_select_union_by_name() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }], @@ -189,6 +190,7 @@ fn test_select_union_by_name() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }], @@ -231,6 +233,7 @@ fn test_select_union_by_name() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }], @@ -264,6 +267,7 @@ fn test_select_union_by_name() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], }], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 8cdfe9248..6ca47e12c 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -323,6 +323,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9ed12ac21..135e5d138 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -58,6 +58,7 @@ fn parse_table_time_travel() { version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( Value::SingleQuotedString(version) ))), + partitions: vec![], }, joins: vec![] },] @@ -309,6 +310,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); @@ -514,6 +516,7 @@ fn parse_substring_in_select() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![] }], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5aeffb0b6..80ef9f981 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1151,6 +1151,7 @@ fn parse_select_with_numeric_prefix_column_name() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![] }], @@ -1200,6 +1201,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![] }], @@ -1260,6 +1262,7 @@ fn parse_update_with_joins() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -1271,6 +1274,7 @@ fn parse_update_with_joins() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ @@ -1376,6 +1380,7 @@ fn parse_substring_in_select() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![] }], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 77e5ba45e..bb3857817 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2893,6 +2893,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 9f5f62f78..f17ca5841 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -46,6 +46,7 @@ fn test_square_brackets_over_db_schema_table_name() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], } @@ -91,6 +92,7 @@ fn test_double_quotes_over_db_schema_table_name() { args: None, with_hints: vec![], version: None, + partitions: vec![], }, joins: vec![], } @@ -111,6 +113,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index dabe66e1d..e1db7ec61 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -224,6 +224,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); From 71c35d4dfddc89cfdd8f67bdc4c5d4e701a355e8 Mon Sep 17 00:00:00 2001 From: Ilya Date: Wed, 20 Sep 2023 04:31:11 +0300 Subject: [PATCH 260/806] Add support for == operator for Sqlite (#970) --- src/tokenizer.rs | 1 + tests/sqlparser_sqlite.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 1b1b1e96c..175b5d3b1 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -885,6 +885,7 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume match chars.peek() { Some('>') => self.consume_and_return(chars, Token::RArrow), + Some('=') => self.consume_and_return(chars, Token::DoubleEq), _ => Ok(Some(Token::Eq)), } } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 8f6cc7572..fd7a22461 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -61,6 +61,14 @@ fn parse_create_virtual_table() { sqlite_and_generic().verified_stmt(sql); } +#[test] +fn double_equality_operator() { + // Sqlite supports this operator: https://www.sqlite.org/lang_expr.html#binaryops + let input = "SELECT a==b FROM t"; + let expected = "SELECT a = b FROM t"; + let _ = sqlite_and_generic().one_statement_parses_to(input, expected); +} + #[test] fn parse_create_table_auto_increment() { let sql = "CREATE TABLE foo (bar INT PRIMARY KEY AUTOINCREMENT)"; From 521ffa945c9b9562d1622cb2860faf0830994bc6 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 21 Sep 2023 13:45:19 -0400 Subject: [PATCH 261/806] Changelog for 0.38.0 release (#973) --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c6c9dcc6..9e3e4e1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,29 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.38.0] 2023-09-21 + +### Added + +* Support `==`operator for Sqlite (#970) - Thanks @marhoily +* Support mysql `PARTITION` to table selection (#959) - Thanks @chunshao90 +* Support `UNNEST` as a table factor for PostgreSQL (#968) @hexedpackets +* Support MySQL `UNIQUE KEY` syntax (#962) - Thanks @artorias1024 +* Support` `GROUP BY ALL` (#964) - @berkaysynnada +* Support multiple actions in one ALTER TABLE statement (#960) - Thanks @ForbesLindesay +* Add `--sqlite param` to CLI (#956) - Thanks @ddol + +### Fixed +* Fix Rust 1.72 clippy lints (#957) - Thanks @alamb + +### Changed +* Add missing token loc in parse err msg (#965) - Thanks @ding-young +* Change how `ANY` and `ALL` expressions are represented in AST (#963) - Thanks @SeanTroyUWO +* Show location info in parse errors (#958) - Thanks @MartinNowak +* Update release documentation (#954) - Thanks @alamb +* Break test and coverage test into separate jobs (#949) - Thanks @alamb + + ## [0.37.0] 2023-08-22 ### Added From 7723ea56c5119c7d1a15233c18eb2aaf48b60dc0 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 21 Sep 2023 13:47:15 -0400 Subject: [PATCH 262/806] chore: Release sqlparser version 0.38.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 43d9f981d..4cc2f1eaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.37.0" +version = "0.38.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 495d0a02d5680c93a8886fde7cec4d9998c9b595 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 2 Oct 2023 13:10:56 +0200 Subject: [PATCH 263/806] Add support for ATTACH DATABASE (#989) --- src/ast/mod.rs | 18 ++++++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 13 +++++++++++++ tests/sqlparser_sqlite.rs | 18 ++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb8830bb1..48274f68c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1429,6 +1429,16 @@ pub enum Statement { name: Ident, operation: AlterRoleOperation, }, + /// ATTACH DATABASE 'path/to/file' AS alias + /// (SQLite-specific) + AttachDatabase { + /// The name to bind to the newly attached database + schema_name: Ident, + /// An expression that indicates the path to the database file + database_file_name: Expr, + /// true if the syntax is 'ATTACH DATABASE', false if it's just 'ATTACH' + database: bool, + }, /// DROP Drop { /// The type of the object to drop: TABLE, VIEW, etc. @@ -1969,6 +1979,14 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::AttachDatabase { + schema_name, + database_file_name, + database, + } => { + let keyword = if *database { "DATABASE " } else { "" }; + write!(f, "ATTACH {keyword}{database_file_name} AS {schema_name}") + } Statement::Analyze { table_name, partitions, diff --git a/src/keywords.rs b/src/keywords.rs index ad0526ccd..eee961350 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -95,6 +95,7 @@ define_keywords!( ASYMMETRIC, AT, ATOMIC, + ATTACH, AUTHORIZATION, AUTOINCREMENT, AUTO_INCREMENT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ba8f5784f..49cd24899 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -456,6 +456,7 @@ impl<'a> Parser<'a> { Ok(Statement::Query(Box::new(self.parse_query()?))) } Keyword::TRUNCATE => Ok(self.parse_truncate()?), + Keyword::ATTACH => Ok(self.parse_attach_database()?), Keyword::MSCK => Ok(self.parse_msck()?), Keyword::CREATE => Ok(self.parse_create()?), Keyword::CACHE => Ok(self.parse_cache_table()?), @@ -543,6 +544,18 @@ impl<'a> Parser<'a> { }) } + pub fn parse_attach_database(&mut self) -> Result { + let database = self.parse_keyword(Keyword::DATABASE); + let database_file_name = self.parse_expr()?; + self.expect_keyword(Keyword::AS)?; + let schema_name = self.parse_identifier()?; + Ok(Statement::AttachDatabase { + database, + schema_name, + database_file_name, + }) + } + pub fn parse_analyze(&mut self) -> Result { self.expect_keyword(Keyword::TABLE)?; let table_name = self.parse_object_name()?; diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index fd7a22461..c4e69d530 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -259,6 +259,24 @@ fn parse_create_table_with_strict() { } } +#[test] +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)); + match verified_stmt { + Statement::AttachDatabase { + schema_name, + database_file_name: Expr::Value(Value::SingleQuotedString(literal_name)), + database: true, + } => { + assert_eq!(schema_name.value, "test"); + assert_eq!(literal_name, "test.db"); + } + _ => unreachable!(), + } +} + fn sqlite() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SQLiteDialect {})], From e718ce6c42365d4fb987ce4977716c28091020a5 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 2 Oct 2023 13:23:25 +0200 Subject: [PATCH 264/806] bigquery: EXTRACT support For DAYOFWEEK, DAYOFYEAR, ISOWEEK, TIME (#980) --- src/ast/value.rs | 8 ++++++++ src/keywords.rs | 3 +++ src/parser/mod.rs | 4 ++++ tests/sqlparser_common.rs | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/src/ast/value.rs b/src/ast/value.rs index 9c18a325c..e6f139256 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -117,6 +117,8 @@ pub enum DateTimeField { Month, Week, Day, + DayOfWeek, + DayOfYear, Date, Hour, Minute, @@ -127,6 +129,7 @@ pub enum DateTimeField { Doy, Epoch, Isodow, + IsoWeek, Isoyear, Julian, Microsecond, @@ -138,6 +141,7 @@ pub enum DateTimeField { Nanosecond, Nanoseconds, Quarter, + Time, Timezone, TimezoneHour, TimezoneMinute, @@ -151,6 +155,8 @@ impl fmt::Display for DateTimeField { DateTimeField::Month => "MONTH", DateTimeField::Week => "WEEK", DateTimeField::Day => "DAY", + DateTimeField::DayOfWeek => "DAYOFWEEK", + DateTimeField::DayOfYear => "DAYOFYEAR", DateTimeField::Date => "DATE", DateTimeField::Hour => "HOUR", DateTimeField::Minute => "MINUTE", @@ -162,6 +168,7 @@ impl fmt::Display for DateTimeField { DateTimeField::Epoch => "EPOCH", DateTimeField::Isodow => "ISODOW", DateTimeField::Isoyear => "ISOYEAR", + DateTimeField::IsoWeek => "ISOWEEK", DateTimeField::Julian => "JULIAN", DateTimeField::Microsecond => "MICROSECOND", DateTimeField::Microseconds => "MICROSECONDS", @@ -172,6 +179,7 @@ impl fmt::Display for DateTimeField { DateTimeField::Nanosecond => "NANOSECOND", DateTimeField::Nanoseconds => "NANOSECONDS", DateTimeField::Quarter => "QUARTER", + DateTimeField::Time => "TIME", DateTimeField::Timezone => "TIMEZONE", DateTimeField::TimezoneHour => "TIMEZONE_HOUR", DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE", diff --git a/src/keywords.rs b/src/keywords.rs index eee961350..6fb74a8e0 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -196,6 +196,8 @@ define_keywords!( DATE, DATETIME, DAY, + DAYOFWEEK, + DAYOFYEAR, DEALLOCATE, DEC, DECADE, @@ -334,6 +336,7 @@ define_keywords!( IS, ISODOW, ISOLATION, + ISOWEEK, ISOYEAR, JAR, JOIN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 49cd24899..279abf968 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1508,6 +1508,8 @@ impl<'a> Parser<'a> { Keyword::MONTH => Ok(DateTimeField::Month), Keyword::WEEK => Ok(DateTimeField::Week), Keyword::DAY => Ok(DateTimeField::Day), + Keyword::DAYOFWEEK => Ok(DateTimeField::DayOfWeek), + Keyword::DAYOFYEAR => Ok(DateTimeField::DayOfYear), Keyword::DATE => Ok(DateTimeField::Date), Keyword::HOUR => Ok(DateTimeField::Hour), Keyword::MINUTE => Ok(DateTimeField::Minute), @@ -1519,6 +1521,7 @@ impl<'a> Parser<'a> { Keyword::EPOCH => Ok(DateTimeField::Epoch), Keyword::ISODOW => Ok(DateTimeField::Isodow), Keyword::ISOYEAR => Ok(DateTimeField::Isoyear), + Keyword::ISOWEEK => Ok(DateTimeField::IsoWeek), Keyword::JULIAN => Ok(DateTimeField::Julian), Keyword::MICROSECOND => Ok(DateTimeField::Microsecond), Keyword::MICROSECONDS => Ok(DateTimeField::Microseconds), @@ -1529,6 +1532,7 @@ impl<'a> Parser<'a> { Keyword::NANOSECOND => Ok(DateTimeField::Nanosecond), Keyword::NANOSECONDS => Ok(DateTimeField::Nanoseconds), Keyword::QUARTER => Ok(DateTimeField::Quarter), + Keyword::TIME => Ok(DateTimeField::Time), Keyword::TIMEZONE => Ok(DateTimeField::Timezone), Keyword::TIMEZONE_HOUR => Ok(DateTimeField::TimezoneHour), Keyword::TIMEZONE_MINUTE => Ok(DateTimeField::TimezoneMinute), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6f780de9e..a9ce3cd6c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2069,6 +2069,8 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(MONTH FROM d)"); verified_stmt("SELECT EXTRACT(WEEK FROM d)"); verified_stmt("SELECT EXTRACT(DAY FROM d)"); + verified_stmt("SELECT EXTRACT(DAYOFWEEK FROM d)"); + verified_stmt("SELECT EXTRACT(DAYOFYEAR FROM d)"); verified_stmt("SELECT EXTRACT(DATE FROM d)"); verified_stmt("SELECT EXTRACT(HOUR FROM d)"); verified_stmt("SELECT EXTRACT(MINUTE FROM d)"); @@ -2082,6 +2084,7 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(DOY FROM d)"); verified_stmt("SELECT EXTRACT(EPOCH FROM d)"); verified_stmt("SELECT EXTRACT(ISODOW FROM d)"); + verified_stmt("SELECT EXTRACT(ISOWEEK FROM d)"); verified_stmt("SELECT EXTRACT(ISOYEAR FROM d)"); verified_stmt("SELECT EXTRACT(JULIAN FROM d)"); verified_stmt("SELECT EXTRACT(MICROSECOND FROM d)"); @@ -2094,6 +2097,7 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(TIMEZONE FROM d)"); verified_stmt("SELECT EXTRACT(TIMEZONE_HOUR FROM d)"); verified_stmt("SELECT EXTRACT(TIMEZONE_MINUTE FROM d)"); + verified_stmt("SELECT EXTRACT(TIME FROM d)"); let res = parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)"); assert_eq!( From 4903bd4b8b8b615a2d1eb0bfad028952321ede56 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 2 Oct 2023 13:39:44 +0200 Subject: [PATCH 265/806] Add test for clickhouse: tokenize `==` as Token::DoubleEq (#981) --- src/test_utils.rs | 3 +++ src/tokenizer.rs | 24 +++++++++++++++++++++++- tests/sqlparser_clickhouse.rs | 8 ++++++++ tests/sqlparser_common.rs | 4 ++-- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index b81cd5f4e..8c64bfacd 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -31,6 +31,9 @@ use crate::parser::{Parser, ParserError}; use crate::tokenizer::Tokenizer; use crate::{ast::*, parser::ParserOptions}; +#[cfg(test)] +use pretty_assertions::assert_eq; + /// Tests use the methods on this struct to invoke the parser on one or /// multiple dialects. pub struct TestedDialects { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 175b5d3b1..067aa5a84 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1368,7 +1368,7 @@ fn peeking_take_while(chars: &mut State, mut predicate: impl FnMut(char) -> bool #[cfg(test)] mod tests { use super::*; - use crate::dialect::{GenericDialect, MsSqlDialect}; + use crate::dialect::{ClickHouseDialect, GenericDialect, MsSqlDialect}; #[test] fn tokenizer_error_impl() { @@ -1414,6 +1414,28 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_clickhouse_double_equal() { + let sql = String::from("SELECT foo=='1'"); + let dialect = ClickHouseDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, &sql); + let tokens = tokenizer.tokenize().unwrap(); + + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Word(Word { + value: "foo".to_string(), + quote_style: None, + keyword: Keyword::NoKeyword, + }), + Token::DoubleEq, + Token::SingleQuotedString("1".to_string()), + ]; + + compare(expected, tokens); + } + #[test] fn tokenize_select_exponent() { let sql = String::from("SELECT 1e10, 1e-10, 1e+10, 1ea, 1e-10a, 1e-10-10"); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index a14598b3d..936b0799a 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -336,6 +336,14 @@ fn parse_create_table() { ); } +#[test] +fn parse_double_equal() { + clickhouse().one_statement_parses_to( + r#"SELECT foo FROM bar WHERE buz == 'buz'"#, + r#"SELECT foo FROM bar WHERE buz = 'buz'"#, + ); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a9ce3cd6c..46503c7f2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6792,10 +6792,10 @@ fn parse_time_functions() { // Validating Parenthesis let sql_without_parens = format!("SELECT {}", func_name); - let mut ast_without_parens = select_localtime_func_call_ast.clone(); + let mut ast_without_parens = select_localtime_func_call_ast; ast_without_parens.special = true; assert_eq!( - &Expr::Function(ast_without_parens.clone()), + &Expr::Function(ast_without_parens), expr_from_projection(&verified_only_select(&sql_without_parens).projection[0]) ); } From ed39329060bbc34d4fc7655f0d9973dacdea5534 Mon Sep 17 00:00:00 2001 From: William Date: Mon, 2 Oct 2023 08:36:17 -0400 Subject: [PATCH 266/806] Add JumpWire to users in README (#990) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1195cc941..454ea6c29 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,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], and [Opteryx]. +[LocustDB], [Ballista], [GlueSQL], [Opteryx], and [JumpWire]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. @@ -179,6 +179,7 @@ licensed as above, without any additional terms or conditions. [Ballista]: https://github.com/apache/arrow-ballista [GlueSQL]: https://github.com/gluesql/gluesql [Opteryx]: https://github.com/mabel-dev/opteryx +[JumpWire]: https://github.com/extragoodlabs/jumpwire [Pratt Parser]: https://tdop.github.io/ [sql-2016-grammar]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html [sql-standard]: https://en.wikipedia.org/wiki/ISO/IEC_9075 From 6ffc3b3a52c6dc49d4f62733ddf3451ff3f9039d Mon Sep 17 00:00:00 2001 From: Ulrich Schmidt-Goertz Date: Mon, 2 Oct 2023 14:42:58 +0200 Subject: [PATCH 267/806] Support DELETE with ORDER BY and LIMIT (MySQL) (#992) --- src/ast/mod.rs | 12 ++++++++++++ src/parser/mod.rs | 13 ++++++++++++- tests/sqlparser_common.rs | 2 ++ tests/sqlparser_mysql.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 48274f68c..f2dbb8899 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1301,6 +1301,10 @@ pub enum Statement { selection: Option, /// RETURNING returning: Option>, + /// ORDER BY (MySQL) + order_by: Vec, + /// LIMIT (MySQL) + limit: Option, }, /// CREATE VIEW CreateView { @@ -2141,6 +2145,8 @@ impl fmt::Display for Statement { using, selection, returning, + order_by, + limit, } => { write!(f, "DELETE ")?; if !tables.is_empty() { @@ -2156,6 +2162,12 @@ impl fmt::Display for Statement { if let Some(returning) = returning { write!(f, " RETURNING {}", display_comma_separated(returning))?; } + if !order_by.is_empty() { + write!(f, " ORDER BY {}", display_comma_separated(order_by))?; + } + if let Some(limit) = limit { + write!(f, " LIMIT {limit}")?; + } Ok(()) } Statement::Close { cursor } => { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 279abf968..a3ebcc475 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5306,12 +5306,21 @@ impl<'a> Parser<'a> { } else { None }; - let returning = if self.parse_keyword(Keyword::RETURNING) { Some(self.parse_comma_separated(Parser::parse_select_item)?) } else { None }; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_order_by_expr)? + } else { + vec![] + }; + let limit = if self.parse_keyword(Keyword::LIMIT) { + self.parse_limit()? + } else { + None + }; Ok(Statement::Delete { tables, @@ -5319,6 +5328,8 @@ impl<'a> Parser<'a> { using, selection, returning, + order_by, + limit, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 46503c7f2..d73061f79 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -525,6 +525,7 @@ fn parse_where_delete_statement() { using, selection, returning, + .. } => { assert_eq!( TableFactor::Table { @@ -565,6 +566,7 @@ fn parse_where_delete_with_alias_statement() { using, selection, returning, + .. } => { assert_eq!( TableFactor::Table { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 80ef9f981..f1a054bfb 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1315,6 +1315,38 @@ fn parse_update_with_joins() { } } +#[test] +fn parse_delete_with_order_by() { + let sql = "DELETE FROM customers ORDER BY id DESC"; + match mysql().verified_stmt(sql) { + Statement::Delete { order_by, .. } => { + assert_eq!( + vec![OrderByExpr { + expr: Expr::Identifier(Ident { + value: "id".to_owned(), + quote_style: None + }), + asc: Some(false), + nulls_first: None, + }], + order_by + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_delete_with_limit() { + let sql = "DELETE FROM customers LIMIT 100"; + match mysql().verified_stmt(sql) { + Statement::Delete { limit, .. } => { + assert_eq!(Some(Expr::Value(number("100"))), limit); + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table_drop_primary_key() { assert_matches!( From 993769ec0267e8684a034ebeb268ae0360785822 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Mon, 2 Oct 2023 14:48:51 +0200 Subject: [PATCH 268/806] Add support for mixed BigQuery table name quoting (#971) Co-authored-by: ifeanyi --- src/parser/mod.rs | 21 ++++++++ src/test_utils.rs | 18 +++++++ tests/sqlparser_bigquery.rs | 95 +++++++++++++++++++++++++++++++++++-- 3 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a3ebcc475..a388a9137 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5041,6 +5041,27 @@ impl<'a> Parser<'a> { 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(|ident| ident.value.contains('.')) + { + idents = idents + .into_iter() + .flat_map(|ident| { + ident + .value + .split('.') + .map(|value| Ident { + value: value.into(), + quote_style: ident.quote_style, + }) + .collect::>() + }) + .collect() + } + Ok(ObjectName(idents)) } diff --git a/src/test_utils.rs b/src/test_utils.rs index 8c64bfacd..f0c5e425a 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -162,6 +162,24 @@ impl TestedDialects { } } + /// Ensures that `sql` parses as a single [`Select`], and that additionally: + /// + /// 1. parsing `sql` results in the same [`Statement`] as parsing + /// `canonical`. + /// + /// 2. re-serializing the result of parsing `sql` produces the same + /// `canonical` sql string + pub fn verified_only_select_with_canonical(&self, query: &str, canonical: &str) -> Select { + let q = match self.one_statement_parses_to(query, canonical) { + Statement::Query(query) => *query, + _ => panic!("Expected Query"), + }; + match *q.body { + SetExpr::Select(s) => *s, + _ => panic!("Expected SetExpr::Select"), + } + } + /// Ensures that `sql` parses as an [`Expr`], and that /// re-serializing the parse result produces the same `sql` /// string (is not modified after a serialization round-trip). diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3502d7dfa..e05581d5f 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -13,6 +13,8 @@ #[macro_use] mod test_utils; +use std::ops::Deref; + use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; use test_utils::*; @@ -84,9 +86,24 @@ fn parse_raw_literal() { #[test] fn parse_table_identifiers() { - fn test_table_ident(ident: &str, expected: Vec) { + /// Parses a table identifier ident and verifies that re-serializing the + /// parsed identifier produces the original ident string. + /// + /// In some cases, re-serializing the result of the parsed ident is not + /// expected to produce the original ident string. canonical is provided + /// instead as the canonical representation of the identifier for comparison. + /// For example, re-serializing the result of ident `foo.bar` produces + /// the equivalent canonical representation `foo`.`bar` + fn test_table_ident(ident: &str, canonical: Option<&str>, expected: Vec) { let sql = format!("SELECT 1 FROM {ident}"); - let select = bigquery().verified_only_select(&sql); + let canonical = canonical.map(|ident| format!("SELECT 1 FROM {ident}")); + + let select = if let Some(canonical) = canonical { + bigquery().verified_only_select_with_canonical(&sql, canonical.deref()) + } else { + bigquery().verified_only_select(&sql) + }; + assert_eq!( select.from, vec![TableWithJoins { @@ -102,26 +119,30 @@ fn parse_table_identifiers() { },] ); } + fn test_table_ident_err(ident: &str) { let sql = format!("SELECT 1 FROM {ident}"); assert!(bigquery().parse_sql_statements(&sql).is_err()); } - test_table_ident("da-sh-es", vec![Ident::new("da-sh-es")]); + test_table_ident("da-sh-es", None, vec![Ident::new("da-sh-es")]); - test_table_ident("`spa ce`", vec![Ident::with_quote('`', "spa ce")]); + test_table_ident("`spa ce`", None, vec![Ident::with_quote('`', "spa ce")]); test_table_ident( "`!@#$%^&*()-=_+`", + None, vec![Ident::with_quote('`', "!@#$%^&*()-=_+")], ); test_table_ident( "_5abc.dataField", + None, vec![Ident::new("_5abc"), Ident::new("dataField")], ); test_table_ident( "`5abc`.dataField", + None, vec![Ident::with_quote('`', "5abc"), Ident::new("dataField")], ); @@ -129,6 +150,7 @@ fn parse_table_identifiers() { test_table_ident( "abc5.dataField", + None, vec![Ident::new("abc5"), Ident::new("dataField")], ); @@ -136,13 +158,76 @@ fn parse_table_identifiers() { test_table_ident( "`GROUP`.dataField", + None, vec![Ident::with_quote('`', "GROUP"), Ident::new("dataField")], ); // TODO: this should be error // test_table_ident_err("GROUP.dataField"); - test_table_ident("abc5.GROUP", vec![Ident::new("abc5"), Ident::new("GROUP")]); + test_table_ident( + "abc5.GROUP", + None, + vec![Ident::new("abc5"), Ident::new("GROUP")], + ); + + test_table_ident( + "`foo.bar.baz`", + Some("`foo`.`bar`.`baz`"), + vec![ + Ident::with_quote('`', "foo"), + Ident::with_quote('`', "bar"), + Ident::with_quote('`', "baz"), + ], + ); + + test_table_ident( + "`foo.bar`.`baz`", + Some("`foo`.`bar`.`baz`"), + vec![ + Ident::with_quote('`', "foo"), + Ident::with_quote('`', "bar"), + Ident::with_quote('`', "baz"), + ], + ); + + test_table_ident( + "`foo`.`bar.baz`", + Some("`foo`.`bar`.`baz`"), + vec![ + Ident::with_quote('`', "foo"), + Ident::with_quote('`', "bar"), + Ident::with_quote('`', "baz"), + ], + ); + + test_table_ident( + "`foo`.`bar`.`baz`", + Some("`foo`.`bar`.`baz`"), + vec![ + Ident::with_quote('`', "foo"), + Ident::with_quote('`', "bar"), + Ident::with_quote('`', "baz"), + ], + ); + + test_table_ident( + "`5abc.dataField`", + Some("`5abc`.`dataField`"), + vec![ + Ident::with_quote('`', "5abc"), + Ident::with_quote('`', "dataField"), + ], + ); + + test_table_ident( + "`_5abc.da-sh-es`", + Some("`_5abc`.`da-sh-es`"), + vec![ + Ident::with_quote('`', "_5abc"), + Ident::with_quote('`', "da-sh-es"), + ], + ); } #[test] From 2786c7eaf1d0b420ca5a5f338b45b6cc0fbd68be Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 2 Oct 2023 17:53:32 +0200 Subject: [PATCH 269/806] clickhouse: add support for LIMIT BY (#977) --- src/ast/query.rs | 7 +++++++ src/parser/mod.rs | 13 ++++++++++++- tests/sqlparser_clickhouse.rs | 18 ++++++++++++++++++ tests/sqlparser_common.rs | 5 +++++ tests/sqlparser_mssql.rs | 3 +++ tests/sqlparser_mysql.rs | 9 +++++++++ tests/sqlparser_postgres.rs | 2 ++ 7 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index af35c37a3..d5f170791 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -35,6 +35,10 @@ pub struct Query { pub order_by: Vec, /// `LIMIT { | ALL }` pub limit: Option, + + /// `LIMIT { } BY { ,,... } }` + pub limit_by: Vec, + /// `OFFSET [ { ROW | ROWS } ]` pub offset: Option, /// `FETCH { FIRST | NEXT } [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES }` @@ -58,6 +62,9 @@ impl fmt::Display for Query { 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 fetch) = self.fetch { write!(f, " {fetch}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a388a9137..dcd731a65 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5431,6 +5431,7 @@ impl<'a> Parser<'a> { with, body: Box::new(SetExpr::Insert(insert)), limit: None, + limit_by: vec![], order_by: vec![], offset: None, fetch: None, @@ -5442,6 +5443,7 @@ impl<'a> Parser<'a> { with, body: Box::new(SetExpr::Update(update)), limit: None, + limit_by: vec![], order_by: vec![], offset: None, fetch: None, @@ -5468,7 +5470,7 @@ impl<'a> Parser<'a> { offset = Some(self.parse_offset()?) } - if dialect_of!(self is GenericDialect | MySqlDialect) + if dialect_of!(self is GenericDialect | MySqlDialect | ClickHouseDialect) && limit.is_some() && offset.is_none() && self.consume_token(&Token::Comma) @@ -5483,6 +5485,14 @@ impl<'a> Parser<'a> { } } + 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 fetch = if self.parse_keyword(Keyword::FETCH) { Some(self.parse_fetch()?) } else { @@ -5499,6 +5509,7 @@ impl<'a> Parser<'a> { body, order_by, limit, + limit_by, offset, fetch, locks, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 936b0799a..9efe4a368 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -25,6 +25,7 @@ use sqlparser::ast::TableFactor::Table; use sqlparser::ast::*; use sqlparser::dialect::ClickHouseDialect; +use sqlparser::dialect::GenericDialect; #[test] fn parse_map_access_expr() { @@ -344,9 +345,26 @@ fn parse_double_equal() { ); } +#[test] +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"#, + ); + 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)"#, + ); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], options: None, } } + +fn clickhouse_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(ClickHouseDialect {}), Box::new(GenericDialect {})], + options: None, + } +} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d73061f79..80e4cdf02 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -261,6 +261,7 @@ fn parse_update_set_from() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -2662,6 +2663,7 @@ fn parse_create_table_as_table() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -2685,6 +2687,7 @@ fn parse_create_table_as_table() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -3976,6 +3979,7 @@ fn parse_interval_and_or_xor() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -6392,6 +6396,7 @@ fn parse_merge() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 135e5d138..f9eb4d8fb 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -92,6 +92,7 @@ fn parse_create_procedure() { body: vec![Statement::Query(Box::new(Query { with: None, limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -493,6 +494,7 @@ fn parse_substring_in_select() { assert_eq!( Box::new(Query { with: None, + body: Box::new(SetExpr::Select(Box::new(Select { distinct: Some(Distinct::Distinct), top: None, @@ -532,6 +534,7 @@ fn parse_substring_in_select() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f1a054bfb..80b9dcfd8 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -562,6 +562,7 @@ fn parse_escaped_quote_identifiers_with_escape() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -604,6 +605,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -643,6 +645,7 @@ fn parse_escaped_backticks_with_escape() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -682,6 +685,7 @@ fn parse_escaped_backticks_with_no_escape() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -956,6 +960,7 @@ fn parse_simple_insert() { })), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -991,6 +996,7 @@ fn parse_empty_row_insert() { })), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -1049,6 +1055,7 @@ fn parse_insert_with_on_duplicate_update() { })), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -1428,6 +1435,7 @@ fn parse_substring_in_select() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -1708,6 +1716,7 @@ fn parse_hex_string_introducer() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index bb3857817..fe336bda7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1000,6 +1000,7 @@ fn parse_copy_to() { }))), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], @@ -2046,6 +2047,7 @@ fn parse_array_subquery_expr() { }), order_by: vec![], limit: None, + limit_by: vec![], offset: None, fetch: None, locks: vec![], From 40e2ecbdf34f0068863ed74b0ae3a8eb410c6401 Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Mon, 2 Oct 2023 10:28:13 -0700 Subject: [PATCH 270/806] snowflake: support for UNPIVOT and a fix for chained PIVOTs (#983) --- src/ast/query.rs | 67 ++++++++++------ src/keywords.rs | 2 + src/parser/mod.rs | 54 +++++++++---- tests/sqlparser_common.rs | 155 +++++++++++++++++++++++++++++++++++--- 4 files changed, 231 insertions(+), 47 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index d5f170791..88b0931de 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -720,13 +720,28 @@ pub enum TableFactor { /// For example `FROM monthly_sales PIVOT(sum(amount) FOR MONTH IN ('JAN', 'FEB'))` /// See Pivot { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - name: ObjectName, - table_alias: Option, + #[cfg_attr(feature = "visitor", visit(with = "visit_table_factor"))] + table: Box, aggregate_function: Expr, // Function expression value_column: Vec, pivot_values: Vec, - pivot_alias: Option, + alias: Option, + }, + /// An UNPIVOT operation on a table. + /// + /// Syntax: + /// ```sql + /// table UNPIVOT(value FOR name IN (column1, [ column2, ... ])) [ alias ] + /// ``` + /// + /// See . + Unpivot { + #[cfg_attr(feature = "visitor", visit(with = "visit_table_factor"))] + table: Box, + value: Ident, + name: Ident, + columns: Vec, + alias: Option, }, } @@ -810,32 +825,42 @@ impl fmt::Display for TableFactor { Ok(()) } TableFactor::Pivot { - name, - table_alias, + table, aggregate_function, value_column, pivot_values, - pivot_alias, + alias, } => { - write!(f, "{}", name)?; - if table_alias.is_some() { - write!(f, " AS {}", table_alias.as_ref().unwrap())?; - } write!( f, - " PIVOT({} FOR {} IN (", + "{} PIVOT({} FOR {} IN ({}))", + table, aggregate_function, - Expr::CompoundIdentifier(value_column.to_vec()) + Expr::CompoundIdentifier(value_column.to_vec()), + display_comma_separated(pivot_values) )?; - for value in pivot_values { - write!(f, "{}", value)?; - if !value.eq(pivot_values.last().unwrap()) { - write!(f, ", ")?; - } + if alias.is_some() { + write!(f, " AS {}", alias.as_ref().unwrap())?; } - write!(f, "))")?; - if pivot_alias.is_some() { - write!(f, " AS {}", pivot_alias.as_ref().unwrap())?; + Ok(()) + } + TableFactor::Unpivot { + table, + value, + name, + columns, + alias, + } => { + write!( + f, + "{} UNPIVOT({} FOR {} IN ({}))", + table, + value, + name, + display_comma_separated(columns) + )?; + if alias.is_some() { + write!(f, " AS {}", alias.as_ref().unwrap())?; } Ok(()) } diff --git a/src/keywords.rs b/src/keywords.rs index 6fb74a8e0..d85708032 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -635,6 +635,7 @@ define_keywords!( UNKNOWN, UNLOGGED, UNNEST, + UNPIVOT, UNSIGNED, UNTIL, UPDATE, @@ -693,6 +694,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::HAVING, Keyword::ORDER, Keyword::PIVOT, + Keyword::UNPIVOT, Keyword::TOP, Keyword::LATERAL, Keyword::VIEW, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dcd731a65..45600f42d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6276,9 +6276,8 @@ impl<'a> Parser<'a> { | TableFactor::Table { alias, .. } | TableFactor::UNNEST { alias, .. } | TableFactor::TableFunction { alias, .. } - | TableFactor::Pivot { - pivot_alias: alias, .. - } + | TableFactor::Pivot { alias, .. } + | TableFactor::Unpivot { alias, .. } | TableFactor::NestedJoin { alias, .. } => { // but not `FROM (mytable AS alias1) AS alias2`. if let Some(inner_alias) = alias { @@ -6357,11 +6356,6 @@ impl<'a> Parser<'a> { let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; - // Pivot - if self.parse_keyword(Keyword::PIVOT) { - return self.parse_pivot_table_factor(name, alias); - } - // MSSQL-specific table hints: let mut with_hints = vec![]; if self.parse_keyword(Keyword::WITH) { @@ -6373,14 +6367,25 @@ impl<'a> Parser<'a> { self.prev_token(); } }; - Ok(TableFactor::Table { + + let mut table = TableFactor::Table { name, alias, args, with_hints, version, partitions, - }) + }; + + while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { + table = match kw { + Keyword::PIVOT => self.parse_pivot_table_factor(table)?, + Keyword::UNPIVOT => self.parse_unpivot_table_factor(table)?, + _ => unreachable!(), + } + } + + Ok(table) } } @@ -6417,8 +6422,7 @@ impl<'a> Parser<'a> { pub fn parse_pivot_table_factor( &mut self, - name: ObjectName, - table_alias: Option, + table: TableFactor, ) -> Result { self.expect_token(&Token::LParen)?; let function_name = match self.next_token().token { @@ -6435,12 +6439,32 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; Ok(TableFactor::Pivot { - name, - table_alias, + table: Box::new(table), aggregate_function: function, value_column, pivot_values, - pivot_alias: alias, + alias, + }) + } + + pub fn parse_unpivot_table_factor( + &mut self, + table: TableFactor, + ) -> Result { + self.expect_token(&Token::LParen)?; + let value = self.parse_identifier()?; + self.expect_keyword(Keyword::FOR)?; + let name = self.parse_identifier()?; + self.expect_keyword(Keyword::IN)?; + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + self.expect_token(&Token::RParen)?; + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + Ok(TableFactor::Unpivot { + table: Box::new(table), + value, + name, + columns, + alias, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 80e4cdf02..3c4a2d9ea 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20,7 +20,7 @@ use matches::assert_matches; use sqlparser::ast::SelectItem::UnnamedExpr; -use sqlparser::ast::TableFactor::Pivot; +use sqlparser::ast::TableFactor::{Pivot, Unpivot}; use sqlparser::ast::*; use sqlparser::dialect::{ AnsiDialect, BigQueryDialect, ClickHouseDialect, DuckDbDialect, GenericDialect, HiveDialect, @@ -7257,10 +7257,16 @@ fn parse_pivot_table() { assert_eq!( verified_only_select(sql).from[0].relation, Pivot { - name: ObjectName(vec![Ident::new("monthly_sales")]), - table_alias: Some(TableAlias { - name: Ident::new("a"), - columns: vec![] + table: Box::new(TableFactor::Table { + name: ObjectName(vec![Ident::new("monthly_sales")]), + alias: Some(TableAlias { + name: Ident::new("a"), + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], }), aggregate_function: Expr::Function(Function { name: ObjectName(vec![Ident::new("SUM")]), @@ -7279,7 +7285,7 @@ fn parse_pivot_table() { Value::SingleQuotedString("MAR".to_string()), Value::SingleQuotedString("APR".to_string()), ], - pivot_alias: Some(TableAlias { + alias: Some(TableAlias { name: Ident { value: "p".to_string(), quote_style: None @@ -7290,17 +7296,15 @@ fn parse_pivot_table() { ); assert_eq!(verified_stmt(sql).to_string(), sql); + // parsing should succeed with empty alias let sql_without_table_alias = concat!( "SELECT * FROM monthly_sales ", "PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ", "ORDER BY EMPID" ); assert_matches!( - verified_only_select(sql_without_table_alias).from[0].relation, - Pivot { - table_alias: None, // parsing should succeed with empty alias - .. - } + &verified_only_select(sql_without_table_alias).from[0].relation, + Pivot { table, .. } if matches!(&**table, TableFactor::Table { alias: None, .. }) ); assert_eq!( verified_stmt(sql_without_table_alias).to_string(), @@ -7308,6 +7312,135 @@ fn parse_pivot_table() { ); } +#[test] +fn parse_unpivot_table() { + let sql = concat!( + "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(vec![Ident::new("sales")]), + alias: Some(TableAlias { + name: Ident::new("s"), + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + }), + value: Ident { + value: "quantity".to_string(), + quote_style: None + }, + + name: Ident { + value: "quarter".to_string(), + quote_style: None + }, + 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(Ident::new) + .collect() + }), + } + ); + assert_eq!(verified_stmt(sql).to_string(), sql); + + let sql_without_aliases = concat!( + "SELECT * FROM sales ", + "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4))" + ); + + assert_matches!( + &verified_only_select(sql_without_aliases).from[0].relation, + Unpivot { + table, + alias: None, + .. + } if matches!(&**table, TableFactor::Table { alias: None, .. }) + ); + assert_eq!( + verified_stmt(sql_without_aliases).to_string(), + sql_without_aliases + ); +} + +#[test] +fn parse_pivot_unpivot_table() { + let sql = concat!( + "SELECT * FROM census AS c ", + "UNPIVOT(population FOR year IN (population_2000, population_2010)) AS u ", + "PIVOT(sum(population) FOR year IN ('population_2000', 'population_2010')) AS p" + ); + + pretty_assertions::assert_eq!( + verified_only_select(sql).from[0].relation, + Pivot { + table: Box::new(Unpivot { + table: Box::new(TableFactor::Table { + name: ObjectName(vec![Ident::new("census")]), + alias: Some(TableAlias { + name: Ident::new("c"), + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + }), + value: Ident { + value: "population".to_string(), + quote_style: None + }, + + name: Ident { + value: "year".to_string(), + quote_style: None + }, + columns: ["population_2000", "population_2010"] + .into_iter() + .map(Ident::new) + .collect(), + alias: Some(TableAlias { + name: Ident::new("u"), + columns: vec![] + }), + }), + aggregate_function: Expr::Function(Function { + name: ObjectName(vec![Ident::new("sum")]), + args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("population")) + ))]), + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + value_column: vec![Ident::new("year")], + pivot_values: vec![ + Value::SingleQuotedString("population_2000".to_string()), + Value::SingleQuotedString("population_2010".to_string()) + ], + alias: Some(TableAlias { + name: Ident::new("p"), + columns: vec![] + }), + } + ); + assert_eq!(verified_stmt(sql).to_string(), sql); +} + /// Makes a predicate that looks like ((user_id = $id) OR user_id = $2...) fn make_where_clause(num: usize) -> String { use std::fmt::Write; From c811e2260505e2651b04e85e886ec09d237a0602 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 2 Oct 2023 19:42:01 +0200 Subject: [PATCH 271/806] =?UTF-8?q?redshift:=20add=20support=20for=20CREAT?= =?UTF-8?q?E=20VIEW=20=E2=80=A6=20WITH=20NO=20SCHEMA=20BINDING=20(#979)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ast/mod.rs | 11 +++++++++-- src/keywords.rs | 1 + src/parser/mod.rs | 10 ++++++++++ tests/sqlparser_common.rs | 12 ++++++++++++ tests/sqlparser_redshift.rs | 14 ++++++++++++++ 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f2dbb8899..6f9d32c8d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -577,7 +577,7 @@ pub enum Expr { /// /// Syntax: /// ```sql - /// MARCH (, , ...) AGAINST ( []) + /// MATCH (, , ...) AGAINST ( []) /// /// = CompoundIdentifier /// = String literal @@ -1316,6 +1316,8 @@ pub enum Statement { query: Box, with_options: Vec, cluster_by: Vec, + /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause + with_no_schema_binding: bool, }, /// CREATE TABLE CreateTable { @@ -2271,6 +2273,7 @@ impl fmt::Display for Statement { materialized, with_options, cluster_by, + with_no_schema_binding, } => { write!( f, @@ -2288,7 +2291,11 @@ impl fmt::Display for Statement { if !cluster_by.is_empty() { write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; } - write!(f, " AS {query}") + write!(f, " AS {query}")?; + if *with_no_schema_binding { + write!(f, " WITH NO SCHEMA BINDING")?; + } + Ok(()) } Statement::CreateTable { name, diff --git a/src/keywords.rs b/src/keywords.rs index d85708032..e1bbf44ae 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -110,6 +110,7 @@ define_keywords!( BIGINT, BIGNUMERIC, BINARY, + BINDING, BLOB, BLOOMFILTER, BOOL, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 45600f42d..5f6788696 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2974,6 +2974,15 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::AS)?; let query = Box::new(self.parse_query()?); // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. + + let with_no_schema_binding = dialect_of!(self is RedshiftSqlDialect | GenericDialect) + && self.parse_keywords(&[ + Keyword::WITH, + Keyword::NO, + Keyword::SCHEMA, + Keyword::BINDING, + ]); + Ok(Statement::CreateView { name, columns, @@ -2982,6 +2991,7 @@ impl<'a> Parser<'a> { or_replace, with_options, cluster_by, + with_no_schema_binding, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3c4a2d9ea..027dc312f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5320,6 +5320,7 @@ fn parse_create_view() { materialized, with_options, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5328,6 +5329,7 @@ fn parse_create_view() { assert!(!or_replace); assert_eq!(with_options, vec![]); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5368,6 +5370,7 @@ fn parse_create_view_with_columns() { query, materialized, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![Ident::new("has"), Ident::new("cols")]); @@ -5376,6 +5379,7 @@ fn parse_create_view_with_columns() { assert!(!materialized); assert!(!or_replace); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5393,6 +5397,7 @@ fn parse_create_or_replace_view() { query, materialized, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -5401,6 +5406,7 @@ fn parse_create_or_replace_view() { assert!(!materialized); assert!(or_replace); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5422,6 +5428,7 @@ fn parse_create_or_replace_materialized_view() { query, materialized, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -5430,6 +5437,7 @@ fn parse_create_or_replace_materialized_view() { assert!(materialized); assert!(or_replace); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5447,6 +5455,7 @@ fn parse_create_materialized_view() { materialized, with_options, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5455,6 +5464,7 @@ fn parse_create_materialized_view() { assert_eq!(with_options, vec![]); assert!(!or_replace); assert_eq!(cluster_by, vec![]); + assert!(!late_binding); } _ => unreachable!(), } @@ -5472,6 +5482,7 @@ fn parse_create_materialized_view_with_cluster_by() { materialized, with_options, cluster_by, + with_no_schema_binding: late_binding, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5480,6 +5491,7 @@ fn parse_create_materialized_view_with_cluster_by() { assert_eq!(with_options, vec![]); assert!(!or_replace); assert_eq!(cluster_by, vec![Ident::new("foo")]); + assert!(!late_binding); } _ => unreachable!(), } diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index f17ca5841..5ae539b3c 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -16,6 +16,7 @@ mod test_utils; use test_utils::*; use sqlparser::ast::*; +use sqlparser::dialect::GenericDialect; use sqlparser::dialect::RedshiftSqlDialect; #[test] @@ -272,6 +273,13 @@ fn redshift() -> TestedDialects { } } +fn redshift_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(RedshiftSqlDialect {}), Box::new(GenericDialect {})], + options: None, + } +} + #[test] fn test_sharp() { let sql = "SELECT #_of_values"; @@ -281,3 +289,9 @@ fn test_sharp() { select.projection[0] ); } + +#[test] +fn test_create_view_with_no_schema_binding() { + redshift_and_generic() + .verified_stmt("CREATE VIEW myevent AS SELECT eventname FROM event WITH NO SCHEMA BINDING"); +} From 02f3d78a920ad90a91d318fbb3b08890c53e1a28 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 5 Oct 2023 15:25:03 -0400 Subject: [PATCH 272/806] Fix for clippy 1.73 (#995) --- src/ast/mod.rs | 2 +- src/ast/visitor.rs | 2 +- src/parser/mod.rs | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6f9d32c8d..d4e2f26ea 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -155,7 +155,7 @@ impl fmt::Display for Ident { let escaped = value::escape_quoted_string(&self.value, q); write!(f, "{q}{escaped}{q}") } - Some(q) if q == '[' => write!(f, "[{}]", self.value), + Some('[') => write!(f, "[{}]", self.value), None => f.write_str(&self.value), _ => panic!("unexpected quote style"), } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index bb7c19678..09cb20a0c 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -490,7 +490,7 @@ where /// /// This demonstrates how to effectively replace an expression with another more complicated one /// that references the original. This example avoids unnecessary allocations by using the -/// [`std::mem`](std::mem) family of functions. +/// [`std::mem`] family of functions. /// /// ``` /// # use sqlparser::parser::Parser; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5f6788696..d0b3cef78 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4701,7 +4701,11 @@ impl<'a> Parser<'a> { pub fn parse_literal_string(&mut self) -> Result { let next_token = self.next_token(); match next_token.token { - Token::Word(Word { value, keyword, .. }) if keyword == Keyword::NoKeyword => Ok(value), + Token::Word(Word { + value, + keyword: Keyword::NoKeyword, + .. + }) => Ok(value), Token::SingleQuotedString(s) => Ok(s), Token::DoubleQuotedString(s) => Ok(s), Token::EscapedStringLiteral(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { @@ -5853,8 +5857,8 @@ impl<'a> Parser<'a> { self.expect_token(&Token::Colon)?; } else if self.parse_keyword(Keyword::ROLE) { let context_modifier = match modifier { - Some(keyword) if keyword == Keyword::LOCAL => ContextModifier::Local, - Some(keyword) if keyword == Keyword::SESSION => ContextModifier::Session, + Some(Keyword::LOCAL) => ContextModifier::Local, + Some(Keyword::SESSION) => ContextModifier::Session, _ => ContextModifier::None, }; @@ -6897,7 +6901,7 @@ impl<'a> Parser<'a> { } } - /// Parse an [`WildcardAdditionalOptions`](WildcardAdditionalOptions) information for wildcard select items. + /// Parse an [`WildcardAdditionalOptions`] information for wildcard select items. /// /// If it is not possible to parse it, will return an option. pub fn parse_wildcard_additional_options( From 5263da68cdaa052dfd4f8989760569eae253253e Mon Sep 17 00:00:00 2001 From: Gabriel Villalonga Simon Date: Thu, 5 Oct 2023 20:32:43 +0100 Subject: [PATCH 273/806] Handle CREATE [TEMPORARY|TEMP] VIEW [IF NOT EXISTS] (#993) --- src/ast/mod.rs | 12 +++++++-- src/parser/mod.rs | 12 +++++++-- tests/sqlparser_common.rs | 55 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_sqlite.rs | 31 ++++++++++++++++++++++ 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d4e2f26ea..d048ccc1b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1318,6 +1318,10 @@ pub enum Statement { cluster_by: Vec, /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause with_no_schema_binding: bool, + /// if true, has SQLite `IF NOT EXISTS` clause + if_not_exists: bool, + /// if true, has SQLite `TEMP` or `TEMPORARY` clause + temporary: bool, }, /// CREATE TABLE CreateTable { @@ -2274,13 +2278,17 @@ impl fmt::Display for Statement { with_options, cluster_by, with_no_schema_binding, + if_not_exists, + temporary, } => { write!( f, - "CREATE {or_replace}{materialized}VIEW {name}", + "CREATE {or_replace}{materialized}{temporary}VIEW {if_not_exists}{name}", or_replace = if *or_replace { "OR REPLACE " } else { "" }, materialized = if *materialized { "MATERIALIZED " } else { "" }, - name = name + name = name, + temporary = if *temporary { "TEMPORARY " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } )?; if !with_options.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_options))?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d0b3cef78..922a791f3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2478,7 +2478,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) + self.parse_create_view(or_replace, temporary) } else if self.parse_keyword(Keyword::EXTERNAL) { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { @@ -2955,9 +2955,15 @@ impl<'a> Parser<'a> { } } - pub fn parse_create_view(&mut self, or_replace: bool) -> Result { + pub fn parse_create_view( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { let materialized = self.parse_keyword(Keyword::MATERIALIZED); self.expect_keyword(Keyword::VIEW)?; + let if_not_exists = dialect_of!(self is SQLiteDialect|GenericDialect) + && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. let name = self.parse_object_name()?; @@ -2992,6 +2998,8 @@ impl<'a> Parser<'a> { with_options, cluster_by, with_no_schema_binding, + if_not_exists, + temporary, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 027dc312f..c0ec456a9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5321,6 +5321,8 @@ fn parse_create_view() { with_options, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5330,6 +5332,8 @@ fn parse_create_view() { assert_eq!(with_options, vec![]); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } @@ -5371,6 +5375,8 @@ fn parse_create_view_with_columns() { materialized, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![Ident::new("has"), Ident::new("cols")]); @@ -5380,6 +5386,39 @@ fn parse_create_view_with_columns() { assert!(!or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_view_temporary() { + let sql = "CREATE TEMPORARY VIEW myschema.myview AS SELECT foo FROM bar"; + match verified_stmt(sql) { + Statement::CreateView { + name, + columns, + query, + or_replace, + materialized, + with_options, + cluster_by, + with_no_schema_binding: late_binding, + if_not_exists, + temporary, + } => { + assert_eq!("myschema.myview", name.to_string()); + assert_eq!(Vec::::new(), columns); + assert_eq!("SELECT foo FROM bar", query.to_string()); + assert!(!materialized); + assert!(!or_replace); + assert_eq!(with_options, vec![]); + assert_eq!(cluster_by, vec![]); + assert!(!late_binding); + assert!(!if_not_exists); + assert!(temporary); } _ => unreachable!(), } @@ -5398,6 +5437,8 @@ fn parse_create_or_replace_view() { materialized, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -5407,6 +5448,8 @@ fn parse_create_or_replace_view() { assert!(or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } @@ -5429,6 +5472,8 @@ fn parse_create_or_replace_materialized_view() { materialized, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -5438,6 +5483,8 @@ fn parse_create_or_replace_materialized_view() { assert!(or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } @@ -5456,6 +5503,8 @@ fn parse_create_materialized_view() { with_options, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5465,6 +5514,8 @@ fn parse_create_materialized_view() { assert!(!or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } @@ -5483,6 +5534,8 @@ fn parse_create_materialized_view_with_cluster_by() { with_options, cluster_by, with_no_schema_binding: late_binding, + if_not_exists, + temporary, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -5492,6 +5545,8 @@ fn parse_create_materialized_view_with_cluster_by() { assert!(!or_replace); assert_eq!(cluster_by, vec![Ident::new("foo")]); assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); } _ => unreachable!(), } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index c4e69d530..39a82cc8b 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -61,6 +61,37 @@ fn parse_create_virtual_table() { sqlite_and_generic().verified_stmt(sql); } +#[test] +fn parse_create_view_temporary_if_not_exists() { + let sql = "CREATE TEMPORARY VIEW IF NOT EXISTS myschema.myview AS SELECT foo FROM bar"; + match sqlite_and_generic().verified_stmt(sql) { + Statement::CreateView { + name, + columns, + query, + or_replace, + materialized, + with_options, + cluster_by, + with_no_schema_binding: late_binding, + if_not_exists, + temporary, + } => { + assert_eq!("myschema.myview", name.to_string()); + assert_eq!(Vec::::new(), columns); + assert_eq!("SELECT foo FROM bar", query.to_string()); + assert!(!materialized); + assert!(!or_replace); + assert_eq!(with_options, vec![]); + assert_eq!(cluster_by, vec![]); + assert!(!late_binding); + assert!(if_not_exists); + assert!(temporary); + } + _ => unreachable!(), + } +} + #[test] fn double_equality_operator() { // Sqlite supports this operator: https://www.sqlite.org/lang_expr.html#binaryops From 83cb734b3c206502dd73998def455da554c37eef Mon Sep 17 00:00:00 2001 From: Zdenko Nevrala Date: Fri, 6 Oct 2023 20:48:18 +0200 Subject: [PATCH 274/806] Support Snowflake/BigQuery TRIM. (#975) --- src/ast/mod.rs | 6 ++++++ src/parser/mod.rs | 14 ++++++++++++++ tests/sqlparser_bigquery.rs | 26 ++++++++++++++++++++++++++ tests/sqlparser_common.rs | 24 ++++++++++++++++++++++++ tests/sqlparser_snowflake.rs | 25 +++++++++++++++++++++++++ 5 files changed, 95 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d048ccc1b..87f7ebb37 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -496,12 +496,14 @@ pub enum Expr { /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) /// TRIM() + /// TRIM(, [, characters]) -- only Snowflake or Bigquery /// ``` Trim { expr: Box, // ([BOTH | LEADING | TRAILING] trim_where: Option, trim_what: Option>, + trim_characters: Option>, }, /// ```sql /// OVERLAY( PLACING FROM [ FOR ] @@ -895,6 +897,7 @@ impl fmt::Display for Expr { expr, trim_where, trim_what, + trim_characters, } => { write!(f, "TRIM(")?; if let Some(ident) = trim_where { @@ -905,6 +908,9 @@ impl fmt::Display for Expr { } else { write!(f, "{expr}")?; } + if let Some(characters) = trim_characters { + write!(f, ", {}", display_comma_separated(characters))?; + } write!(f, ")") } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 922a791f3..95f1f8edc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1315,6 +1315,7 @@ impl<'a> Parser<'a> { /// ```sql /// TRIM ([WHERE] ['text' FROM] 'text') /// TRIM ('text') + /// TRIM(, [, characters]) -- only Snowflake or BigQuery /// ``` pub fn parse_trim_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; @@ -1336,6 +1337,18 @@ impl<'a> Parser<'a> { expr: Box::new(expr), trim_where, trim_what: Some(trim_what), + trim_characters: None, + }) + } else if self.consume_token(&Token::Comma) + && dialect_of!(self is SnowflakeDialect | BigQueryDialect | GenericDialect) + { + let characters = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + Ok(Expr::Trim { + expr: Box::new(expr), + trim_where: None, + trim_what: None, + trim_characters: Some(characters), }) } else { self.expect_token(&Token::RParen)?; @@ -1343,6 +1356,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), trim_where, trim_what: None, + trim_characters: None, }) } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index e05581d5f..7a9a8d1c4 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -17,6 +17,7 @@ use std::ops::Deref; use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; +use sqlparser::parser::ParserError; use test_utils::*; #[test] @@ -549,3 +550,28 @@ fn parse_map_access_offset() { bigquery().verified_only_select(sql); } } + +#[test] +fn test_bigquery_trim() { + let real_sql = r#"SELECT customer_id, TRIM(item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; + assert_eq!(bigquery().verified_stmt(real_sql).to_string(), real_sql); + + let sql_only_select = "SELECT TRIM('xyz', 'a')"; + let select = bigquery().verified_only_select(sql_only_select); + assert_eq!( + &Expr::Trim { + expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value(Value::SingleQuotedString("a".to_owned()))]), + }, + 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()), + bigquery().parse_sql_statements(error_sql).unwrap_err() + ); +} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c0ec456a9..1511aa76e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5225,6 +5225,30 @@ fn parse_trim() { ParserError::ParserError("Expected ), found: 'xyz'".to_owned()), parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')").unwrap_err() ); + + //keep Snowflake/BigQuery TRIM syntax failing + let all_expected_snowflake = TestedDialects { + dialects: vec![ + //Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + //Box::new(SnowflakeDialect {}), + Box::new(HiveDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(MySqlDialect {}), + //Box::new(BigQueryDialect {}), + Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), + ], + options: None, + }; + assert_eq!( + ParserError::ParserError("Expected ), found: 'a'".to_owned()), + all_expected_snowflake + .parse_sql_statements("SELECT TRIM('xyz', 'a')") + .unwrap_err() + ); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e1db7ec61..e92656d0b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1039,3 +1039,28 @@ fn test_snowflake_stage_object_names() { } } } + +#[test] +fn test_snowflake_trim() { + let real_sql = r#"SELECT customer_id, TRIM(sub_items.value:item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; + assert_eq!(snowflake().verified_stmt(real_sql).to_string(), real_sql); + + let sql_only_select = "SELECT TRIM('xyz', 'a')"; + let select = snowflake().verified_only_select(sql_only_select); + assert_eq!( + &Expr::Trim { + expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value(Value::SingleQuotedString("a".to_owned()))]), + }, + 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()), + snowflake().parse_sql_statements(error_sql).unwrap_err() + ); +} From c68e9775a22acf00e54b33542b10ac6d1a8cf887 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Fri, 20 Oct 2023 20:33:12 +0200 Subject: [PATCH 275/806] Support bigquery `CAST AS x [STRING|DATE] FORMAT` syntax (#978) --- src/ast/mod.rs | 64 ++++++++++++++++++++++++++++++++++-- src/parser/mod.rs | 23 +++++++++++++ tests/sqlparser_bigquery.rs | 35 ++++++++++++++++++-- tests/sqlparser_common.rs | 10 ++++++ tests/sqlparser_postgres.rs | 3 +- tests/sqlparser_snowflake.rs | 1 + 6 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 87f7ebb37..fc15efbc4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -322,6 +322,16 @@ impl fmt::Display for JsonOperator { } } +/// Options for `CAST` / `TRY_CAST` +/// BigQuery: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CastFormat { + Value(Value), + ValueAtTimeZone(Value, Value), +} + /// An SQL expression of any type. /// /// The parser does not distinguish between expressions of different types @@ -437,12 +447,18 @@ pub enum Expr { Cast { expr: Box, data_type: DataType, + // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery + // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax + format: Option, }, /// TRY_CAST an expression to a different data type e.g. `TRY_CAST(foo AS VARCHAR(123))` // this differs from CAST in the choice of how to implement invalid conversions TryCast { expr: Box, data_type: DataType, + // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery + // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax + format: Option, }, /// SAFE_CAST an expression to a different data type e.g. `SAFE_CAST(foo AS FLOAT64)` // only available for BigQuery: https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-and-operators#safe_casting @@ -450,6 +466,9 @@ pub enum Expr { SafeCast { expr: Box, data_type: DataType, + // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery + // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax + format: Option, }, /// AT a timestamp to a different timezone e.g. `FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00'` AtTimeZone { @@ -597,6 +616,15 @@ pub enum Expr { }, } +impl fmt::Display for CastFormat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CastFormat::Value(v) => write!(f, "{v}"), + CastFormat::ValueAtTimeZone(v, tz) => write!(f, "{v} AT TIME ZONE {tz}"), + } + } +} + impl fmt::Display for Expr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -753,9 +781,39 @@ impl fmt::Display for Expr { write!(f, "{op}{expr}") } } - Expr::Cast { expr, data_type } => write!(f, "CAST({expr} AS {data_type})"), - Expr::TryCast { expr, data_type } => write!(f, "TRY_CAST({expr} AS {data_type})"), - Expr::SafeCast { expr, data_type } => write!(f, "SAFE_CAST({expr} AS {data_type})"), + Expr::Cast { + expr, + data_type, + format, + } => { + if let Some(format) = format { + write!(f, "CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "CAST({expr} AS {data_type})") + } + } + Expr::TryCast { + expr, + data_type, + format, + } => { + if let Some(format) = format { + write!(f, "TRY_CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "TRY_CAST({expr} AS {data_type})") + } + } + Expr::SafeCast { + expr, + data_type, + format, + } => { + if let Some(format) = format { + write!(f, "SAFE_CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "SAFE_CAST({expr} AS {data_type})") + } + } Expr::Extract { field, expr } => write!(f, "EXTRACT({field} FROM {expr})"), Expr::Ceil { expr, field } => { if field == &DateTimeField::NoDateTime { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 95f1f8edc..829b299af 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1139,16 +1139,34 @@ impl<'a> Parser<'a> { }) } + pub fn parse_optional_cast_format(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::FORMAT) { + let value = self.parse_value()?; + if self.parse_keywords(&[Keyword::AT, Keyword::TIME, Keyword::ZONE]) { + Ok(Some(CastFormat::ValueAtTimeZone( + value, + self.parse_value()?, + ))) + } else { + Ok(Some(CastFormat::Value(value))) + } + } else { + Ok(None) + } + } + /// Parse a SQL CAST function e.g. `CAST(expr AS FLOAT)` pub fn parse_cast_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; self.expect_keyword(Keyword::AS)?; let data_type = self.parse_data_type()?; + let format = self.parse_optional_cast_format()?; self.expect_token(&Token::RParen)?; Ok(Expr::Cast { expr: Box::new(expr), data_type, + format, }) } @@ -1158,10 +1176,12 @@ impl<'a> Parser<'a> { let expr = self.parse_expr()?; self.expect_keyword(Keyword::AS)?; let data_type = self.parse_data_type()?; + let format = self.parse_optional_cast_format()?; self.expect_token(&Token::RParen)?; Ok(Expr::TryCast { expr: Box::new(expr), data_type, + format, }) } @@ -1171,10 +1191,12 @@ impl<'a> Parser<'a> { let expr = self.parse_expr()?; self.expect_keyword(Keyword::AS)?; let data_type = self.parse_data_type()?; + let format = self.parse_optional_cast_format()?; self.expect_token(&Token::RParen)?; Ok(Expr::SafeCast { expr: Box::new(expr), data_type, + format, }) } @@ -2101,6 +2123,7 @@ impl<'a> Parser<'a> { Ok(Expr::Cast { expr: Box::new(expr), data_type: self.parse_data_type()?, + format: None, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 7a9a8d1c4..b3f683b9a 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -304,8 +304,39 @@ fn parse_trailing_comma() { #[test] fn parse_cast_type() { - let sql = r#"SELECT SAFE_CAST(1 AS INT64)"#; - bigquery().verified_only_select(sql); + let sql = r"SELECT SAFE_CAST(1 AS INT64)"; + bigquery_and_generic().verified_only_select(sql); +} + +#[test] +fn parse_cast_date_format() { + let sql = + r"SELECT CAST(date_valid_from AS DATE FORMAT 'YYYY-MM-DD') AS date_valid_from FROM foo"; + bigquery_and_generic().verified_only_select(sql); +} + +#[test] +fn parse_cast_time_format() { + let sql = r"SELECT CAST(TIME '21:30:00' AS STRING FORMAT 'PM') AS date_time_to_string"; + bigquery_and_generic().verified_only_select(sql); +} + +#[test] +fn parse_cast_timestamp_format_tz() { + let sql = r"SELECT CAST(TIMESTAMP '2008-12-25 00:00:00+00:00' AS STRING FORMAT 'TZH' AT TIME ZONE 'Asia/Kolkata') AS date_time_to_string"; + bigquery_and_generic().verified_only_select(sql); +} + +#[test] +fn parse_cast_string_to_bytes_format() { + let sql = r"SELECT CAST('Hello' AS BYTES FORMAT 'ASCII') AS string_to_bytes"; + bigquery_and_generic().verified_only_select(sql); +} + +#[test] +fn parse_cast_bytes_to_string_format() { + let sql = r"SELECT CAST(B'\x48\x65\x6c\x6c\x6f' AS STRING FORMAT 'ASCII') AS bytes_to_string"; + bigquery_and_generic().verified_only_select(sql); } #[test] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1511aa76e..ff8bdd7a4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1934,6 +1934,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::BigInt(None), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -1944,6 +1945,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::TinyInt(None), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -1970,6 +1972,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Nvarchar(Some(50)), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -1980,6 +1983,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Clob(None), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -1990,6 +1994,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Clob(Some(50)), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -2000,6 +2005,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Binary(Some(50)), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -2010,6 +2016,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Varbinary(Some(50)), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -2020,6 +2027,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Blob(None), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -2030,6 +2038,7 @@ fn parse_cast() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Blob(Some(50)), + format: None, }, expr_from_projection(only(&select.projection)) ); @@ -2043,6 +2052,7 @@ fn parse_try_cast() { &Expr::TryCast { expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::BigInt(None), + format: None, }, expr_from_projection(only(&select.projection)) ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fe336bda7..654723668 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1782,7 +1782,8 @@ fn parse_array_index_expr() { })), data_type: DataType::Array(Some(Box::new(DataType::Array(Some(Box::new( DataType::Int(None) - )))))) + )))))), + format: None, }))), indexes: vec![num[1].clone(), num[2].clone()], }, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e92656d0b..bb988665d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -167,6 +167,7 @@ fn parse_array() { &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("a"))), data_type: DataType::Array(None), + format: None, }, expr_from_projection(only(&select.projection)) ); From 88510f662563786a6e3af6b1ed109444bcd332e7 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Fri, 20 Oct 2023 21:49:18 +0200 Subject: [PATCH 276/806] fix column `COLLATE` not displayed (#1012) --- src/ast/ddl.rs | 3 +++ tests/sqlparser_common.rs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a4640d557..f1575d979 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -517,6 +517,9 @@ pub struct ColumnDef { impl fmt::Display for ColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {}", self.name, self.data_type)?; + if let Some(collation) = &self.collation { + write!(f, " COLLATE {collation}")?; + } for option in &self.options { write!(f, " {option}")?; } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ff8bdd7a4..3b8775e45 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7635,3 +7635,8 @@ fn parse_create_type() { create_type ); } + +#[test] +fn parse_create_table_collate() { + pg_and_generic().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); +} From c03586b727a659bb6d22d77910f4d4e9b9d9688c Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Fri, 20 Oct 2023 22:13:22 +0200 Subject: [PATCH 277/806] Support mysql `RLIKE` and `REGEXP` binary operators (#1017) --- src/ast/mod.rs | 21 +++++++++++++++++++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 19 +++++++++++++++++-- src/test_utils.rs | 2 +- tests/sqlparser_mysql.rs | 12 ++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index fc15efbc4..3b0030017 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -429,6 +429,14 @@ pub enum Expr { pattern: Box, escape_char: Option, }, + /// MySQL: RLIKE regex or REGEXP regex + RLike { + negated: bool, + expr: Box, + pattern: Box, + // true for REGEXP, false for RLIKE (no difference in semantics) + regexp: bool, + }, /// Any operation e.g. `foo > ANY(bar)`, comparison operator is one of [=, >, <, =>, =<, !=] AnyOp { left: Box, @@ -740,6 +748,19 @@ impl fmt::Display for Expr { pattern ), }, + Expr::RLike { + negated, + expr, + pattern, + regexp, + } => write!( + f, + "{} {}{} {}", + expr, + if *negated { "NOT " } else { "" }, + if *regexp { "REGEXP" } else { "RLIKE" }, + pattern + ), Expr::SimilarTo { negated, expr, diff --git a/src/keywords.rs b/src/keywords.rs index e1bbf44ae..6327ccc84 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -498,6 +498,7 @@ define_keywords!( REFERENCES, REFERENCING, REGCLASS, + REGEXP, REGR_AVGX, REGR_AVGY, REGR_COUNT, @@ -524,6 +525,7 @@ define_keywords!( RETURNS, REVOKE, RIGHT, + RLIKE, ROLE, ROLLBACK, ROLLUP, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 829b299af..0065f7987 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1932,10 +1932,21 @@ impl<'a> Parser<'a> { | Keyword::BETWEEN | Keyword::LIKE | Keyword::ILIKE - | Keyword::SIMILAR => { + | Keyword::SIMILAR + | Keyword::REGEXP + | Keyword::RLIKE => { self.prev_token(); let negated = self.parse_keyword(Keyword::NOT); - if self.parse_keyword(Keyword::IN) { + let regexp = self.parse_keyword(Keyword::REGEXP); + let rlike = self.parse_keyword(Keyword::RLIKE); + if regexp || rlike { + Ok(Expr::RLike { + negated, + expr: Box::new(expr), + pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), + regexp, + }) + } else if self.parse_keyword(Keyword::IN) { self.parse_in(expr, negated) } else if self.parse_keyword(Keyword::BETWEEN) { self.parse_between(expr, negated) @@ -2178,6 +2189,8 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), _ => Ok(0), }, @@ -2186,6 +2199,8 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::DIV => Ok(Self::MUL_DIV_MOD_OP_PREC), diff --git a/src/test_utils.rs b/src/test_utils.rs index f0c5e425a..76a3e073b 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -111,7 +111,7 @@ impl TestedDialects { /// 2. re-serializing the result of parsing `sql` produces the same /// `canonical` sql string pub fn one_statement_parses_to(&self, sql: &str, canonical: &str) -> Statement { - let mut statements = self.parse_sql_statements(sql).unwrap(); + let mut statements = self.parse_sql_statements(sql).expect(sql); assert_eq!(statements.len(), 1); if !canonical.is_empty() && sql != canonical { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 80b9dcfd8..6e59198d7 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1454,6 +1454,18 @@ fn parse_show_variables() { mysql_and_generic().verified_stmt("SHOW VARIABLES WHERE value = '3306'"); } +#[test] +fn parse_rlike_and_regexp() { + for s in &[ + "SELECT 1 WHERE 'a' RLIKE '^a$'", + "SELECT 1 WHERE 'a' REGEXP '^a$'", + "SELECT 1 WHERE 'a' NOT RLIKE '^a$'", + "SELECT 1 WHERE 'a' NOT REGEXP '^a$'", + ] { + mysql_and_generic().verified_only_select(s); + } +} + #[test] fn parse_kill() { let stmt = mysql_and_generic().verified_stmt("KILL CONNECTION 5"); From 5c10668dbb60bccaf11f224013d333a48e32ec38 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Tue, 24 Oct 2023 01:37:31 +0400 Subject: [PATCH 278/806] Add support for UNION DISTINCT BY NAME syntax (#997) Co-authored-by: Andrew Lamb --- src/ast/query.rs | 5 +- src/parser/mod.rs | 4 +- tests/sqlparser_duckdb.rs | 232 ++++++++++++++------------------------ 3 files changed, 89 insertions(+), 152 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 88b0931de..824fab1ba 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -120,7 +120,8 @@ impl fmt::Display for SetExpr { SetQuantifier::All | SetQuantifier::Distinct | SetQuantifier::ByName - | SetQuantifier::AllByName => write!(f, " {set_quantifier}")?, + | SetQuantifier::AllByName + | SetQuantifier::DistinctByName => write!(f, " {set_quantifier}")?, SetQuantifier::None => write!(f, "{set_quantifier}")?, } write!(f, " {right}")?; @@ -160,6 +161,7 @@ pub enum SetQuantifier { Distinct, ByName, AllByName, + DistinctByName, None, } @@ -170,6 +172,7 @@ impl fmt::Display for SetQuantifier { SetQuantifier::Distinct => write!(f, "DISTINCT"), SetQuantifier::ByName => write!(f, "BY NAME"), SetQuantifier::AllByName => write!(f, "ALL BY NAME"), + SetQuantifier::DistinctByName => write!(f, "DISTINCT BY NAME"), SetQuantifier::None => write!(f, ""), } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0065f7987..68a8cef1f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5696,7 +5696,9 @@ impl<'a> Parser<'a> { pub fn parse_set_quantifier(&mut self, op: &Option) -> SetQuantifier { match op { Some(SetOperator::Union) => { - if self.parse_keywords(&[Keyword::BY, Keyword::NAME]) { + if self.parse_keywords(&[Keyword::DISTINCT, Keyword::BY, Keyword::NAME]) { + SetQuantifier::DistinctByName + } else if self.parse_keywords(&[Keyword::BY, Keyword::NAME]) { SetQuantifier::ByName } else if self.parse_keyword(Keyword::ALL) { if self.parse_keywords(&[Keyword::BY, Keyword::NAME]) { diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index b05cc0dd4..db11d1e77 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -132,155 +132,87 @@ fn test_create_table_macro() { #[test] fn test_select_union_by_name() { - let ast = duckdb().verified_query("SELECT * FROM capitals UNION BY NAME SELECT * FROM weather"); - let expected = Box::::new(SetExpr::SetOperation { - op: SetOperator::Union, - set_quantifier: SetQuantifier::ByName, - left: Box::::new(SetExpr::Select(Box::new(Select { - distinct: None, - top: None, - projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: None, - opt_except: None, - opt_rename: None, - opt_replace: None, - })], - into: None, - from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "capitals".to_string(), - quote_style: None, - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - }, - joins: vec![], - }], - lateral_views: vec![], - selection: None, - group_by: GroupByExpr::Expressions(vec![]), - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - named_window: vec![], - qualify: None, - }))), - right: Box::::new(SetExpr::Select(Box::new(Select { - distinct: None, - top: None, - projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: None, - opt_except: None, - opt_rename: None, - opt_replace: None, - })], - into: None, - from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "weather".to_string(), - quote_style: None, - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - }, - joins: vec![], - }], - lateral_views: vec![], - selection: None, - group_by: GroupByExpr::Expressions(vec![]), - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - named_window: vec![], - qualify: None, - }))), - }); - - assert_eq!(ast.body, expected); + let q1 = "SELECT * FROM capitals UNION BY NAME SELECT * FROM weather"; + let q2 = "SELECT * FROM capitals UNION ALL BY NAME SELECT * FROM weather"; + let q3 = "SELECT * FROM capitals UNION DISTINCT BY NAME SELECT * FROM weather"; - let ast = - duckdb().verified_query("SELECT * FROM capitals UNION ALL BY NAME SELECT * FROM weather"); - let expected = Box::::new(SetExpr::SetOperation { - op: SetOperator::Union, - set_quantifier: SetQuantifier::AllByName, - left: Box::::new(SetExpr::Select(Box::new(Select { - distinct: None, - top: None, - projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: None, - opt_except: None, - opt_rename: None, - opt_replace: None, - })], - into: None, - from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "capitals".to_string(), - quote_style: None, - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - }, - joins: vec![], - }], - lateral_views: vec![], - selection: None, - group_by: GroupByExpr::Expressions(vec![]), - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - named_window: vec![], - qualify: None, - }))), - right: Box::::new(SetExpr::Select(Box::new(Select { - distinct: None, - top: None, - projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { - opt_exclude: None, - opt_except: None, - opt_rename: None, - opt_replace: None, - })], - into: None, - from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "weather".to_string(), - quote_style: None, - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - }, - joins: vec![], - }], - lateral_views: vec![], - selection: None, - group_by: GroupByExpr::Expressions(vec![]), - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - named_window: vec![], - qualify: None, - }))), - }); - assert_eq!(ast.body, expected); + for (ast, expected_quantifier) in &[ + (duckdb().verified_query(q1), SetQuantifier::ByName), + (duckdb().verified_query(q2), SetQuantifier::AllByName), + (duckdb().verified_query(q3), SetQuantifier::DistinctByName), + ] { + let expected = Box::::new(SetExpr::SetOperation { + op: SetOperator::Union, + set_quantifier: *expected_quantifier, + left: Box::::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: None, + opt_except: None, + opt_rename: None, + opt_replace: None, + })], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "capitals".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: GroupByExpr::Expressions(vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + }))), + right: Box::::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_exclude: None, + opt_except: None, + opt_rename: None, + opt_replace: None, + })], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "weather".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: GroupByExpr::Expressions(vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + }))), + }); + assert_eq!(ast.body, expected); + } } From 56f24ce2361bb2f9ee9d7566c3b1ce256ee02d8b Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Mon, 23 Oct 2023 14:50:45 -0700 Subject: [PATCH 279/806] Support subquery as function arg w/o parens in Snowflake dialect (#996) --- src/parser/mod.rs | 20 +++++++++++++++++++- tests/sqlparser_snowflake.rs | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 68a8cef1f..1c1d8b23e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1507,7 +1507,7 @@ impl<'a> Parser<'a> { within_group: false, })); } - // Snowflake defines ORDERY BY in within group instead of inside the function like + // Snowflake defines ORDER BY in within group instead of inside the function like // ANSI SQL. self.expect_token(&Token::RParen)?; let within_group = if self.parse_keywords(&[Keyword::WITHIN, Keyword::GROUP]) { @@ -6914,6 +6914,24 @@ impl<'a> Parser<'a> { if self.consume_token(&Token::RParen) { Ok((vec![], vec![])) } else { + // Snowflake permits a subquery to be passed as an argument without + // an enclosing set of parens if it's the only argument. + if dialect_of!(self is SnowflakeDialect) + && self + .parse_one_of_keywords(&[Keyword::WITH, Keyword::SELECT]) + .is_some() + { + self.prev_token(); + let subquery = self.parse_query()?; + self.expect_token(&Token::RParen)?; + return Ok(( + vec![FunctionArg::Unnamed(FunctionArgExpr::from( + WildcardExpr::Expr(Expr::Subquery(Box::new(subquery))), + ))], + vec![], + )); + } + let args = self.parse_comma_separated(Parser::parse_function_args)?; let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { self.parse_comma_separated(Parser::parse_order_by_expr)? diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index bb988665d..79c9eb1ea 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1065,3 +1065,23 @@ fn test_snowflake_trim() { snowflake().parse_sql_statements(error_sql).unwrap_err() ); } + +#[test] +fn parse_subquery_function_argument() { + // Snowflake allows passing an unparenthesized subquery as the single + // argument to a function. + snowflake().one_statement_parses_to( + "SELECT parse_json(SELECT '{}')", + "SELECT parse_json((SELECT '{}'))", + ); + + // Subqueries that begin with WITH work too. + snowflake().one_statement_parses_to( + "SELECT parse_json(WITH q AS (SELECT '{}' AS foo) SELECT foo FROM q)", + "SELECT parse_json((WITH q AS (SELECT '{}' AS foo) SELECT foo FROM q))", + ); + + // Commas are parsed as part of the subquery, not additional arguments to + // the function. + snowflake().one_statement_parses_to("SELECT func(SELECT 1, 2)", "SELECT func((SELECT 1, 2))"); +} From e857a452016d82dfc00398a5483ce9551dff9565 Mon Sep 17 00:00:00 2001 From: Lukasz Stefaniak Date: Mon, 23 Oct 2023 23:55:11 +0200 Subject: [PATCH 280/806] Support `SELECT * EXCEPT/REPLACE` syntax from ClickHouse (#1013) --- src/ast/query.rs | 2 ++ src/parser/mod.rs | 37 +++++++++++++++++++++++------------ tests/sqlparser_clickhouse.rs | 18 +++++++++++++++++ 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 824fab1ba..4289b0bde 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -434,11 +434,13 @@ pub struct WildcardAdditionalOptions { /// `[EXCLUDE...]`. pub opt_exclude: Option, /// `[EXCEPT...]`. + /// Clickhouse syntax: pub opt_except: Option, /// `[RENAME ...]`. pub opt_rename: Option, /// `[REPLACE]` /// BigQuery syntax: + /// Clickhouse syntax: pub opt_replace: Option, } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1c1d8b23e..9e0d595cb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6993,7 +6993,8 @@ impl<'a> Parser<'a> { } else { None }; - let opt_except = if dialect_of!(self is GenericDialect | BigQueryDialect) { + let opt_except = if dialect_of!(self is GenericDialect | BigQueryDialect | ClickHouseDialect) + { self.parse_optional_select_item_except()? } else { None @@ -7004,7 +7005,8 @@ impl<'a> Parser<'a> { None }; - let opt_replace = if dialect_of!(self is GenericDialect | BigQueryDialect) { + let opt_replace = if dialect_of!(self is GenericDialect | BigQueryDialect | ClickHouseDialect) + { self.parse_optional_select_item_replace()? } else { None @@ -7047,18 +7049,27 @@ impl<'a> Parser<'a> { &mut self, ) -> Result, ParserError> { let opt_except = if self.parse_keyword(Keyword::EXCEPT) { - let idents = self.parse_parenthesized_column_list(Mandatory, false)?; - match &idents[..] { - [] => { - return self.expected( - "at least one column should be parsed by the expect clause", - self.peek_token(), - )?; + if self.peek_token().token == Token::LParen { + let idents = self.parse_parenthesized_column_list(Mandatory, false)?; + match &idents[..] { + [] => { + return self.expected( + "at least one column should be parsed by the expect clause", + self.peek_token(), + )?; + } + [first, idents @ ..] => Some(ExceptSelectItem { + first_element: first.clone(), + additional_elements: idents.to_vec(), + }), } - [first, idents @ ..] => Some(ExceptSelectItem { - first_element: first.clone(), - additional_elements: idents.to_vec(), - }), + } else { + // Clickhouse allows EXCEPT column_name + let ident = self.parse_identifier()?; + Some(ExceptSelectItem { + first_element: ident, + additional_elements: vec![], + }) } } else { None diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 9efe4a368..8cca0da0b 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -355,6 +355,24 @@ fn parse_limit_by() { ); } +#[test] +fn parse_select_star_except() { + clickhouse().verified_stmt("SELECT * EXCEPT (prev_status) FROM anomalies"); +} + +#[test] +fn parse_select_star_except_no_parens() { + clickhouse().one_statement_parses_to( + "SELECT * EXCEPT prev_status FROM anomalies", + "SELECT * EXCEPT (prev_status) FROM anomalies", + ); +} + +#[test] +fn parse_select_star_replace() { + clickhouse().verified_stmt("SELECT * REPLACE (i + 1 AS i) FROM columns_transformers"); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], From ce62fe6d274d354fef34fad919b58f6ba16c61a3 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 24 Oct 2023 00:06:39 +0200 Subject: [PATCH 281/806] Support `FILTER` in over clause (#1007) Co-authored-by: Andrew Lamb --- README.md | 2 +- src/ast/mod.rs | 9 +++++++++ src/ast/visitor.rs | 2 +- src/dialect/sqlite.rs | 4 ++++ src/parser/mod.rs | 14 ++++++++++++++ tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_clickhouse.rs | 4 ++++ tests/sqlparser_common.rs | 19 +++++++++++++++++++ tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 1 + tests/sqlparser_mysql.rs | 6 ++++++ tests/sqlparser_postgres.rs | 6 ++++++ tests/sqlparser_redshift.rs | 1 + tests/sqlparser_snowflake.rs | 1 + tests/sqlparser_sqlite.rs | 33 +++++++++++++++++++++++++++++++++ 15 files changed, 102 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 454ea6c29..e987c2a21 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ println!("AST: {:?}", ast); This outputs ```rust -AST: [Query(Query { ctes: [], body: Select(Select { distinct: false, projection: [UnnamedExpr(Identifier("a")), UnnamedExpr(Identifier("b")), UnnamedExpr(Value(Long(123))), UnnamedExpr(Function(Function { name: ObjectName(["myfunc"]), args: [Identifier("b")], over: None, distinct: false }))], from: [TableWithJoins { relation: Table { name: ObjectName(["table_1"]), alias: None, args: [], with_hints: [] }, joins: [] }], selection: Some(BinaryOp { left: BinaryOp { left: Identifier("a"), op: Gt, right: Identifier("b") }, op: And, right: BinaryOp { left: Identifier("b"), op: Lt, right: Value(Long(100)) } }), group_by: [], having: None }), order_by: [OrderByExpr { expr: Identifier("a"), asc: Some(false) }, OrderByExpr { expr: Identifier("b"), asc: None }], limit: None, offset: None, fetch: None })] +AST: [Query(Query { ctes: [], body: Select(Select { distinct: false, projection: [UnnamedExpr(Identifier("a")), UnnamedExpr(Identifier("b")), UnnamedExpr(Value(Long(123))), UnnamedExpr(Function(Function { name: ObjectName(["myfunc"]), args: [Identifier("b")], filter: None, over: None, distinct: false }))], from: [TableWithJoins { relation: Table { name: ObjectName(["table_1"]), alias: None, args: [], with_hints: [] }, joins: [] }], selection: Some(BinaryOp { left: BinaryOp { left: Identifier("a"), op: Gt, right: Identifier("b") }, op: And, right: BinaryOp { left: Identifier("b"), op: Lt, right: Value(Long(100)) } }), group_by: [], having: None }), order_by: [OrderByExpr { expr: Identifier("a"), asc: Some(false) }, OrderByExpr { expr: Identifier("b"), asc: None }], limit: None, offset: None, fetch: None })] ``` diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3b0030017..11ce9b810 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1070,8 +1070,11 @@ impl Display for WindowType { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WindowSpec { + /// `OVER (PARTITION BY ...)` pub partition_by: Vec, + /// `OVER (ORDER BY ...)` pub order_by: Vec, + /// `OVER (window frame)` pub window_frame: Option, } @@ -3729,6 +3732,8 @@ impl fmt::Display for CloseCursor { pub struct Function { pub name: ObjectName, pub args: Vec, + /// e.g. `x > 5` in `COUNT(x) FILTER (WHERE x > 5)` + pub filter: Option>, pub over: Option, // aggregate functions may specify eg `COUNT(DISTINCT x)` pub distinct: bool, @@ -3777,6 +3782,10 @@ impl fmt::Display for Function { display_comma_separated(&self.order_by), )?; + if let Some(filter_cond) = &self.filter { + write!(f, " FILTER (WHERE {filter_cond})")?; + } + if let Some(o) = &self.over { write!(f, " OVER {o}")?; } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 09cb20a0c..4e025f962 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -506,7 +506,7 @@ where /// *expr = Expr::Function(Function { /// name: ObjectName(vec![Ident::new("f")]), /// args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(old_expr))], -/// over: None, distinct: false, special: false, order_by: vec![], +/// filter: None, over: None, distinct: false, special: false, order_by: vec![], /// }); /// } /// ControlFlow::<()>::Continue(()) diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index fa21224f6..37c7c7fa7 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -35,6 +35,10 @@ impl Dialect for SQLiteDialect { || ('\u{007f}'..='\u{ffff}').contains(&ch) } + fn supports_filter_during_aggregation(&self) -> bool { + true + } + fn is_identifier_part(&self, ch: char) -> bool { self.is_identifier_start(ch) || ch.is_ascii_digit() } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9e0d595cb..3bf5228c4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -772,6 +772,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name: ObjectName(vec![w.to_ident()]), args: vec![], + filter: None, over: None, distinct: false, special: true, @@ -957,6 +958,17 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let distinct = self.parse_all_or_distinct()?.is_some(); let (args, order_by) = self.parse_optional_args_with_orderby()?; + let filter = if self.dialect.supports_filter_during_aggregation() + && self.parse_keyword(Keyword::FILTER) + && self.consume_token(&Token::LParen) + && self.parse_keyword(Keyword::WHERE) + { + let filter = Some(Box::new(self.parse_expr()?)); + self.expect_token(&Token::RParen)?; + filter + } else { + None + }; let over = if self.parse_keyword(Keyword::OVER) { if self.consume_token(&Token::LParen) { let window_spec = self.parse_window_spec()?; @@ -970,6 +982,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name, args, + filter, over, distinct, special: false, @@ -987,6 +1000,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name, args, + filter: None, over: None, distinct: false, special, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index b3f683b9a..fe95b1873 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -564,6 +564,7 @@ fn parse_map_access_offset() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( number("0") ))),], + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 8cca0da0b..7d9cb0309 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -50,6 +50,7 @@ fn parse_map_access_expr() { Value::SingleQuotedString("endpoint".to_string()) ))), ], + filter: None, over: None, distinct: false, special: false, @@ -89,6 +90,7 @@ fn parse_map_access_expr() { Value::SingleQuotedString("app".to_string()) ))), ], + filter: None, over: None, distinct: false, special: false, @@ -138,6 +140,7 @@ fn parse_array_fn() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new("x1")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new("x2")))), ], + filter: None, over: None, distinct: false, special: false, @@ -196,6 +199,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3b8775e45..9eb52f6ec 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -875,6 +875,7 @@ fn parse_select_count_wildcard() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], + filter: None, over: None, distinct: false, special: false, @@ -895,6 +896,7 @@ fn parse_select_count_distinct() { op: UnaryOperator::Plus, expr: Box::new(Expr::Identifier(Ident::new("x"))), }))], + filter: None, over: None, distinct: true, special: false, @@ -1862,6 +1864,7 @@ fn parse_select_having() { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], + filter: None, over: None, distinct: false, special: false, @@ -1887,6 +1890,7 @@ fn parse_select_qualify() { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("ROW_NUMBER")]), args: vec![], + filter: None, over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![Expr::Identifier(Ident::new("p"))], order_by: vec![OrderByExpr { @@ -3342,6 +3346,7 @@ fn parse_scalar_function_in_projection() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("id")) ))], + filter: None, over: None, distinct: false, special: false, @@ -3461,6 +3466,7 @@ fn parse_named_argument_function() { ))), }, ], + filter: None, over: None, distinct: false, special: false, @@ -3492,6 +3498,7 @@ fn parse_window_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), args: vec![], + filter: None, over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![], order_by: vec![OrderByExpr { @@ -3535,6 +3542,7 @@ fn test_parse_named_window() { quote_style: None, }), ))], + filter: None, over: Some(WindowType::NamedWindow(Ident { value: "window1".to_string(), quote_style: None, @@ -3560,6 +3568,7 @@ fn test_parse_named_window() { quote_style: None, }), ))], + filter: None, over: Some(WindowType::NamedWindow(Ident { value: "window2".to_string(), quote_style: None, @@ -4029,6 +4038,7 @@ fn parse_at_timezone() { quote_style: None, }]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero.clone()))], + filter: None, over: None, distinct: false, special: false, @@ -4056,6 +4066,7 @@ fn parse_at_timezone() { quote_style: None, },],), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero))], + filter: None, over: None, distinct: false, special: false, @@ -4067,6 +4078,7 @@ fn parse_at_timezone() { Value::SingleQuotedString("%Y-%m-%dT%H".to_string()), ),),), ], + filter: None, over: None, distinct: false, special: false, @@ -4225,6 +4237,7 @@ fn parse_table_function() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( Value::SingleQuotedString("1".to_owned()), )))], + filter: None, over: None, distinct: false, special: false, @@ -4376,6 +4389,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("3")))), ], + filter: None, over: None, distinct: false, special: false, @@ -4405,6 +4419,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("3")))), ], + filter: None, over: None, distinct: false, special: false, @@ -4416,6 +4431,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("5")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("6")))), ], + filter: None, over: None, distinct: false, special: false, @@ -6888,6 +6904,7 @@ fn parse_time_functions() { let select_localtime_func_call_ast = Function { name: ObjectName(vec![Ident::new(func_name)]), args: vec![], + filter: None, over: None, distinct: false, special: false, @@ -7374,6 +7391,7 @@ fn parse_pivot_table() { args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::CompoundIdentifier(vec![Ident::new("a"), Ident::new("amount"),]) ))]), + filter: None, over: None, distinct: false, special: false, @@ -7523,6 +7541,7 @@ fn parse_pivot_unpivot_table() { args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("population")) ))]), + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 6ca47e12c..6f3a8f994 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -346,6 +346,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index f9eb4d8fb..ebadf95f2 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -334,6 +334,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 6e59198d7..3bcb84439 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1071,6 +1071,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("description")) ))], + filter: None, over: None, distinct: false, special: false, @@ -1084,6 +1085,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_create")) ))], + filter: None, over: None, distinct: false, special: false, @@ -1097,6 +1099,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_read")) ))], + filter: None, over: None, distinct: false, special: false, @@ -1110,6 +1113,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_update")) ))], + filter: None, over: None, distinct: false, special: false, @@ -1123,6 +1127,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_delete")) ))], + filter: None, over: None, distinct: false, special: false, @@ -1512,6 +1517,7 @@ fn parse_table_colum_option_on_update() { option: ColumnOption::OnUpdate(Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_TIMESTAMP")]), args: vec![], + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 654723668..0256579db 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2275,6 +2275,7 @@ fn test_composite_value() { named: true } )))], + filter: None, over: None, distinct: false, special: false, @@ -2436,6 +2437,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_CATALOG")]), args: vec![], + filter: None, over: None, distinct: false, special: true, @@ -2447,6 +2449,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_USER")]), args: vec![], + filter: None, over: None, distinct: false, special: true, @@ -2458,6 +2461,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("SESSION_USER")]), args: vec![], + filter: None, over: None, distinct: false, special: true, @@ -2469,6 +2473,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("USER")]), args: vec![], + filter: None, over: None, distinct: false, special: true, @@ -2919,6 +2924,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 5ae539b3c..6238d1eca 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -137,6 +137,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 79c9eb1ea..3319af7b9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -248,6 +248,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + filter: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 39a82cc8b..8d7ccf315 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -290,6 +290,39 @@ fn parse_create_table_with_strict() { } } +#[test] +fn parse_window_function_with_filter() { + for func_name in [ + "row_number", + "rank", + "max", + "count", + "user_defined_function", + ] { + let sql = format!("SELECT {}(x) FILTER (WHERE y) OVER () FROM t", func_name); + let select = sqlite().verified_only_select(&sql); + assert_eq!(select.to_string(), sql); + assert_eq!( + select.projection, + vec![SelectItem::UnnamedExpr(Expr::Function(Function { + name: ObjectName(vec![Ident::new(func_name)]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("x")) + ))], + over: Some(WindowType::WindowSpec(WindowSpec { + partition_by: vec![], + order_by: vec![], + window_frame: None, + })), + filter: Some(Box::new(Expr::Identifier(Ident::new("y")))), + distinct: false, + special: false, + order_by: vec![] + }))] + ); + } +} + #[test] fn parse_attach_database() { let sql = "ATTACH DATABASE 'test.db' AS test"; From 2798b65b42c529bd089742a2028e94d59d82e493 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 23 Oct 2023 18:07:00 -0400 Subject: [PATCH 282/806] snowflake/generic: `position` can be the name of a column (#1022) Co-authored-by: Lukasz Stefaniak --- src/parser/mod.rs | 4 +++- tests/sqlparser_snowflake.rs | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3bf5228c4..e79f31bac 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -794,7 +794,9 @@ impl<'a> Parser<'a> { Keyword::EXTRACT => self.parse_extract_expr(), Keyword::CEIL => self.parse_ceil_floor_expr(true), Keyword::FLOOR => self.parse_ceil_floor_expr(false), - Keyword::POSITION => self.parse_position_expr(), + Keyword::POSITION if self.peek_token().token == Token::LParen => { + self.parse_position_expr() + } Keyword::SUBSTRING => self.parse_substring_expr(), Keyword::OVERLAY => self.parse_overlay_expr(), Keyword::TRIM => self.parse_trim_expr(), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 3319af7b9..7e6f18138 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1067,6 +1067,12 @@ fn test_snowflake_trim() { ); } +#[test] +fn parse_position_not_function_columns() { + snowflake_and_generic() + .verified_stmt("SELECT position FROM tbl1 WHERE position NOT IN ('first', 'last')"); +} + #[test] fn parse_subquery_function_argument() { // Snowflake allows passing an unparenthesized subquery as the single From 8b2a248d7b90edce93e8c443d31a790d553fc0c2 Mon Sep 17 00:00:00 2001 From: Ilya Date: Tue, 24 Oct 2023 01:07:39 +0300 Subject: [PATCH 283/806] parse SQLite pragma statement (#969) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 18 ++++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 28 ++++++++++++++++++++++++ tests/sqlparser_sqlite.rs | 45 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 11ce9b810..5aa42c96f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1914,6 +1914,12 @@ pub enum Statement { name: ObjectName, representation: UserDefinedTypeRepresentation, }, + // PRAGMA . = + Pragma { + name: ObjectName, + value: Option, + is_eq: bool, + }, } impl fmt::Display for Statement { @@ -3276,6 +3282,18 @@ impl fmt::Display for Statement { } => { write!(f, "CREATE TYPE {name} AS {representation}") } + Statement::Pragma { name, value, is_eq } => { + write!(f, "PRAGMA {name}")?; + if value.is_some() { + let val = value.as_ref().unwrap(); + if *is_eq { + write!(f, " = {val}")?; + } else { + write!(f, "({val})")?; + } + } + Ok(()) + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index 6327ccc84..405203601 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -472,6 +472,7 @@ define_keywords!( POSITION, POSITION_REGEX, POWER, + PRAGMA, PRECEDES, PRECEDING, PRECISION, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e79f31bac..f83f019ea 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -491,6 +491,8 @@ impl<'a> Parser<'a> { Keyword::EXECUTE => Ok(self.parse_execute()?), Keyword::PREPARE => Ok(self.parse_prepare()?), Keyword::MERGE => Ok(self.parse_merge()?), + // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html + Keyword::PRAGMA => Ok(self.parse_pragma()?), _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -7502,6 +7504,32 @@ impl<'a> Parser<'a> { }) } + // PRAGMA [schema-name '.'] pragma-name [('=' pragma-value) | '(' pragma-value ')'] + pub fn parse_pragma(&mut self) -> Result { + let name = self.parse_object_name()?; + if self.consume_token(&Token::LParen) { + let value = self.parse_number_value()?; + self.expect_token(&Token::RParen)?; + Ok(Statement::Pragma { + name, + value: Some(value), + is_eq: false, + }) + } else if self.consume_token(&Token::Eq) { + Ok(Statement::Pragma { + name, + value: Some(self.parse_number_value()?), + is_eq: true, + }) + } else { + Ok(Statement::Pragma { + name, + value: None, + is_eq: false, + }) + } + } + /// ```sql /// CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] /// ``` diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 8d7ccf315..2fdd4e3de 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -24,6 +24,51 @@ use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SQLiteDialect}; use sqlparser::tokenizer::Token; +#[test] +fn pragma_no_value() { + let sql = "PRAGMA cache_size"; + match sqlite_and_generic().verified_stmt(sql) { + Statement::Pragma { + name, + value: None, + is_eq: false, + } => { + assert_eq!("cache_size", name.to_string()); + } + _ => unreachable!(), + } +} +#[test] +fn pragma_eq_style() { + let sql = "PRAGMA cache_size = 10"; + match sqlite_and_generic().verified_stmt(sql) { + Statement::Pragma { + name, + value: Some(val), + is_eq: true, + } => { + assert_eq!("cache_size", name.to_string()); + assert_eq!("10", val.to_string()); + } + _ => unreachable!(), + } +} +#[test] +fn pragma_funciton_style() { + let sql = "PRAGMA cache_size(10)"; + match sqlite_and_generic().verified_stmt(sql) { + Statement::Pragma { + name, + value: Some(val), + is_eq: false, + } => { + assert_eq!("cache_size", name.to_string()); + assert_eq!("10", val.to_string()); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_without_rowid() { let sql = "CREATE TABLE t (a INT) WITHOUT ROWID"; From 6739d377bd2c5acfbc4d4631651ee7a857caefec Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 23 Oct 2023 18:09:02 -0400 Subject: [PATCH 284/806] Add docstrings for `Dialect`s, update README (#1016) --- README.md | 31 +++++++++++++++++++++---------- src/dialect/ansi.rs | 1 + src/dialect/bigquery.rs | 1 + src/dialect/clickhouse.rs | 1 + src/dialect/duckdb.rs | 1 + src/dialect/generic.rs | 2 ++ src/dialect/hive.rs | 1 + src/dialect/mod.rs | 3 +++ src/dialect/mssql.rs | 2 +- src/dialect/mysql.rs | 2 +- src/dialect/postgresql.rs | 1 + src/dialect/redshift.rs | 1 + src/dialect/snowflake.rs | 1 + src/dialect/sqlite.rs | 1 + 14 files changed, 37 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e987c2a21..58f5b8d48 100644 --- a/README.md +++ b/README.md @@ -124,28 +124,36 @@ parser](docs/custom_sql_parser.md). ## Contributing Contributions are highly encouraged! However, the bandwidth we have to -maintain this crate is fairly limited. +maintain this crate is limited. Please read the following sections carefully. -Pull requests that add support for or fix a bug in a feature in the -SQL standard, or a feature in a popular RDBMS, like Microsoft SQL +### New Syntax + +The most commonly accepted PRs add support for or fix a bug in a feature in the +SQL standard, or a a popular RDBMS, such as Microsoft SQL Server or PostgreSQL, will likely be accepted after a brief -review. +review. Any SQL feature that is dialect specific should be parsed by *both* the relevant [`Dialect`] +as well as [`GenericDialect`]. + +### Major API Changes The current maintainers do not plan for any substantial changes to -this crate's API at this time. And thus, PRs proposing major refactors +this crate's API. PRs proposing major refactors are not likely to be accepted. -Please be aware that, while we hope to review PRs in a reasonably -timely fashion, it may take a while. In order to speed the process, +### Testing + +While we hope to review PRs in a reasonably +timely fashion, it may take a week or more. In order to speed the process, please make sure the PR passes all CI checks, and includes tests demonstrating your code works as intended (and to avoid regressions). Remember to also test error paths. PRs without tests will not be reviewed or merged. Since the CI ensures that `cargo test`, `cargo fmt`, and `cargo clippy`, pass you -will likely want to run all three commands locally before submitting +should likely to run all three commands locally before submitting your PR. +### Filing Issues If you are unable to submit a patch, feel free to file an issue instead. Please try to include: @@ -156,8 +164,9 @@ try to include: * links to documentation for the feature for a few of the most popular databases that support it. -If you need support for a feature, you will likely need to implement -it yourself. Our goal as maintainers is to facilitate the integration +Unfortunately, if you need support for a feature, you will likely need to implement +it yourself, or file a well enough described ticket that another member of the community can do so. +Our goal as maintainers is to facilitate the integration of various features from various contributors, but not to provide the implementations ourselves, as we simply don't have the resources. @@ -183,3 +192,5 @@ licensed as above, without any additional terms or conditions. [Pratt Parser]: https://tdop.github.io/ [sql-2016-grammar]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html [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 diff --git a/src/dialect/ansi.rs b/src/dialect/ansi.rs index 14c83ae16..d07bc07eb 100644 --- a/src/dialect/ansi.rs +++ b/src/dialect/ansi.rs @@ -12,6 +12,7 @@ use crate::dialect::Dialect; +/// A [`Dialect`] for [ANSI SQL](https://en.wikipedia.org/wiki/SQL:2011). #[derive(Debug)] pub struct AnsiDialect {} diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 8266a32f0..46f27fea4 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -12,6 +12,7 @@ use crate::dialect::Dialect; +/// A [`Dialect`] for [Google Bigquery](https://cloud.google.com/bigquery/) #[derive(Debug, Default)] pub struct BigQueryDialect; diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 395116f9c..50fbde99e 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -12,6 +12,7 @@ use crate::dialect::Dialect; +// A [`Dialect`] for [ClickHouse](https://clickhouse.com/). #[derive(Debug)] pub struct ClickHouseDialect {} diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 4e6e9d9a4..a4f9309e6 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -12,6 +12,7 @@ use crate::dialect::Dialect; +/// A [`Dialect`] for [DuckDB](https://duckdb.org/) #[derive(Debug, Default)] pub struct DuckDbDialect; diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8310954cd..4be4b9e23 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -12,6 +12,8 @@ use crate::dialect::Dialect; +/// A permissive, general purpose [`Dialect`], which parses a wide variety of SQL +/// statements, from many different dialects. #[derive(Debug, Default)] pub struct GenericDialect; diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index 96cefb1d9..20800c1d3 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -12,6 +12,7 @@ use crate::dialect::Dialect; +/// A [`Dialect`] for [Hive](https://hive.apache.org/). #[derive(Debug)] pub struct HiveDialect {} diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index e174528b0..625f9ce0a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -64,6 +64,9 @@ macro_rules! dialect_of { /// custom extensions or various historical reasons. This trait /// encapsulates the parsing differences between dialects. /// +/// [`GenericDialect`] is the most permissive dialect, and parses the union of +/// all the other dialects, when there is no ambiguity. +/// /// # Examples /// Most users create a [`Dialect`] directly, as shown on the [module /// level documentation]: diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index f04398100..26ecd4782 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -12,7 +12,7 @@ use crate::dialect::Dialect; -// [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/) dialect +/// A [`Dialect`] for [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/) #[derive(Debug)] pub struct MsSqlDialect {} diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 0f914ed02..8c3de74b7 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -19,7 +19,7 @@ use crate::{ keywords::Keyword, }; -/// [MySQL](https://www.mysql.com/) +/// A [`Dialect`] for [MySQL](https://www.mysql.com/) #[derive(Debug)] pub struct MySqlDialect {} diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index d131ff9c6..a0b192c85 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -16,6 +16,7 @@ use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; +/// A [`Dialect`] for [PostgreSQL](https://www.postgresql.org/) #[derive(Debug)] pub struct PostgreSqlDialect {} diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index c85f3dc20..73457ab30 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -16,6 +16,7 @@ use core::str::Chars; use super::PostgreSqlDialect; +/// A [`Dialect`] for [RedShift](https://aws.amazon.com/redshift/) #[derive(Debug)] pub struct RedshiftSqlDialect {} diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 713394a1e..33425e846 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -28,6 +28,7 @@ use alloc::vec::Vec; #[cfg(not(feature = "std"))] use alloc::{format, vec}; +/// A [`Dialect`] for [Snowflake](https://www.snowflake.com/) #[derive(Debug, Default)] pub struct SnowflakeDialect; diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 37c7c7fa7..68515d24f 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -15,6 +15,7 @@ use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; +/// A [`Dialect`] for [SQLite](https://www.sqlite.org) #[derive(Debug)] pub struct SQLiteDialect {} From 86aa1b96be1c1fbf56cbe7cb04e12370df53605c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Emin=20KARAKA=C5=9E?= Date: Tue, 24 Oct 2023 12:45:25 +0300 Subject: [PATCH 285/806] Support `INSERT IGNORE` in `MySql` and `GenericDialect` (#1004) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 6 +++++- src/parser/mod.rs | 4 ++++ tests/sqlparser_mysql.rs | 41 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5aa42c96f..17f6d3a04 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1296,6 +1296,8 @@ pub enum Statement { Insert { /// Only for Sqlite or: Option, + /// Only for mysql + ignore: bool, /// INTO - optional keyword into: bool, /// TABLE @@ -2126,6 +2128,7 @@ impl fmt::Display for Statement { } Statement::Insert { or, + ignore, into, table_name, overwrite, @@ -2142,8 +2145,9 @@ impl fmt::Display for Statement { } else { write!( f, - "INSERT{over}{int}{tbl} {table_name} ", + "INSERT{ignore}{over}{int}{tbl} {table_name} ", table_name = table_name, + ignore = if *ignore { " IGNORE" } else { "" }, over = if *overwrite { " OVERWRITE" } else { "" }, int = if *into { " INTO" } else { "" }, tbl = if *table { " TABLE" } else { "" } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f83f019ea..d0b11ffea 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6755,6 +6755,9 @@ impl<'a> Parser<'a> { None }; + let ignore = dialect_of!(self is MySqlDialect | GenericDialect) + && self.parse_keyword(Keyword::IGNORE); + let action = self.parse_one_of_keywords(&[Keyword::INTO, Keyword::OVERWRITE]); let into = action == Some(Keyword::INTO); let overwrite = action == Some(Keyword::OVERWRITE); @@ -6852,6 +6855,7 @@ impl<'a> Parser<'a> { Ok(Statement::Insert { or, table_name, + ignore, into, overwrite, partitioned, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 3bcb84439..8391bbadb 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -972,6 +972,47 @@ fn parse_simple_insert() { } } +#[test] +fn parse_ignore_insert() { + let sql = r"INSERT IGNORE INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; + + match mysql_and_generic().verified_stmt(sql) { + Statement::Insert { + table_name, + columns, + source, + on, + ignore, + .. + } => { + assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); + assert!(on.is_none()); + assert!(ignore); + assert_eq!( + Box::new(Query { + with: None, + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![vec![ + Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), + Expr::Value(number("1")) + ]] + })), + order_by: vec![], + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![] + }), + source + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_empty_row_insert() { let sql = "INSERT INTO tb () VALUES (), ()"; From 57090537f0b2984681ff9333c57f8a8ce7c995cb Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 24 Oct 2023 12:30:05 +0200 Subject: [PATCH 286/806] Test that `regexp` can be used as an identifier in postgres (#1018) --- tests/sqlparser_postgres.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0256579db..64fcbd38a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3390,6 +3390,13 @@ fn parse_truncate() { ); } +#[test] +fn parse_select_regexp_as_column_name() { + pg_and_generic().verified_only_select( + "SELECT REGEXP.REGEXP AS REGEXP FROM REGEXP AS REGEXP WHERE REGEXP.REGEXP", + ); +} + #[test] fn parse_create_table_with_alias() { let sql = "CREATE TABLE public.datatype_aliases From 9832adb37651da83483263cd652ff6ab01a7060f Mon Sep 17 00:00:00 2001 From: Chris A Date: Tue, 24 Oct 2023 05:33:51 -0500 Subject: [PATCH 287/806] Support "with" identifiers surrounded by backticks in `GenericDialect` (#1010) --- src/dialect/mod.rs | 2 +- tests/sqlparser_hive.rs | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 625f9ce0a..856cfe1c9 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -95,7 +95,7 @@ pub trait Dialect: Debug + Any { /// MySQL, MS SQL, and sqlite). You can accept one of characters listed /// in `Word::matching_end_quote` here fn is_delimited_identifier_start(&self, ch: char) -> bool { - ch == '"' + ch == '"' || ch == '`' } /// Determine if quoted characters are proper for identifier fn is_proper_identifier_inside_quotes(&self, mut _chars: Peekable>) -> bool { diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 6f3a8f994..f63b9cef9 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -20,7 +20,7 @@ use sqlparser::ast::{ SelectItem, Statement, TableFactor, UnaryOperator, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect}; -use sqlparser::parser::ParserError; +use sqlparser::parser::{ParserError, ParserOptions}; use sqlparser::test_utils::*; #[test] @@ -32,6 +32,20 @@ fn parse_table_create() { hive().verified_stmt(iof); } +fn generic(options: Option) -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + options, + } +} + +#[test] +fn parse_describe() { + let describe = r#"DESCRIBE namespace.`table`"#; + hive().verified_stmt(describe); + generic(None).verified_stmt(describe); +} + #[test] fn parse_insert_overwrite() { let insert_partitions = r#"INSERT OVERWRITE TABLE db.new_table PARTITION (a = '1', b) SELECT a, b, c FROM db.table"#; @@ -265,13 +279,8 @@ fn parse_create_function() { _ => unreachable!(), } - let generic = TestedDialects { - dialects: vec![Box::new(GenericDialect {})], - options: None, - }; - assert_eq!( - generic.parse_sql_statements(sql).unwrap_err(), + generic(None).parse_sql_statements(sql).unwrap_err(), ParserError::ParserError( "Expected an object type after CREATE, found: FUNCTION".to_string() ) From 004a8dc5ddbbbfc0935c09fb572cd6161af33525 Mon Sep 17 00:00:00 2001 From: Chris A Date: Tue, 24 Oct 2023 06:19:01 -0500 Subject: [PATCH 288/806] Support multiple `PARTITION` statements in `ALTER TABLE ADD` statement (#1011) Co-authored-by: Chris A Co-authored-by: Andrew Lamb --- src/ast/ddl.rs | 24 +++++++++++++++++++++--- src/ast/mod.rs | 5 +++-- src/dialect/generic.rs | 4 ++++ src/parser/mod.rs | 22 +++++++++++++++++----- tests/sqlparser_hive.rs | 6 ++++++ 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f1575d979..da2c8c9e4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -69,7 +69,7 @@ pub enum AlterTableOperation { /// Add Partitions AddPartitions { if_not_exists: bool, - new_partitions: Vec, + new_partitions: Vec, }, DropPartitions { partitions: Vec, @@ -119,8 +119,8 @@ impl fmt::Display for AlterTableOperation { new_partitions, } => write!( f, - "ADD{ine} PARTITION ({})", - display_comma_separated(new_partitions), + "ADD{ine} {}", + display_separated(new_partitions, " "), ine = if *if_not_exists { " IF NOT EXISTS" } else { "" } ), AlterTableOperation::AddConstraint(c) => write!(f, "ADD {c}"), @@ -771,3 +771,21 @@ impl fmt::Display for UserDefinedTypeCompositeAttributeDef { Ok(()) } } + +/// PARTITION statement used in ALTER TABLE et al. such as in Hive SQL +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Partition { + pub partitions: Vec, +} + +impl fmt::Display for Partition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "PARTITION ({})", + display_comma_separated(&self.partitions) + ) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 17f6d3a04..4c69d3ed0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -31,8 +31,9 @@ pub use self::data_type::{ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue}; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, - ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, ProcedureParam, ReferentialAction, - TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ColumnOptionDef, GeneratedAs, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, + ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, + UserDefinedTypeRepresentation, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 4be4b9e23..ea5cc6c34 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -18,6 +18,10 @@ use crate::dialect::Dialect; pub struct GenericDialect; impl Dialect for GenericDialect { + fn is_delimited_identifier_start(&self, ch: char) -> bool { + ch == '"' || ch == '`' + } + fn is_identifier_start(&self, ch: char) -> bool { ch.is_alphabetic() || ch == '_' || ch == '#' || ch == '@' } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d0b11ffea..8930b0f49 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4195,6 +4195,13 @@ impl<'a> Parser<'a> { Ok(SqlOption { name, value }) } + pub fn parse_partition(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let partitions = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + Ok(Partition { partitions }) + } + 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()? { @@ -4202,13 +4209,18 @@ impl<'a> Parser<'a> { } else { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - if self.parse_keyword(Keyword::PARTITION) { - self.expect_token(&Token::LParen)?; - let partitions = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; + let mut new_partitions = vec![]; + loop { + if self.parse_keyword(Keyword::PARTITION) { + new_partitions.push(self.parse_partition()?); + } else { + break; + } + } + if !new_partitions.is_empty() { AlterTableOperation::AddPartitions { if_not_exists, - new_partitions: partitions, + new_partitions, } } else { let column_keyword = self.parse_keyword(Keyword::COLUMN); diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index f63b9cef9..534a224ea 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -128,6 +128,12 @@ fn test_add_partition() { hive().verified_stmt(add); } +#[test] +fn test_add_multiple_partitions() { + let add = "ALTER TABLE db.table ADD IF NOT EXISTS PARTITION (`a` = 'asdf', `b` = 2) PARTITION (`a` = 'asdh', `b` = 3)"; + hive().verified_stmt(add); +} + #[test] fn test_drop_partition() { let drop = "ALTER TABLE db.table DROP PARTITION (a = 1)"; From c5a7d6ccb97292ace1399f24f88dbb1027c0987f Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 24 Oct 2023 13:20:12 +0200 Subject: [PATCH 289/806] Support for single-quoted identifiers (#1021) Co-authored-by: Andrew Lamb --- src/parser/mod.rs | 38 ++++++++++++++++++++++++++------------ tests/sqlparser_sqlite.rs | 5 +++++ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8930b0f49..eb7c4a008 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -620,18 +620,29 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { - Token::Word(w) if self.peek_token().token == Token::Period => { - let mut id_parts: Vec = vec![w.to_ident()]; - - while self.consume_token(&Token::Period) { - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => id_parts.push(w.to_ident()), - Token::Mul => { - return Ok(WildcardExpr::QualifiedWildcard(ObjectName(id_parts))); - } - _ => { - return self.expected("an identifier or a '*' after '.'", next_token); + t @ (Token::Word(_) | Token::SingleQuotedString(_)) => { + if self.peek_token().token == Token::Period { + let mut id_parts: Vec = vec![match t { + Token::Word(w) => w.to_ident(), + Token::SingleQuotedString(s) => Ident::with_quote('\'', s), + _ => unreachable!(), // We matched above + }]; + + while self.consume_token(&Token::Period) { + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) => id_parts.push(w.to_ident()), + Token::SingleQuotedString(s) => { + // SQLite has single-quoted identifiers + id_parts.push(Ident::with_quote('\'', s)) + } + Token::Mul => { + return Ok(WildcardExpr::QualifiedWildcard(ObjectName(id_parts))); + } + _ => { + return self + .expected("an identifier or a '*' after '.'", next_token); + } } } } @@ -830,6 +841,9 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { Token::Word(w) => id_parts.push(w.to_ident()), + Token::SingleQuotedString(s) => { + id_parts.push(Ident::with_quote('\'', s)) + } _ => { return self .expected("an identifier or a '*' after '.'", next_token); diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 2fdd4e3de..b657acddf 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -335,6 +335,11 @@ fn parse_create_table_with_strict() { } } +#[test] +fn parse_single_quoted_identified() { + sqlite().verified_only_select("SELECT 't'.*, t.'x' FROM 't'"); + // TODO: add support for select 't'.x +} #[test] fn parse_window_function_with_filter() { for func_name in [ From 8262abcd311e2b129cfba369e7332efb833db188 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 24 Oct 2023 07:35:59 -0400 Subject: [PATCH 290/806] Improve documentation on Parser::consume_token and friends (#994) --- src/parser/mod.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index eb7c4a008..a1323f7a8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2346,7 +2346,7 @@ impl<'a> Parser<'a> { } } - /// Report unexpected token + /// Report `found` was encountered instead of `expected` pub fn expected(&self, expected: &str, found: TokenWithLocation) -> Result { parser_err!( format!("Expected {expected}, found: {found}"), @@ -2354,7 +2354,8 @@ impl<'a> Parser<'a> { ) } - /// Look for an expected keyword and consume it if it exists + /// If the current token is the `expected` keyword, consume it and returns + /// true. Otherwise, no tokens are consumed and returns false. #[must_use] pub fn parse_keyword(&mut self, expected: Keyword) -> bool { match self.peek_token().token { @@ -2366,7 +2367,9 @@ impl<'a> Parser<'a> { } } - /// Look for an expected sequence of keywords and consume them if they exist + /// If the current and subsequent tokens exactly match the `keywords` + /// sequence, consume them and returns true. Otherwise, no tokens are + /// consumed and returns false #[must_use] pub fn parse_keywords(&mut self, keywords: &[Keyword]) -> bool { let index = self.index; @@ -2381,7 +2384,9 @@ impl<'a> Parser<'a> { true } - /// Look for one of the given keywords and return the one that matches. + /// 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`. #[must_use] pub fn parse_one_of_keywords(&mut self, keywords: &[Keyword]) -> Option { match self.peek_token().token { @@ -2398,7 +2403,8 @@ impl<'a> Parser<'a> { } } - /// Bail out if the current token is not one of the expected keywords, or consume it if it is + /// If the current token is one of the expected keywords, consume the token + /// and return the keyword that matches. Otherwise, return an error. pub fn expect_one_of_keywords(&mut self, keywords: &[Keyword]) -> Result { if let Some(keyword) = self.parse_one_of_keywords(keywords) { Ok(keyword) @@ -2411,7 +2417,8 @@ impl<'a> Parser<'a> { } } - /// Bail out if the current token is not an expected keyword, or consume it if it is + /// If the current token is the `expected` keyword, consume the token. + /// Otherwise return an error. pub fn expect_keyword(&mut self, expected: Keyword) -> Result<(), ParserError> { if self.parse_keyword(expected) { Ok(()) @@ -2420,8 +2427,8 @@ impl<'a> Parser<'a> { } } - /// Bail out if the following tokens are not the expected sequence of - /// keywords, or consume them if they are. + /// If the current and subsequent tokens exactly match the `keywords` + /// sequence, consume them and returns Ok. Otherwise, return an Error. pub fn expect_keywords(&mut self, expected: &[Keyword]) -> Result<(), ParserError> { for &kw in expected { self.expect_keyword(kw)?; From b89edaa98b6f8cbea105b59fdc455c300d28a828 Mon Sep 17 00:00:00 2001 From: yuval-illumex <85674443+yuval-illumex@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:45:59 +0300 Subject: [PATCH 291/806] Support `IGNORE|RESPECT` NULLs clause in window functions (#998) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 26 ++++++++++++++++ src/ast/visitor.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 16 ++++++++++ tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_clickhouse.rs | 4 +++ tests/sqlparser_common.rs | 58 +++++++++++++++++++++++++++++++++++ tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 1 + tests/sqlparser_mysql.rs | 6 ++++ tests/sqlparser_postgres.rs | 6 ++++ tests/sqlparser_redshift.rs | 1 + tests/sqlparser_snowflake.rs | 1 + tests/sqlparser_sqlite.rs | 1 + 14 files changed, 124 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4c69d3ed0..b52bdf846 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1161,6 +1161,26 @@ impl fmt::Display for WindowFrameUnits { } } +/// Specifies Ignore / Respect NULL within window functions. +/// For example +/// `FIRST_VALUE(column2) IGNORE NULLS OVER (PARTITION BY column1)` +#[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 NullTreatment { + IgnoreNulls, + RespectNulls, +} + +impl fmt::Display for NullTreatment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + NullTreatment::IgnoreNulls => "IGNORE NULLS", + NullTreatment::RespectNulls => "RESPECT NULLS", + }) + } +} + /// Specifies [WindowFrame]'s `start_bound` and `end_bound` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -3757,6 +3777,8 @@ pub struct Function { pub args: Vec, /// e.g. `x > 5` in `COUNT(x) FILTER (WHERE x > 5)` pub filter: Option>, + // Snowflake/MSSQL supports diffrent options for null treatment in rank functions + pub null_treatment: Option, pub over: Option, // aggregate functions may specify eg `COUNT(DISTINCT x)` pub distinct: bool, @@ -3809,6 +3831,10 @@ impl fmt::Display for Function { write!(f, " FILTER (WHERE {filter_cond})")?; } + if let Some(o) = &self.null_treatment { + write!(f, " {o}")?; + } + if let Some(o) = &self.over { write!(f, " OVER {o}")?; } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 4e025f962..99db16107 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -506,6 +506,7 @@ where /// *expr = Expr::Function(Function { /// name: ObjectName(vec![Ident::new("f")]), /// args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(old_expr))], +/// null_treatment: None, /// filter: None, over: None, distinct: false, special: false, order_by: vec![], /// }); /// } diff --git a/src/keywords.rs b/src/keywords.rs index 405203601..dec324cfb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -518,6 +518,7 @@ define_keywords!( REPLACE, REPLICATION, RESET, + RESPECT, RESTRICT, RESULT, RETAIN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a1323f7a8..4a465ec99 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -785,6 +785,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name: ObjectName(vec![w.to_ident()]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -987,6 +988,19 @@ impl<'a> Parser<'a> { } else { None }; + let null_treatment = match self.parse_one_of_keywords(&[Keyword::RESPECT, Keyword::IGNORE]) + { + Some(keyword) => { + self.expect_keyword(Keyword::NULLS)?; + + match keyword { + Keyword::RESPECT => Some(NullTreatment::RespectNulls), + Keyword::IGNORE => Some(NullTreatment::IgnoreNulls), + _ => None, + } + } + None => None, + }; let over = if self.parse_keyword(Keyword::OVER) { if self.consume_token(&Token::LParen) { let window_spec = self.parse_window_spec()?; @@ -1000,6 +1014,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name, args, + null_treatment, filter, over, distinct, @@ -1018,6 +1033,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name, args, + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index fe95b1873..e72a99b49 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -564,6 +564,7 @@ fn parse_map_access_offset() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( number("0") ))),], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 7d9cb0309..e7c85c2a3 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -50,6 +50,7 @@ fn parse_map_access_expr() { Value::SingleQuotedString("endpoint".to_string()) ))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -90,6 +91,7 @@ fn parse_map_access_expr() { Value::SingleQuotedString("app".to_string()) ))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -140,6 +142,7 @@ fn parse_array_fn() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new("x1")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new("x2")))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -199,6 +202,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9eb52f6ec..5eb70b09b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -875,6 +875,7 @@ fn parse_select_count_wildcard() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], + null_treatment: None, filter: None, over: None, distinct: false, @@ -896,6 +897,7 @@ fn parse_select_count_distinct() { op: UnaryOperator::Plus, expr: Box::new(Expr::Identifier(Ident::new("x"))), }))], + null_treatment: None, filter: None, over: None, distinct: true, @@ -1864,6 +1866,7 @@ fn parse_select_having() { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1890,6 +1893,7 @@ fn parse_select_qualify() { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("ROW_NUMBER")]), args: vec![], + null_treatment: None, filter: None, over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![Expr::Identifier(Ident::new("p"))], @@ -2287,6 +2291,45 @@ fn parse_agg_with_order_by() { } } +#[test] +fn parse_window_rank_function() { + let supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(HiveDialect {}), + Box::new(SnowflakeDialect {}), + ], + options: None, + }; + + for sql in [ + "SELECT column1, column2, FIRST_VALUE(column2) OVER (PARTITION BY column1 ORDER BY column2 NULLS LAST) AS column2_first FROM t1", + "SELECT column1, column2, FIRST_VALUE(column2) OVER (ORDER BY column2 NULLS LAST) AS column2_first FROM t1", + "SELECT col_1, col_2, LAG(col_2) OVER (ORDER BY col_1) FROM t1", + "SELECT LAG(col_2, 1, 0) OVER (ORDER BY col_1) FROM t1", + "SELECT LAG(col_2, 1, 0) OVER (PARTITION BY col_3 ORDER BY col_1)", + ] { + supported_dialects.verified_stmt(sql); + } + + let supported_dialects_nulls = TestedDialects { + dialects: vec![Box::new(MsSqlDialect {}), Box::new(SnowflakeDialect {})], + options: None, + }; + + for sql in [ + "SELECT column1, column2, FIRST_VALUE(column2) IGNORE NULLS OVER (PARTITION BY column1 ORDER BY column2 NULLS LAST) AS column2_first FROM t1", + "SELECT column1, column2, FIRST_VALUE(column2) RESPECT NULLS OVER (PARTITION BY column1 ORDER BY column2 NULLS LAST) AS column2_first FROM t1", + "SELECT LAG(col_2, 1, 0) IGNORE NULLS OVER (ORDER BY col_1) FROM t1", + "SELECT LAG(col_2, 1, 0) RESPECT NULLS OVER (ORDER BY col_1) FROM t1", + ] { + supported_dialects_nulls.verified_stmt(sql); + } +} + #[test] fn parse_create_table() { let sql = "CREATE TABLE uk_cities (\ @@ -3346,6 +3389,7 @@ fn parse_scalar_function_in_projection() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("id")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -3466,6 +3510,7 @@ fn parse_named_argument_function() { ))), }, ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -3498,6 +3543,7 @@ fn parse_window_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), args: vec![], + null_treatment: None, filter: None, over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![], @@ -3542,6 +3588,7 @@ fn test_parse_named_window() { quote_style: None, }), ))], + null_treatment: None, filter: None, over: Some(WindowType::NamedWindow(Ident { value: "window1".to_string(), @@ -3568,6 +3615,7 @@ fn test_parse_named_window() { quote_style: None, }), ))], + null_treatment: None, filter: None, over: Some(WindowType::NamedWindow(Ident { value: "window2".to_string(), @@ -4038,6 +4086,7 @@ fn parse_at_timezone() { quote_style: None, }]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero.clone()))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4066,6 +4115,7 @@ fn parse_at_timezone() { quote_style: None, },],), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4078,6 +4128,7 @@ fn parse_at_timezone() { Value::SingleQuotedString("%Y-%m-%dT%H".to_string()), ),),), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4237,6 +4288,7 @@ fn parse_table_function() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( Value::SingleQuotedString("1".to_owned()), )))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4389,6 +4441,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("3")))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4419,6 +4472,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("3")))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4431,6 +4485,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("5")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("6")))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -6904,6 +6959,7 @@ fn parse_time_functions() { let select_localtime_func_call_ast = Function { name: ObjectName(vec![Ident::new(func_name)]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -7391,6 +7447,7 @@ fn parse_pivot_table() { args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::CompoundIdentifier(vec![Ident::new("a"), Ident::new("amount"),]) ))]), + null_treatment: None, filter: None, over: None, distinct: false, @@ -7541,6 +7598,7 @@ fn parse_pivot_unpivot_table() { args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("population")) ))]), + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 534a224ea..66eef09e1 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -361,6 +361,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ebadf95f2..4aa993fa0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -334,6 +334,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8391bbadb..2788dfabe 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1112,6 +1112,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("description")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1126,6 +1127,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_create")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1140,6 +1142,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_read")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1154,6 +1157,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_update")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1168,6 +1172,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_delete")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1558,6 +1563,7 @@ fn parse_table_colum_option_on_update() { option: ColumnOption::OnUpdate(Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_TIMESTAMP")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 64fcbd38a..18b5fe6f7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2275,6 +2275,7 @@ fn test_composite_value() { named: true } )))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2437,6 +2438,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_CATALOG")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2449,6 +2451,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_USER")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2461,6 +2464,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("SESSION_USER")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2473,6 +2477,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("USER")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2924,6 +2929,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 6238d1eca..6fa647d38 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -137,6 +137,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7e6f18138..19a62b61d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -249,6 +249,7 @@ fn parse_delimited_identifiers() { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], filter: None, + null_treatment: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index b657acddf..4935f1f50 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -359,6 +359,7 @@ fn parse_window_function_with_filter() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("x")) ))], + null_treatment: None, over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![], order_by: vec![], From 79933846861551a05afc00979516fe8111796492 Mon Sep 17 00:00:00 2001 From: yuval-illumex <85674443+yuval-illumex@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:05:43 +0300 Subject: [PATCH 292/806] Support `date` 'key' when using semi structured data (#1023) --- src/parser/mod.rs | 2 +- tests/sqlparser_snowflake.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4a465ec99..1a586514c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4726,7 +4726,7 @@ impl<'a> Parser<'a> { )?, }, // Case when Snowflake Semi-structured data like key:value - Keyword::NoKeyword | Keyword::LOCATION | Keyword::TYPE if dialect_of!(self is SnowflakeDialect | GenericDialect) => { + Keyword::NoKeyword | Keyword::LOCATION | Keyword::TYPE | Keyword::DATE if dialect_of!(self is SnowflakeDialect | GenericDialect) => { Ok(Value::UnQuotedString(w.value)) } _ => self.expected( diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 19a62b61d..54d6b5542 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -208,6 +208,17 @@ fn parse_json_using_colon() { select.projection[0] ); + let sql = "SELECT a:date FROM t"; + let select = snowflake().verified_only_select(sql); + assert_eq!( + SelectItem::UnnamedExpr(Expr::JsonAccess { + left: Box::new(Expr::Identifier(Ident::new("a"))), + operator: JsonOperator::Colon, + right: Box::new(Expr::Value(Value::UnQuotedString("date".to_string()))), + }), + select.projection[0] + ); + snowflake().one_statement_parses_to("SELECT a:b::int FROM t", "SELECT CAST(a:b AS INT) FROM t"); } From 65317edcb9ac1cf58badf336bcd55d52fd6cecca Mon Sep 17 00:00:00 2001 From: yuval-illumex <85674443+yuval-illumex@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:53:09 +0300 Subject: [PATCH 293/806] Support Snowflake - allow number as placeholder (e.g. `:1`) (#1001) --- src/parser/mod.rs | 9 ++++++++- tests/sqlparser_snowflake.rs | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1a586514c..45ce81ac0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4759,7 +4759,14 @@ impl<'a> Parser<'a> { Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), Token::Placeholder(ref s) => Ok(Value::Placeholder(s.to_string())), tok @ Token::Colon | tok @ Token::AtSign => { - let ident = self.parse_identifier()?; + // Not calling self.parse_identifier()? because only in placeholder we want to check numbers as idfentifies + // This because snowflake allows numbers as placeholders + let next_token = self.next_token(); + let ident = match next_token.token { + Token::Word(w) => Ok(w.to_ident()), + Token::Number(w, false) => Ok(Ident::new(w)), + _ => self.expected("placeholder", next_token), + }?; let placeholder = tok.to_string() + &ident.value; Ok(Value::Placeholder(placeholder)) } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 54d6b5542..a959a4a4e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1079,6 +1079,20 @@ fn test_snowflake_trim() { ); } +#[test] +fn test_number_placeholder() { + let sql_only_select = "SELECT :1"; + let select = snowflake().verified_only_select(sql_only_select); + assert_eq!( + &Expr::Value(Value::Placeholder(":1".into())), + expr_from_projection(only(&select.projection)) + ); + + snowflake() + .parse_sql_statements("alter role 1 with name = 'foo'") + .expect_err("should have failed"); +} + #[test] fn parse_position_not_function_columns() { snowflake_and_generic() From 2f437db2a68724e4ae709df22f53999d24804ac7 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Wed, 25 Oct 2023 18:57:33 +0200 Subject: [PATCH 294/806] Support for BigQuery `struct`, `array` and `bytes` , `int64`, `float64` datatypes (#1003) --- src/ast/data_type.rs | 67 ++++- src/ast/mod.rs | 58 ++++- src/keywords.rs | 5 + src/parser/mod.rs | 236 ++++++++++++++++- tests/sqlparser_bigquery.rs | 489 +++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 101 +++++--- tests/sqlparser_postgres.rs | 8 +- tests/sqlparser_snowflake.rs | 2 +- 8 files changed, 901 insertions(+), 65 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 2a6a004f4..506de815d 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::ast::ObjectName; +use crate::ast::{display_comma_separated, ObjectName, StructField}; use super::value::escape_single_quote_string; @@ -71,6 +71,10 @@ pub enum DataType { /// [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), + /// Variable-length binary data with optional length. + /// + /// [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] /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type @@ -125,6 +129,10 @@ pub enum DataType { /// /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html Int4(Option), + /// Integer type in [bigquery] + /// + /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#integer_types + Int64, /// 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 @@ -149,6 +157,10 @@ pub enum DataType { /// /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html Float4, + /// Floating point in [bigquery] + /// + /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#floating_point_types + Float64, /// Floating point e.g. REAL Real, /// Float8 as alias for Double in [postgresql] @@ -190,18 +202,23 @@ pub enum DataType { Regclass, /// Text Text, - /// String - String, + /// String with optional length. + String(Option), /// Bytea Bytea, /// Custom type such as enums Custom(ObjectName, Vec), /// Arrays - Array(Option>), + Array(ArrayElemTypeDef), /// Enums Enum(Vec), /// Set Set(Vec), + /// Struct + /// + /// [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), } impl fmt::Display for DataType { @@ -231,6 +248,7 @@ impl fmt::Display for DataType { format_type_with_optional_length(f, "VARBINARY", size, false) } DataType::Blob(size) => format_type_with_optional_length(f, "BLOB", size, false), + DataType::Bytes(size) => format_type_with_optional_length(f, "BYTES", size, false), DataType::Numeric(info) => { write!(f, "NUMERIC{info}") } @@ -274,6 +292,9 @@ impl fmt::Display for DataType { DataType::Int4(zerofill) => { format_type_with_optional_length(f, "INT4", zerofill, false) } + DataType::Int64 => { + write!(f, "INT64") + } DataType::UnsignedInt4(zerofill) => { format_type_with_optional_length(f, "INT4", zerofill, true) } @@ -297,6 +318,7 @@ impl fmt::Display for DataType { } DataType::Real => write!(f, "REAL"), DataType::Float4 => write!(f, "FLOAT4"), + DataType::Float64 => write!(f, "FLOAT64"), DataType::Double => write!(f, "DOUBLE"), DataType::Float8 => write!(f, "FLOAT8"), DataType::DoublePrecision => write!(f, "DOUBLE PRECISION"), @@ -316,15 +338,13 @@ impl fmt::Display for DataType { DataType::JSON => write!(f, "JSON"), DataType::Regclass => write!(f, "REGCLASS"), DataType::Text => write!(f, "TEXT"), - DataType::String => write!(f, "STRING"), + DataType::String(size) => format_type_with_optional_length(f, "STRING", size, false), DataType::Bytea => write!(f, "BYTEA"), - DataType::Array(ty) => { - if let Some(t) = &ty { - write!(f, "{t}[]") - } else { - write!(f, "ARRAY") - } - } + DataType::Array(ty) => match ty { + ArrayElemTypeDef::None => write!(f, "ARRAY"), + ArrayElemTypeDef::SquareBracket(t) => write!(f, "{t}[]"), + ArrayElemTypeDef::AngleBracket(t) => write!(f, "ARRAY<{t}>"), + }, DataType::Custom(ty, modifiers) => { if modifiers.is_empty() { write!(f, "{ty}") @@ -352,6 +372,13 @@ impl fmt::Display for DataType { } write!(f, ")") } + DataType::Struct(fields) => { + if !fields.is_empty() { + write!(f, "STRUCT<{}>", display_comma_separated(fields)) + } else { + write!(f, "STRUCT") + } + } } } } @@ -533,3 +560,19 @@ impl fmt::Display for CharLengthUnits { } } } + +/// Represents the data type of the elements in an array (if any) as well as +/// the syntax used to declare the array. +/// +/// For example: Bigquery/Hive use `ARRAY` whereas snowflake uses ARRAY. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ArrayElemTypeDef { + /// `ARRAY` + None, + /// `ARRAY` + AngleBracket(Box), + /// `[]INT` + SquareBracket(Box), +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b52bdf846..ab917dc4c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; pub use self::data_type::{ - CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, + ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, }; pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue}; pub use self::ddl::{ @@ -323,6 +323,27 @@ impl fmt::Display for JsonOperator { } } +/// A field definition within a struct. +/// +/// [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))] +pub struct StructField { + pub field_name: Option, + pub field_type: DataType, +} + +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) + } else { + write!(f, "{}", self.field_type) + } + } +} + /// Options for `CAST` / `TRY_CAST` /// BigQuery: #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -597,6 +618,26 @@ pub enum Expr { Rollup(Vec>), /// ROW / TUPLE a single value, such as `SELECT (1, 2)` Tuple(Vec), + /// `BigQuery` specific `Struct` literal expression [1] + /// Syntax: + /// ```sql + /// STRUCT<[field_name] field_type, ...>( expr1 [, ... ]) + /// ``` + /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type + Struct { + /// Struct values. + values: Vec, + /// Struct field definitions. + fields: Vec, + }, + /// `BigQuery` specific: An named expression in a typeless struct [1] + /// + /// Syntax + /// ```sql + /// 1 AS A + /// ``` + /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type + Named { expr: Box, name: Ident }, /// An array index expression e.g. `(ARRAY[1, 2])[1]` or `(current_schemas(FALSE))[1]` ArrayIndex { obj: Box, indexes: Vec }, /// An array expression e.g. `ARRAY[1, 2]` @@ -997,6 +1038,21 @@ impl fmt::Display for Expr { Expr::Tuple(exprs) => { write!(f, "({})", display_comma_separated(exprs)) } + Expr::Struct { values, fields } => { + if !fields.is_empty() { + write!( + f, + "STRUCT<{}>({})", + display_comma_separated(fields), + display_comma_separated(values) + ) + } else { + write!(f, "STRUCT({})", display_comma_separated(values)) + } + } + Expr::Named { expr, name } => { + write!(f, "{} AS {}", expr, name) + } Expr::ArrayIndex { obj, indexes } => { write!(f, "{obj}")?; for i in indexes { diff --git a/src/keywords.rs b/src/keywords.rs index dec324cfb..2941c8176 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -120,6 +120,7 @@ define_keywords!( BY, BYPASSRLS, BYTEA, + BYTES, CACHE, CALL, CALLED, @@ -270,6 +271,7 @@ define_keywords!( FIRST_VALUE, FLOAT, FLOAT4, + FLOAT64, FLOAT8, FLOOR, FOLLOWING, @@ -293,6 +295,7 @@ define_keywords!( FUSION, GENERATE, GENERATED, + GEOGRAPHY, GET, GLOBAL, GRANT, @@ -328,6 +331,7 @@ define_keywords!( INT, INT2, INT4, + INT64, INT8, INTEGER, INTERSECT, @@ -584,6 +588,7 @@ define_keywords!( STORED, STRICT, STRING, + STRUCT, SUBMULTISET, SUBSTRING, SUBSTRING_REGEX, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 45ce81ac0..eb4ef68a6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -30,7 +30,7 @@ use IsOptional::*; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::*; use crate::dialect::*; -use crate::keywords::{self, Keyword}; +use crate::keywords::{self, Keyword, ALL_KEYWORDS}; use crate::tokenizer::*; mod alter; @@ -197,6 +197,26 @@ impl std::error::Error for ParserError {} // By default, allow expressions up to this deep before erroring const DEFAULT_REMAINING_DEPTH: usize = 50; +/// Composite types declarations using angle brackets syntax can be arbitrary +/// nested such that the following declaration is possible: +/// `ARRAY>` +/// But the tokenizer recognizes the `>>` as a ShiftRight token. +/// We work-around that limitation when parsing a data type by accepting +/// either a `>` or `>>` token in such cases, remembering which variant we +/// matched. +/// In the latter case having matched a `>>`, the parent type will not look to +/// match its closing `>` as a result since that will have taken place at the +/// child type. +/// +/// See [Parser::parse_data_type] for details +struct MatchedTrailingBracket(bool); + +impl From for MatchedTrailingBracket { + fn from(value: bool) -> Self { + Self(value) + } +} + /// Options that control how the [`Parser`] parses SQL text #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParserOptions { @@ -833,6 +853,10 @@ impl<'a> Parser<'a> { Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => { self.parse_match_against() } + Keyword::STRUCT if dialect_of!(self is BigQueryDialect | GenericDialect) => { + self.prev_token(); + self.parse_bigquery_struct_literal() + } // Here `w` is a word, check if it's a part of a multi-part // identifier, a function call, or a simple identifier: _ => match self.peek_token().token { @@ -1798,6 +1822,172 @@ impl<'a> Parser<'a> { })) } + /// Bigquery specific: Parse a struct literal + /// Syntax + /// ```sql + /// -- typed + /// STRUCT<[field_name] field_type, ...>( expr1 [, ... ]) + /// -- typeless + /// STRUCT( expr1 [AS field_name] [, ... ]) + /// ``` + fn parse_bigquery_struct_literal(&mut self) -> Result { + let (fields, trailing_bracket) = + self.parse_struct_type_def(Self::parse_big_query_struct_field_def)?; + if trailing_bracket.0 { + return parser_err!("unmatched > in STRUCT literal", self.peek_token().location); + } + + self.expect_token(&Token::LParen)?; + let values = self + .parse_comma_separated(|parser| parser.parse_struct_field_expr(!fields.is_empty()))?; + self.expect_token(&Token::RParen)?; + + Ok(Expr::Struct { values, fields }) + } + + /// Parse an expression value for a bigquery struct [1] + /// Syntax + /// ```sql + /// expr [AS name] + /// ``` + /// + /// Parameter typed_syntax is set to true if the expression + /// is to be parsed as a field expression declared using typed + /// struct syntax [2], and false if using typeless struct syntax [3]. + /// + /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#constructing_a_struct + /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#typed_struct_syntax + /// [3]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#typeless_struct_syntax + fn parse_struct_field_expr(&mut self, typed_syntax: bool) -> Result { + let expr = self.parse_expr()?; + if self.parse_keyword(Keyword::AS) { + if typed_syntax { + return parser_err!("Typed syntax does not allow AS", { + self.prev_token(); + self.peek_token().location + }); + } + let field_name = self.parse_identifier()?; + Ok(Expr::Named { + expr: expr.into(), + name: field_name, + }) + } else { + Ok(expr) + } + } + + /// Parse a Struct type definition as a sequence of field-value pairs. + /// The syntax of the Struct elem differs by dialect so it is customised + /// by the `elem_parser` argument. + /// + /// Syntax + /// ```sql + /// Hive: + /// STRUCT + /// + /// BigQuery: + /// STRUCT<[field_name] field_type> + /// ``` + fn parse_struct_type_def( + &mut self, + mut elem_parser: F, + ) -> Result<(Vec, MatchedTrailingBracket), ParserError> + where + F: FnMut(&mut Parser<'a>) -> Result<(StructField, MatchedTrailingBracket), ParserError>, + { + let start_token = self.peek_token(); + self.expect_keyword(Keyword::STRUCT)?; + + // Nothing to do if we have no type information. + if Token::Lt != self.peek_token() { + return Ok((Default::default(), false.into())); + } + self.next_token(); + + let mut field_defs = vec![]; + let trailing_bracket = loop { + let (def, trailing_bracket) = elem_parser(self)?; + field_defs.push(def); + if !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.location); + } + }; + + Ok(( + field_defs, + self.expect_closing_angle_bracket(trailing_bracket)?, + )) + } + + /// Parse a field definition in a BigQuery struct. + /// Syntax: + /// + /// ```sql + /// [field_name] field_type + /// ``` + fn parse_big_query_struct_field_def( + &mut self, + ) -> Result<(StructField, MatchedTrailingBracket), ParserError> { + let is_anonymous_field = if let Token::Word(w) = self.peek_token().token { + ALL_KEYWORDS + .binary_search(&w.value.to_uppercase().as_str()) + .is_ok() + } else { + false + }; + + let field_name = if is_anonymous_field { + None + } else { + Some(self.parse_identifier()?) + }; + + let (field_type, trailing_bracket) = self.parse_data_type_helper()?; + + Ok(( + StructField { + field_name, + field_type, + }, + trailing_bracket, + )) + } + + /// For nested types that use the angle bracket syntax, this matches either + /// `>`, `>>` or nothing depending on which variant is expected (specified by the previously + /// matched `trailing_bracket` argument). It returns whether there is a trailing + /// left to be matched - (i.e. if '>>' was matched). + fn expect_closing_angle_bracket( + &mut self, + trailing_bracket: MatchedTrailingBracket, + ) -> Result { + let trailing_bracket = if !trailing_bracket.0 { + match self.peek_token().token { + Token::Gt => { + self.next_token(); + false.into() + } + Token::ShiftRight => { + self.next_token(); + true.into() + } + _ => return self.expected(">", self.peek_token()), + } + } else { + false.into() + }; + + Ok(trailing_bracket) + } + /// Parse an operator following an expression pub fn parse_infix(&mut self, expr: Expr, precedence: u8) -> Result { // allow the dialect to override infix parsing @@ -4876,7 +5066,22 @@ impl<'a> Parser<'a> { /// Parse a SQL datatype (in the context of a CREATE TABLE statement for example) pub fn parse_data_type(&mut self) -> Result { + let (ty, trailing_bracket) = self.parse_data_type_helper()?; + if trailing_bracket.0 { + return parser_err!( + format!("unmatched > after parsing data type {ty}"), + self.peek_token() + ); + } + + Ok(ty) + } + + fn parse_data_type_helper( + &mut self, + ) -> Result<(DataType, MatchedTrailingBracket), ParserError> { let next_token = self.next_token(); + let mut trailing_bracket = false.into(); let mut data = match next_token.token { Token::Word(w) => match w.keyword { Keyword::BOOLEAN => Ok(DataType::Boolean), @@ -4884,6 +5089,7 @@ impl<'a> Parser<'a> { Keyword::FLOAT => Ok(DataType::Float(self.parse_optional_precision()?)), Keyword::REAL => Ok(DataType::Real), Keyword::FLOAT4 => Ok(DataType::Float4), + Keyword::FLOAT64 => Ok(DataType::Float64), Keyword::FLOAT8 => Ok(DataType::Float8), Keyword::DOUBLE => { if self.parse_keyword(Keyword::PRECISION) { @@ -4940,6 +5146,7 @@ impl<'a> Parser<'a> { Ok(DataType::Int4(optional_precision?)) } } + Keyword::INT64 => Ok(DataType::Int64), Keyword::INTEGER => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { @@ -4994,6 +5201,7 @@ impl<'a> Parser<'a> { Keyword::BINARY => Ok(DataType::Binary(self.parse_optional_precision()?)), Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_optional_precision()?)), Keyword::BLOB => Ok(DataType::Blob(self.parse_optional_precision()?)), + Keyword::BYTES => Ok(DataType::Bytes(self.parse_optional_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), Keyword::DATETIME => Ok(DataType::Datetime(self.parse_optional_precision()?)), @@ -5037,7 +5245,7 @@ impl<'a> Parser<'a> { Keyword::INTERVAL => Ok(DataType::Interval), Keyword::JSON => Ok(DataType::JSON), Keyword::REGCLASS => Ok(DataType::Regclass), - Keyword::STRING => Ok(DataType::String), + Keyword::STRING => Ok(DataType::String(self.parse_optional_precision()?)), Keyword::TEXT => Ok(DataType::Text), Keyword::BYTEA => Ok(DataType::Bytea), Keyword::NUMERIC => Ok(DataType::Numeric( @@ -5059,17 +5267,23 @@ impl<'a> Parser<'a> { Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)), Keyword::ARRAY => { if dialect_of!(self is SnowflakeDialect) { - Ok(DataType::Array(None)) + Ok(DataType::Array(ArrayElemTypeDef::None)) } else { - // Hive array syntax. Note that nesting arrays - or other Hive syntax - // that ends with > will fail due to "C++" problem - >> is parsed as - // Token::ShiftRight self.expect_token(&Token::Lt)?; - let inside_type = self.parse_data_type()?; - self.expect_token(&Token::Gt)?; - Ok(DataType::Array(Some(Box::new(inside_type)))) + let (inside_type, _trailing_bracket) = self.parse_data_type_helper()?; + trailing_bracket = self.expect_closing_angle_bracket(_trailing_bracket)?; + Ok(DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( + inside_type, + )))) } } + Keyword::STRUCT if dialect_of!(self is BigQueryDialect) => { + self.prev_token(); + let (field_defs, _trailing_bracket) = + self.parse_struct_type_def(Self::parse_big_query_struct_field_def)?; + trailing_bracket = _trailing_bracket; + Ok(DataType::Struct(field_defs)) + } _ => { self.prev_token(); let type_name = self.parse_object_name()?; @@ -5087,9 +5301,9 @@ impl<'a> Parser<'a> { // Keyword::ARRAY syntax from above while self.consume_token(&Token::LBracket) { self.expect_token(&Token::RBracket)?; - data = DataType::Array(Some(Box::new(data))) + data = DataType::Array(ArrayElemTypeDef::SquareBracket(Box::new(data))) } - Ok(data) + Ok((data, trailing_bracket)) } pub fn parse_string_values(&mut self) -> Result, ParserError> { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index e72a99b49..006927e46 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -13,6 +13,7 @@ #[macro_use] mod test_utils; +use sqlparser::ast; use std::ops::Deref; use sqlparser::ast::*; @@ -85,6 +86,494 @@ fn parse_raw_literal() { panic!("invalid query") } +#[test] +fn parse_nested_data_types() { + let sql = "CREATE TABLE table (x STRUCT, b BYTES(42)>, y ARRAY>)"; + match bigquery().one_statement_parses_to(sql, sql) { + Statement::CreateTable { name, columns, .. } => { + assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!( + columns, + vec![ + ColumnDef { + name: Ident::new("x"), + data_type: DataType::Struct(vec![ + StructField { + field_name: Some("a".into()), + field_type: DataType::Array(ArrayElemTypeDef::AngleBracket( + Box::new(DataType::Int64,) + )) + }, + StructField { + field_name: Some("b".into()), + field_type: DataType::Bytes(Some(42)) + }, + ]), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident::new("y"), + data_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( + DataType::Struct(vec![StructField { + field_name: None, + field_type: DataType::Int64, + }]), + ))), + collation: None, + options: vec![], + }, + ] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_invalid_brackets() { + let sql = "SELECT STRUCT>(NULL)"; + assert_eq!( + bigquery().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("unmatched > in STRUCT literal".to_string()) + ); + + let sql = "SELECT STRUCT>>(NULL)"; + assert_eq!( + bigquery().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected (, found: >".to_string()) + ); + + let sql = "CREATE TABLE table (x STRUCT>>)"; + assert_eq!( + bigquery().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 + // syntax: (expr1, expr2 [, ... ]) + let sql = "SELECT (1, 2, 3), (1, 1.0, '123', true)"; + let select = bigquery().verified_only_select(sql); + assert_eq!(2, select.projection.len()); + assert_eq!( + &Expr::Tuple(vec![ + Expr::Value(number("1")), + Expr::Value(number("2")), + Expr::Value(number("3")), + ]), + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Tuple(vec![ + Expr::Value(number("1")), + Expr::Value(number("1.0")), + Expr::Value(Value::SingleQuotedString("123".to_string())), + Expr::Value(Value::Boolean(true)) + ]), + expr_from_projection(&select.projection[1]) + ); +} + +#[test] +fn parse_typeless_struct_syntax() { + // typeless struct syntax https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#typeless_struct_syntax + // syntax: STRUCT( expr1 [AS field_name] [, ... ]) + let sql = "SELECT STRUCT(1, 2, 3), STRUCT('abc'), STRUCT(1, t.str_col), STRUCT(1 AS a, 'abc' AS b), STRUCT(str_col AS abc)"; + let select = bigquery().verified_only_select(sql); + assert_eq!(5, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![ + Expr::Value(number("1")), + Expr::Value(number("2")), + Expr::Value(number("3")), + ], + fields: Default::default() + }, + expr_from_projection(&select.projection[0]) + ); + + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(Value::SingleQuotedString("abc".to_string())),], + fields: Default::default() + }, + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Struct { + values: vec![ + Expr::Value(number("1")), + Expr::CompoundIdentifier(vec![Ident::from("t"), Ident::from("str_col")]), + ], + fields: Default::default() + }, + expr_from_projection(&select.projection[2]) + ); + assert_eq!( + &Expr::Struct { + values: vec![ + Expr::Named { + expr: Expr::Value(number("1")).into(), + name: Ident::from("a") + }, + Expr::Named { + expr: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(), + name: Ident::from("b") + }, + ], + fields: Default::default() + }, + expr_from_projection(&select.projection[3]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Named { + expr: Expr::Identifier(Ident::from("str_col")).into(), + name: Ident::from("abc") + }], + fields: Default::default() + }, + expr_from_projection(&select.projection[4]) + ); +} + +#[test] +fn parse_typed_struct_syntax() { + // typed struct syntax https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#typed_struct_syntax + // syntax: STRUCT<[field_name] field_type, ...>( expr1 [, ... ]) + + let sql = r#"SELECT STRUCT(5), STRUCT(1, t.str_col), STRUCT, str STRUCT>(nested_col)"#; + let select = bigquery().verified_only_select(sql); + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(number("5")),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Int64, + }] + }, + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Struct { + values: vec![ + Expr::Value(number("1")), + Expr::CompoundIdentifier(vec![ + Ident { + value: "t".into(), + quote_style: None, + }, + Ident { + value: "str_col".into(), + quote_style: None, + }, + ]), + ], + fields: vec![ + StructField { + field_name: Some(Ident { + value: "x".into(), + quote_style: None, + }), + field_type: DataType::Int64 + }, + StructField { + field_name: Some(Ident { + value: "y".into(), + quote_style: None, + }), + field_type: DataType::String(None) + }, + ] + }, + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Identifier(Ident { + value: "nested_col".into(), + quote_style: None, + }),], + fields: vec![ + StructField { + field_name: Some("arr".into()), + field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( + DataType::Float64 + ))) + }, + StructField { + field_name: Some("str".into()), + field_type: DataType::Struct(vec![StructField { + field_name: None, + field_type: DataType::Bool + }]) + }, + ] + }, + expr_from_projection(&select.projection[2]) + ); + + let sql = r#"SELECT STRUCT>(nested_col)"#; + let select = bigquery().verified_only_select(sql); + assert_eq!(1, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Identifier(Ident { + value: "nested_col".into(), + quote_style: None, + }),], + fields: vec![ + StructField { + field_name: Some("x".into()), + field_type: DataType::Struct(Default::default()) + }, + StructField { + field_name: Some("y".into()), + field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( + DataType::Struct(Default::default()) + ))) + }, + ] + }, + expr_from_projection(&select.projection[0]) + ); + + let sql = r#"SELECT STRUCT(true), STRUCT(B'abc')"#; + let select = bigquery().verified_only_select(sql); + assert_eq!(2, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(Value::Boolean(true)),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Bool + }] + }, + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(Value::SingleQuotedByteStringLiteral( + "abc".into() + )),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Bytes(Some(42)) + }] + }, + expr_from_projection(&select.projection[1]) + ); + + let sql = r#"SELECT STRUCT("2011-05-05"), STRUCT(DATETIME '1999-01-01 01:23:34.45'), STRUCT(5.0), STRUCT(1)"#; + let select = bigquery().verified_only_select(sql); + assert_eq!(4, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(Value::DoubleQuotedString( + "2011-05-05".to_string() + )),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Date + }] + }, + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::TypedString { + data_type: DataType::Datetime(None), + value: "1999-01-01 01:23:34.45".to_string() + },], + fields: vec![StructField { + field_name: None, + field_type: DataType::Datetime(None) + }] + }, + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(number("5.0")),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Float64 + }] + }, + expr_from_projection(&select.projection[2]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(number("1")),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Int64 + }] + }, + expr_from_projection(&select.projection[3]) + ); + + let sql = r#"SELECT STRUCT(INTERVAL '1-2 3 4:5:6.789999'), STRUCT(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#; + let select = bigquery().verified_only_select(sql); + assert_eq!(2, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Interval(ast::Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString( + "1-2 3 4:5:6.789999".to_string() + ))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None + }),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Interval + }] + }, + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::TypedString { + data_type: DataType::JSON, + value: r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.to_string() + },], + fields: vec![StructField { + field_name: None, + field_type: DataType::JSON + }] + }, + expr_from_projection(&select.projection[1]) + ); + + let sql = r#"SELECT STRUCT("foo"), STRUCT(TIMESTAMP '2008-12-25 15:30:00 America/Los_Angeles'), STRUCT= CompoundIdentifier /// = String literal /// ``` - /// - /// /// [(1)]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match MatchAgainst { /// `(, , ...)`. @@ -1375,6 +1376,9 @@ pub enum Password { visit(with = "visit_statement") )] pub enum Statement { + /// ```sql + /// ANALYZE + /// ``` /// Analyze (Hive) Analyze { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] @@ -1386,6 +1390,9 @@ pub enum Statement { noscan: bool, compute_statistics: bool, }, + /// ```sql + /// TRUNCATE + /// ``` /// Truncate (Hive) Truncate { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] @@ -1394,6 +1401,9 @@ pub enum Statement { /// TABLE - optional keyword; table: bool, }, + /// ```sql + /// MSCK + /// ``` /// Msck (Hive) Msck { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] @@ -1401,9 +1411,13 @@ pub enum Statement { repair: bool, partition_action: Option, }, + /// ```sql /// SELECT + /// ``` Query(Box), + /// ```sql /// INSERT + /// ``` Insert { /// Only for Sqlite or: Option, @@ -1438,7 +1452,13 @@ pub enum Statement { file_format: Option, source: Box, }, + /// ```sql + /// CALL + /// ``` Call(Function), + /// ```sql + /// COPY [TO | FROM] ... + /// ``` Copy { /// The source of 'COPY TO', or the target of 'COPY FROM' source: CopySource, @@ -1473,12 +1493,17 @@ pub enum Statement { copy_options: DataLoadingOptions, validation_mode: Option, }, - /// Close - closes the portal underlying an open cursor. + /// ```sql + /// CLOSE + /// ``` + /// Closes the portal underlying an open cursor. Close { /// Cursor name cursor: CloseCursor, }, + /// ```sql /// UPDATE + /// ``` Update { /// TABLE table: TableWithJoins, @@ -1491,7 +1516,9 @@ pub enum Statement { /// RETURNING returning: Option>, }, + /// ```sql /// DELETE + /// ``` Delete { /// Multi tables delete are supported in mysql tables: Vec, @@ -1508,7 +1535,9 @@ pub enum Statement { /// LIMIT (MySQL) limit: Option, }, + /// ```sql /// CREATE VIEW + /// ``` CreateView { or_replace: bool, materialized: bool, @@ -1525,7 +1554,9 @@ pub enum Statement { /// if true, has SQLite `TEMP` or `TEMPORARY` clause temporary: bool, }, + /// ```sql /// CREATE TABLE + /// ``` CreateTable { or_replace: bool, temporary: bool, @@ -1567,7 +1598,10 @@ pub enum Statement { /// then strict typing rules apply to that table. strict: bool, }, - /// SQLite's `CREATE VIRTUAL TABLE .. USING ()` + /// ```sql + /// CREATE VIRTUAL TABLE .. USING ()` + /// ``` + /// Sqlite specific statement CreateVirtualTable { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] name: ObjectName, @@ -1575,7 +1609,9 @@ pub enum Statement { module_name: Ident, module_args: Vec, }, - /// CREATE INDEX + /// ```sql + /// `CREATE INDEX` + /// ``` CreateIndex { /// index name name: Option, @@ -1590,7 +1626,9 @@ pub enum Statement { nulls_distinct: Option, predicate: Option, }, + /// ```sql /// CREATE ROLE + /// ``` /// See [postgres](https://www.postgresql.org/docs/current/sql-createrole.html) CreateRole { names: Vec, @@ -1614,7 +1652,9 @@ pub enum Statement { // MSSQL authorization_owner: Option, }, + /// ```sql /// ALTER TABLE + /// ``` AlterTable { /// Table name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] @@ -1623,11 +1663,16 @@ pub enum Statement { only: bool, operations: Vec, }, + /// ```sql + /// ALTER INDEX + /// ``` AlterIndex { name: ObjectName, operation: AlterIndexOperation, }, + /// ```sql /// ALTER VIEW + /// ``` AlterView { /// View name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] @@ -1636,12 +1681,16 @@ pub enum Statement { query: Box, with_options: Vec, }, + /// ```sql /// ALTER ROLE + /// ``` AlterRole { name: Ident, operation: AlterRoleOperation, }, + /// ```sql /// ATTACH DATABASE 'path/to/file' AS alias + /// ``` /// (SQLite-specific) AttachDatabase { /// The name to bind to the newly attached database @@ -1651,7 +1700,9 @@ pub enum Statement { /// true if the syntax is 'ATTACH DATABASE', false if it's just 'ATTACH' database: bool, }, - /// DROP + /// ```sql + /// DROP [TABLE, VIEW, ...] + /// ``` Drop { /// The type of the object to drop: TABLE, VIEW, etc. object_type: ObjectType, @@ -1671,7 +1722,9 @@ pub enum Statement { /// MySQL-specific "TEMPORARY" keyword temporary: bool, }, - /// DROP Function + /// ```sql + /// DROP FUNCTION + /// ``` DropFunction { if_exists: bool, /// One or more function to drop @@ -1679,7 +1732,10 @@ pub enum Statement { /// `CASCADE` or `RESTRICT` option: Option, }, - /// DECLARE - Declaring Cursor Variables + /// ```sql + /// DECLARE + /// ``` + /// Declare Cursor Variables /// /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. @@ -1702,7 +1758,10 @@ pub enum Statement { hold: Option, query: Box, }, - /// FETCH - retrieve rows from a query using a cursor + /// ```sql + /// FETCH + /// ``` + /// Retrieve rows from a query using a cursor /// /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. @@ -1713,14 +1772,18 @@ pub enum Statement { /// Optional, It's possible to fetch rows form cursor to the table into: Option, }, + /// ```sql /// DISCARD [ ALL | PLANS | SEQUENCES | TEMPORARY | TEMP ] + /// ``` /// /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. - Discard { - object_type: DiscardObject, - }, - /// SET `[ SESSION | LOCAL ]` ROLE role_name. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4]. + Discard { object_type: DiscardObject }, + /// ```sql + /// SET [ SESSION | LOCAL ] ROLE role_name + /// ``` + /// + /// Sets sesssion 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 @@ -1751,36 +1814,35 @@ pub enum Statement { /// /// Note: this is a PostgreSQL-specific statements /// `SET TIME ZONE ` is an alias for `SET timezone TO ` in PostgreSQL - SetTimeZone { - local: bool, - value: Expr, - }, + SetTimeZone { local: bool, value: Expr }, + /// ```sql /// SET NAMES 'charset_name' [COLLATE 'collation_name'] + /// ``` /// /// Note: this is a MySQL-specific statement. SetNames { charset_name: String, collation_name: Option, }, + /// ```sql /// SET NAMES DEFAULT + /// ``` /// /// Note: this is a MySQL-specific statement. SetNamesDefault {}, - /// SHOW FUNCTIONS + /// `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 VARIABLES + /// ``` /// /// Note: this is a MySQL-specific statement. ShowVariables { @@ -1788,14 +1850,18 @@ pub enum Statement { global: bool, session: bool, }, + /// ```sql /// SHOW CREATE TABLE + /// ``` /// /// Note: this is a MySQL-specific statement. ShowCreate { obj_type: ShowCreateObject, obj_name: ObjectName, }, + /// ```sql /// SHOW COLUMNS + /// ``` /// /// Note: this is a MySQL-specific statement. ShowColumns { @@ -1805,8 +1871,9 @@ pub enum Statement { table_name: ObjectName, filter: Option, }, + /// ```sql /// SHOW TABLES - /// + /// ``` /// Note: this is a MySQL-specific statement. ShowTables { extended: bool, @@ -1814,34 +1881,42 @@ pub enum Statement { db_name: Option, filter: Option, }, + /// ```sql /// SHOW COLLATION + /// ``` /// /// Note: this is a MySQL-specific statement. - ShowCollation { - filter: Option, - }, + ShowCollation { filter: Option }, + /// ```sql /// USE + /// ``` /// /// Note: This is a MySQL-specific statement. - Use { - db_name: Ident, - }, - /// `START [ TRANSACTION | WORK ] | START TRANSACTION } ...` + Use { db_name: Ident }, + /// ```sql + /// START [ TRANSACTION | WORK ] | START TRANSACTION } ... + /// ``` /// If `begin` is false. /// + /// ```sql /// `BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...` + /// ``` /// If `begin` is true StartTransaction { modes: Vec, begin: bool, }, - /// `SET TRANSACTION ...` + /// ```sql + /// SET TRANSACTION ... + /// ``` SetTransaction { modes: Vec, snapshot: Option, session: bool, }, - /// `COMMENT ON ...` + /// ```sql + /// COMMENT ON ... + /// ``` /// /// Note: this is a PostgreSQL-specific statement. Comment { @@ -1852,22 +1927,28 @@ pub enum Statement { /// See if_exists: bool, }, - /// `COMMIT [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ]` - Commit { - chain: bool, - }, - /// `ROLLBACK [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] [ TO [ SAVEPOINT ] savepoint_name ]` + /// ```sql + /// COMMIT [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] + /// ``` + Commit { chain: bool }, + /// ```sql + /// ROLLBACK [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] [ TO [ SAVEPOINT ] savepoint_name ] + /// ``` Rollback { chain: bool, savepoint: Option, }, + /// ```sql /// CREATE SCHEMA + /// ``` CreateSchema { /// ` | AUTHORIZATION | AUTHORIZATION ` schema_name: SchemaName, if_not_exists: bool, }, + /// ```sql /// CREATE DATABASE + /// ``` CreateDatabase { db_name: ObjectName, if_not_exists: bool, @@ -1927,12 +2008,16 @@ pub enum Statement { copy_options: DataLoadingOptions, comment: Option, }, - /// `ASSERT [AS ]` + /// ```sql + /// ASSERT [AS ] + /// ``` Assert { condition: Expr, message: Option, }, + /// ```sql /// GRANT privileges ON objects TO grantees + /// ``` Grant { privileges: Privileges, objects: GrantObjects, @@ -1940,7 +2025,9 @@ pub enum Statement { with_grant_option: bool, granted_by: Option, }, + /// ```sql /// REVOKE privileges ON objects FROM grantees + /// ``` Revoke { privileges: Privileges, objects: GrantObjects, @@ -1948,21 +2035,21 @@ pub enum Statement { granted_by: Option, cascade: bool, }, - /// `DEALLOCATE [ PREPARE ] { name | ALL }` + /// ```sql + /// DEALLOCATE [ PREPARE ] { name | ALL } + /// ``` /// /// Note: this is a PostgreSQL-specific statement. - Deallocate { - name: Ident, - prepare: bool, - }, - /// `EXECUTE name [ ( parameter [, ...] ) ]` + Deallocate { name: Ident, prepare: bool }, + /// ```sql + /// EXECUTE name [ ( parameter [, ...] ) ] + /// ``` /// /// Note: this is a PostgreSQL-specific statement. - Execute { - name: Ident, - parameters: Vec, - }, - /// `PREPARE name [ ( data_type [, ...] ) ] AS statement` + Execute { name: Ident, parameters: Vec }, + /// ```sql + /// PREPARE name [ ( data_type [, ...] ) ] AS statement + /// ``` /// /// Note: this is a PostgreSQL-specific statement. Prepare { @@ -1970,7 +2057,9 @@ pub enum Statement { data_types: Vec, statement: Box, }, + /// ```sql /// KILL [CONNECTION | QUERY | MUTATION] + /// ``` /// /// See /// See @@ -1979,7 +2068,9 @@ pub enum Statement { // processlist_id id: u64, }, + /// ```sql /// EXPLAIN TABLE + /// ``` /// Note: this is a MySQL-specific statement. See ExplainTable { /// If true, query used the MySQL `DESCRIBE` alias for explain @@ -1988,7 +2079,9 @@ pub enum Statement { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, }, - /// EXPLAIN / DESCRIBE for select_statement + /// ```sql + /// [EXPLAIN | DESCRIBE + /// [EXPLAIN | DESC | DESCRIBE] /// ``` Explain { - // If true, query used the MySQL `DESCRIBE` alias for explain - describe_alias: bool, + /// `EXPLAIN | DESC | DESCRIBE` + describe_alias: DescribeAlias, /// Carry out the command and show actual run times and other statistics. analyze: bool, // Display additional information regarding the plan. @@ -2611,12 +2613,13 @@ impl fmt::Display for Statement { } Statement::ExplainTable { describe_alias, + hive_format, table_name, } => { - if *describe_alias { - write!(f, "DESCRIBE ")?; - } else { - write!(f, "EXPLAIN ")?; + write!(f, "{describe_alias} ")?; + + if let Some(format) = hive_format { + write!(f, "{} ", format)?; } write!(f, "{table_name}") @@ -2628,11 +2631,7 @@ impl fmt::Display for Statement { statement, format, } => { - if *describe_alias { - write!(f, "DESCRIBE ")?; - } else { - write!(f, "EXPLAIN ")?; - } + write!(f, "{describe_alias} ")?; if *analyze { write!(f, "ANALYZE ")?; @@ -4925,6 +4924,44 @@ impl fmt::Display for HiveDelimiter { } } +#[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 HiveDescribeFormat { + Extended, + Formatted, +} + +impl fmt::Display for HiveDescribeFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use HiveDescribeFormat::*; + f.write_str(match self { + Extended => "EXTENDED", + Formatted => "FORMATTED", + }) + } +} + +#[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 DescribeAlias { + Describe, + Explain, + Desc, +} + +impl fmt::Display for DescribeAlias { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use DescribeAlias::*; + f.write_str(match self { + Describe => "DESCRIBE", + Explain => "EXPLAIN", + Desc => "DESC", + }) + } +} + #[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/keywords.rs b/src/keywords.rs index dee5eb5cd..c94a6227c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -303,6 +303,7 @@ define_keywords!( FORCE_QUOTE, FOREIGN, FORMAT, + FORMATTED, FORWARD, FRAME_ROW, FREE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 271ff41e3..6d7ac3604 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -464,8 +464,9 @@ impl<'a> Parser<'a> { Token::Word(w) => match w.keyword { Keyword::KILL => Ok(self.parse_kill()?), Keyword::FLUSH => Ok(self.parse_flush()?), - Keyword::DESCRIBE => Ok(self.parse_explain(true)?), - Keyword::EXPLAIN => Ok(self.parse_explain(false)?), + Keyword::DESC => Ok(self.parse_explain(DescribeAlias::Desc)?), + Keyword::DESCRIBE => Ok(self.parse_explain(DescribeAlias::Describe)?), + Keyword::EXPLAIN => Ok(self.parse_explain(DescribeAlias::Explain)?), Keyword::ANALYZE => Ok(self.parse_analyze()?), Keyword::SELECT | Keyword::WITH | Keyword::VALUES => { self.prev_token(); @@ -6805,7 +6806,10 @@ impl<'a> Parser<'a> { Ok(Statement::Kill { modifier, id }) } - pub fn parse_explain(&mut self, describe_alias: bool) -> Result { + pub fn parse_explain( + &mut self, + describe_alias: DescribeAlias, + ) -> Result { let analyze = self.parse_keyword(Keyword::ANALYZE); let verbose = self.parse_keyword(Keyword::VERBOSE); let mut format = None; @@ -6825,9 +6829,17 @@ impl<'a> Parser<'a> { format, }), _ => { + let mut hive_format = None; + match self.parse_one_of_keywords(&[Keyword::EXTENDED, Keyword::FORMATTED]) { + Some(Keyword::EXTENDED) => hive_format = Some(HiveDescribeFormat::Extended), + Some(Keyword::FORMATTED) => hive_format = Some(HiveDescribeFormat::Formatted), + _ => {} + } + let table_name = self.parse_object_name(false)?; Ok(Statement::ExplainTable { describe_alias, + hive_format, table_name, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 033d613dc..f81456849 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3932,19 +3932,32 @@ fn run_explain_analyze( #[test] fn parse_explain_table() { - let validate_explain = |query: &str, expected_describe_alias: bool| match verified_stmt(query) { - Statement::ExplainTable { - describe_alias, - table_name, - } => { - assert_eq!(describe_alias, expected_describe_alias); - assert_eq!("test_identifier", table_name.to_string()); - } - _ => panic!("Unexpected Statement, must be ExplainTable"), - }; + let validate_explain = + |query: &str, expected_describe_alias: DescribeAlias| match verified_stmt(query) { + Statement::ExplainTable { + describe_alias, + hive_format, + table_name, + } => { + assert_eq!(describe_alias, expected_describe_alias); + assert_eq!(hive_format, None); + assert_eq!("test_identifier", table_name.to_string()); + } + _ => panic!("Unexpected Statement, must be ExplainTable"), + }; + + validate_explain("EXPLAIN test_identifier", DescribeAlias::Explain); + validate_explain("DESCRIBE test_identifier", DescribeAlias::Describe); +} - validate_explain("EXPLAIN test_identifier", false); - validate_explain("DESCRIBE test_identifier", true); +#[test] +fn explain_describe() { + verified_stmt("DESCRIBE test.table"); +} + +#[test] +fn explain_desc() { + verified_stmt("DESC test.table"); } #[test] diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index f54ee10c3..788c937a6 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -48,6 +48,16 @@ fn parse_describe() { generic(None).verified_stmt(describe); } +#[test] +fn explain_describe_formatted() { + hive().verified_stmt("DESCRIBE FORMATTED test.table"); +} + +#[test] +fn explain_describe_extended() { + hive().verified_stmt("DESCRIBE EXTENDED test.table"); +} + #[test] fn parse_insert_overwrite() { let insert_partitions = r#"INSERT OVERWRITE TABLE db.new_table PARTITION (a = '1', b) SELECT a, b, c FROM db.table"#; From 1af1a6e87aaefca448c4bf0426f3116a0c8c24be Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 3 Mar 2024 02:29:21 -0500 Subject: [PATCH 384/806] Version 0.44.0 CHANGELOG (#1161) --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9628bca62..e0c9b22db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,42 @@ changes that break via addition as "Added". ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.44.0] 2024-03-02 + +### Added +* Support EXPLAIN / DESCR / DESCRIBE [FORMATTED | EXTENDED] (#1156) - Thanks @jonathanlehtoalamb +* Support ALTER TABLE ... SET LOCATION (#1154) - Thanks @jonathanlehto +* Support `ROW FORMAT DELIMITED` in Hive (#1155) - Thanks @jonathanlehto +* Support `SERDEPROPERTIES` for `CREATE TABLE` with Hive (#1152) - Thanks @jonathanlehto +* Support `EXECUTE ... USING` for Postgres (#1153) - Thanks @jonathanlehto +* Support Postgres style `CREATE FUNCTION` in GenericDialect (#1159) - Thanks @alamb +* Support `SET TBLPROPERTIES` (#1151) - Thanks @jonathanlehto +* Support `UNLOAD` statement (#1150) - Thanks @jonathanlehto +* Support `MATERIALIZED CTEs` (#1148) - Thanks @ReppCodes +* Support `DECLARE` syntax for snowflake and bigquery (#1122) - Thanks @iffyio +* Support `SELECT AS VALUE` and `SELECT AS STRUCT` for BigQuery (#1135) - Thanks @lustefaniak +* Support `(+)` outer join syntax (#1145) - Thanks @jmhain +* Support `INSERT INTO ... SELECT ... RETURNING`(#1132) - Thanks @lovasoa +* Support DuckDB `INSTALL` and `LOAD` (#1127) - Thanks @universalmind303 +* Support `=` operator in function args (#1128) - Thanks @universalmind303 +* Support `CREATE VIEW IF NOT EXISTS` (#1118) - Thanks @7phs +* Support `UPDATE FROM` for SQLite (further to #694) (#1117) - Thanks @ggaughan +* Support optional `DELETE FROM` statement (#1120) - Thanks @iffyio +* Support MySQL `SHOW STATUS` statement (#1119) - Thanks invm + +### Fixed +* Clean up nightly clippy lints (#1158) - Thanks @alamb +* Handle escape, unicode, and hex in tokenize_escaped_single_quoted_string (#1146) - Thanks @JasonLi-cn +* Fix panic while parsing `REPLACE` (#1140) - THanks @jjbayer +* Fix clippy warning from rust 1.76 (#1130) - Thanks @alamb +* Fix release instructions (#1115) - Thanks @alamb + +### Changed +* Add `parse_keyword_with_tokens` for paring keyword and tokens combination (#1141) - Thanks @viirya +* Add ParadeDB to list of known users (#1142) - Thanks @philippemnoel +* Accept JSON_TABLE both as an unquoted table name and a table-valued function (#1134) - Thanks @lovasoa + + ## [0.43.1] 2024-01-22 ### Changes * Fixed CHANGELOG From 5da66adda99d8cf7a36c1512d41815d94ce542b5 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 3 Mar 2024 02:30:34 -0500 Subject: [PATCH 385/806] chore: Release sqlparser version 0.44.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5cbe610fc..ed3d88b9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.43.1" +version = "0.44.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 11899fd0cb2b8347496daede2b33264bbef50929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Emin=20KARAKA=C5=9E?= Date: Fri, 8 Mar 2024 23:03:49 +0300 Subject: [PATCH 386/806] Support `row_alias` and `col_aliases` in `INSERT` statement for mysql and generic dialects (#1136) --- src/ast/mod.rs | 21 +++++++ src/parser/mod.rs | 14 +++++ tests/sqlparser_mysql.rs | 108 +++++++++++++++++++++++++++++++++++- tests/sqlparser_postgres.rs | 9 ++- 4 files changed, 148 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d8688c1ab..f2cfc974c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1759,6 +1759,8 @@ pub enum Statement { replace_into: bool, /// Only for mysql priority: Option, + /// Only for mysql + insert_alias: Option, }, /// ```sql /// INSTALL @@ -2773,6 +2775,7 @@ impl fmt::Display for Statement { returning, replace_into, priority, + insert_alias, } => { let table_name = if let Some(alias) = table_alias { format!("{table_name} AS {alias}") @@ -2822,6 +2825,16 @@ impl fmt::Display for Statement { write!(f, "DEFAULT VALUES")?; } + if let Some(insert_alias) = insert_alias { + write!(f, " AS {0}", insert_alias.row_alias)?; + + if let Some(col_aliases) = &insert_alias.col_aliases { + if !col_aliases.is_empty() { + write!(f, " ({})", display_comma_separated(col_aliases))?; + } + } + } + if let Some(on) = on { write!(f, "{on}")?; } @@ -4194,6 +4207,14 @@ pub enum OnInsert { OnConflict(OnConflict), } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct InsertAliases { + pub row_alias: ObjectName, + pub col_aliases: Option>, +} + #[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 6d7ac3604..145e19007 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8419,6 +8419,19 @@ impl<'a> Parser<'a> { (columns, partitioned, after_columns, source) }; + let insert_alias = if dialect_of!(self is MySqlDialect | GenericDialect) + && self.parse_keyword(Keyword::AS) + { + let row_alias = self.parse_object_name(false)?; + let col_aliases = Some(self.parse_parenthesized_column_list(Optional, false)?); + Some(InsertAliases { + row_alias, + col_aliases, + }) + } else { + None + }; + let on = if self.parse_keyword(Keyword::ON) { if self.parse_keyword(Keyword::CONFLICT) { let conflict_target = @@ -8488,6 +8501,7 @@ impl<'a> Parser<'a> { returning, replace_into, priority, + insert_alias, }) } } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 06982be49..af2a2184a 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -19,7 +19,7 @@ use matches::assert_matches; use sqlparser::ast::MysqlInsertPriority::{Delayed, HighPriority, LowPriority}; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; -use sqlparser::parser::ParserOptions; +use sqlparser::parser::{ParserError, ParserOptions}; use sqlparser::tokenizer::Token; use test_utils::*; @@ -1330,6 +1330,112 @@ fn parse_priority_insert() { } } +#[test] +fn parse_insert_as() { + let sql = r"INSERT INTO `table` (`date`) VALUES ('2024-01-01') AS `alias`"; + match mysql_and_generic().verified_stmt(sql) { + Statement::Insert { + table_name, + columns, + source, + insert_alias, + .. + } => { + assert_eq!( + ObjectName(vec![Ident::with_quote('`', "table")]), + table_name + ); + assert_eq!(vec![Ident::with_quote('`', "date")], columns); + let insert_alias = insert_alias.unwrap(); + + assert_eq!( + ObjectName(vec![Ident::with_quote('`', "alias")]), + insert_alias.row_alias + ); + assert_eq!(Some(vec![]), insert_alias.col_aliases); + assert_eq!( + Some(Box::new(Query { + with: None, + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![vec![Expr::Value(Value::SingleQuotedString( + "2024-01-01".to_string() + ))]] + })), + order_by: vec![], + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + })), + source + ); + } + _ => unreachable!(), + } + + let sql = r"INSERT INTO `table` (`date`) VALUES ('2024-01-01') AS `alias` ()"; + assert!(matches!( + mysql_and_generic().parse_sql_statements(sql), + Err(ParserError::ParserError(_)) + )); + + let sql = r"INSERT INTO `table` (`id`, `date`) VALUES (1, '2024-01-01') AS `alias` (`mek_id`, `mek_date`)"; + match mysql_and_generic().verified_stmt(sql) { + Statement::Insert { + table_name, + columns, + source, + insert_alias, + .. + } => { + assert_eq!( + ObjectName(vec![Ident::with_quote('`', "table")]), + table_name + ); + assert_eq!( + vec![Ident::with_quote('`', "id"), Ident::with_quote('`', "date")], + columns + ); + let insert_alias = insert_alias.unwrap(); + assert_eq!( + ObjectName(vec![Ident::with_quote('`', "alias")]), + insert_alias.row_alias + ); + assert_eq!( + Some(vec![ + Ident::with_quote('`', "mek_id"), + Ident::with_quote('`', "mek_date") + ]), + insert_alias.col_aliases + ); + assert_eq!( + Some(Box::new(Query { + with: None, + body: Box::new(SetExpr::Values(Values { + explicit_row: false, + rows: vec![vec![ + Expr::Value(number("1")), + Expr::Value(Value::SingleQuotedString("2024-01-01".to_string())) + ]] + })), + order_by: vec![], + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + })), + source + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_replace_insert() { let sql = r"REPLACE DELAYED INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 45ec277e9..9de4b981f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3764,7 +3764,8 @@ fn test_simple_postgres_insert_with_alias() { on: None, returning: None, replace_into: false, - priority: None + priority: None, + insert_alias: None } ) } @@ -3830,7 +3831,8 @@ fn test_simple_postgres_insert_with_alias() { on: None, returning: None, replace_into: false, - priority: None + priority: None, + insert_alias: None } ) } @@ -3892,7 +3894,8 @@ fn test_simple_insert_with_quoted_alias() { on: None, returning: None, replace_into: false, - priority: None + priority: None, + insert_alias: None, } ) } From 929c646bba9cdef6bf774b53bae0c19e0798ef2d Mon Sep 17 00:00:00 2001 From: Michiel De Backker Date: Mon, 11 Mar 2024 21:27:25 +0100 Subject: [PATCH 387/806] Add identifier quote style to `Dialect` trait (#1170) --- src/dialect/mod.rs | 23 +++++++++++++++++++++++ src/dialect/mysql.rs | 4 ++++ src/dialect/postgresql.rs | 4 ++++ src/dialect/sqlite.rs | 4 ++++ 4 files changed, 35 insertions(+) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c53670263..682e5924c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -108,6 +108,10 @@ pub trait Dialect: Debug + Any { fn is_delimited_identifier_start(&self, ch: char) -> bool { ch == '"' || ch == '`' } + /// Return the character used to quote identifiers. + fn identifier_quote_style(&self, _identifier: &str) -> Option { + None + } /// Determine if quoted characters are proper for identifier fn is_proper_identifier_inside_quotes(&self, mut _chars: Peekable>) -> bool { true @@ -262,6 +266,21 @@ mod tests { dialect_from_str(v).unwrap() } + #[test] + fn identifier_quote_style() { + let tests: Vec<(&dyn Dialect, &str, Option)> = vec![ + (&GenericDialect {}, "id", None), + (&SQLiteDialect {}, "id", Some('`')), + (&PostgreSqlDialect {}, "id", Some('"')), + ]; + + for (dialect, ident, expected) in tests { + let actual = dialect.identifier_quote_style(ident); + + assert_eq!(actual, expected); + } + } + #[test] fn parse_with_wrapped_dialect() { /// Wrapper for a dialect. In a real-world example, this wrapper @@ -283,6 +302,10 @@ mod tests { self.0.is_delimited_identifier_start(ch) } + fn identifier_quote_style(&self, identifier: &str) -> Option { + self.0.identifier_quote_style(identifier) + } + fn is_proper_identifier_inside_quotes( &self, chars: std::iter::Peekable>, diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 95b358a7e..d0dbe923c 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -44,6 +44,10 @@ impl Dialect for MySqlDialect { ch == '`' } + fn identifier_quote_style(&self, _identifier: &str) -> Option { + Some('`') + } + fn parse_infix( &self, parser: &mut crate::parser::Parser, diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index cbd150511..f179111e0 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -21,6 +21,10 @@ use crate::tokenizer::Token; pub struct PostgreSqlDialect {} impl Dialect for PostgreSqlDialect { + fn identifier_quote_style(&self, _identifier: &str) -> Option { + Some('"') + } + fn is_identifier_start(&self, ch: char) -> bool { // See https://www.postgresql.org/docs/11/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS // We don't yet support identifiers beginning with "letters with diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 622fddee6..daad6a159 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -32,6 +32,10 @@ impl Dialect for SQLiteDialect { ch == '`' || ch == '"' || ch == '[' } + fn identifier_quote_style(&self, _identifier: &str) -> Option { + Some('`') + } + fn is_identifier_start(&self, ch: char) -> bool { // See https://www.sqlite.org/draft/tokenreq.html ch.is_ascii_lowercase() From 6b03a259aac09d7c44fbfd7f2588f31392b730d8 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Wed, 13 Mar 2024 16:08:27 +0100 Subject: [PATCH 388/806] Parse `SUBSTRING` `FROM` syntax in all dialects, reflect change in the AST (#1173) --- src/ast/mod.rs | 9 ++++-- src/dialect/mod.rs | 8 ------ src/dialect/mssql.rs | 4 --- src/parser/mod.rs | 58 +++++++++++++-------------------------- tests/sqlparser_common.rs | 45 ++++-------------------------- tests/sqlparser_mysql.rs | 4 +-- tests/sqlparser_sqlite.rs | 12 ++++++++ 7 files changed, 46 insertions(+), 94 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f2cfc974c..3e8354e15 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -559,13 +559,18 @@ pub enum Expr { /// ```sql /// SUBSTRING( [FROM ] [FOR ]) /// ``` + /// or + /// ```sql + /// SUBSTRING(, , ) + /// ``` Substring { expr: Box, substring_from: Option>, substring_for: Option>, - // Some dialects use `SUBSTRING(expr [FROM start] [FOR len])` syntax while others omit FROM, - // FOR keywords (e.g. Microsoft SQL Server). This flags is used for formatting. + /// false if the expression is represented using the `SUBSTRING(expr [FROM start] [FOR len])` syntax + /// true if the expression is represented using the `SUBSTRING(expr, start, len)` syntax + /// This flag is used for formatting. special: bool, }, /// ```sql diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 682e5924c..2873cca2c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -135,10 +135,6 @@ pub trait Dialect: Debug + Any { fn supports_group_by_expr(&self) -> bool { false } - /// Returns true if the dialect supports `SUBSTRING(expr [FROM start] [FOR len])` expressions - fn supports_substring_from_for_expr(&self) -> bool { - true - } /// Returns true if the dialect supports `(NOT) IN ()` expressions fn supports_in_empty_list(&self) -> bool { false @@ -325,10 +321,6 @@ mod tests { self.0.supports_group_by_expr() } - fn supports_substring_from_for_expr(&self) -> bool { - self.0.supports_substring_from_for_expr() - } - fn supports_in_empty_list(&self) -> bool { self.0.supports_in_empty_list() } diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index c7bf11864..6362a52b8 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -40,8 +40,4 @@ impl Dialect for MsSqlDialect { fn convert_type_before_value(&self) -> bool { true } - - fn supports_substring_from_for_expr(&self) -> bool { - false - } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 145e19007..a7190563f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1525,47 +1525,27 @@ impl<'a> Parser<'a> { } pub fn parse_substring_expr(&mut self) -> Result { - if self.dialect.supports_substring_from_for_expr() { - // PARSE SUBSTRING (EXPR [FROM 1] [FOR 3]) - self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; - let mut from_expr = None; - if self.parse_keyword(Keyword::FROM) || self.consume_token(&Token::Comma) { - from_expr = Some(self.parse_expr()?); - } - - let mut to_expr = None; - if self.parse_keyword(Keyword::FOR) || self.consume_token(&Token::Comma) { - to_expr = Some(self.parse_expr()?); - } - self.expect_token(&Token::RParen)?; - - Ok(Expr::Substring { - expr: Box::new(expr), - substring_from: from_expr.map(Box::new), - substring_for: to_expr.map(Box::new), - special: false, - }) - } else { - // PARSE SUBSTRING(EXPR, start, length) - self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; - - self.expect_token(&Token::Comma)?; - let from_expr = Some(self.parse_expr()?); - - self.expect_token(&Token::Comma)?; - let to_expr = Some(self.parse_expr()?); - - self.expect_token(&Token::RParen)?; + // PARSE SUBSTRING (EXPR [FROM 1] [FOR 3]) + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + let mut from_expr = None; + let special = self.consume_token(&Token::Comma); + if special || self.parse_keyword(Keyword::FROM) { + from_expr = Some(self.parse_expr()?); + } - Ok(Expr::Substring { - expr: Box::new(expr), - substring_from: from_expr.map(Box::new), - substring_for: to_expr.map(Box::new), - special: true, - }) + let mut to_expr = None; + if self.parse_keyword(Keyword::FOR) || self.consume_token(&Token::Comma) { + to_expr = Some(self.parse_expr()?); } + self.expect_token(&Token::RParen)?; + + Ok(Expr::Substring { + expr: Box::new(expr), + substring_from: from_expr.map(Box::new), + substring_for: to_expr.map(Box::new), + special, + }) } pub fn parse_overlay_expr(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f81456849..62d5f2962 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5761,45 +5761,12 @@ fn parse_scalar_subqueries() { #[test] fn parse_substring() { - let from_for_supported_dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(AnsiDialect {}), - Box::new(SnowflakeDialect {}), - Box::new(HiveDialect {}), - Box::new(RedshiftSqlDialect {}), - Box::new(MySqlDialect {}), - Box::new(BigQueryDialect {}), - Box::new(SQLiteDialect {}), - Box::new(DuckDbDialect {}), - ], - options: None, - }; - - let from_for_unsupported_dialects = TestedDialects { - dialects: vec![Box::new(MsSqlDialect {})], - options: None, - }; - - from_for_supported_dialects - .one_statement_parses_to("SELECT SUBSTRING('1')", "SELECT SUBSTRING('1')"); - - from_for_supported_dialects.one_statement_parses_to( - "SELECT SUBSTRING('1' FROM 1)", - "SELECT SUBSTRING('1' FROM 1)", - ); - - from_for_supported_dialects.one_statement_parses_to( - "SELECT SUBSTRING('1' FROM 1 FOR 3)", - "SELECT SUBSTRING('1' FROM 1 FOR 3)", - ); - - from_for_unsupported_dialects - .one_statement_parses_to("SELECT SUBSTRING('1', 1, 3)", "SELECT SUBSTRING('1', 1, 3)"); - - from_for_supported_dialects - .one_statement_parses_to("SELECT SUBSTRING('1' FOR 3)", "SELECT SUBSTRING('1' FOR 3)"); + verified_stmt("SELECT SUBSTRING('1')"); + verified_stmt("SELECT SUBSTRING('1' FROM 1)"); + verified_stmt("SELECT SUBSTRING('1' FROM 1 FOR 3)"); + verified_stmt("SELECT SUBSTRING('1', 1, 3)"); + verified_stmt("SELECT SUBSTRING('1', 1)"); + verified_stmt("SELECT SUBSTRING('1' FOR 3)"); } #[test] diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index af2a2184a..8ffb78ae2 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1911,7 +1911,7 @@ fn parse_substring_in_select() { let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; match mysql().one_statement_parses_to( sql, - "SELECT DISTINCT SUBSTRING(description FROM 0 FOR 1) FROM test", + "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test", ) { Statement::Query(query) => { assert_eq!( @@ -1927,7 +1927,7 @@ fn parse_substring_in_select() { })), substring_from: Some(Box::new(Expr::Value(number("0")))), substring_for: Some(Box::new(Expr::Value(number("1")))), - special: false, + special: true, })], into: None, from: vec![TableWithJoins { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 6c8b507de..3452355a8 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -413,6 +413,18 @@ fn parse_single_quoted_identified() { sqlite().verified_only_select("SELECT 't'.*, t.'x' FROM 't'"); // TODO: add support for select 't'.x } + +#[test] +fn parse_substring() { + // SQLite supports the SUBSTRING function since v3.34, but does not support the SQL standard + // SUBSTRING(expr FROM start FOR length) syntax. + // https://www.sqlite.org/lang_corefunc.html#substr + sqlite().verified_only_select("SELECT SUBSTRING('SQLITE', 3, 4)"); + sqlite().verified_only_select("SELECT SUBSTR('SQLITE', 3, 4)"); + sqlite().verified_only_select("SELECT SUBSTRING('SQLITE', 3)"); + sqlite().verified_only_select("SELECT SUBSTR('SQLITE', 3)"); +} + #[test] fn parse_window_function_with_filter() { for func_name in [ From 44727891713114b72546facf21a335606380845a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Milenkovi=C4=87?= Date: Sun, 24 Mar 2024 18:20:15 +0000 Subject: [PATCH 389/806] Add support for $$ in generic dialect ... (#1185) --- src/parser/mod.rs | 3 ++- tests/sqlparser_postgres.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a7190563f..674d0692b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5954,7 +5954,8 @@ impl<'a> Parser<'a> { pub fn parse_function_definition(&mut self) -> Result { let peek_token = self.peek_token(); match peek_token.token { - Token::DollarQuotedString(value) if dialect_of!(self is PostgreSqlDialect) => { + Token::DollarQuotedString(value) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => + { self.next_token(); Ok(FunctionDefinition::DoubleDollarDef(value.value)) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9de4b981f..4a92cd45c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3305,7 +3305,7 @@ fn parse_create_function() { let sql = "CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL IMMUTABLE RETURN a + b"; assert_eq!( - pg().verified_stmt(sql), + pg_and_generic().verified_stmt(sql), Statement::CreateFunction { or_replace: true, temporary: false, From e747c9c2af08f4ea12e8d1692adf95998209e2a1 Mon Sep 17 00:00:00 2001 From: gstvg <28798827+gstvg@users.noreply.github.com> Date: Fri, 29 Mar 2024 10:39:52 -0300 Subject: [PATCH 390/806] Add support for DuckDB struct literal syntax (#1194) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 28 +++++++++++++ src/parser/mod.rs | 43 +++++++++++++++++++ tests/sqlparser_duckdb.rs | 87 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3e8354e15..7818dacd3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -347,6 +347,23 @@ impl fmt::Display for StructField { } } +/// A dictionary field within a dictionary. +/// +/// [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))] +pub struct DictionaryField { + pub key: Ident, + pub value: Box, +} + +impl fmt::Display for DictionaryField { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.key, self.value) + } +} + /// Options for `CAST` / `TRY_CAST` /// BigQuery: #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -687,6 +704,14 @@ pub enum Expr { expr: Box, name: Ident, }, + /// `DuckDB` specific `Struct` literal expression [1] + /// + /// Syntax: + /// ```sql + /// syntax: {'field_name': expr1[, ... ]} + /// ``` + /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + Dictionary(Vec), /// An array index expression e.g. `(ARRAY[1, 2])[1]` or `(current_schemas(FALSE))[1]` ArrayIndex { obj: Box, @@ -1146,6 +1171,9 @@ impl fmt::Display for Expr { Expr::Named { expr, name } => { write!(f, "{} AS {}", expr, name) } + Expr::Dictionary(fields) => { + write!(f, "{{{}}}", display_comma_separated(fields)) + } Expr::ArrayIndex { obj, indexes } => { write!(f, "{obj}")?; for i in indexes { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 674d0692b..2a5e9567a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1117,6 +1117,10 @@ impl<'a> Parser<'a> { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } + Token::LBrace if dialect_of!(self is DuckDbDialect | GenericDialect) => { + self.prev_token(); + self.parse_duckdb_struct_literal() + } _ => self.expected("an expression:", next_token), }?; @@ -2127,6 +2131,45 @@ impl<'a> Parser<'a> { )) } + /// DuckDB specific: Parse a duckdb dictionary [1] + /// + /// Syntax: + /// + /// ```sql + /// {'field_name': expr1[, ... ]} + /// ``` + /// + /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + fn parse_duckdb_struct_literal(&mut self) -> Result { + self.expect_token(&Token::LBrace)?; + + let fields = self.parse_comma_separated(Self::parse_duckdb_dictionary_field)?; + + self.expect_token(&Token::RBrace)?; + + Ok(Expr::Dictionary(fields)) + } + + /// Parse a field for a duckdb dictionary [1] + /// Syntax + /// ```sql + /// 'name': expr + /// ``` + /// + /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + fn parse_duckdb_dictionary_field(&mut self) -> Result { + let key = self.parse_identifier(false)?; + + self.expect_token(&Token::Colon)?; + + let expr = self.parse_expr()?; + + Ok(DictionaryField { + key, + value: Box::new(expr), + }) + } + /// For nested types that use the angle bracket syntax, this matches either /// `>`, `>>` or nothing depending on which variant is expected (specified by the previously /// matched `trailing_bracket` argument). It returns whether there is a trailing diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 45ae01bf0..a29d40084 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -246,3 +246,90 @@ fn test_duckdb_load_extension() { stmt ); } + +#[test] +fn test_duckdb_struct_literal() { + //struct literal syntax https://duckdb.org/docs/sql/data_types/struct#creating-structs + //syntax: {'field_name': expr1[, ... ]} + let sql = "SELECT {'a': 1, 'b': 2, 'c': 3}, [{'a': 'abc'}], {'a': 1, 'b': [t.str_col]}, {'a': 1, 'b': 'abc'}, {'abc': str_col}, {'a': {'aa': 1}}"; + let select = duckdb_and_generic().verified_only_select(sql); + assert_eq!(6, select.projection.len()); + assert_eq!( + &Expr::Dictionary(vec![ + DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Box::new(Expr::Value(number("1"))), + }, + DictionaryField { + key: Ident::with_quote('\'', "b"), + value: Box::new(Expr::Value(number("2"))), + }, + DictionaryField { + key: Ident::with_quote('\'', "c"), + value: Box::new(Expr::Value(number("3"))), + }, + ],), + expr_from_projection(&select.projection[0]) + ); + + assert_eq!( + &Expr::Array(Array { + elem: vec![Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Box::new(Expr::Value(Value::SingleQuotedString("abc".to_string()))), + },],)], + named: false + }), + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Dictionary(vec![ + DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Box::new(Expr::Value(number("1"))), + }, + DictionaryField { + key: Ident::with_quote('\'', "b"), + value: Box::new(Expr::Array(Array { + elem: vec![Expr::CompoundIdentifier(vec![ + Ident::from("t"), + Ident::from("str_col") + ])], + named: false + })), + }, + ],), + expr_from_projection(&select.projection[2]) + ); + assert_eq!( + &Expr::Dictionary(vec![ + DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Expr::Value(number("1")).into(), + }, + DictionaryField { + key: Ident::with_quote('\'', "b"), + value: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(), + }, + ],), + expr_from_projection(&select.projection[3]) + ); + assert_eq!( + &Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "abc"), + value: Expr::Identifier(Ident::from("str_col")).into(), + }],), + expr_from_projection(&select.projection[4]) + ); + assert_eq!( + &Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "aa"), + value: Expr::Value(number("1")).into(), + }],) + .into(), + }],), + expr_from_projection(&select.projection[5]) + ); +} From 14b33ac493b9c2214487f3d389d4287e038f8fc0 Mon Sep 17 00:00:00 2001 From: gstvg <28798827+gstvg@users.noreply.github.com> Date: Sat, 6 Apr 2024 13:46:36 -0300 Subject: [PATCH 391/806] Add support for DuckDB functions named arguments with assignment operator (#1195) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 3 +++ src/parser/mod.rs | 17 +++++++++++++++-- src/tokenizer.rs | 8 ++++---- tests/sqlparser_duckdb.rs | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7818dacd3..a378b58b1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4584,6 +4584,8 @@ pub enum FunctionArgOperator { Equals, /// function(arg1 => value1) RightArrow, + /// function(arg1 := value1) + Assignment, } impl fmt::Display for FunctionArgOperator { @@ -4591,6 +4593,7 @@ impl fmt::Display for FunctionArgOperator { match self { FunctionArgOperator::Equals => f.write_str("="), FunctionArgOperator::RightArrow => f.write_str("=>"), + FunctionArgOperator::Assignment => f.write_str(":="), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2a5e9567a..a3d7a7cfc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3485,7 +3485,7 @@ impl<'a> Parser<'a> { let name = self.parse_identifier(false)?; let default_expr = - if self.consume_token(&Token::DuckAssignment) || self.consume_token(&Token::RArrow) { + if self.consume_token(&Token::Assignment) || self.consume_token(&Token::RArrow) { Some(self.parse_expr()?) } else { None @@ -4183,7 +4183,7 @@ impl<'a> Parser<'a> { self.next_token(); // Skip `DEFAULT` Some(DeclareAssignment::Default(Box::new(self.parse_expr()?))) } - Token::DuckAssignment => { + Token::Assignment => { self.next_token(); // Skip `:=` Some(DeclareAssignment::DuckAssignment(Box::new( self.parse_expr()?, @@ -8602,6 +8602,19 @@ impl<'a> Parser<'a> { arg, operator: FunctionArgOperator::Equals, }) + } else if dialect_of!(self is DuckDbDialect | GenericDialect) + && self.peek_nth_token(1) == Token::Assignment + { + let name = self.parse_identifier(false)?; + + self.expect_token(&Token::Assignment)?; + let arg = self.parse_expr()?.into(); + + Ok(FunctionArg::Named { + name, + arg, + operator: FunctionArgOperator::Assignment, + }) } else { Ok(FunctionArg::Unnamed(self.parse_wildcard_expr()?.into())) } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index a1a2eae2d..e31fccca9 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -117,8 +117,8 @@ pub enum Token { Colon, /// DoubleColon `::` (used for casting in PostgreSQL) DoubleColon, - /// Assignment `:=` (used for keyword argument in DuckDB macros) - DuckAssignment, + /// Assignment `:=` (used for keyword argument in DuckDB macros and some functions, and for variable declarations in DuckDB and Snowflake) + Assignment, /// SemiColon `;` used as separator for COPY and payload SemiColon, /// Backslash `\` used in terminating the COPY payload with `\.` @@ -239,7 +239,7 @@ impl fmt::Display for Token { Token::Period => f.write_str("."), Token::Colon => f.write_str(":"), Token::DoubleColon => f.write_str("::"), - Token::DuckAssignment => f.write_str(":="), + Token::Assignment => f.write_str(":="), Token::SemiColon => f.write_str(";"), Token::Backslash => f.write_str("\\"), Token::LBracket => f.write_str("["), @@ -959,7 +959,7 @@ impl<'a> Tokenizer<'a> { chars.next(); match chars.peek() { Some(':') => self.consume_and_return(chars, Token::DoubleColon), - Some('=') => self.consume_and_return(chars, Token::DuckAssignment), + Some('=') => self.consume_and_return(chars, Token::Assignment), _ => Ok(Some(Token::Colon)), } } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a29d40084..e41109d95 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -333,3 +333,37 @@ fn test_duckdb_struct_literal() { expr_from_projection(&select.projection[5]) ); } + +#[test] +fn test_duckdb_named_argument_function_with_assignment_operator() { + let sql = "SELECT FUN(a := '1', b := '2') FROM foo"; + let select = duckdb_and_generic().verified_only_select(sql); + assert_eq!( + &Expr::Function(Function { + name: ObjectName(vec![Ident::new("FUN")]), + args: vec![ + FunctionArg::Named { + name: Ident::new("a"), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "1".to_owned() + ))), + operator: FunctionArgOperator::Assignment + }, + FunctionArg::Named { + name: Ident::new("b"), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "2".to_owned() + ))), + operator: FunctionArgOperator::Assignment + }, + ], + null_treatment: None, + filter: None, + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + expr_from_projection(only(&select.projection)) + ); +} From 2bf93a470c759b0ec5898ce12bff6fa266c05c0c Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Sat, 6 Apr 2024 07:03:00 -1000 Subject: [PATCH 392/806] Support `PARALLEL ... and for `..ON NULL INPUT ...` to `CREATE FUNCTION` (#1202) --- src/ast/mod.rs | 52 ++++++++++++++++++++++++++++++++++++- src/keywords.rs | 5 ++++ src/parser/mod.rs | 40 ++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 48 ++++++++++++++++++++++++++++++++-- 4 files changed, 142 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a378b58b1..9df0b5deb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5683,6 +5683,46 @@ impl fmt::Display for FunctionBehavior { } } +/// These attributes describe the behavior of the function when called with a null argument. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FunctionCalledOnNull { + CalledOnNullInput, + ReturnsNullOnNullInput, + Strict, +} + +impl fmt::Display for FunctionCalledOnNull { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionCalledOnNull::CalledOnNullInput => write!(f, "CALLED ON NULL INPUT"), + FunctionCalledOnNull::ReturnsNullOnNullInput => write!(f, "RETURNS NULL ON NULL INPUT"), + FunctionCalledOnNull::Strict => write!(f, "STRICT"), + } + } +} + +/// If it is safe for PostgreSQL to call the function from multiple threads at once +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FunctionParallel { + Unsafe, + Restricted, + Safe, +} + +impl fmt::Display for FunctionParallel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionParallel::Unsafe => write!(f, "PARALLEL UNSAFE"), + FunctionParallel::Restricted => write!(f, "PARALLEL RESTRICTED"), + FunctionParallel::Safe => write!(f, "PARALLEL SAFE"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -5703,7 +5743,7 @@ impl fmt::Display for FunctionDefinition { /// Postgres specific feature. /// -/// See [Postgresdocs](https://www.postgresql.org/docs/15/sql-createfunction.html) +/// See [Postgres docs](https://www.postgresql.org/docs/15/sql-createfunction.html) /// for more details #[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -5713,6 +5753,10 @@ pub struct CreateFunctionBody { pub language: Option, /// IMMUTABLE | STABLE | VOLATILE pub behavior: Option, + /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT + pub called_on_null: Option, + /// PARALLEL { UNSAFE | RESTRICTED | SAFE } + pub parallel: Option, /// AS 'definition' /// /// Note that Hive's `AS class_name` is also parsed here. @@ -5731,6 +5775,12 @@ impl fmt::Display for CreateFunctionBody { if let Some(behavior) = &self.behavior { write!(f, " {behavior}")?; } + if let Some(called_on_null) = &self.called_on_null { + write!(f, " {called_on_null}")?; + } + if let Some(parallel) = &self.parallel { + write!(f, " {parallel}")?; + } if let Some(definition) = &self.as_ { write!(f, " AS {definition}")?; } diff --git a/src/keywords.rs b/src/keywords.rs index c94a6227c..fa7d133e3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -353,6 +353,7 @@ define_keywords!( INITIALLY, INNER, INOUT, + INPUT, INPUTFORMAT, INSENSITIVE, INSERT, @@ -498,6 +499,7 @@ define_keywords!( OVERLAY, OVERWRITE, OWNED, + PARALLEL, PARAMETER, PARQUET, PARTITION, @@ -570,6 +572,7 @@ define_keywords!( RESPECT, RESTART, RESTRICT, + RESTRICTED, RESULT, RESULTSET, RETAIN, @@ -589,6 +592,7 @@ define_keywords!( ROW_NUMBER, RULE, RUN, + SAFE, SAFE_CAST, SAVEPOINT, SCHEMA, @@ -704,6 +708,7 @@ define_keywords!( UNLOGGED, UNNEST, UNPIVOT, + UNSAFE, UNSIGNED, UNTIL, UPDATE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a3d7a7cfc..235c1f1df 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3437,6 +3437,46 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::VOLATILE) { ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE")?; body.behavior = Some(FunctionBehavior::Volatile); + } else if self.parse_keywords(&[ + Keyword::CALLED, + Keyword::ON, + Keyword::NULL, + Keyword::INPUT, + ]) { + ensure_not_set( + &body.called_on_null, + "CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT", + )?; + body.called_on_null = Some(FunctionCalledOnNull::CalledOnNullInput); + } else if self.parse_keywords(&[ + Keyword::RETURNS, + Keyword::NULL, + Keyword::ON, + Keyword::NULL, + Keyword::INPUT, + ]) { + ensure_not_set( + &body.called_on_null, + "CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT", + )?; + body.called_on_null = Some(FunctionCalledOnNull::ReturnsNullOnNullInput); + } else if self.parse_keyword(Keyword::STRICT) { + ensure_not_set( + &body.called_on_null, + "CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT", + )?; + body.called_on_null = Some(FunctionCalledOnNull::Strict); + } else if self.parse_keyword(Keyword::PARALLEL) { + ensure_not_set(&body.parallel, "PARALLEL { UNSAFE | RESTRICTED | SAFE }")?; + if self.parse_keyword(Keyword::UNSAFE) { + body.parallel = Some(FunctionParallel::Unsafe); + } else if self.parse_keyword(Keyword::RESTRICTED) { + body.parallel = Some(FunctionParallel::Restricted); + } else if self.parse_keyword(Keyword::SAFE) { + body.parallel = Some(FunctionParallel::Safe); + } else { + return self.expected("one of UNSAFE | RESTRICTED | SAFE", self.peek_token()); + } } else if self.parse_keyword(Keyword::RETURN) { ensure_not_set(&body.return_, "RETURN")?; body.return_ = Some(self.parse_expr()?); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 4a92cd45c..8515956f6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3280,7 +3280,7 @@ fn parse_similar_to() { #[test] fn parse_create_function() { - let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE AS 'select $1 + $2;'"; + let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'"; assert_eq!( pg_and_generic().verified_stmt(sql), Statement::CreateFunction { @@ -3295,6 +3295,8 @@ fn parse_create_function() { params: CreateFunctionBody { language: Some("SQL".into()), behavior: Some(FunctionBehavior::Immutable), + called_on_null: Some(FunctionCalledOnNull::Strict), + parallel: Some(FunctionParallel::Safe), as_: Some(FunctionDefinition::SingleQuotedDef( "select $1 + $2;".into() )), @@ -3303,7 +3305,7 @@ fn parse_create_function() { } ); - let sql = "CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL IMMUTABLE RETURN a + b"; + let sql = "CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT PARALLEL RESTRICTED RETURN a + b"; assert_eq!( pg_and_generic().verified_stmt(sql), Statement::CreateFunction { @@ -3323,6 +3325,40 @@ fn parse_create_function() { params: CreateFunctionBody { language: Some("SQL".into()), behavior: Some(FunctionBehavior::Immutable), + called_on_null: Some(FunctionCalledOnNull::ReturnsNullOnNullInput), + parallel: Some(FunctionParallel::Restricted), + return_: Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier("a".into())), + op: BinaryOperator::Plus, + right: Box::new(Expr::Identifier("b".into())), + }), + ..Default::default() + }, + } + ); + + let sql = "CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE CALLED ON NULL INPUT PARALLEL UNSAFE RETURN a + b"; + assert_eq!( + pg_and_generic().verified_stmt(sql), + Statement::CreateFunction { + or_replace: true, + temporary: false, + name: ObjectName(vec![Ident::new("add")]), + args: Some(vec![ + OperateFunctionArg::with_name("a", DataType::Integer(None)), + OperateFunctionArg { + mode: Some(ArgMode::In), + name: Some("b".into()), + data_type: DataType::Integer(None), + default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), + } + ]), + return_type: Some(DataType::Integer(None)), + params: CreateFunctionBody { + language: Some("SQL".into()), + behavior: Some(FunctionBehavior::Stable), + called_on_null: Some(FunctionCalledOnNull::CalledOnNullInput), + parallel: Some(FunctionParallel::Unsafe), return_: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier("a".into())), op: BinaryOperator::Plus, @@ -3348,6 +3384,8 @@ fn parse_create_function() { params: CreateFunctionBody { language: Some("plpgsql".into()), behavior: None, + called_on_null: None, + parallel: None, return_: None, as_: Some(FunctionDefinition::DoubleDollarDef( " BEGIN RETURN i + 1; END; ".into() @@ -3358,6 +3396,12 @@ fn parse_create_function() { ); } +#[test] +fn parse_incorrect_create_function_parallel() { + let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL PARALLEL BLAH AS 'select $1 + $2;'"; + assert!(pg().parse_sql_statements(sql).is_err()); +} + #[test] fn parse_drop_function() { let sql = "DROP FUNCTION IF EXISTS test_func"; From e976a2ee43e3310c19c2f55942e928b27f29fc55 Mon Sep 17 00:00:00 2001 From: sunxunle <163647374+sunxunle@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:06:53 +0800 Subject: [PATCH 393/806] chore: fix some comments (#1184) Signed-off-by: sunxunle --- README.md | 2 +- src/ast/mod.rs | 2 +- src/dialect/mod.rs | 2 +- tests/sqlparser_sqlite.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8a4b5d986..512f5f6c0 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ maintain this crate is limited. Please read the following sections carefully. ### New Syntax The most commonly accepted PRs add support for or fix a bug in a feature in the -SQL standard, or a a popular RDBMS, such as Microsoft SQL +SQL standard, or a popular RDBMS, such as Microsoft SQL Server or PostgreSQL, will likely be accepted after a brief review. Any SQL feature that is dialect specific should be parsed by *both* the relevant [`Dialect`] as well as [`GenericDialect`]. diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9df0b5deb..9b3bf3f62 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4649,7 +4649,7 @@ pub struct Function { pub args: Vec, /// e.g. `x > 5` in `COUNT(x) FILTER (WHERE x > 5)` pub filter: Option>, - // Snowflake/MSSQL supports diffrent options for null treatment in rank functions + // Snowflake/MSSQL supports different options for null treatment in rank functions pub null_treatment: Option, pub over: Option, // aggregate functions may specify eg `COUNT(DISTINCT x)` diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 2873cca2c..0e7257b7b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -93,7 +93,7 @@ macro_rules! dialect_of { pub trait Dialect: Debug + Any { /// Determine the [`TypeId`] of this dialect. /// - /// By default, return the same [`TypeId`] as [`Any::type_id`]. Can be overriden + /// By default, return the same [`TypeId`] as [`Any::type_id`]. Can be overridden /// by dialects that behave like other dialects /// (for example when wrapping a dialect). fn dialect(&self) -> TypeId { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 3452355a8..0352b4ec6 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -55,7 +55,7 @@ fn pragma_eq_style() { } } #[test] -fn pragma_funciton_style() { +fn pragma_function_style() { let sql = "PRAGMA cache_size(10)"; match sqlite_and_generic().verified_stmt(sql) { Statement::Pragma { @@ -103,7 +103,7 @@ fn pragma_function_string_style() { } #[test] -fn pragma_eq_placehoder_style() { +fn pragma_eq_placeholder_style() { let sql = "PRAGMA table_info = ?"; match sqlite_and_generic().verified_stmt(sql) { Statement::Pragma { From 3bf40485c59c29391c3ee82b2b3e3801b449b9bf Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Sat, 6 Apr 2024 10:08:40 -0700 Subject: [PATCH 394/806] Fix parsing of equality binop in function argument (#1182) --- src/dialect/duckdb.rs | 4 ++++ src/dialect/mod.rs | 4 ++++ src/parser/mod.rs | 4 +++- src/test_utils.rs | 35 +++++++++++++++++++++-------------- tests/sqlparser_common.rs | 35 ++++++++++++++++++++++++++++++++--- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index a4f9309e6..f08545b99 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -33,4 +33,8 @@ impl Dialect for DuckDbDialect { fn supports_group_by_expr(&self) -> bool { true } + + fn supports_named_fn_args_with_eq_operator(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 0e7257b7b..2463121e7 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -143,6 +143,10 @@ pub trait Dialect: Debug + Any { fn supports_start_transaction_modifier(&self) -> bool { false } + /// Returns true if the dialect supports named arguments of the form FUN(a = '1', b = '2'). + fn supports_named_fn_args_with_eq_operator(&self) -> bool { + false + } /// Returns true if the dialect has a CONVERT function which accepts a type first /// and an expression second, e.g. `CONVERT(varchar, 1)` fn convert_type_before_value(&self) -> bool { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 235c1f1df..231de4d20 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8631,7 +8631,9 @@ impl<'a> Parser<'a> { arg, operator: FunctionArgOperator::RightArrow, }) - } else if self.peek_nth_token(1) == Token::Eq { + } else if self.dialect.supports_named_fn_args_with_eq_operator() + && self.peek_nth_token(1) == Token::Eq + { let name = self.parse_identifier(false)?; self.expect_token(&Token::Eq)?; diff --git a/src/test_utils.rs b/src/test_utils.rs index 4a54a6826..dd198f7dd 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -68,7 +68,7 @@ impl TestedDialects { } Some((dialect, parsed)) }) - .unwrap() + .expect("tested dialects cannot be empty") .1 } @@ -195,15 +195,6 @@ impl TestedDialects { /// Returns all available dialects. pub fn all_dialects() -> TestedDialects { - all_dialects_except(|_| false) -} - -/// Returns available dialects. The `except` predicate is used -/// to filter out specific dialects. -pub fn all_dialects_except(except: F) -> TestedDialects -where - F: Fn(&dyn Dialect) -> bool, -{ let all_dialects = vec![ Box::new(GenericDialect {}) as Box, Box::new(PostgreSqlDialect {}) as Box, @@ -218,14 +209,30 @@ where Box::new(DuckDbDialect {}) as Box, ]; TestedDialects { - dialects: all_dialects - .into_iter() - .filter(|d| !except(d.as_ref())) - .collect(), + dialects: all_dialects, options: None, } } +/// Returns all dialects matching the given predicate. +pub fn all_dialects_where(predicate: F) -> TestedDialects +where + F: Fn(&dyn Dialect) -> bool, +{ + let mut dialects = all_dialects(); + dialects.dialects.retain(|d| predicate(&**d)); + dialects +} + +/// Returns available dialects. The `except` predicate is used +/// to filter out specific dialects. +pub fn all_dialects_except(except: F) -> TestedDialects +where + F: Fn(&dyn Dialect) -> bool, +{ + all_dialects_where(|d| !except(d)) +} + pub fn assert_eq_vec(expected: &[&str], actual: &[T]) { assert_eq!( expected, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 62d5f2962..f78eda0cc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -33,8 +33,8 @@ use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::{Parser, ParserError, ParserOptions}; use sqlparser::tokenizer::Tokenizer; use test_utils::{ - all_dialects, alter_table_op, assert_eq_vec, expr_from_projection, join, number, only, table, - table_alias, TestedDialects, + all_dialects, all_dialects_where, alter_table_op, assert_eq_vec, expr_from_projection, join, + number, only, table, table_alias, TestedDialects, }; #[macro_use] @@ -4045,7 +4045,9 @@ fn parse_named_argument_function() { #[test] fn parse_named_argument_function_with_eq_operator() { let sql = "SELECT FUN(a = '1', b = '2') FROM foo"; - let select = verified_only_select(sql); + + let select = all_dialects_where(|d| d.supports_named_fn_args_with_eq_operator()) + .verified_only_select(sql); assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("FUN")]), @@ -4074,6 +4076,33 @@ fn parse_named_argument_function_with_eq_operator() { }), expr_from_projection(only(&select.projection)) ); + + // Ensure that bar = 42 in a function argument parses as an equality binop + // rather than a named function argument. + assert_eq!( + all_dialects_except(|d| d.supports_named_fn_args_with_eq_operator()) + .verified_expr("foo(bar = 42)"), + Expr::Function(Function { + name: ObjectName(vec![Ident::new("foo")]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("bar"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("42"))), + }, + ))], + filter: None, + null_treatment: None, + over: None, + distinct: false, + special: false, + order_by: vec![], + }) + ); + + // TODO: should this parse for all dialects? + all_dialects_except(|d| d.supports_named_fn_args_with_eq_operator()) + .verified_expr("iff(1 = 1, 1, 0)"); } #[test] From 05af4e049c1da491c9823d79d5fbc0dd4e9af36a Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 7 Apr 2024 07:08:55 -0400 Subject: [PATCH 395/806] Cleanup CREATE FUNCTION tests (#1203) --- tests/sqlparser_postgres.rs | 102 ++++-------------------------------- 1 file changed, 9 insertions(+), 93 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 8515956f6..3747aef70 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -804,9 +804,7 @@ Kwara & Kogi PHP ₱ USD $ \N Some other value \\."#; - let ast = pg_and_generic().one_statement_parses_to(sql, ""); - println!("{ast:#?}"); - //assert_eq!(sql, ast.to_string()); + pg_and_generic().one_statement_parses_to(sql, ""); } #[test] @@ -3304,98 +3302,16 @@ fn parse_create_function() { }, } ); - - let sql = "CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT PARALLEL RESTRICTED RETURN a + b"; - assert_eq!( - pg_and_generic().verified_stmt(sql), - Statement::CreateFunction { - or_replace: true, - temporary: false, - name: ObjectName(vec![Ident::new("add")]), - args: Some(vec![ - OperateFunctionArg::with_name("a", DataType::Integer(None)), - OperateFunctionArg { - mode: Some(ArgMode::In), - name: Some("b".into()), - data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), - } - ]), - return_type: Some(DataType::Integer(None)), - params: CreateFunctionBody { - language: Some("SQL".into()), - behavior: Some(FunctionBehavior::Immutable), - called_on_null: Some(FunctionCalledOnNull::ReturnsNullOnNullInput), - parallel: Some(FunctionParallel::Restricted), - return_: Some(Expr::BinaryOp { - left: Box::new(Expr::Identifier("a".into())), - op: BinaryOperator::Plus, - right: Box::new(Expr::Identifier("b".into())), - }), - ..Default::default() - }, - } - ); - - let sql = "CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE CALLED ON NULL INPUT PARALLEL UNSAFE RETURN a + b"; - assert_eq!( - pg_and_generic().verified_stmt(sql), - Statement::CreateFunction { - or_replace: true, - temporary: false, - name: ObjectName(vec![Ident::new("add")]), - args: Some(vec![ - OperateFunctionArg::with_name("a", DataType::Integer(None)), - OperateFunctionArg { - mode: Some(ArgMode::In), - name: Some("b".into()), - data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), - } - ]), - return_type: Some(DataType::Integer(None)), - params: CreateFunctionBody { - language: Some("SQL".into()), - behavior: Some(FunctionBehavior::Stable), - called_on_null: Some(FunctionCalledOnNull::CalledOnNullInput), - parallel: Some(FunctionParallel::Unsafe), - return_: Some(Expr::BinaryOp { - left: Box::new(Expr::Identifier("a".into())), - op: BinaryOperator::Plus, - right: Box::new(Expr::Identifier("b".into())), - }), - ..Default::default() - }, - } - ); - - let sql = r#"CREATE OR REPLACE FUNCTION increment(i INTEGER) RETURNS INTEGER LANGUAGE plpgsql AS $$ BEGIN RETURN i + 1; END; $$"#; - assert_eq!( - pg().verified_stmt(sql), - Statement::CreateFunction { - or_replace: true, - temporary: false, - name: ObjectName(vec![Ident::new("increment")]), - args: Some(vec![OperateFunctionArg::with_name( - "i", - DataType::Integer(None) - )]), - return_type: Some(DataType::Integer(None)), - params: CreateFunctionBody { - language: Some("plpgsql".into()), - behavior: None, - called_on_null: None, - parallel: None, - return_: None, - as_: Some(FunctionDefinition::DoubleDollarDef( - " BEGIN RETURN i + 1; END; ".into() - )), - using: None - }, - } - ); } +#[test] +fn parse_create_function_detailed() { + pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL IMMUTABLE PARALLEL RESTRICTED RETURN a + b"); + pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT PARALLEL RESTRICTED RETURN a + b"); + pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE PARALLEL UNSAFE RETURN a + b"); + pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE CALLED ON NULL INPUT PARALLEL UNSAFE RETURN a + b"); + pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION increment(i INTEGER) RETURNS INTEGER LANGUAGE plpgsql AS $$ BEGIN RETURN i + 1; END; $$"#); +} #[test] fn parse_incorrect_create_function_parallel() { let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL PARALLEL BLAH AS 'select $1 + $2;'"; From 83c5d8191bef0b1dcdc4b8c9bae6657811d9df4c Mon Sep 17 00:00:00 2001 From: Nikita-str <42584606+Nikita-str@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:12:48 +0300 Subject: [PATCH 396/806] solve `stack overflow` on `RecursionLimitExceeded` during debug building (#1171) Co-authored-by: Andrew Lamb --- src/parser/mod.rs | 97 +++++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 231de4d20..fcb3e3391 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -470,7 +470,7 @@ impl<'a> Parser<'a> { Keyword::ANALYZE => Ok(self.parse_analyze()?), Keyword::SELECT | Keyword::WITH | Keyword::VALUES => { self.prev_token(); - Ok(Statement::Query(Box::new(self.parse_query()?))) + Ok(Statement::Query(self.parse_boxed_query()?)) } Keyword::TRUNCATE => Ok(self.parse_truncate()?), Keyword::ATTACH => Ok(self.parse_attach_database()?), @@ -530,7 +530,7 @@ impl<'a> Parser<'a> { }, Token::LParen => { self.prev_token(); - Ok(Statement::Query(Box::new(self.parse_query()?))) + Ok(Statement::Query(self.parse_boxed_query()?)) } _ => self.expected("an SQL statement", next_token), } @@ -1084,7 +1084,7 @@ impl<'a> Parser<'a> { let expr = if self.parse_keyword(Keyword::SELECT) || self.parse_keyword(Keyword::WITH) { self.prev_token(); - Expr::Subquery(Box::new(self.parse_query()?)) + Expr::Subquery(self.parse_boxed_query()?) } else { let exprs = self.parse_comma_separated(Parser::parse_expr)?; match exprs.len() { @@ -1465,7 +1465,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let exists_node = Expr::Exists { negated, - subquery: Box::new(self.parse_query()?), + subquery: self.parse_boxed_query()?, }; self.expect_token(&Token::RParen)?; Ok(exists_node) @@ -1654,9 +1654,9 @@ impl<'a> Parser<'a> { // Parses an array constructed from a subquery pub fn parse_array_subquery(&mut self) -> Result { - let query = self.parse_query()?; + let query = self.parse_boxed_query()?; self.expect_token(&Token::RParen)?; - Ok(Expr::ArraySubquery(Box::new(query))) + Ok(Expr::ArraySubquery(query)) } /// Parse a SQL LISTAGG expression, e.g. `LISTAGG(...) WITHIN GROUP (ORDER BY ...)`. @@ -2554,7 +2554,7 @@ impl<'a> Parser<'a> { self.prev_token(); Expr::InSubquery { expr: Box::new(expr), - subquery: Box::new(self.parse_query()?), + subquery: self.parse_boxed_query()?, negated, } } else { @@ -3637,7 +3637,7 @@ impl<'a> Parser<'a> { } self.expect_keyword(Keyword::AS)?; - let query = Box::new(self.parse_query()?); + let query = self.parse_boxed_query()?; // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. let with_no_schema_binding = dialect_of!(self is RedshiftSqlDialect | GenericDialect) @@ -4032,7 +4032,7 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::FOR)?; - let query = Some(Box::new(self.parse_query()?)); + let query = Some(self.parse_boxed_query()?); Ok(Statement::Declare { stmts: vec![Declare { @@ -4126,7 +4126,7 @@ impl<'a> Parser<'a> { match self.peek_token().token { Token::Word(w) if w.keyword == Keyword::SELECT => ( Some(DeclareType::Cursor), - Some(Box::new(self.parse_query()?)), + Some(self.parse_boxed_query()?), None, None, ), @@ -4650,7 +4650,7 @@ impl<'a> Parser<'a> { // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { - Some(Box::new(self.parse_query()?)) + Some(self.parse_boxed_query()?) } else { None }; @@ -5646,7 +5646,7 @@ impl<'a> Parser<'a> { let with_options = self.parse_options(Keyword::WITH)?; self.expect_keyword(Keyword::AS)?; - let query = Box::new(self.parse_query()?); + let query = self.parse_boxed_query()?; Ok(Statement::AlterView { name, @@ -5686,7 +5686,7 @@ impl<'a> Parser<'a> { pub fn parse_copy(&mut self) -> Result { let source; if self.consume_token(&Token::LParen) { - source = CopySource::Query(Box::new(self.parse_query()?)); + source = CopySource::Query(self.parse_boxed_query()?); self.expect_token(&Token::RParen)?; } else { let table_name = self.parse_object_name(false)?; @@ -6910,6 +6910,15 @@ impl<'a> Parser<'a> { } } + /// Call's [`Self::parse_query`] returning a `Box`'ed result. + /// + /// This function can be used to reduce the stack size required in debug + /// builds. Instead of `sizeof(Query)` only a pointer (`Box`) + /// is used. + fn parse_boxed_query(&mut self) -> Result, ParserError> { + self.parse_query().map(Box::new) + } + /// Parse a query expression, i.e. a `SELECT` statement optionally /// preceded with some `WITH` CTE declarations and optionally followed /// by `ORDER BY`. Unlike some other parse_... methods, this one doesn't @@ -6924,13 +6933,10 @@ impl<'a> Parser<'a> { } else { None }; - if self.parse_keyword(Keyword::INSERT) { - let insert = self.parse_insert()?; - Ok(Query { with, - body: Box::new(SetExpr::Insert(insert)), + body: self.parse_insert_setexpr_boxed()?, limit: None, limit_by: vec![], order_by: vec![], @@ -6940,10 +6946,9 @@ impl<'a> Parser<'a> { for_clause: None, }) } else if self.parse_keyword(Keyword::UPDATE) { - let update = self.parse_update()?; Ok(Query { with, - body: Box::new(SetExpr::Update(update)), + body: self.parse_update_setexpr_boxed()?, limit: None, limit_by: vec![], order_by: vec![], @@ -6953,7 +6958,7 @@ impl<'a> Parser<'a> { for_clause: None, }) } else { - let body = Box::new(self.parse_query_body(0)?); + let body = self.parse_boxed_query_body(0)?; let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { self.parse_comma_separated(Parser::parse_order_by_expr)? @@ -7143,7 +7148,7 @@ impl<'a> Parser<'a> { } } self.expect_token(&Token::LParen)?; - let query = Box::new(self.parse_query()?); + let query = self.parse_boxed_query()?; self.expect_token(&Token::RParen)?; let alias = TableAlias { name, @@ -7167,7 +7172,7 @@ impl<'a> Parser<'a> { } } self.expect_token(&Token::LParen)?; - let query = Box::new(self.parse_query()?); + let query = self.parse_boxed_query()?; self.expect_token(&Token::RParen)?; let alias = TableAlias { name, columns }; Cte { @@ -7183,6 +7188,15 @@ impl<'a> Parser<'a> { Ok(cte) } + /// Call's [`Self::parse_query_body`] returning a `Box`'ed result. + /// + /// This function can be used to reduce the stack size required in debug + /// builds. Instead of `sizeof(QueryBody)` only a pointer (`Box`) + /// is used. + fn parse_boxed_query_body(&mut self, precedence: u8) -> Result, ParserError> { + self.parse_query_body(precedence).map(Box::new) + } + /// Parse a "query body", which is an expression with roughly the /// following grammar: /// ```sql @@ -7191,16 +7205,19 @@ impl<'a> Parser<'a> { /// subquery ::= query_body [ order_by_limit ] /// set_operation ::= query_body { 'UNION' | 'EXCEPT' | 'INTERSECT' } [ 'ALL' ] query_body /// ``` + /// + /// If you need `Box` then maybe there is sense to use `parse_boxed_query_body` + /// due to prevent stack overflow in debug building(to reserve less memory on stack). pub fn parse_query_body(&mut self, precedence: u8) -> Result { // We parse the expression using a Pratt parser, as in `parse_expr()`. // Start by parsing a restricted SELECT or a `(subquery)`: let mut expr = if self.parse_keyword(Keyword::SELECT) { - SetExpr::Select(Box::new(self.parse_select()?)) + SetExpr::Select(self.parse_select().map(Box::new)?) } else if self.consume_token(&Token::LParen) { // CTEs are not allowed here, but the parser currently accepts them - let subquery = self.parse_query()?; + let subquery = self.parse_boxed_query()?; self.expect_token(&Token::RParen)?; - SetExpr::Query(Box::new(subquery)) + SetExpr::Query(subquery) } else if self.parse_keyword(Keyword::VALUES) { let is_mysql = dialect_of!(self is MySqlDialect); SetExpr::Values(self.parse_values(is_mysql)?) @@ -7233,7 +7250,7 @@ impl<'a> Parser<'a> { left: Box::new(expr), op: op.unwrap(), set_quantifier, - right: Box::new(self.parse_query_body(next_precedence)?), + right: self.parse_boxed_query_body(next_precedence)?, }; } @@ -8147,7 +8164,7 @@ impl<'a> Parser<'a> { &mut self, lateral: IsLateral, ) -> Result { - let subquery = Box::new(self.parse_query()?); + let subquery = self.parse_boxed_query()?; self.expect_token(&Token::RParen)?; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; Ok(TableFactor::Derived { @@ -8395,6 +8412,13 @@ impl<'a> Parser<'a> { Ok(insert.clone()) } + /// Parse an INSERT statement, returning a `Box`ed SetExpr + /// + /// This is used to reduce the size of the stack frames in debug builds + fn parse_insert_setexpr_boxed(&mut self) -> Result, ParserError> { + Ok(Box::new(SetExpr::Insert(self.parse_insert()?))) + } + /// Parse an INSERT statement pub fn parse_insert(&mut self) -> Result { let or = if !dialect_of!(self is SQLiteDialect) { @@ -8445,7 +8469,7 @@ impl<'a> Parser<'a> { } else { None }; - let source = Box::new(self.parse_query()?); + let source = self.parse_boxed_query()?; Ok(Statement::Directory { local, path, @@ -8478,7 +8502,7 @@ impl<'a> Parser<'a> { // Hive allows you to specify columns after partitions as well if you want. let after_columns = self.parse_parenthesized_column_list(Optional, false)?; - let source = Some(Box::new(self.parse_query()?)); + let source = Some(self.parse_boxed_query()?); (columns, partitioned, after_columns, source) }; @@ -8581,6 +8605,13 @@ impl<'a> Parser<'a> { } } + /// Parse an UPDATE statement, returning a `Box`ed SetExpr + /// + /// This is used to reduce the size of the stack frames in debug builds + fn parse_update_setexpr_boxed(&mut self) -> Result, ParserError> { + Ok(Box::new(SetExpr::Update(self.parse_update()?))) + } + pub fn parse_update(&mut self) -> Result { let table = self.parse_table_and_joins()?; self.expect_keyword(Keyword::SET)?; @@ -8686,11 +8717,11 @@ impl<'a> Parser<'a> { .is_some() { self.prev_token(); - let subquery = self.parse_query()?; + let subquery = self.parse_boxed_query()?; self.expect_token(&Token::RParen)?; return Ok(( vec![FunctionArg::Unnamed(FunctionArgExpr::from(Expr::Subquery( - Box::new(subquery), + subquery, )))], vec![], )); @@ -9204,7 +9235,7 @@ impl<'a> Parser<'a> { pub fn parse_unload(&mut self) -> Result { self.expect_token(&Token::LParen)?; - let query = self.parse_query()?; + let query = self.parse_boxed_query()?; self.expect_token(&Token::RParen)?; self.expect_keyword(Keyword::TO)?; @@ -9213,7 +9244,7 @@ impl<'a> Parser<'a> { let with_options = self.parse_options(Keyword::WITH)?; Ok(Statement::Unload { - query: Box::new(query), + query, to, with: with_options, }) From 23103302e62b13351ecc33703bfdcef609166f41 Mon Sep 17 00:00:00 2001 From: Nikita-str <42584606+Nikita-str@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:20:21 +0300 Subject: [PATCH 397/806] Support named windows in `OVER (window_definition)` clause (#1166) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 15 ++++++++- src/parser/mod.rs | 9 +++++ tests/sqlparser_common.rs | 69 ++++++++++++++++++++++++++++++++++++++- tests/sqlparser_sqlite.rs | 1 + 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9b3bf3f62..dfdc86e06 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1246,11 +1246,19 @@ impl Display for WindowType { } } -/// A window specification (i.e. `OVER (PARTITION BY .. ORDER BY .. etc.)`) +/// A window specification (i.e. `OVER ([window_name] PARTITION BY .. ORDER BY .. etc.)`) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WindowSpec { + /// Optional window name. + /// + /// You can find it at least in [MySQL][1], [BigQuery][2], [PostgreSQL][3] + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/window-functions-named-windows.html + /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/window-function-calls + /// [3]: https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS + pub window_name: Option, /// `OVER (PARTITION BY ...)` pub partition_by: Vec, /// `OVER (ORDER BY ...)` @@ -1262,7 +1270,12 @@ pub struct WindowSpec { impl fmt::Display for WindowSpec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut delim = ""; + if let Some(window_name) = &self.window_name { + delim = " "; + write!(f, "{window_name}")?; + } if !self.partition_by.is_empty() { + f.write_str(delim)?; delim = " "; write!( f, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fcb3e3391..568d89e36 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9528,6 +9528,13 @@ 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.maybe_parse(|parser| parser.parse_identifier(false)) + } + _ => None, + }; + let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { self.parse_comma_separated(Parser::parse_expr)? } else { @@ -9538,6 +9545,7 @@ impl<'a> Parser<'a> { } else { vec![] }; + let window_frame = if !self.consume_token(&Token::RParen) { let window_frame = self.parse_window_frame()?; self.expect_token(&Token::RParen)?; @@ -9546,6 +9554,7 @@ impl<'a> Parser<'a> { None }; Ok(WindowSpec { + window_name, partition_by, order_by, window_frame, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f78eda0cc..6e33dce9b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2062,6 +2062,7 @@ fn parse_select_qualify() { null_treatment: None, filter: None, over: Some(WindowType::WindowSpec(WindowSpec { + window_name: None, partition_by: vec![Expr::Identifier(Ident::new("p"))], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), @@ -4122,7 +4123,10 @@ fn parse_window_functions() { GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) \ FROM foo"; let select = verified_only_select(sql); - assert_eq!(7, select.projection.len()); + + const EXPECTED_PROJ_QTY: usize = 7; + assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); + assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), @@ -4130,6 +4134,7 @@ fn parse_window_functions() { null_treatment: None, filter: None, over: Some(WindowType::WindowSpec(WindowSpec { + window_name: None, partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("dt")), @@ -4144,6 +4149,66 @@ fn parse_window_functions() { }), expr_from_projection(&select.projection[0]) ); + + for i in 0..EXPECTED_PROJ_QTY { + assert!(matches!( + expr_from_projection(&select.projection[i]), + Expr::Function(Function { + over: Some(WindowType::WindowSpec(WindowSpec { + window_name: None, + .. + })), + .. + }) + )); + } +} + +#[test] +fn parse_named_window_functions() { + let supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + ], + options: None, + }; + + let sql = "SELECT row_number() OVER (w ORDER BY dt DESC), \ + sum(foo) OVER (win PARTITION BY a, b ORDER BY c, d \ + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) \ + FROM foo \ + WINDOW w AS (PARTITION BY x), win AS (ORDER BY y)"; + supported_dialects.verified_stmt(sql); + + let select = verified_only_select(sql); + + const EXPECTED_PROJ_QTY: usize = 2; + assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); + + const EXPECTED_WIN_NAMES: [&str; 2] = ["w", "win"]; + for (i, win_name) in EXPECTED_WIN_NAMES.iter().enumerate() { + assert!(matches!( + expr_from_projection(&select.projection[i]), + Expr::Function(Function { + over: Some(WindowType::WindowSpec(WindowSpec { + window_name: Some(Ident { value, .. }), + .. + })), + .. + }) if value == win_name + )); + } + + let sql = "SELECT \ + FIRST_VALUE(x) OVER (w ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS first, \ + FIRST_VALUE(x) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS last, \ + SUM(y) OVER (win PARTITION BY x) AS last \ + FROM EMPLOYEE \ + WINDOW w AS (PARTITION BY x), win AS (w ORDER BY y)"; + supported_dialects.verified_stmt(sql); } #[test] @@ -4244,6 +4309,7 @@ fn test_parse_named_window() { quote_style: None, }, WindowSpec { + window_name: None, partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident { @@ -4262,6 +4328,7 @@ fn test_parse_named_window() { quote_style: None, }, WindowSpec { + window_name: None, partition_by: vec![Expr::Identifier(Ident { value: "C11".to_string(), quote_style: None, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 0352b4ec6..c9d5d98cd 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -446,6 +446,7 @@ fn parse_window_function_with_filter() { ))], null_treatment: None, over: Some(WindowType::WindowSpec(WindowSpec { + window_name: None, partition_by: vec![], order_by: vec![], window_frame: None, From 732e1ec1fc1abe23ead675a04f70dc0a2806ef97 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Sun, 7 Apr 2024 14:31:04 +0200 Subject: [PATCH 398/806] BigQuery support inline comment with hash syntax (#1192) --- src/tokenizer.rs | 2 +- tests/sqlparser_common.rs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index e31fccca9..1ceec705b 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -984,7 +984,7 @@ impl<'a> Tokenizer<'a> { } '{' => self.consume_and_return(chars, Token::LBrace), '}' => self.consume_and_return(chars, Token::RBrace), - '#' if dialect_of!(self is SnowflakeDialect) => { + '#' if dialect_of!(self is SnowflakeDialect | BigQueryDialect) => { chars.next(); // consume the '#', starting a snowflake single-line comment let comment = self.tokenize_single_line_comment(chars); Ok(Some(Token::Whitespace(Whitespace::SingleLineComment { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6e33dce9b..f474e1166 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8607,6 +8607,24 @@ fn test_release_savepoint() { one_statement_parses_to("RELEASE test1", "RELEASE SAVEPOINT test1"); } +#[test] +fn test_comment_hash_syntax() { + let dialects = TestedDialects { + dialects: vec![Box::new(BigQueryDialect {}), Box::new(SnowflakeDialect {})], + options: None, + }; + let sql = r#" + # comment + SELECT a, b, c # , d, e + FROM T + ####### comment ################# + WHERE true + # comment + "#; + let canonical = "SELECT a, b, c FROM T WHERE true"; + dialects.verified_only_select_with_canonical(sql, canonical); +} + #[test] fn test_buffer_reuse() { let d = GenericDialect {}; From 20c57547847b353a797dc830b1449a3d6b9135ad Mon Sep 17 00:00:00 2001 From: xring Date: Sun, 7 Apr 2024 20:43:23 +0800 Subject: [PATCH 399/806] Support `[FIRST | AFTER column_name]` support in `ALTER TABLE` for MySQL (#1180) --- src/ast/ddl.rs | 25 +++-- src/ast/mod.rs | 22 +++++ src/keywords.rs | 1 + src/parser/mod.rs | 22 +++++ tests/sqlparser_common.rs | 2 + tests/sqlparser_mysql.rs | 189 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 2 + 7 files changed, 257 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9e3137d94..080e8c4da 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -25,8 +25,8 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName, SequenceOptions, - SqlOption, + display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, + ObjectName, SequenceOptions, SqlOption, }; use crate::tokenizer::Token; @@ -45,6 +45,8 @@ pub enum AlterTableOperation { if_not_exists: bool, /// . column_def: ColumnDef, + /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] + column_position: Option, }, /// `DISABLE ROW LEVEL SECURITY` /// @@ -129,6 +131,8 @@ pub enum AlterTableOperation { new_name: Ident, data_type: DataType, options: Vec, + /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] + column_position: Option, }, /// `RENAME CONSTRAINT TO ` /// @@ -171,6 +175,7 @@ impl fmt::Display for AlterTableOperation { column_keyword, if_not_exists, column_def, + column_position, } => { write!(f, "ADD")?; if *column_keyword { @@ -181,6 +186,10 @@ impl fmt::Display for AlterTableOperation { } write!(f, " {column_def}")?; + if let Some(position) = column_position { + write!(f, " {position}")?; + } + Ok(()) } AlterTableOperation::AlterColumn { column_name, op } => { @@ -271,13 +280,17 @@ impl fmt::Display for AlterTableOperation { new_name, data_type, options, + column_position, } => { write!(f, "CHANGE COLUMN {old_name} {new_name} {data_type}")?; - if options.is_empty() { - Ok(()) - } else { - write!(f, " {}", display_separated(options, " ")) + if !options.is_empty() { + write!(f, " {}", display_separated(options, " "))?; } + if let Some(position) = column_position { + write!(f, " {position}")?; + } + + Ok(()) } AlterTableOperation::RenameConstraint { old_name, new_name } => { write!(f, "RENAME CONSTRAINT {old_name} TO {new_name}") diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dfdc86e06..2eebbc604 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6018,6 +6018,28 @@ impl fmt::Display for HiveSetLocation { } } +/// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MySQLColumnPosition { + First, + After(Ident), +} + +impl Display for MySQLColumnPosition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MySQLColumnPosition::First => Ok(write!(f, "FIRST")?), + MySQLColumnPosition::After(ident) => { + let column_name = &ident.value; + Ok(write!(f, "AFTER {column_name}")?) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/keywords.rs b/src/keywords.rs index fa7d133e3..91842672d 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -73,6 +73,7 @@ define_keywords!( ACTION, ADD, ADMIN, + AFTER, AGAINST, ALL, ALLOCATE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 568d89e36..57e24d218 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5358,10 +5358,14 @@ impl<'a> Parser<'a> { }; let column_def = self.parse_column_def()?; + + let column_position = self.parse_column_position()?; + AlterTableOperation::AddColumn { column_keyword, if_not_exists, column_def, + column_position, } } } @@ -5490,11 +5494,14 @@ impl<'a> Parser<'a> { options.push(option); } + let column_position = self.parse_column_position()?; + AlterTableOperation::ChangeColumn { old_name, new_name, data_type, options, + column_position, } } else if self.parse_keyword(Keyword::ALTER) { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] @@ -9608,6 +9615,21 @@ impl<'a> Parser<'a> { Ok(partitions) } + fn parse_column_position(&mut self) -> Result, ParserError> { + if dialect_of!(self is MySqlDialect | GenericDialect) { + if self.parse_keyword(Keyword::FIRST) { + Ok(Some(MySQLColumnPosition::First)) + } else if self.parse_keyword(Keyword::AFTER) { + let ident = self.parse_identifier(false)?; + Ok(Some(MySQLColumnPosition::After(ident))) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + /// Consume the parser and return its underlying token buffer pub fn into_tokens(self) -> Vec { self.tokens diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f474e1166..c8551e1fe 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3512,11 +3512,13 @@ fn parse_alter_table() { column_keyword, if_not_exists, column_def, + column_position, } => { assert!(column_keyword); assert!(!if_not_exists); assert_eq!("foo", column_def.name.to_string()); assert_eq!("TEXT", column_def.data_type.to_string()); + assert_eq!(None, column_position); } _ => unreachable!(), }; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8ffb78ae2..59314c1d9 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1875,6 +1875,120 @@ fn parse_delete_with_limit() { } } +#[test] +fn parse_alter_table_add_column() { + match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT FIRST") { + Statement::AlterTable { + name, + if_exists, + only, + operations, + location: _, + } => { + assert_eq!(name.to_string(), "tab"); + assert!(!if_exists); + assert!(!only); + assert_eq!( + operations, + vec![AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "b".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + column_position: Some(MySQLColumnPosition::First), + },] + ); + } + _ => unreachable!(), + } + + match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT AFTER foo") { + Statement::AlterTable { + name, + if_exists, + only, + operations, + location: _, + } => { + assert_eq!(name.to_string(), "tab"); + assert!(!if_exists); + assert!(!only); + assert_eq!( + operations, + vec![AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "b".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("foo"), + quote_style: None + })), + },] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_add_columns() { + match mysql() + .verified_stmt("ALTER TABLE tab ADD COLUMN a TEXT FIRST, ADD COLUMN b INT AFTER foo") + { + Statement::AlterTable { + name, + if_exists, + only, + operations, + location: _, + } => { + assert_eq!(name.to_string(), "tab"); + assert!(!if_exists); + assert!(!only); + assert_eq!( + operations, + vec![ + AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "a".into(), + data_type: DataType::Text, + collation: None, + options: vec![], + }, + column_position: Some(MySQLColumnPosition::First), + }, + AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "b".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("foo"), + quote_style: None, + })), + }, + ] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table_drop_primary_key() { assert_matches!( @@ -1891,6 +2005,7 @@ fn parse_alter_table_change_column() { new_name: Ident::new("desc"), data_type: DataType::Text, options: vec![ColumnOption::NotNull], + column_position: None, }; let sql1 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL"; @@ -1904,6 +2019,80 @@ fn parse_alter_table_change_column() { &expected_name.to_string(), ); assert_eq!(expected_operation, operation); + + let expected_operation = AlterTableOperation::ChangeColumn { + old_name: Ident::new("description"), + new_name: Ident::new("desc"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::First), + }; + let sql3 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL FIRST"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql3), &expected_name.to_string()); + assert_eq!(expected_operation, operation); + + let expected_operation = AlterTableOperation::ChangeColumn { + old_name: Ident::new("description"), + new_name: Ident::new("desc"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("foo"), + quote_style: None, + })), + }; + let sql4 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL AFTER foo"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql4), &expected_name.to_string()); + assert_eq!(expected_operation, operation); +} + +#[test] +fn parse_alter_table_change_column_with_column_position() { + let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_operation_first = AlterTableOperation::ChangeColumn { + old_name: Ident::new("description"), + new_name: Ident::new("desc"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::First), + }; + + let sql1 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL FIRST"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql1), &expected_name.to_string()); + assert_eq!(expected_operation_first, operation); + + let sql2 = "ALTER TABLE orders CHANGE description desc TEXT NOT NULL FIRST"; + let operation = alter_table_op_with_name( + mysql().one_statement_parses_to(sql2, sql1), + &expected_name.to_string(), + ); + assert_eq!(expected_operation_first, operation); + + let expected_operation_after = AlterTableOperation::ChangeColumn { + old_name: Ident::new("description"), + new_name: Ident::new("desc"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("total_count"), + quote_style: None, + })), + }; + + let sql1 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL AFTER total_count"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql1), &expected_name.to_string()); + assert_eq!(expected_operation_after, operation); + + let sql2 = "ALTER TABLE orders CHANGE description desc TEXT NOT NULL AFTER total_count"; + let operation = alter_table_op_with_name( + mysql().one_statement_parses_to(sql2, sql1), + &expected_name.to_string(), + ); + assert_eq!(expected_operation_after, operation); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3747aef70..ea5c9875b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -694,6 +694,7 @@ fn parse_alter_table_add_columns() { collation: None, options: vec![], }, + column_position: None, }, AlterTableOperation::AddColumn { column_keyword: true, @@ -704,6 +705,7 @@ fn parse_alter_table_add_columns() { collation: None, options: vec![], }, + column_position: None, }, ] ); From 17ef71e42b72df46ee133b9e242fa93881d05a66 Mon Sep 17 00:00:00 2001 From: Maciej Obuchowski Date: Sun, 7 Apr 2024 14:45:59 +0200 Subject: [PATCH 400/806] Fix parse `COPY INTO` stage names without parens for SnowFlake (#1187) Signed-off-by: Maciej Obuchowski --- src/dialect/snowflake.rs | 6 +++++- tests/sqlparser_snowflake.rs | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 8ffaf5944..1d9d983e5 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -155,6 +155,10 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result { + parser.prev_token(); + break; + } Token::AtSign => ident.push('@'), Token::Tilde => ident.push('~'), Token::Mod => ident.push('%'), @@ -219,7 +223,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { } _ => { parser.prev_token(); - from_stage = parser.parse_object_name(false)?; + from_stage = parse_snowflake_stage_name(parser)?; stage_params = parse_stage_params(parser)?; // as diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 65755f685..49b440506 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1337,6 +1337,48 @@ fn test_snowflake_stage_object_names() { } } +#[test] +fn test_snowflake_copy_into() { + let sql = "COPY INTO a.b FROM @namespace.stage_name"; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + into, from_stage, .. + } => { + assert_eq!(into, ObjectName(vec![Ident::new("a"), Ident::new("b")])); + assert_eq!( + from_stage, + ObjectName(vec![Ident::new("@namespace"), Ident::new("stage_name")]) + ) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_copy_into_stage_name_ends_with_parens() { + let sql = "COPY INTO SCHEMA.SOME_MONITORING_SYSTEM FROM (SELECT t.$1:st AS st FROM @schema.general_finished)"; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + into, from_stage, .. + } => { + assert_eq!( + into, + ObjectName(vec![ + Ident::new("SCHEMA"), + Ident::new("SOME_MONITORING_SYSTEM") + ]) + ); + assert_eq!( + from_stage, + ObjectName(vec![Ident::new("@schema"), Ident::new("general_finished")]) + ) + } + _ => unreachable!(), + } +} + #[test] fn test_snowflake_trim() { let real_sql = r#"SELECT customer_id, TRIM(sub_items.value:item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; From 6da8828c1b664a87f8ffe0cd68609aec046f436c Mon Sep 17 00:00:00 2001 From: yassun7010 <47286750+yassun7010@users.noreply.github.com> Date: Wed, 10 Apr 2024 05:19:27 +0900 Subject: [PATCH 401/806] feat: support tailing commas on snowflake dialect. (#1205) --- src/parser/mod.rs | 5 +++-- tests/sqlparser_snowflake.rs | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 57e24d218..48eaed92c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2946,14 +2946,15 @@ impl<'a> Parser<'a> { /// Parse a comma-separated list of 1+ SelectItem pub fn parse_projection(&mut self) -> Result, ParserError> { - // BigQuery allows trailing commas, but only in project lists + // BigQuery and Snowflake allow trailing commas, but only in project lists // e.g. `SELECT 1, 2, FROM t` // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#trailing_commas + // https://docs.snowflake.com/en/release-notes/2024/8_11#select-supports-trailing-commas // // This pattern could be captured better with RAII type semantics, but it's quite a bit of // code to add for just one case, so we'll just do it manually here. let old_value = self.options.trailing_commas; - self.options.trailing_commas |= dialect_of!(self is BigQueryDialect); + self.options.trailing_commas |= dialect_of!(self is BigQueryDialect | SnowflakeDialect); let ret = self.parse_comma_separated(|p| p.parse_select_item()); self.options.trailing_commas = old_value; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 49b440506..880129f82 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1536,3 +1536,8 @@ fn parse_comma_outer_join() { "SELECT t1.c1, t2.c2 FROM t1, t2 WHERE t1.c1 = t2.c2 (+)", ); } + +#[test] +fn test_sf_trailing_commas() { + snowflake().verified_only_select_with_canonical("SELECT 1, 2, FROM t", "SELECT 1, 2 FROM t"); +} From 8f67d1a713006e7189fafecc8b833ee919438248 Mon Sep 17 00:00:00 2001 From: Nikita-str <42584606+Nikita-str@users.noreply.github.com> Date: Tue, 9 Apr 2024 23:20:24 +0300 Subject: [PATCH 402/806] Support MySQL `UNIQUE` table constraint (#1164) Co-authored-by: Andrew Lamb --- src/ast/ddl.rs | 176 ++++++++++++++++++++++++++--- src/ast/mod.rs | 2 +- src/parser/mod.rs | 121 +++++++++++++++----- tests/sqlparser_mysql.rs | 236 ++++++++++++++++++++++++++++++--------- 4 files changed, 441 insertions(+), 94 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 080e8c4da..d86ebad9d 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -15,7 +15,7 @@ #[cfg(not(feature = "std"))] use alloc::{boxed::Box, string::String, vec::Vec}; -use core::fmt; +use core::fmt::{self, Write}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -397,12 +397,68 @@ impl fmt::Display for AlterColumnOperation { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TableConstraint { - /// `[ CONSTRAINT ] { PRIMARY KEY | UNIQUE } ()` + /// MySQL [definition][1] for `UNIQUE` constraints statements:\ + /// * `[CONSTRAINT []] UNIQUE [] [index_type] () ` + /// + /// where: + /// * [index_type][2] is `USING {BTREE | HASH}` + /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` + /// * [index_type_display][4] is `[INDEX | KEY]` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html + /// [2]: IndexType + /// [3]: IndexOption + /// [4]: KeyOrIndexDisplay Unique { + /// Constraint name. + /// + /// Can be not the same as `index_name` name: Option, + /// Index name + index_name: Option, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + index_type_display: KeyOrIndexDisplay, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + index_type: Option, + /// Identifiers of the columns that are unique. columns: Vec, - /// Whether this is a `PRIMARY KEY` or just a `UNIQUE` constraint - is_primary: bool, + index_options: Vec, + characteristics: Option, + }, + /// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\ + /// * `[CONSTRAINT []] PRIMARY KEY [index_name] [index_type] () ` + /// + /// Actually the specification have no `[index_name]` but the next query will complete successfully: + /// ```sql + /// CREATE TABLE unspec_table ( + /// xid INT NOT NULL, + /// CONSTRAINT p_name PRIMARY KEY index_name USING BTREE (xid) + /// ); + /// ``` + /// + /// where: + /// * [index_type][2] is `USING {BTREE | HASH}` + /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html + /// [2]: IndexType + /// [3]: IndexOption + PrimaryKey { + /// Constraint name. + /// + /// Can be not the same as `index_name` + name: Option, + /// Index name + index_name: Option, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + index_type: Option, + /// Identifiers of the columns that form the primary key. + columns: Vec, + index_options: Vec, characteristics: Option, }, /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () @@ -472,22 +528,51 @@ impl fmt::Display for TableConstraint { match self { TableConstraint::Unique { name, + index_name, + index_type_display, + index_type, columns, - is_primary, + index_options, characteristics, } => { write!( f, - "{}{} ({})", + "{}UNIQUE{index_type_display:>}{}{} ({})", display_constraint_name(name), - if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }, - display_comma_separated(columns) + display_option_spaced(index_name), + display_option(" USING ", "", index_type), + display_comma_separated(columns), )?; - if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + if !index_options.is_empty() { + write!(f, " {}", display_separated(index_options, " "))?; + } + + write!(f, "{}", display_option_spaced(characteristics))?; + Ok(()) + } + TableConstraint::PrimaryKey { + name, + index_name, + index_type, + columns, + index_options, + characteristics, + } => { + write!( + f, + "{}PRIMARY KEY{}{} ({})", + display_constraint_name(name), + display_option_spaced(index_name), + display_option(" USING ", "", index_type), + display_comma_separated(columns), + )?; + + if !index_options.is_empty() { + write!(f, " {}", display_separated(index_options, " "))?; } + write!(f, "{}", display_option_spaced(characteristics))?; Ok(()) } TableConstraint::ForeignKey { @@ -550,9 +635,7 @@ impl fmt::Display for TableConstraint { write!(f, "SPATIAL")?; } - if !matches!(index_type_display, KeyOrIndexDisplay::None) { - write!(f, " {index_type_display}")?; - } + write!(f, "{index_type_display:>}")?; if let Some(name) = opt_index_name { write!(f, " {name}")?; @@ -585,8 +668,20 @@ pub enum KeyOrIndexDisplay { Index, } +impl KeyOrIndexDisplay { + pub fn is_none(self) -> bool { + matches!(self, Self::None) + } +} + impl fmt::Display for KeyOrIndexDisplay { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let left_space = matches!(f.align(), Some(fmt::Alignment::Right)); + + if left_space && !self.is_none() { + f.write_char(' ')? + } + match self { KeyOrIndexDisplay::None => { write!(f, "") @@ -626,6 +721,30 @@ impl fmt::Display for IndexType { } } } + +/// MySQLs index option. +/// +/// This structure used here [`MySQL` CREATE TABLE][1], [`MySQL` CREATE INDEX][2]. +/// +/// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html +/// [2]: https://dev.mysql.com/doc/refman/8.3/en/create-index.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 IndexOption { + Using(IndexType), + Comment(String), +} + +impl fmt::Display for IndexOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Using(index_type) => write!(f, "USING {index_type}"), + Self::Comment(s) => write!(f, "COMMENT '{s}'"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -909,6 +1028,7 @@ pub enum GeneratedExpressionMode { Stored, } +#[must_use] fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); impl<'a> fmt::Display for ConstraintName<'a> { @@ -922,6 +1042,36 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { ConstraintName(name) } +/// If `option` is +/// * `Some(inner)` => create display struct for `"{prefix}{inner}{postfix}"` +/// * `_` => do nothing +#[must_use] +fn display_option<'a, T: fmt::Display>( + prefix: &'a str, + postfix: &'a str, + option: &'a Option, +) -> impl fmt::Display + 'a { + struct OptionDisplay<'a, T>(&'a str, &'a str, &'a Option); + impl<'a, T: fmt::Display> fmt::Display for OptionDisplay<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(inner) = self.2 { + let (prefix, postfix) = (self.0, self.1); + write!(f, "{prefix}{inner}{postfix}")?; + } + Ok(()) + } + } + OptionDisplay(prefix, postfix, option) +} + +/// If `option` is +/// * `Some(inner)` => create display struct for `" {inner}"` +/// * `_` => do nothing +#[must_use] +fn display_option_spaced(option: &Option) -> impl fmt::Display + '_ { + display_option(" ", "", option) +} + /// ` = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]` /// /// Used in UNIQUE and foreign key constraints. The individual settings may occur in any order. diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2eebbc604..a469338d6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -33,7 +33,7 @@ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue} pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ConstraintCharacteristics, DeferrableInitial, GeneratedAs, - GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, + GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 48eaed92c..7bd3ffb21 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5149,23 +5149,49 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { - Token::Word(w) if w.keyword == Keyword::PRIMARY || w.keyword == Keyword::UNIQUE => { - let is_primary = w.keyword == Keyword::PRIMARY; - - // parse optional [KEY] - let _ = self.parse_keyword(Keyword::KEY); + Token::Word(w) if w.keyword == Keyword::UNIQUE => { + let index_type_display = self.parse_index_type_display(); + if !dialect_of!(self is GenericDialect | MySqlDialect) + && !index_type_display.is_none() + { + return self + .expected("`index_name` or `(column_name [, ...])`", self.peek_token()); + } - // optional constraint name - let name = self - .maybe_parse(|parser| parser.parse_identifier(false)) - .or(name); + // optional index name + let index_name = self.parse_optional_indent(); + let index_type = self.parse_optional_using_then_index_type()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::Unique { name, + index_name, + index_type_display, + index_type, columns, - is_primary, + index_options, + characteristics, + })) + } + Token::Word(w) if w.keyword == Keyword::PRIMARY => { + // after `PRIMARY` always stay `KEY` + self.expect_keyword(Keyword::KEY)?; + + // optional index name + let index_name = self.parse_optional_indent(); + let index_type = self.parse_optional_using_then_index_type()?; + + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let index_options = self.parse_index_options()?; + let characteristics = self.parse_constraint_characteristics()?; + Ok(Some(TableConstraint::PrimaryKey { + name, + index_name, + index_type, + columns, + index_options, characteristics, })) } @@ -5209,20 +5235,17 @@ impl<'a> Parser<'a> { } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) - && dialect_of!(self is GenericDialect | MySqlDialect) => + && dialect_of!(self is GenericDialect | MySqlDialect) + && name.is_none() => { let display_as_key = w.keyword == Keyword::KEY; let name = match self.peek_token().token { Token::Word(word) if word.keyword == Keyword::USING => None, - _ => self.maybe_parse(|parser| parser.parse_identifier(false)), + _ => self.parse_optional_indent(), }; - let index_type = if self.parse_keyword(Keyword::USING) { - Some(self.parse_index_type()?) - } else { - None - }; + let index_type = self.parse_optional_using_then_index_type()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; Ok(Some(TableConstraint::Index { @@ -5248,15 +5271,9 @@ impl<'a> Parser<'a> { let fulltext = w.keyword == Keyword::FULLTEXT; - let index_type_display = if self.parse_keyword(Keyword::KEY) { - KeyOrIndexDisplay::Key - } else if self.parse_keyword(Keyword::INDEX) { - KeyOrIndexDisplay::Index - } else { - KeyOrIndexDisplay::None - }; + let index_type_display = self.parse_index_type_display(); - let opt_index_name = self.maybe_parse(|parser| parser.parse_identifier(false)); + let opt_index_name = self.parse_optional_indent(); let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -5313,6 +5330,56 @@ impl<'a> Parser<'a> { } } + /// Parse [USING {BTREE | HASH}] + pub fn parse_optional_using_then_index_type( + &mut self, + ) -> Result, ParserError> { + if self.parse_keyword(Keyword::USING) { + Ok(Some(self.parse_index_type()?)) + } else { + Ok(None) + } + } + + /// Parse `[ident]`, mostly `ident` is name, like: + /// `window_name`, `index_name`, ... + pub fn parse_optional_indent(&mut self) -> Option { + self.maybe_parse(|parser| parser.parse_identifier(false)) + } + + #[must_use] + pub fn parse_index_type_display(&mut self) -> KeyOrIndexDisplay { + if self.parse_keyword(Keyword::KEY) { + KeyOrIndexDisplay::Key + } else if self.parse_keyword(Keyword::INDEX) { + KeyOrIndexDisplay::Index + } else { + KeyOrIndexDisplay::None + } + } + + pub fn parse_optional_index_option(&mut self) -> Result, ParserError> { + if let Some(index_type) = self.parse_optional_using_then_index_type()? { + Ok(Some(IndexOption::Using(index_type))) + } else if self.parse_keyword(Keyword::COMMENT) { + let s = self.parse_literal_string()?; + Ok(Some(IndexOption::Comment(s))) + } else { + Ok(None) + } + } + + pub fn parse_index_options(&mut self) -> Result, ParserError> { + let mut options = Vec::new(); + + loop { + match self.parse_optional_index_option()? { + Some(index_option) => options.push(index_option), + None => return Ok(options), + } + } + } + pub fn parse_sql_option(&mut self) -> Result { let name = self.parse_identifier(false)?; self.expect_token(&Token::Eq)?; @@ -9537,9 +9604,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.maybe_parse(|parser| parser.parse_identifier(false)) - } + Token::Word(word) if word.keyword == Keyword::NoKeyword => self.parse_optional_indent(), _ => None, }; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 59314c1d9..5f64079a6 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -500,63 +500,186 @@ fn parse_create_table_auto_increment() { } } -#[test] -fn parse_create_table_unique_key() { - let sql = "CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, UNIQUE KEY bar_key (bar))"; - let canonical = "CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, CONSTRAINT bar_key UNIQUE (bar))"; - match mysql().one_statement_parses_to(sql, canonical) { - Statement::CreateTable { +/// if `unique_index_type_display` is `Some` create `TableConstraint::Unique` +/// otherwise create `TableConstraint::Primary` +fn table_constraint_unique_primary_ctor( + name: Option, + index_name: Option, + index_type: Option, + columns: Vec, + index_options: Vec, + characteristics: Option, + unique_index_type_display: Option, +) -> TableConstraint { + match unique_index_type_display { + Some(index_type_display) => TableConstraint::Unique { name, + index_name, + index_type_display, + index_type, columns, - constraints, - .. - } => { - assert_eq!(name.to_string(), "foo"); - assert_eq!( - vec![TableConstraint::Unique { - name: Some(Ident::new("bar_key")), - columns: vec![Ident::new("bar")], - is_primary: false, - characteristics: None, - }], - constraints - ); - assert_eq!( - vec![ - ColumnDef { - name: Ident::new("id"), - data_type: DataType::Int(None), - collation: None, - options: vec![ - ColumnOptionDef { - name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None + index_options, + characteristics, + }, + None => TableConstraint::PrimaryKey { + name, + index_name, + index_type, + columns, + index_options, + characteristics, + }, + } +} + +#[test] +fn parse_create_table_primary_and_unique_key() { + let sqls = ["UNIQUE KEY", "PRIMARY KEY"] + .map(|key_ty|format!("CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, CONSTRAINT bar_key {key_ty} (bar))")); + + let index_type_display = [Some(KeyOrIndexDisplay::Key), None]; + + for (sql, index_type_display) in sqls.iter().zip(index_type_display) { + match mysql().one_statement_parses_to(sql, "") { + Statement::CreateTable { + name, + columns, + constraints, + .. + } => { + assert_eq!(name.to_string(), "foo"); + + let expected_constraint = table_constraint_unique_primary_ctor( + Some(Ident::new("bar_key")), + None, + None, + vec![Ident::new("bar")], + vec![], + None, + index_type_display, + ); + assert_eq!(vec![expected_constraint], constraints); + + assert_eq!( + vec![ + ColumnDef { + name: Ident::new("id"), + data_type: DataType::Int(None), + collation: None, + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Unique { + is_primary: true, + characteristics: None + }, }, - }, - ColumnOptionDef { + ColumnOptionDef { + name: None, + option: ColumnOption::DialectSpecific(vec![ + Token::make_keyword("AUTO_INCREMENT") + ]), + }, + ], + }, + ColumnDef { + name: Ident::new("bar"), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { name: None, - option: ColumnOption::DialectSpecific(vec![Token::make_keyword( - "AUTO_INCREMENT" - )]), - }, - ], - }, - ColumnDef { - name: Ident::new("bar"), - data_type: DataType::Int(None), - collation: None, - options: vec![ColumnOptionDef { - name: None, - option: ColumnOption::NotNull, - },], - }, - ], - columns - ); + option: ColumnOption::NotNull, + },], + }, + ], + columns + ); + } + _ => unreachable!(), } - _ => unreachable!(), + } +} + +#[test] +fn parse_create_table_primary_and_unique_key_with_index_options() { + let sqls = ["UNIQUE INDEX", "PRIMARY KEY"] + .map(|key_ty|format!("CREATE TABLE foo (bar INT, var INT, CONSTRAINT constr {key_ty} index_name (bar, var) USING HASH COMMENT 'yes, ' USING BTREE COMMENT 'MySQL allows')")); + + let index_type_display = [Some(KeyOrIndexDisplay::Index), None]; + + for (sql, index_type_display) in sqls.iter().zip(index_type_display) { + match mysql_and_generic().one_statement_parses_to(sql, "") { + Statement::CreateTable { + name, constraints, .. + } => { + assert_eq!(name.to_string(), "foo"); + + let expected_constraint = table_constraint_unique_primary_ctor( + Some(Ident::new("constr")), + Some(Ident::new("index_name")), + None, + vec![Ident::new("bar"), Ident::new("var")], + vec![ + IndexOption::Using(IndexType::Hash), + IndexOption::Comment("yes, ".into()), + IndexOption::Using(IndexType::BTree), + IndexOption::Comment("MySQL allows".into()), + ], + None, + index_type_display, + ); + assert_eq!(vec![expected_constraint], constraints); + } + _ => unreachable!(), + } + + mysql_and_generic().verified_stmt(sql); + } +} + +#[test] +fn parse_create_table_primary_and_unique_key_with_index_type() { + let sqls = ["UNIQUE", "PRIMARY KEY"].map(|key_ty| { + format!("CREATE TABLE foo (bar INT, {key_ty} index_name USING BTREE (bar) USING HASH)") + }); + + let index_type_display = [Some(KeyOrIndexDisplay::None), None]; + + for (sql, index_type_display) in sqls.iter().zip(index_type_display) { + match mysql_and_generic().one_statement_parses_to(sql, "") { + Statement::CreateTable { + name, constraints, .. + } => { + assert_eq!(name.to_string(), "foo"); + + let expected_constraint = table_constraint_unique_primary_ctor( + None, + Some(Ident::new("index_name")), + Some(IndexType::BTree), + vec![Ident::new("bar")], + vec![IndexOption::Using(IndexType::Hash)], + None, + index_type_display, + ); + assert_eq!(vec![expected_constraint], constraints); + } + _ => unreachable!(), + } + mysql_and_generic().verified_stmt(sql); + } + + let sql = "CREATE TABLE foo (bar INT, UNIQUE INDEX index_name USING BTREE (bar) USING HASH)"; + mysql_and_generic().verified_stmt(sql); + let sql = "CREATE TABLE foo (bar INT, PRIMARY KEY index_name USING BTREE (bar) USING HASH)"; + mysql_and_generic().verified_stmt(sql); +} + +#[test] +fn parse_create_table_primary_and_unique_key_characteristic_test() { + let sqls = ["UNIQUE INDEX", "PRIMARY KEY"] + .map(|key_ty|format!("CREATE TABLE x (y INT, CONSTRAINT constr {key_ty} (y) NOT DEFERRABLE INITIALLY IMMEDIATE)")); + for sql in &sqls { + mysql_and_generic().verified_stmt(sql); } } @@ -2333,6 +2456,15 @@ fn parse_create_table_with_index_definition() { ); } +#[test] +fn parse_create_table_unallow_constraint_then_index() { + let sql = "CREATE TABLE foo (bar INT, CONSTRAINT constr INDEX index (bar))"; + assert!(mysql_and_generic().parse_sql_statements(sql).is_err()); + + let sql = "CREATE TABLE foo (bar INT, INDEX index (bar))"; + assert!(mysql_and_generic().parse_sql_statements(sql).is_ok()); +} + #[test] fn parse_create_table_with_fulltext_definition() { mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT (id))"); From 241da85d67b8dd401ee086c324c3194792715cf7 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 9 Apr 2024 16:21:08 -0400 Subject: [PATCH 403/806] Support `CREATE/DROP SECRET` for duckdb dialect (#1208) Co-authored-by: Jichao Sun <4977515+JichaoS@users.noreply.github.com> --- src/ast/mod.rs | 169 ++++++++++++++++++++++++++++++++++++ src/keywords.rs | 4 + src/parser/mod.rs | 174 +++++++++++++++++++++++++++++++++++++- tests/sqlparser_duckdb.rs | 141 ++++++++++++++++++++++++++++++ 4 files changed, 486 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a469338d6..8fc696baa 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2040,6 +2040,19 @@ pub enum Statement { authorization_owner: Option, }, /// ```sql + /// CREATE SECRET + /// ``` + /// See [duckdb](https://duckdb.org/docs/sql/statements/create_secret.html) + CreateSecret { + or_replace: bool, + temporary: Option, + if_not_exists: bool, + name: Option, + storage_specifier: Option, + secret_type: Ident, + options: Vec, + }, + /// ```sql /// ALTER TABLE /// ``` AlterTable { @@ -2088,6 +2101,31 @@ pub enum Statement { /// true if the syntax is 'ATTACH DATABASE', false if it's just 'ATTACH' database: bool, }, + /// (DuckDB-specific) + /// ```sql + /// ATTACH 'sqlite_file.db' AS sqlite_db (READ_ONLY, TYPE SQLITE); + /// ``` + /// See + AttachDuckDBDatabase { + if_not_exists: bool, + /// true if the syntax is 'ATTACH DATABASE', false if it's just 'ATTACH' + database: bool, + /// An expression that indicates the path to the database file + database_path: Ident, + database_alias: Option, + attach_options: Vec, + }, + /// (DuckDB-specific) + /// ```sql + /// DETACH db_alias; + /// ``` + /// See + DetachDuckDBDatabase { + if_exists: bool, + /// true if the syntax is 'DETACH DATABASE', false if it's just 'DETACH' + database: bool, + database_alias: Ident, + }, /// ```sql /// DROP [TABLE, VIEW, ...] /// ``` @@ -2121,6 +2159,15 @@ pub enum Statement { option: Option, }, /// ```sql + /// DROP SECRET + /// ``` + DropSecret { + if_exists: bool, + temporary: Option, + name: Ident, + storage_specifier: Option, + }, + /// ```sql /// DECLARE /// ``` /// Declare Cursor Variables @@ -2772,6 +2819,40 @@ impl fmt::Display for Statement { let keyword = if *database { "DATABASE " } else { "" }; write!(f, "ATTACH {keyword}{database_file_name} AS {schema_name}") } + Statement::AttachDuckDBDatabase { + if_not_exists, + database, + database_path, + database_alias, + attach_options, + } => { + write!( + f, + "ATTACH{database}{if_not_exists} {database_path}", + database = if *database { " DATABASE" } else { "" }, + if_not_exists = if *if_not_exists { " IF NOT EXISTS" } else { "" }, + )?; + if let Some(alias) = database_alias { + write!(f, " AS {alias}")?; + } + if !attach_options.is_empty() { + write!(f, " ({})", display_comma_separated(attach_options))?; + } + Ok(()) + } + Statement::DetachDuckDBDatabase { + if_exists, + database, + database_alias, + } => { + write!( + f, + "DETACH{database}{if_exists} {database_alias}", + database = if *database { " DATABASE" } else { "" }, + if_exists = if *if_exists { " IF EXISTS" } else { "" }, + )?; + Ok(()) + } Statement::Analyze { table_name, partitions, @@ -3556,6 +3637,41 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateSecret { + or_replace, + temporary, + if_not_exists, + name, + storage_specifier, + secret_type, + options, + } => { + write!( + f, + "CREATE {or_replace}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + )?; + if let Some(t) = temporary { + write!(f, "{}", if *t { "TEMPORARY " } else { "PERSISTENT " })?; + } + write!( + f, + "SECRET {if_not_exists}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if let Some(n) = name { + write!(f, "{n} ")?; + }; + if let Some(s) = storage_specifier { + write!(f, "IN {s} ")?; + } + write!(f, "( TYPE {secret_type}",)?; + if !options.is_empty() { + write!(f, ", {o}", o = display_comma_separated(options))?; + } + write!(f, " )")?; + Ok(()) + } Statement::AlterTable { name, if_exists, @@ -3636,6 +3752,26 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DropSecret { + if_exists, + temporary, + name, + storage_specifier, + } => { + write!(f, "DROP ")?; + if let Some(t) = temporary { + write!(f, "{}", if *t { "TEMPORARY " } else { "PERSISTENT " })?; + } + write!( + f, + "SECRET {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + )?; + if let Some(s) = storage_specifier { + write!(f, " FROM {s}")?; + } + Ok(()) + } Statement::Discard { object_type } => { write!(f, "DISCARD {object_type}")?; Ok(()) @@ -5070,6 +5206,39 @@ impl fmt::Display for SqlOption { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SecretOption { + pub key: Ident, + pub value: Ident, +} + +impl fmt::Display for SecretOption { + 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))] +pub enum AttachDuckDBDatabaseOption { + ReadOnly(Option), + Type(Ident), +} + +impl fmt::Display for AttachDuckDBDatabaseOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + 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), + } + } +} + #[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 91842672d..12a376b2a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -224,6 +224,7 @@ define_keywords!( DEREF, DESC, DESCRIBE, + DETACH, DETAIL, DETERMINISTIC, DIRECTORY, @@ -514,6 +515,7 @@ define_keywords!( PERCENTILE_DISC, PERCENT_RANK, PERIOD, + PERSISTENT, PIVOT, PLACING, PLANS, @@ -543,6 +545,7 @@ define_keywords!( RCFILE, READ, READS, + READ_ONLY, REAL, RECURSIVE, REF, @@ -601,6 +604,7 @@ define_keywords!( SCROLL, SEARCH, SECOND, + SECRET, SECURITY, SELECT, SEMI, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7bd3ffb21..6fce36844 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -473,7 +473,16 @@ impl<'a> Parser<'a> { Ok(Statement::Query(self.parse_boxed_query()?)) } Keyword::TRUNCATE => Ok(self.parse_truncate()?), - Keyword::ATTACH => Ok(self.parse_attach_database()?), + Keyword::ATTACH => { + if dialect_of!(self is DuckDbDialect) { + Ok(self.parse_attach_duckdb_database()?) + } else { + Ok(self.parse_attach_database()?) + } + } + Keyword::DETACH if dialect_of!(self is DuckDbDialect | GenericDialect) => { + Ok(self.parse_detach_duckdb_database()?) + } Keyword::MSCK => Ok(self.parse_msck()?), Keyword::CREATE => Ok(self.parse_create()?), Keyword::CACHE => Ok(self.parse_cache_table()?), @@ -666,6 +675,72 @@ impl<'a> Parser<'a> { }) } + pub fn parse_attach_duckdb_database_options( + &mut self, + ) -> Result, ParserError> { + if !self.consume_token(&Token::LParen) { + return Ok(vec![]); + } + + let mut options = vec![]; + loop { + if self.parse_keyword(Keyword::READ_ONLY) { + let boolean = if self.parse_keyword(Keyword::TRUE) { + Some(true) + } else if self.parse_keyword(Keyword::FALSE) { + Some(false) + } else { + None + }; + options.push(AttachDuckDBDatabaseOption::ReadOnly(boolean)); + } else if self.parse_keyword(Keyword::TYPE) { + let ident = self.parse_identifier(false)?; + options.push(AttachDuckDBDatabaseOption::Type(ident)); + } else { + return self.expected("expected one of: ), READ_ONLY, TYPE", self.peek_token()); + }; + + if self.consume_token(&Token::RParen) { + return Ok(options); + } else if self.consume_token(&Token::Comma) { + continue; + } else { + return self.expected("expected one of: ')', ','", self.peek_token()); + } + } + } + + pub fn parse_attach_duckdb_database(&mut self) -> Result { + let database = self.parse_keyword(Keyword::DATABASE); + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let database_path = self.parse_identifier(false)?; + let database_alias = if self.parse_keyword(Keyword::AS) { + Some(self.parse_identifier(false)?) + } else { + None + }; + + let attach_options = self.parse_attach_duckdb_database_options()?; + Ok(Statement::AttachDuckDBDatabase { + if_not_exists, + database, + database_path, + database_alias, + attach_options, + }) + } + + pub fn parse_detach_duckdb_database(&mut self) -> Result { + let database = self.parse_keyword(Keyword::DATABASE); + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let database_alias = self.parse_identifier(false)?; + Ok(Statement::DetachDuckDBDatabase { + if_exists, + database, + database_alias, + }) + } + pub fn parse_attach_database(&mut self) -> Result { let database = self.parse_keyword(Keyword::DATABASE); let database_file_name = self.parse_expr()?; @@ -3075,6 +3150,8 @@ impl<'a> Parser<'a> { let temporary = self .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) .is_some(); + let persistent = dialect_of!(self is DuckDbDialect) + && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); if self.parse_keyword(Keyword::TABLE) { self.parse_create_table(or_replace, temporary, global, transient) } else if self.parse_keyword(Keyword::MATERIALIZED) || self.parse_keyword(Keyword::VIEW) { @@ -3086,6 +3163,8 @@ impl<'a> Parser<'a> { self.parse_create_function(or_replace, temporary) } else if self.parse_keyword(Keyword::MACRO) { self.parse_create_macro(or_replace, temporary) + } else if self.parse_keyword(Keyword::SECRET) { + self.parse_create_secret(or_replace, temporary, persistent) } else if or_replace { self.expected( "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE", @@ -3116,6 +3195,65 @@ impl<'a> Parser<'a> { } } + /// See [DuckDB Docs](https://duckdb.org/docs/sql/statements/create_secret.html) for more details. + pub fn parse_create_secret( + &mut self, + or_replace: bool, + temporary: bool, + persistent: bool, + ) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + + let mut storage_specifier = None; + let mut name = None; + if self.peek_token() != Token::LParen { + if self.parse_keyword(Keyword::IN) { + storage_specifier = self.parse_identifier(false).ok() + } else { + name = self.parse_identifier(false).ok(); + } + + // Storage specifier may follow the name + if storage_specifier.is_none() + && self.peek_token() != Token::LParen + && self.parse_keyword(Keyword::IN) + { + storage_specifier = self.parse_identifier(false).ok(); + } + } + + self.expect_token(&Token::LParen)?; + self.expect_keyword(Keyword::TYPE)?; + let secret_type = self.parse_identifier(false)?; + + let mut options = Vec::new(); + if self.consume_token(&Token::Comma) { + options.append(&mut self.parse_comma_separated(|p| { + let key = p.parse_identifier(false)?; + let value = p.parse_identifier(false)?; + Ok(SecretOption { key, value }) + })?); + } + self.expect_token(&Token::RParen)?; + + let temp = match (temporary, persistent) { + (true, false) => Some(true), + (false, true) => Some(false), + (false, false) => None, + _ => self.expected("TEMPORARY or PERSISTENT", self.peek_token())?, + }; + + Ok(Statement::CreateSecret { + or_replace, + temporary: temp, + if_not_exists, + name, + storage_specifier, + secret_type, + options, + }) + } + /// Parse a CACHE TABLE statement pub fn parse_cache_table(&mut self) -> Result { let (mut table_flag, mut options, mut has_as, mut query) = (None, vec![], false, None); @@ -3889,8 +4027,10 @@ impl<'a> Parser<'a> { pub fn parse_drop(&mut self) -> Result { // MySQL dialect supports `TEMPORARY` - let temporary = dialect_of!(self is MySqlDialect | GenericDialect) + let temporary = dialect_of!(self is MySqlDialect | GenericDialect | DuckDbDialect) && self.parse_keyword(Keyword::TEMPORARY); + let persistent = dialect_of!(self is DuckDbDialect) + && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); let object_type = if self.parse_keyword(Keyword::TABLE) { ObjectType::Table @@ -3908,6 +4048,8 @@ impl<'a> Parser<'a> { ObjectType::Stage } else if self.parse_keyword(Keyword::FUNCTION) { return self.parse_drop_function(); + } else if self.parse_keyword(Keyword::SECRET) { + return self.parse_drop_secret(temporary, persistent); } else { return self.expected( "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION, STAGE or SEQUENCE after DROP", @@ -3980,6 +4122,34 @@ impl<'a> Parser<'a> { Ok(DropFunctionDesc { name, args }) } + /// See [DuckDB Docs](https://duckdb.org/docs/sql/statements/create_secret.html) for more details. + fn parse_drop_secret( + &mut self, + temporary: bool, + persistent: bool, + ) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier(false)?; + let storage_specifier = if self.parse_keyword(Keyword::FROM) { + self.parse_identifier(false).ok() + } else { + None + }; + let temp = match (temporary, persistent) { + (true, false) => Some(true), + (false, true) => Some(false), + (false, false) => None, + _ => self.expected("TEMPORARY or PERSISTENT", self.peek_token())?, + }; + + Ok(Statement::DropSecret { + if_exists, + temporary: temp, + name, + storage_specifier, + }) + } + /// Parse a `DECLARE` statement. /// /// ```sql diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index e41109d95..d6a6b7d4b 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -334,6 +334,147 @@ fn test_duckdb_struct_literal() { ); } +#[test] +fn test_create_secret() { + let sql = r#"CREATE OR REPLACE PERSISTENT SECRET IF NOT EXISTS name IN storage ( TYPE type, key1 value1, key2 value2 )"#; + let stmt = duckdb().verified_stmt(sql); + assert_eq!( + Statement::CreateSecret { + or_replace: true, + temporary: Some(false), + if_not_exists: true, + name: Some(Ident::new("name")), + storage_specifier: Some(Ident::new("storage")), + secret_type: Ident::new("type"), + options: vec![ + SecretOption { + key: Ident::new("key1"), + value: Ident::new("value1"), + }, + SecretOption { + key: Ident::new("key2"), + value: Ident::new("value2"), + } + ] + }, + stmt + ); +} + +#[test] +fn test_create_secret_simple() { + let sql = r#"CREATE SECRET ( TYPE type )"#; + let stmt = duckdb().verified_stmt(sql); + assert_eq!( + Statement::CreateSecret { + or_replace: false, + temporary: None, + if_not_exists: false, + name: None, + storage_specifier: None, + secret_type: Ident::new("type"), + options: vec![] + }, + stmt + ); +} + +#[test] +fn test_drop_secret() { + let sql = r#"DROP PERSISTENT SECRET IF EXISTS secret FROM storage"#; + let stmt = duckdb().verified_stmt(sql); + assert_eq!( + Statement::DropSecret { + if_exists: true, + temporary: Some(false), + name: Ident::new("secret"), + storage_specifier: Some(Ident::new("storage")) + }, + stmt + ); +} + +#[test] +fn test_drop_secret_simple() { + let sql = r#"DROP SECRET secret"#; + let stmt = duckdb().verified_stmt(sql); + assert_eq!( + Statement::DropSecret { + if_exists: false, + temporary: None, + name: Ident::new("secret"), + storage_specifier: None + }, + stmt + ); +} + +#[test] +fn test_attach_database() { + let sql = r#"ATTACH DATABASE IF NOT EXISTS 'sqlite_file.db' AS sqlite_db (READ_ONLY false, TYPE SQLITE)"#; + let stmt = duckdb().verified_stmt(sql); + assert_eq!( + Statement::AttachDuckDBDatabase { + if_not_exists: true, + database: true, + database_path: Ident::with_quote('\'', "sqlite_file.db"), + database_alias: Some(Ident::new("sqlite_db")), + attach_options: vec![ + AttachDuckDBDatabaseOption::ReadOnly(Some(false)), + AttachDuckDBDatabaseOption::Type(Ident::new("SQLITE")), + ] + }, + stmt + ); +} + +#[test] +fn test_attach_database_simple() { + let sql = r#"ATTACH 'postgres://user.name:pass-word@some.url.com:5432/postgres'"#; + let stmt = duckdb().verified_stmt(sql); + assert_eq!( + Statement::AttachDuckDBDatabase { + if_not_exists: false, + database: false, + database_path: Ident::with_quote( + '\'', + "postgres://user.name:pass-word@some.url.com:5432/postgres" + ), + database_alias: None, + attach_options: vec![] + }, + stmt + ); +} + +#[test] +fn test_detach_database() { + let sql = r#"DETACH DATABASE IF EXISTS db_name"#; + let stmt = duckdb().verified_stmt(sql); + assert_eq!( + Statement::DetachDuckDBDatabase { + if_exists: true, + database: true, + database_alias: Ident::new("db_name"), + }, + stmt + ); +} + +#[test] +fn test_detach_database_simple() { + let sql = r#"DETACH db_name"#; + let stmt = duckdb().verified_stmt(sql); + assert_eq!( + Statement::DetachDuckDBDatabase { + if_exists: false, + database: false, + database_alias: Ident::new("db_name"), + }, + stmt + ); +} + #[test] fn test_duckdb_named_argument_function_with_assignment_operator() { let sql = "SELECT FUN(a := '1', b := '2') FROM foo"; From 8dd213cff28e36b98f21dca09dbb94a10b5af22b Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Tue, 9 Apr 2024 23:05:31 +0200 Subject: [PATCH 404/806] BigQuery: support unquoted hyphen in table/view declaration (#1178) --- src/parser/mod.rs | 10 +++++---- tests/sqlparser_bigquery.rs | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6fce36844..5daf861f4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3749,7 +3749,8 @@ impl<'a> Parser<'a> { && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. - let name = self.parse_object_name(false)?; + let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); + let name = self.parse_object_name(allow_unquoted_hyphen)?; let columns = self.parse_view_columns()?; let mut options = CreateTableOptions::None; let with_options = self.parse_options(Keyword::WITH)?; @@ -4736,8 +4737,9 @@ impl<'a> Parser<'a> { global: Option, transient: bool, ) -> Result { + let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let table_name = self.parse_object_name(false)?; + let table_name = self.parse_object_name(allow_unquoted_hyphen)?; // Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs let on_cluster = if self.parse_keywords(&[Keyword::ON, Keyword::CLUSTER]) { @@ -4752,13 +4754,13 @@ impl<'a> Parser<'a> { }; let like = if self.parse_keyword(Keyword::LIKE) || self.parse_keyword(Keyword::ILIKE) { - self.parse_object_name(false).ok() + self.parse_object_name(allow_unquoted_hyphen).ok() } else { None }; let clone = if self.parse_keyword(Keyword::CLONE) { - self.parse_object_name(false).ok() + self.parse_object_name(allow_unquoted_hyphen).ok() } else { None }; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 7bc715a0c..d9081461b 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -206,6 +206,51 @@ fn parse_create_view_if_not_exists() { } } +#[test] +fn parse_create_view_with_unquoted_hyphen() { + let sql = "CREATE VIEW IF NOT EXISTS my-pro-ject.mydataset.myview AS SELECT 1"; + match bigquery().verified_stmt(sql) { + Statement::CreateView { + name, + query, + if_not_exists, + .. + } => { + assert_eq!("my-pro-ject.mydataset.myview", name.to_string()); + assert_eq!("SELECT 1", query.to_string()); + assert!(if_not_exists); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_table_with_unquoted_hyphen() { + let sql = "CREATE TABLE my-pro-ject.mydataset.mytable (x INT64)"; + match bigquery().verified_stmt(sql) { + Statement::CreateTable { name, columns, .. } => { + assert_eq!( + name, + ObjectName(vec![ + "my-pro-ject".into(), + "mydataset".into(), + "mytable".into() + ]) + ); + assert_eq!( + vec![ColumnDef { + name: Ident::new("x"), + data_type: DataType::Int64, + collation: None, + options: vec![] + },], + columns + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_with_options() { let sql = concat!( From 127be973692153a141eeaf4c8d21b418ba2ffb9a Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Tue, 9 Apr 2024 23:16:03 +0200 Subject: [PATCH 405/806] Support more `DateTimeField` variants (#1191) --- src/ast/mod.rs | 6 +- src/ast/value.rs | 114 +++++++++++++++++++++++------------ src/parser/mod.rs | 19 +++++- tests/sqlparser_bigquery.rs | 13 ++++ tests/sqlparser_common.rs | 10 ++- tests/sqlparser_snowflake.rs | 13 ++++ 6 files changed, 129 insertions(+), 46 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8fc696baa..c5386f878 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -229,7 +229,7 @@ impl fmt::Display for Interval { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let value = self.value.as_ref(); match ( - self.leading_field, + &self.leading_field, self.leading_precision, self.fractional_seconds_precision, ) { @@ -248,13 +248,13 @@ impl fmt::Display for Interval { } _ => { write!(f, "INTERVAL {value}")?; - if let Some(leading_field) = self.leading_field { + if let Some(leading_field) = &self.leading_field { write!(f, " {leading_field}")?; } if let Some(leading_precision) = self.leading_precision { write!(f, " ({leading_precision})")?; } - if let Some(last_field) = self.last_field { + if let Some(last_field) = &self.last_field { write!(f, " TO {last_field}")?; } if let Some(fractional_seconds_precision) = self.fractional_seconds_precision { diff --git a/src/ast/value.rs b/src/ast/value.rs index a9c74d4a8..d596cd648 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -12,6 +12,13 @@ #[cfg(not(feature = "std"))] use alloc::string::String; + +#[cfg(not(feature = "std"))] +use alloc::format; + +#[cfg(not(feature = "std"))] +use alloc::string::ToString; + use core::fmt; #[cfg(feature = "bigdecimal")] @@ -20,6 +27,7 @@ use bigdecimal::BigDecimal; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::ast::Ident; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; @@ -109,17 +117,25 @@ impl fmt::Display for DollarQuotedString { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum DateTimeField { Year, Month, - Week, + /// Week optionally followed by a WEEKDAY. + /// + /// ```sql + /// WEEK(MONDAY) + /// ``` + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/date_functions#extract) + Week(Option), Day, DayOfWeek, DayOfYear, Date, + Datetime, Hour, Minute, Second, @@ -148,47 +164,67 @@ pub enum DateTimeField { TimezoneMinute, TimezoneRegion, NoDateTime, + /// Arbitrary abbreviation or custom date-time part. + /// + /// ```sql + /// EXTRACT(q FROM CURRENT_TIMESTAMP) + /// ``` + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/functions-date-time#supported-date-and-time-parts) + Custom(Ident), } impl fmt::Display for DateTimeField { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match self { - DateTimeField::Year => "YEAR", - DateTimeField::Month => "MONTH", - DateTimeField::Week => "WEEK", - DateTimeField::Day => "DAY", - DateTimeField::DayOfWeek => "DAYOFWEEK", - DateTimeField::DayOfYear => "DAYOFYEAR", - DateTimeField::Date => "DATE", - DateTimeField::Hour => "HOUR", - DateTimeField::Minute => "MINUTE", - DateTimeField::Second => "SECOND", - DateTimeField::Century => "CENTURY", - DateTimeField::Decade => "DECADE", - DateTimeField::Dow => "DOW", - DateTimeField::Doy => "DOY", - DateTimeField::Epoch => "EPOCH", - DateTimeField::Isodow => "ISODOW", - DateTimeField::Isoyear => "ISOYEAR", - DateTimeField::IsoWeek => "ISOWEEK", - DateTimeField::Julian => "JULIAN", - DateTimeField::Microsecond => "MICROSECOND", - DateTimeField::Microseconds => "MICROSECONDS", - DateTimeField::Millenium => "MILLENIUM", - DateTimeField::Millennium => "MILLENNIUM", - DateTimeField::Millisecond => "MILLISECOND", - DateTimeField::Milliseconds => "MILLISECONDS", - DateTimeField::Nanosecond => "NANOSECOND", - DateTimeField::Nanoseconds => "NANOSECONDS", - DateTimeField::Quarter => "QUARTER", - DateTimeField::Time => "TIME", - DateTimeField::Timezone => "TIMEZONE", - DateTimeField::TimezoneAbbr => "TIMEZONE_ABBR", - DateTimeField::TimezoneHour => "TIMEZONE_HOUR", - DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE", - DateTimeField::TimezoneRegion => "TIMEZONE_REGION", - DateTimeField::NoDateTime => "NODATETIME", - }) + f.write_str( + match self { + DateTimeField::Year => "YEAR".to_string(), + DateTimeField::Month => "MONTH".to_string(), + DateTimeField::Week(week_day) => { + format!( + "WEEK{}", + week_day + .as_ref() + .map(|w| format!("({w})")) + .unwrap_or_default() + ) + } + DateTimeField::Day => "DAY".to_string(), + DateTimeField::DayOfWeek => "DAYOFWEEK".to_string(), + DateTimeField::DayOfYear => "DAYOFYEAR".to_string(), + DateTimeField::Date => "DATE".to_string(), + DateTimeField::Datetime => "DATETIME".to_string(), + DateTimeField::Hour => "HOUR".to_string(), + DateTimeField::Minute => "MINUTE".to_string(), + DateTimeField::Second => "SECOND".to_string(), + DateTimeField::Century => "CENTURY".to_string(), + DateTimeField::Decade => "DECADE".to_string(), + DateTimeField::Dow => "DOW".to_string(), + DateTimeField::Doy => "DOY".to_string(), + DateTimeField::Epoch => "EPOCH".to_string(), + DateTimeField::Isodow => "ISODOW".to_string(), + DateTimeField::Isoyear => "ISOYEAR".to_string(), + DateTimeField::IsoWeek => "ISOWEEK".to_string(), + DateTimeField::Julian => "JULIAN".to_string(), + DateTimeField::Microsecond => "MICROSECOND".to_string(), + DateTimeField::Microseconds => "MICROSECONDS".to_string(), + DateTimeField::Millenium => "MILLENIUM".to_string(), + DateTimeField::Millennium => "MILLENNIUM".to_string(), + DateTimeField::Millisecond => "MILLISECOND".to_string(), + DateTimeField::Milliseconds => "MILLISECONDS".to_string(), + DateTimeField::Nanosecond => "NANOSECOND".to_string(), + DateTimeField::Nanoseconds => "NANOSECONDS".to_string(), + DateTimeField::Quarter => "QUARTER".to_string(), + DateTimeField::Time => "TIME".to_string(), + DateTimeField::Timezone => "TIMEZONE".to_string(), + DateTimeField::TimezoneAbbr => "TIMEZONE_ABBR".to_string(), + DateTimeField::TimezoneHour => "TIMEZONE_HOUR".to_string(), + DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE".to_string(), + DateTimeField::TimezoneRegion => "TIMEZONE_REGION".to_string(), + DateTimeField::NoDateTime => "NODATETIME".to_string(), + DateTimeField::Custom(custom) => format!("{custom}"), + } + .as_str(), + ) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5daf861f4..7cdf07bd7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1857,11 +1857,23 @@ impl<'a> Parser<'a> { Token::Word(w) => match w.keyword { Keyword::YEAR => Ok(DateTimeField::Year), Keyword::MONTH => Ok(DateTimeField::Month), - Keyword::WEEK => Ok(DateTimeField::Week), + Keyword::WEEK => { + let week_day = if dialect_of!(self is BigQueryDialect | GenericDialect) + && self.consume_token(&Token::LParen) + { + let week_day = self.parse_identifier(false)?; + self.expect_token(&Token::RParen)?; + Some(week_day) + } else { + None + }; + Ok(DateTimeField::Week(week_day)) + } Keyword::DAY => Ok(DateTimeField::Day), Keyword::DAYOFWEEK => Ok(DateTimeField::DayOfWeek), Keyword::DAYOFYEAR => Ok(DateTimeField::DayOfYear), Keyword::DATE => Ok(DateTimeField::Date), + Keyword::DATETIME => Ok(DateTimeField::Datetime), Keyword::HOUR => Ok(DateTimeField::Hour), Keyword::MINUTE => Ok(DateTimeField::Minute), Keyword::SECOND => Ok(DateTimeField::Second), @@ -1889,6 +1901,11 @@ impl<'a> Parser<'a> { Keyword::TIMEZONE_HOUR => Ok(DateTimeField::TimezoneHour), Keyword::TIMEZONE_MINUTE => Ok(DateTimeField::TimezoneMinute), Keyword::TIMEZONE_REGION => Ok(DateTimeField::TimezoneRegion), + _ if dialect_of!(self is SnowflakeDialect | GenericDialect) => { + self.prev_token(); + let custom = self.parse_identifier(false)?; + Ok(DateTimeField::Custom(custom)) + } _ => self.expected("date/time field", next_token), }, _ => self.expected("date/time field", next_token), diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index d9081461b..391f97517 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1462,6 +1462,19 @@ fn test_bigquery_trim() { ); } +#[test] +fn parse_extract_weekday() { + let sql = "SELECT EXTRACT(WEEK(MONDAY) FROM d)"; + let select = bigquery_and_generic().verified_only_select(sql); + assert_eq!( + &Expr::Extract { + field: DateTimeField::Week(Some(Ident::new("MONDAY"))), + expr: Box::new(Expr::Identifier(Ident::new("d"))), + }, + expr_from_projection(only(&select.projection)), + ); +} + #[test] fn test_select_as_struct() { bigquery().verified_only_select("SELECT * FROM (SELECT AS VALUE STRUCT(123 AS a, false AS b))"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c8551e1fe..c67dcb5b6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2271,6 +2271,7 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(DAYOFWEEK FROM d)"); verified_stmt("SELECT EXTRACT(DAYOFYEAR FROM d)"); verified_stmt("SELECT EXTRACT(DATE FROM d)"); + verified_stmt("SELECT EXTRACT(DATETIME FROM d)"); verified_stmt("SELECT EXTRACT(HOUR FROM d)"); verified_stmt("SELECT EXTRACT(MINUTE FROM d)"); verified_stmt("SELECT EXTRACT(SECOND FROM d)"); @@ -2300,7 +2301,8 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(TIMEZONE_REGION FROM d)"); verified_stmt("SELECT EXTRACT(TIME FROM d)"); - let res = parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)"); + let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let res = dialects.parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)"); assert_eq!( ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), res.unwrap_err() @@ -2338,7 +2340,8 @@ fn parse_ceil_datetime() { verified_stmt("SELECT CEIL(d TO SECOND) FROM df"); verified_stmt("SELECT CEIL(d TO MILLISECOND) FROM df"); - let res = parse_sql_statements("SELECT CEIL(d TO JIFFY) FROM df"); + let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let res = dialects.parse_sql_statements("SELECT CEIL(d TO JIFFY) FROM df"); assert_eq!( ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), res.unwrap_err() @@ -2364,7 +2367,8 @@ fn parse_floor_datetime() { verified_stmt("SELECT FLOOR(d TO SECOND) FROM df"); verified_stmt("SELECT FLOOR(d TO MILLISECOND) FROM df"); - let res = parse_sql_statements("SELECT FLOOR(d TO JIFFY) FROM df"); + let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let res = dialects.parse_sql_statements("SELECT FLOOR(d TO JIFFY) FROM df"); assert_eq!( ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), res.unwrap_err() diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 880129f82..5c13457b6 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1472,6 +1472,19 @@ fn parse_top() { ); } +#[test] +fn parse_extract_custom_part() { + let sql = "SELECT EXTRACT(eod FROM d)"; + let select = snowflake_and_generic().verified_only_select(sql); + assert_eq!( + &Expr::Extract { + field: DateTimeField::Custom(Ident::new("eod")), + expr: Box::new(Expr::Identifier(Ident::new("d"))), + }, + expr_from_projection(only(&select.projection)), + ); +} + #[test] fn parse_comma_outer_join() { // compound identifiers From eda86d8ed79978872dcc5ac729ab1b95da3d6ed4 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Tue, 9 Apr 2024 23:21:22 +0200 Subject: [PATCH 406/806] Add support for arbitrary map access expr (#1179) --- src/ast/mod.rs | 46 ++++++++++++++++----- src/parser/mod.rs | 77 ++++++++++++++++------------------- tests/sqlparser_bigquery.rs | 71 ++++++++++++++++++-------------- tests/sqlparser_clickhouse.rs | 74 +++++++++++++++++---------------- tests/sqlparser_common.rs | 42 +++++++++++++++++++ 5 files changed, 194 insertions(+), 116 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c5386f878..e02741aac 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -374,6 +374,40 @@ pub enum CastFormat { ValueAtTimeZone(Value, Value), } +/// Represents the syntax/style used in a map access. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MapAccessSyntax { + /// Access using bracket notation. `mymap[mykey]` + Bracket, + /// Access using period notation. `mymap.mykey` + Period, +} + +/// Expression used to access a value in a nested structure. +/// +/// Example: `SAFE_OFFSET(0)` in +/// ```sql +/// SELECT mymap[SAFE_OFFSET(0)]; +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MapAccessKey { + pub key: Expr, + pub syntax: MapAccessSyntax, +} + +impl fmt::Display for MapAccessKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.syntax { + MapAccessSyntax::Bracket => write!(f, "[{}]", self.key), + MapAccessSyntax::Period => write!(f, ".{}", self.key), + } + } +} + /// An SQL expression of any type. /// /// The parser does not distinguish between expressions of different types @@ -638,7 +672,7 @@ pub enum Expr { /// MapAccess { column: Box, - keys: Vec, + keys: Vec, }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), @@ -774,15 +808,7 @@ impl fmt::Display for Expr { match self { Expr::Identifier(s) => write!(f, "{s}"), Expr::MapAccess { column, keys } => { - write!(f, "{column}")?; - for k in keys { - match k { - k @ Expr::Value(Value::Number(_, _)) => write!(f, "[{k}]")?, - Expr::Value(Value::SingleQuotedString(s)) => write!(f, "[\"{s}\"]")?, - _ => write!(f, "[{k}]")?, - } - } - Ok(()) + write!(f, "{column}{}", display_separated(keys, "")) } Expr::Wildcard => f.write_str("*"), Expr::QualifiedWildcard(prefix) => write!(f, "{}.*", prefix), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7cdf07bd7..5bae7a133 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2608,23 +2608,43 @@ impl<'a> Parser<'a> { } pub fn parse_map_access(&mut self, expr: Expr) -> Result { - let key = self.parse_map_key()?; - let tok = self.consume_token(&Token::RBracket); - debug!("Tok: {}", tok); - let mut key_parts: Vec = vec![key]; - while self.consume_token(&Token::LBracket) { - let key = self.parse_map_key()?; - let tok = self.consume_token(&Token::RBracket); - debug!("Tok: {}", tok); - key_parts.push(key); - } - match expr { - e @ Expr::Identifier(_) | e @ Expr::CompoundIdentifier(_) => Ok(Expr::MapAccess { - column: Box::new(e), - keys: key_parts, - }), - _ => Ok(expr), + let key = self.parse_expr()?; + self.expect_token(&Token::RBracket)?; + + let mut keys = vec![MapAccessKey { + key, + syntax: MapAccessSyntax::Bracket, + }]; + loop { + let key = match self.peek_token().token { + Token::LBracket => { + self.next_token(); // consume `[` + let key = self.parse_expr()?; + self.expect_token(&Token::RBracket)?; + MapAccessKey { + key, + syntax: MapAccessSyntax::Bracket, + } + } + // Access on BigQuery nested and repeated expressions can + // mix notations in the same expression. + // https://cloud.google.com/bigquery/docs/nested-repeated#query_nested_and_repeated_columns + Token::Period if dialect_of!(self is BigQueryDialect) => { + self.next_token(); // consume `.` + MapAccessKey { + key: self.parse_expr()?, + syntax: MapAccessSyntax::Period, + } + } + _ => break, + }; + keys.push(key); } + + Ok(Expr::MapAccess { + column: Box::new(expr), + keys, + }) } /// Parses the parens following the `[ NOT ] IN` operator @@ -6329,31 +6349,6 @@ impl<'a> Parser<'a> { } } - /// Parse a map key string - pub fn parse_map_key(&mut self) -> Result { - let next_token = self.next_token(); - match next_token.token { - // handle bigquery offset subscript operator which overlaps with OFFSET operator - Token::Word(Word { value, keyword, .. }) - if (dialect_of!(self is BigQueryDialect) && keyword == Keyword::OFFSET) => - { - self.parse_function(ObjectName(vec![Ident::new(value)])) - } - Token::Word(Word { value, keyword, .. }) if (keyword == Keyword::NoKeyword) => { - if self.peek_token() == Token::LParen { - return self.parse_function(ObjectName(vec![Ident::new(value)])); - } - Ok(Expr::Value(Value::SingleQuotedString(value))) - } - Token::SingleQuotedString(s) => Ok(Expr::Value(Value::SingleQuotedString(s))), - #[cfg(not(feature = "bigdecimal"))] - Token::Number(s, _) => Ok(Expr::Value(Value::Number(s, false))), - #[cfg(feature = "bigdecimal")] - Token::Number(s, _) => Ok(Expr::Value(Value::Number(s.parse().unwrap(), false))), - _ => self.expected("literal string, number or function", next_token), - } - } - /// Parse a SQL datatype (in the context of a CREATE TABLE statement for example) pub fn parse_data_type(&mut self) -> Result { let (ty, trailing_bracket) = self.parse_data_type_helper()?; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 391f97517..c8f1bb7c1 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1402,39 +1402,48 @@ fn bigquery_and_generic() -> TestedDialects { } #[test] -fn parse_map_access_offset() { - let sql = "SELECT d[offset(0)]"; - let _select = bigquery().verified_only_select(sql); - assert_eq!( - _select.projection[0], - SelectItem::UnnamedExpr(Expr::MapAccess { - column: Box::new(Expr::Identifier(Ident { - value: "d".to_string(), - quote_style: None, - })), - keys: vec![Expr::Function(Function { - name: ObjectName(vec!["offset".into()]), - args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - number("0") - ))),], - null_treatment: None, - filter: None, - over: None, - distinct: false, - special: false, - order_by: vec![], - })], - }) - ); +fn parse_map_access_expr() { + let sql = "users[-1][safe_offset(2)].a.b"; + let expr = bigquery().verified_expr(sql); - // test other operators - for sql in [ - "SELECT d[SAFE_OFFSET(0)]", - "SELECT d[ORDINAL(0)]", - "SELECT d[SAFE_ORDINAL(0)]", - ] { - bigquery().verified_only_select(sql); + fn map_access_key(key: Expr, syntax: MapAccessSyntax) -> MapAccessKey { + MapAccessKey { key, syntax } } + let expected = Expr::MapAccess { + column: Expr::Identifier(Ident::new("users")).into(), + keys: vec![ + map_access_key( + Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Expr::Value(number("1")).into(), + }, + MapAccessSyntax::Bracket, + ), + map_access_key( + Expr::Function(Function { + name: ObjectName(vec![Ident::new("safe_offset")]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + number("2"), + )))], + filter: None, + null_treatment: None, + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + MapAccessSyntax::Bracket, + ), + map_access_key( + Expr::CompoundIdentifier(vec![Ident::new("a"), Ident::new("b")]), + MapAccessSyntax::Period, + ), + ], + }; + assert_eq!(expr, expected); + + let sql = "SELECT myfunc()[-1].a[SAFE_OFFSET(2)].b"; + bigquery().verified_only_select(sql); } #[test] diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 1cbe34c5c..a3fcc612b 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -39,23 +39,26 @@ fn parse_map_access_expr() { value: "string_values".to_string(), quote_style: None, })), - keys: vec![Expr::Function(Function { - name: ObjectName(vec!["indexOf".into()]), - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new( - "string_names" - )))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("endpoint".to_string()) - ))), - ], - null_treatment: None, - filter: None, - over: None, - distinct: false, - special: false, - order_by: vec![], - })], + keys: vec![MapAccessKey { + key: Expr::Function(Function { + name: ObjectName(vec!["indexOf".into()]), + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier( + Ident::new("string_names") + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("endpoint".to_string()) + ))), + ], + null_treatment: None, + filter: None, + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + syntax: MapAccessSyntax::Bracket + }], })], into: None, from: vec![TableWithJoins { @@ -80,23 +83,26 @@ fn parse_map_access_expr() { right: Box::new(BinaryOp { left: Box::new(MapAccess { column: Box::new(Identifier(Ident::new("string_value"))), - keys: vec![Expr::Function(Function { - name: ObjectName(vec![Ident::new("indexOf")]), - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier( - Ident::new("string_name") - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("app".to_string()) - ))), - ], - null_treatment: None, - filter: None, - over: None, - distinct: false, - special: false, - order_by: vec![], - })], + keys: vec![MapAccessKey { + key: Expr::Function(Function { + name: ObjectName(vec![Ident::new("indexOf")]), + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier( + Ident::new("string_name") + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("app".to_string()) + ))), + ], + null_treatment: None, + filter: None, + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + syntax: MapAccessSyntax::Bracket + }], }), op: BinaryOperator::NotEq, right: Box::new(Expr::Value(Value::SingleQuotedString("foo".to_string()))), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c67dcb5b6..c94bd3779 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8643,3 +8643,45 @@ fn test_buffer_reuse() { p.parse_statements().unwrap(); let _ = p.into_tokens(); } + +#[test] +fn parse_map_access_expr() { + let sql = "users[-1][safe_offset(2)]"; + let dialects = TestedDialects { + dialects: vec![Box::new(BigQueryDialect {}), Box::new(ClickHouseDialect {})], + options: None, + }; + let expr = dialects.verified_expr(sql); + let expected = Expr::MapAccess { + column: Expr::Identifier(Ident::new("users")).into(), + keys: vec![ + MapAccessKey { + key: Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Expr::Value(number("1")).into(), + }, + syntax: MapAccessSyntax::Bracket, + }, + MapAccessKey { + key: Expr::Function(Function { + name: ObjectName(vec![Ident::new("safe_offset")]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + number("2"), + )))], + filter: None, + null_treatment: None, + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + syntax: MapAccessSyntax::Bracket, + }, + ], + }; + assert_eq!(expr, expected); + + for sql in ["users[1]", "a[array_length(b) - 1 + 2][c + 3][d * 4]"] { + let _ = dialects.verified_expr(sql); + } +} From a0ed14ce023ec14162ad2406fe655b487b85aa84 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 9 Apr 2024 17:23:22 -0400 Subject: [PATCH 407/806] Do not allocate in `impl Display for DateTimeField` (#1209) --- src/ast/value.rs | 97 +++++++++++++++++++++--------------------------- 1 file changed, 43 insertions(+), 54 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index d596cd648..84fdf00ae 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -13,12 +13,6 @@ #[cfg(not(feature = "std"))] use alloc::string::String; -#[cfg(not(feature = "std"))] -use alloc::format; - -#[cfg(not(feature = "std"))] -use alloc::string::ToString; - use core::fmt; #[cfg(feature = "bigdecimal")] @@ -175,56 +169,51 @@ pub enum DateTimeField { impl fmt::Display for DateTimeField { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str( - match self { - DateTimeField::Year => "YEAR".to_string(), - DateTimeField::Month => "MONTH".to_string(), - DateTimeField::Week(week_day) => { - format!( - "WEEK{}", - week_day - .as_ref() - .map(|w| format!("({w})")) - .unwrap_or_default() - ) + match self { + DateTimeField::Year => write!(f, "YEAR"), + DateTimeField::Month => write!(f, "MONTH"), + DateTimeField::Week(week_day) => { + write!(f, "WEEK")?; + if let Some(week_day) = week_day { + write!(f, "({week_day})")? } - DateTimeField::Day => "DAY".to_string(), - DateTimeField::DayOfWeek => "DAYOFWEEK".to_string(), - DateTimeField::DayOfYear => "DAYOFYEAR".to_string(), - DateTimeField::Date => "DATE".to_string(), - DateTimeField::Datetime => "DATETIME".to_string(), - DateTimeField::Hour => "HOUR".to_string(), - DateTimeField::Minute => "MINUTE".to_string(), - DateTimeField::Second => "SECOND".to_string(), - DateTimeField::Century => "CENTURY".to_string(), - DateTimeField::Decade => "DECADE".to_string(), - DateTimeField::Dow => "DOW".to_string(), - DateTimeField::Doy => "DOY".to_string(), - DateTimeField::Epoch => "EPOCH".to_string(), - DateTimeField::Isodow => "ISODOW".to_string(), - DateTimeField::Isoyear => "ISOYEAR".to_string(), - DateTimeField::IsoWeek => "ISOWEEK".to_string(), - DateTimeField::Julian => "JULIAN".to_string(), - DateTimeField::Microsecond => "MICROSECOND".to_string(), - DateTimeField::Microseconds => "MICROSECONDS".to_string(), - DateTimeField::Millenium => "MILLENIUM".to_string(), - DateTimeField::Millennium => "MILLENNIUM".to_string(), - DateTimeField::Millisecond => "MILLISECOND".to_string(), - DateTimeField::Milliseconds => "MILLISECONDS".to_string(), - DateTimeField::Nanosecond => "NANOSECOND".to_string(), - DateTimeField::Nanoseconds => "NANOSECONDS".to_string(), - DateTimeField::Quarter => "QUARTER".to_string(), - DateTimeField::Time => "TIME".to_string(), - DateTimeField::Timezone => "TIMEZONE".to_string(), - DateTimeField::TimezoneAbbr => "TIMEZONE_ABBR".to_string(), - DateTimeField::TimezoneHour => "TIMEZONE_HOUR".to_string(), - DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE".to_string(), - DateTimeField::TimezoneRegion => "TIMEZONE_REGION".to_string(), - DateTimeField::NoDateTime => "NODATETIME".to_string(), - DateTimeField::Custom(custom) => format!("{custom}"), + Ok(()) } - .as_str(), - ) + DateTimeField::Day => write!(f, "DAY"), + DateTimeField::DayOfWeek => write!(f, "DAYOFWEEK"), + DateTimeField::DayOfYear => write!(f, "DAYOFYEAR"), + DateTimeField::Date => write!(f, "DATE"), + DateTimeField::Datetime => write!(f, "DATETIME"), + DateTimeField::Hour => write!(f, "HOUR"), + DateTimeField::Minute => write!(f, "MINUTE"), + DateTimeField::Second => write!(f, "SECOND"), + DateTimeField::Century => write!(f, "CENTURY"), + DateTimeField::Decade => write!(f, "DECADE"), + DateTimeField::Dow => write!(f, "DOW"), + DateTimeField::Doy => write!(f, "DOY"), + DateTimeField::Epoch => write!(f, "EPOCH"), + DateTimeField::Isodow => write!(f, "ISODOW"), + DateTimeField::Isoyear => write!(f, "ISOYEAR"), + DateTimeField::IsoWeek => write!(f, "ISOWEEK"), + DateTimeField::Julian => write!(f, "JULIAN"), + DateTimeField::Microsecond => write!(f, "MICROSECOND"), + DateTimeField::Microseconds => write!(f, "MICROSECONDS"), + DateTimeField::Millenium => write!(f, "MILLENIUM"), + DateTimeField::Millennium => write!(f, "MILLENNIUM"), + DateTimeField::Millisecond => write!(f, "MILLISECOND"), + DateTimeField::Milliseconds => write!(f, "MILLISECONDS"), + DateTimeField::Nanosecond => write!(f, "NANOSECOND"), + DateTimeField::Nanoseconds => write!(f, "NANOSECONDS"), + DateTimeField::Quarter => write!(f, "QUARTER"), + DateTimeField::Time => write!(f, "TIME"), + DateTimeField::Timezone => write!(f, "TIMEZONE"), + DateTimeField::TimezoneAbbr => write!(f, "TIMEZONE_ABBR"), + DateTimeField::TimezoneHour => write!(f, "TIMEZONE_HOUR"), + DateTimeField::TimezoneMinute => write!(f, "TIMEZONE_MINUTE"), + DateTimeField::TimezoneRegion => write!(f, "TIMEZONE_REGION"), + DateTimeField::NoDateTime => write!(f, "NODATETIME"), + DateTimeField::Custom(custom) => write!(f, "{custom}"), + } } } From e5c860213b1d1c73a0090f4ca16023cbdbf81b58 Mon Sep 17 00:00:00 2001 From: ZacJW Date: Fri, 12 Apr 2024 11:38:04 +0100 Subject: [PATCH 408/806] Fix dollar quoted string tokenizer (#1193) --- src/tokenizer.rs | 130 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 25 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 1ceec705b..b239d990e 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1119,38 +1119,49 @@ impl<'a> Tokenizer<'a> { if let Some('$') = chars.peek() { chars.next(); - s.push_str(&peeking_take_while(chars, |ch| ch != '$')); - match chars.peek() { - Some('$') => { - chars.next(); - for c in value.chars() { - let next_char = chars.next(); - if Some(c) != next_char { - return self.tokenizer_error( - chars.location(), - format!( - "Unterminated dollar-quoted string at or near \"{value}\"" - ), - ); + 'searching_for_end: loop { + s.push_str(&peeking_take_while(chars, |ch| ch != '$')); + match chars.peek() { + Some('$') => { + chars.next(); + let mut maybe_s = String::from("$"); + for c in value.chars() { + if let Some(next_char) = chars.next() { + maybe_s.push(next_char); + if next_char != c { + // This doesn't match the dollar quote delimiter so this + // is not the end of the string. + s.push_str(&maybe_s); + continue 'searching_for_end; + } + } else { + return self.tokenizer_error( + chars.location(), + "Unterminated dollar-quoted, expected $", + ); + } + } + if chars.peek() == Some(&'$') { + chars.next(); + maybe_s.push('$'); + // maybe_s matches the end delimiter + break 'searching_for_end; + } else { + // This also doesn't match the dollar quote delimiter as there are + // more characters before the second dollar so this is not the end + // of the string. + s.push_str(&maybe_s); + continue 'searching_for_end; } } - - if let Some('$') = chars.peek() { - chars.next(); - } else { + _ => { return self.tokenizer_error( chars.location(), - "Unterminated dollar-quoted string, expected $", - ); + "Unterminated dollar-quoted, expected $", + ) } } - _ => { - return self.tokenizer_error( - chars.location(), - "Unterminated dollar-quoted, expected $", - ); - } } } else { return Ok(Token::Placeholder(String::from("$") + &value)); @@ -1906,6 +1917,75 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_dollar_quoted_string_tagged() { + let sql = String::from( + "SELECT $tag$dollar '$' quoted strings have $tags like this$ or like this $$$tag$", + ); + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::DollarQuotedString(DollarQuotedString { + value: "dollar '$' quoted strings have $tags like this$ or like this $$".into(), + tag: Some("tag".into()), + }), + ]; + compare(expected, tokens); + } + + #[test] + fn tokenize_dollar_quoted_string_tagged_unterminated() { + let sql = String::from("SELECT $tag$dollar '$' quoted strings have $tags like this$ or like this $$$different tag$"); + let dialect = GenericDialect {}; + assert_eq!( + Tokenizer::new(&dialect, &sql).tokenize(), + Err(TokenizerError { + message: "Unterminated dollar-quoted, expected $".into(), + location: Location { + line: 1, + column: 91 + } + }) + ); + } + + #[test] + fn tokenize_dollar_quoted_string_untagged() { + let sql = + String::from("SELECT $$within dollar '$' quoted strings have $tags like this$ $$"); + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::DollarQuotedString(DollarQuotedString { + value: "within dollar '$' quoted strings have $tags like this$ ".into(), + tag: None, + }), + ]; + compare(expected, tokens); + } + + #[test] + fn tokenize_dollar_quoted_string_untagged_unterminated() { + let sql = String::from( + "SELECT $$dollar '$' quoted strings have $tags like this$ or like this $different tag$", + ); + let dialect = GenericDialect {}; + assert_eq!( + Tokenizer::new(&dialect, &sql).tokenize(), + Err(TokenizerError { + message: "Unterminated dollar-quoted string".into(), + location: Location { + line: 1, + column: 86 + } + }) + ); + } + #[test] fn tokenize_right_arrow() { let sql = String::from("FUNCTION(key=>value)"); From acc5dd937622b60a1f47c3d1679df6ca94f1198b Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 12 Apr 2024 06:52:11 -0400 Subject: [PATCH 409/806] CHANGELOG for 0.45.0 (#1213) --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c9b22db..ecd57703c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,37 @@ changes that break via addition as "Added". ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.45.0] 2024-04-12 + +### Added +* Support `DateTimeField` variants: `CUSTOM` and `WEEK(MONDAY)` (#1191) - Thanks @iffyio +* Support for arbitrary expr in `MapAccessSyntax` (#1179) - Thanks @iffyio +* Support unquoted hyphen in table/view declaration for BigQuery (#1178) - Thanks @iffyio +* Support `CREATE/DROP SECRET` for duckdb dialect (#1208) - Thanks @JichaoS +* Support MySQL `UNIQUE` table constraint (#1164) - Thanks @Nikita-str +* Support tailing commas on Snowflake. (#1205) - Thanks @yassun7010 +* Support `[FIRST | AFTER column_name]` in `ALTER TABLE` for MySQL (#1180) - Thanks @xring +* Support inline comment with hash syntax for BigQuery (#1192) - Thanks @iffyio +* Support named windows in OVER (window_definition) clause (#1166) - Thanks @Nikita-str +* Support PARALLEL ... and for ..ON NULL INPUT ... to CREATE FUNCTION` (#1202) - Thanks @dimfeld +* Support DuckDB functions named arguments with assignment operator (#1195) - Thanks @alamb +* Support DuckDB struct literal syntax (#1194) - Thanks @gstvg +* Support `$$` in generic dialect ... (#1185)- Thanks @milenkovicm +* Support row_alias and col_aliases in `INSERT` statement for MySQL and Generic dialects (#1136) - Thanks @emin100 + +### Fixed +* Fix dollar quoted string tokenizer (#1193) - Thanks @ZacJW +* Do not allocate in `impl Display` for `DateTimeField` (#1209) - Thanks @alamb +* Fix parse `COPY INTO` stage names without parens for SnowFlake (#1187) - Thanks @mobuchowski +* Solve stack overflow on RecursionLimitExceeded on debug builds (#1171) - Thanks @Nikita-str +* Fix parsing of equality binary operator in function argument (#1182) - Thanks @jmhain +* Fix some comments (#1184) - Thanks @sunxunle + +### Changed +* Cleanup `CREATE FUNCTION` tests (#1203) - Thanks @alamb +* Parse `SUBSTRING FROM` syntax in all dialects, reflect change in the AST (#1173) - Thanks @lovasoa +* Add identifier quote style to Dialect trait (#1170) - Thanks @backkem + ## [0.44.0] 2024-03-02 ### Added From 2f03fad3394549fc629ebefd1298ada9d3333831 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 12 Apr 2024 06:54:36 -0400 Subject: [PATCH 410/806] chore: Release sqlparser version 0.45.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ed3d88b9f..3c5d4651c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.44.0" +version = "0.45.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 9db20e293f8eac2d8029146a62809ce5069d1034 Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Sun, 21 Apr 2024 05:20:41 -0700 Subject: [PATCH 411/806] fix: have wildcard replace work in duckdb and snowflake syntax (#1226) --- src/parser/mod.rs | 2 +- tests/sqlparser_bigquery.rs | 43 ------------------- tests/sqlparser_clickhouse.rs | 5 --- tests/sqlparser_common.rs | 78 +++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 49 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5bae7a133..de96625be 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9059,7 +9059,7 @@ impl<'a> Parser<'a> { None }; - let opt_replace = if dialect_of!(self is GenericDialect | BigQueryDialect | ClickHouseDialect) + let opt_replace = if dialect_of!(self is GenericDialect | BigQueryDialect | ClickHouseDialect | DuckDbDialect | SnowflakeDialect) { self.parse_optional_select_item_replace()? } else { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index c8f1bb7c1..43e6a84b7 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1280,49 +1280,6 @@ fn test_select_wildcard_with_except() { ); } -#[test] -fn test_select_wildcard_with_replace() { - let select = bigquery_and_generic() - .verified_only_select(r#"SELECT * REPLACE ('widget' AS item_name) FROM orders"#); - let expected = SelectItem::Wildcard(WildcardAdditionalOptions { - opt_replace: Some(ReplaceSelectItem { - items: vec![Box::new(ReplaceSelectElement { - expr: Expr::Value(Value::SingleQuotedString("widget".to_owned())), - column_name: Ident::new("item_name"), - as_keyword: true, - })], - }), - ..Default::default() - }); - assert_eq!(expected, select.projection[0]); - - let select = bigquery_and_generic().verified_only_select( - r#"SELECT * REPLACE (quantity / 2 AS quantity, 3 AS order_id) FROM orders"#, - ); - let expected = SelectItem::Wildcard(WildcardAdditionalOptions { - opt_replace: Some(ReplaceSelectItem { - items: vec![ - Box::new(ReplaceSelectElement { - expr: Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("quantity"))), - op: BinaryOperator::Divide, - right: Box::new(Expr::Value(number("2"))), - }, - column_name: Ident::new("quantity"), - as_keyword: true, - }), - Box::new(ReplaceSelectElement { - expr: Expr::Value(number("3")), - column_name: Ident::new("order_id"), - as_keyword: true, - }), - ], - }), - ..Default::default() - }); - assert_eq!(expected, select.projection[0]); -} - #[test] fn parse_big_query_declare() { for (sql, expected_names, expected_data_type, expected_assigned_expr) in [ diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index a3fcc612b..22396d064 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -382,11 +382,6 @@ fn parse_select_star_except_no_parens() { ); } -#[test] -fn parse_select_star_replace() { - clickhouse().verified_stmt("SELECT * REPLACE (i + 1 AS i) FROM columns_transformers"); -} - fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c94bd3779..6c95b6c56 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8685,3 +8685,81 @@ fn parse_map_access_expr() { let _ = dialects.verified_expr(sql); } } + +#[test] +fn test_select_wildcard_with_replace() { + let sql = r#"SELECT * REPLACE (lower(city) AS city) FROM addresses"#; + let dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(BigQueryDialect {}), + Box::new(ClickHouseDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(DuckDbDialect {}), + ], + options: None, + }; + let select = dialects.verified_only_select(sql); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_replace: Some(ReplaceSelectItem { + items: vec![Box::new(ReplaceSelectElement { + expr: Expr::Function(Function { + name: ObjectName(vec![Ident::new("lower")]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("city")), + ))], + filter: None, + null_treatment: None, + over: None, + distinct: false, + special: false, + order_by: vec![], + }), + column_name: Ident::new("city"), + as_keyword: true, + })], + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); + + let select = + dialects.verified_only_select(r#"SELECT * REPLACE ('widget' AS item_name) FROM orders"#); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_replace: Some(ReplaceSelectItem { + items: vec![Box::new(ReplaceSelectElement { + expr: Expr::Value(Value::SingleQuotedString("widget".to_owned())), + column_name: Ident::new("item_name"), + as_keyword: true, + })], + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); + + let select = dialects.verified_only_select( + r#"SELECT * REPLACE (quantity / 2 AS quantity, 3 AS order_id) FROM orders"#, + ); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_replace: Some(ReplaceSelectItem { + items: vec![ + Box::new(ReplaceSelectElement { + expr: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("quantity"))), + op: BinaryOperator::Divide, + right: Box::new(Expr::Value(number("2"))), + }, + column_name: Ident::new("quantity"), + as_keyword: true, + }), + Box::new(ReplaceSelectElement { + expr: Expr::Value(number("3")), + column_name: Ident::new("order_id"), + as_keyword: true, + }), + ], + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); +} From d1f67bdc4731b22f780280adf9203ccc48e87ed7 Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Sun, 21 Apr 2024 05:21:58 -0700 Subject: [PATCH 412/806] Preserve double colon casts (and simplify cast representations) (#1221) --- src/ast/mod.rs | 90 ++++++++++++++++++------------------ src/parser/mod.rs | 47 +++++-------------- tests/sqlparser_common.rs | 13 +++++- tests/sqlparser_postgres.rs | 14 +++--- tests/sqlparser_snowflake.rs | 3 +- 5 files changed, 79 insertions(+), 88 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e02741aac..17b1819b6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -408,6 +408,26 @@ impl fmt::Display for MapAccessKey { } } +/// The syntax used for in a cast expression. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CastKind { + /// The standard SQL cast syntax, e.g. `CAST( as )` + Cast, + /// A cast that returns `NULL` on failure, e.g. `TRY_CAST( as )`. + /// + /// See . + /// See . + TryCast, + /// A cast that returns `NULL` on failure, bigQuery-specific , e.g. `SAFE_CAST( as )`. + /// + /// See . + SafeCast, + /// ` :: ` + DoubleColon, +} + /// An SQL expression of any type. /// /// The parser does not distinguish between expressions of different types @@ -546,25 +566,7 @@ pub enum Expr { }, /// `CAST` an expression to a different data type e.g. `CAST(foo AS VARCHAR(123))` Cast { - expr: Box, - data_type: DataType, - // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery - // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax - format: Option, - }, - /// `TRY_CAST` an expression to a different data type e.g. `TRY_CAST(foo AS VARCHAR(123))` - // this differs from CAST in the choice of how to implement invalid conversions - TryCast { - expr: Box, - data_type: DataType, - // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery - // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax - format: Option, - }, - /// `SAFE_CAST` an expression to a different data type e.g. `SAFE_CAST(foo AS FLOAT64)` - // only available for BigQuery: https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-and-operators#safe_casting - // this works the same as `TRY_CAST` - SafeCast { + kind: CastKind, expr: Box, data_type: DataType, // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery @@ -989,38 +991,36 @@ impl fmt::Display for Expr { write!(f, ")") } Expr::Cast { + kind, expr, data_type, format, - } => { - if let Some(format) = format { - write!(f, "CAST({expr} AS {data_type} FORMAT {format})") - } else { - write!(f, "CAST({expr} AS {data_type})") + } => match kind { + CastKind::Cast => { + if let Some(format) = format { + write!(f, "CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "CAST({expr} AS {data_type})") + } } - } - Expr::TryCast { - expr, - data_type, - format, - } => { - if let Some(format) = format { - write!(f, "TRY_CAST({expr} AS {data_type} FORMAT {format})") - } else { - write!(f, "TRY_CAST({expr} AS {data_type})") + CastKind::TryCast => { + if let Some(format) = format { + write!(f, "TRY_CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "TRY_CAST({expr} AS {data_type})") + } } - } - Expr::SafeCast { - expr, - data_type, - format, - } => { - if let Some(format) = format { - write!(f, "SAFE_CAST({expr} AS {data_type} FORMAT {format})") - } else { - write!(f, "SAFE_CAST({expr} AS {data_type})") + CastKind::SafeCast => { + if let Some(format) = format { + write!(f, "SAFE_CAST({expr} AS {data_type} FORMAT {format})") + } else { + write!(f, "SAFE_CAST({expr} AS {data_type})") + } } - } + CastKind::DoubleColon => { + write!(f, "{expr}::{data_type}") + } + }, Expr::Extract { field, expr } => write!(f, "EXTRACT({field} FROM {expr})"), Expr::Ceil { expr, field } => { if field == &DateTimeField::NoDateTime { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index de96625be..a45c9da33 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1004,9 +1004,9 @@ impl<'a> Parser<'a> { } Keyword::CASE => self.parse_case_expr(), Keyword::CONVERT => self.parse_convert_expr(), - Keyword::CAST => self.parse_cast_expr(), - Keyword::TRY_CAST => self.parse_try_cast_expr(), - Keyword::SAFE_CAST => self.parse_safe_cast_expr(), + Keyword::CAST => self.parse_cast_expr(CastKind::Cast), + Keyword::TRY_CAST => self.parse_cast_expr(CastKind::TryCast), + Keyword::SAFE_CAST => self.parse_cast_expr(CastKind::SafeCast), Keyword::EXISTS => self.parse_exists_expr(false), Keyword::EXTRACT => self.parse_extract_expr(), Keyword::CEIL => self.parse_ceil_floor_expr(true), @@ -1491,7 +1491,7 @@ impl<'a> Parser<'a> { } /// Parse a SQL CAST function e.g. `CAST(expr AS FLOAT)` - pub fn parse_cast_expr(&mut self) -> Result { + pub fn parse_cast_expr(&mut self, kind: CastKind) -> Result { self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; self.expect_keyword(Keyword::AS)?; @@ -1499,36 +1499,7 @@ impl<'a> Parser<'a> { let format = self.parse_optional_cast_format()?; self.expect_token(&Token::RParen)?; Ok(Expr::Cast { - expr: Box::new(expr), - data_type, - format, - }) - } - - /// Parse a SQL TRY_CAST function e.g. `TRY_CAST(expr AS FLOAT)` - pub fn parse_try_cast_expr(&mut self) -> Result { - self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; - self.expect_keyword(Keyword::AS)?; - let data_type = self.parse_data_type()?; - let format = self.parse_optional_cast_format()?; - self.expect_token(&Token::RParen)?; - Ok(Expr::TryCast { - expr: Box::new(expr), - data_type, - format, - }) - } - - /// Parse a BigQuery SAFE_CAST function e.g. `SAFE_CAST(expr AS FLOAT64)` - pub fn parse_safe_cast_expr(&mut self) -> Result { - self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; - self.expect_keyword(Keyword::AS)?; - let data_type = self.parse_data_type()?; - let format = self.parse_optional_cast_format()?; - self.expect_token(&Token::RParen)?; - Ok(Expr::SafeCast { + kind, expr: Box::new(expr), data_type, format, @@ -2528,7 +2499,12 @@ impl<'a> Parser<'a> { ), } } else if Token::DoubleColon == tok { - self.parse_pg_cast(expr) + Ok(Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(expr), + data_type: self.parse_data_type()?, + format: None, + }) } else if Token::ExclamationMark == tok { // PostgreSQL factorial operation Ok(Expr::UnaryOp { @@ -2702,6 +2678,7 @@ impl<'a> Parser<'a> { /// 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, expr: Box::new(expr), data_type: self.parse_data_type()?, format: None, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6c95b6c56..3aa84b923 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2107,6 +2107,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::BigInt(None), format: None, @@ -2118,6 +2119,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::TinyInt(None), format: None, @@ -2145,6 +2147,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Nvarchar(Some(50)), format: None, @@ -2156,6 +2159,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Clob(None), format: None, @@ -2167,6 +2171,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Clob(Some(50)), format: None, @@ -2178,6 +2183,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Binary(Some(50)), format: None, @@ -2189,6 +2195,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Varbinary(Some(50)), format: None, @@ -2200,6 +2207,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Blob(None), format: None, @@ -2211,6 +2219,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Blob(Some(50)), format: None, @@ -2222,6 +2231,7 @@ fn parse_cast() { let select = verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("details"))), data_type: DataType::JSONB, format: None, @@ -2235,7 +2245,8 @@ fn parse_try_cast() { let sql = "SELECT TRY_CAST(id AS BIGINT) FROM customer"; let select = verified_only_select(sql); assert_eq!( - &Expr::TryCast { + &Expr::Cast { + kind: CastKind::TryCast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::BigInt(None), format: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ea5c9875b..38e32780d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -328,6 +328,7 @@ fn parse_create_table_with_defaults() { location: None, .. } => { + use pretty_assertions::assert_eq; assert_eq!("public.customer", name.to_string()); assert_eq!( columns, @@ -422,9 +423,7 @@ fn parse_create_table_with_defaults() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Default( - pg().verified_expr("CAST(now() AS TEXT)") - ) + option: ColumnOption::Default(pg().verified_expr("now()::TEXT")) }, ColumnOptionDef { name: None, @@ -498,15 +497,15 @@ fn parse_create_table_from_pg_dump() { active int )"; pg().one_statement_parses_to(sql, "CREATE TABLE public.customer (\ - customer_id INTEGER DEFAULT nextval(CAST('public.customer_customer_id_seq' AS REGCLASS)) NOT NULL, \ + customer_id INTEGER DEFAULT nextval('public.customer_customer_id_seq'::REGCLASS) NOT NULL, \ store_id SMALLINT NOT NULL, \ first_name CHARACTER VARYING(45) NOT NULL, \ last_name CHARACTER VARYING(45) NOT NULL, \ info TEXT[], \ address_id SMALLINT NOT NULL, \ activebool BOOLEAN DEFAULT true NOT NULL, \ - create_date DATE DEFAULT CAST(now() AS DATE) NOT NULL, \ - create_date1 DATE DEFAULT CAST(CAST('now' AS TEXT) AS DATE) NOT NULL, \ + create_date DATE DEFAULT now()::DATE NOT NULL, \ + create_date1 DATE DEFAULT 'now'::TEXT::DATE NOT NULL, \ last_update TIMESTAMP WITHOUT TIME ZONE DEFAULT now(), \ release_year public.year, \ active INT\ @@ -1448,11 +1447,13 @@ fn parse_execute() { parameters: vec![], using: vec![ Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Value(Value::Number("1337".parse().unwrap(), false))), data_type: DataType::SmallInt(None), format: None }, Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Value(Value::Number("7331".parse().unwrap(), false))), data_type: DataType::SmallInt(None), format: None @@ -1908,6 +1909,7 @@ fn parse_array_index_expr() { assert_eq!( &Expr::ArrayIndex { obj: Box::new(Expr::Nested(Box::new(Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Array(Array { elem: vec![Expr::Array(Array { elem: vec![num[2].clone(), num[3].clone(),], diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5c13457b6..b76e84ed4 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -168,6 +168,7 @@ fn parse_array() { let select = snowflake().verified_only_select(sql); assert_eq!( &Expr::Cast { + kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("a"))), data_type: DataType::Array(ArrayElemTypeDef::None), format: None, @@ -228,7 +229,7 @@ fn parse_json_using_colon() { select.projection[0] ); - snowflake().one_statement_parses_to("SELECT a:b::int FROM t", "SELECT CAST(a:b AS INT) FROM t"); + snowflake().verified_stmt("SELECT a:b::INT FROM t"); let sql = "SELECT a:start, a:end FROM t"; let select = snowflake().verified_only_select(sql); From 4604628c435ca2522a4cce5ec3a9e99f2c677311 Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Sun, 21 Apr 2024 05:22:08 -0700 Subject: [PATCH 413/806] feat: implement select * ilike for snowflake (#1228) --- src/ast/mod.rs | 4 ++-- src/ast/query.rs | 29 +++++++++++++++++++++++++++ src/parser/mod.rs | 28 +++++++++++++++++++++++++- tests/sqlparser_common.rs | 1 + tests/sqlparser_duckdb.rs | 2 ++ tests/sqlparser_snowflake.rs | 39 ++++++++++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 17b1819b6..31924e051 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,8 +40,8 @@ pub use self::ddl::{ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ Cte, CteAsMaterialized, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, ForClause, - ForJson, ForXml, GroupByExpr, IdentWithAlias, Join, JoinConstraint, JoinOperator, - JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, + ForJson, ForXml, GroupByExpr, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, + JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, diff --git a/src/ast/query.rs b/src/ast/query.rs index bf33cdee6..391ef51d8 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -474,6 +474,9 @@ impl fmt::Display for IdentWithAlias { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WildcardAdditionalOptions { + /// `[ILIKE...]`. + /// Snowflake syntax: + pub opt_ilike: Option, /// `[EXCLUDE...]`. pub opt_exclude: Option, /// `[EXCEPT...]`. @@ -489,6 +492,9 @@ pub struct WildcardAdditionalOptions { impl fmt::Display for WildcardAdditionalOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ilike) = &self.opt_ilike { + write!(f, " {ilike}")?; + } if let Some(exclude) = &self.opt_exclude { write!(f, " {exclude}")?; } @@ -505,6 +511,29 @@ impl fmt::Display for WildcardAdditionalOptions { } } +/// Snowflake `ILIKE` information. +/// +/// # Syntax +/// ```plaintext +/// ILIKE +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IlikeSelectItem { + pub pattern: String, +} + +impl fmt::Display for IlikeSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ILIKE '{}'", + value::escape_single_quote_string(&self.pattern) + )?; + Ok(()) + } +} /// Snowflake `EXCLUDE` information. /// /// # Syntax diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a45c9da33..6779dfd0f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9018,7 +9018,13 @@ impl<'a> Parser<'a> { pub fn parse_wildcard_additional_options( &mut self, ) -> Result { - let opt_exclude = if dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) + let opt_ilike = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_ilike()? + } else { + None + }; + let opt_exclude = if opt_ilike.is_none() + && dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) { self.parse_optional_select_item_exclude()? } else { @@ -9044,6 +9050,7 @@ impl<'a> Parser<'a> { }; Ok(WildcardAdditionalOptions { + opt_ilike, opt_exclude, opt_except, opt_rename, @@ -9051,6 +9058,25 @@ impl<'a> Parser<'a> { }) } + /// Parse an [`Ilike`](IlikeSelectItem) information for wildcard select items. + /// + /// If it is not possible to parse it, will return an option. + pub fn parse_optional_select_item_ilike( + &mut self, + ) -> Result, ParserError> { + let opt_ilike = if self.parse_keyword(Keyword::ILIKE) { + let next_token = self.next_token(); + let pattern = match next_token.token { + Token::SingleQuotedString(s) => s, + _ => return self.expected("ilike pattern", next_token), + }; + Some(IlikeSelectItem { pattern }) + } else { + None + }; + Ok(opt_ilike) + } + /// Parse an [`Exclude`](ExcludeSelectItem) information for wildcard select items. /// /// If it is not possible to parse it, will return an option. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3aa84b923..bbc0f0b2f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6622,6 +6622,7 @@ fn lateral_function() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_ilike: None, opt_exclude: None, opt_except: None, opt_rename: None, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index d6a6b7d4b..fd420c8a3 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -148,6 +148,7 @@ fn test_select_union_by_name() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_ilike: None, opt_exclude: None, opt_except: None, opt_rename: None, @@ -183,6 +184,7 @@ fn test_select_union_by_name() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_ilike: None, opt_exclude: None, opt_except: None, opt_rename: None, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b76e84ed4..56060a0d7 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1555,3 +1555,42 @@ fn parse_comma_outer_join() { fn test_sf_trailing_commas() { snowflake().verified_only_select_with_canonical("SELECT 1, 2, FROM t", "SELECT 1, 2 FROM t"); } + +#[test] +fn test_select_wildcard_with_ilike() { + let select = snowflake_and_generic().verified_only_select(r#"SELECT * ILIKE '%id%' FROM tbl"#); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_ilike: Some(IlikeSelectItem { + pattern: "%id%".to_owned(), + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); +} + +#[test] +fn test_select_wildcard_with_ilike_double_quote() { + let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE "%id" FROM tbl"#); + assert_eq!( + res.unwrap_err().to_string(), + "sql parser error: Expected ilike pattern, found: \"%id\"" + ); +} + +#[test] +fn test_select_wildcard_with_ilike_number() { + let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE 42 FROM tbl"#); + assert_eq!( + res.unwrap_err().to_string(), + "sql parser error: Expected ilike pattern, found: 42" + ); +} + +#[test] +fn test_select_wildcard_with_ilike_replace() { + let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE '%id%' EXCLUDE col FROM tbl"#); + assert_eq!( + res.unwrap_err().to_string(), + "sql parser error: Expected end of statement, found: EXCLUDE" + ); +} From 7b49c69b3a14ba3a2763dd4c9b33913e6e5649ca Mon Sep 17 00:00:00 2001 From: Kould Date: Sun, 21 Apr 2024 20:32:53 +0800 Subject: [PATCH 414/806] Support `Modify Column` for MySQL dialect (#1216) --- src/ast/ddl.rs | 24 +++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 17 ++++++++ tests/sqlparser_mysql.rs | 93 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d86ebad9d..de514550b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -134,6 +134,14 @@ pub enum AlterTableOperation { /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] column_position: Option, }, + // CHANGE [ COLUMN ] [ ] + ModifyColumn { + col_name: Ident, + data_type: DataType, + options: Vec, + /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] + column_position: Option, + }, /// `RENAME CONSTRAINT TO ` /// /// Note: this is a PostgreSQL-specific operation. @@ -292,6 +300,22 @@ impl fmt::Display for AlterTableOperation { Ok(()) } + AlterTableOperation::ModifyColumn { + col_name, + data_type, + options, + column_position, + } => { + write!(f, "MODIFY COLUMN {col_name} {data_type}")?; + if !options.is_empty() { + write!(f, " {}", display_separated(options, " "))?; + } + if let Some(position) = column_position { + write!(f, " {position}")?; + } + + Ok(()) + } AlterTableOperation::RenameConstraint { old_name, new_name } => { write!(f, "RENAME CONSTRAINT {old_name} TO {new_name}") } diff --git a/src/keywords.rs b/src/keywords.rs index 12a376b2a..fcc344bcd 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -438,6 +438,7 @@ define_keywords!( MOD, MODE, MODIFIES, + MODIFY, MODULE, MONTH, MSCK, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6779dfd0f..9910c889a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5757,6 +5757,23 @@ impl<'a> Parser<'a> { options, column_position, } + } else if self.parse_keyword(Keyword::MODIFY) { + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] + let col_name = self.parse_identifier(false)?; + let data_type = self.parse_data_type()?; + let mut options = vec![]; + while let Some(option) = self.parse_optional_column_option()? { + options.push(option); + } + + let column_position = self.parse_column_position()?; + + AlterTableOperation::ModifyColumn { + col_name, + data_type, + options, + column_position, + } } else if self.parse_keyword(Keyword::ALTER) { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let column_name = self.parse_identifier(false)?; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5f64079a6..e53f434d5 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2218,6 +2218,99 @@ fn parse_alter_table_change_column_with_column_position() { assert_eq!(expected_operation_after, operation); } +#[test] +fn parse_alter_table_modify_column() { + let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_operation = AlterTableOperation::ModifyColumn { + col_name: Ident::new("description"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: None, + }; + + let sql1 = "ALTER TABLE orders MODIFY COLUMN description TEXT NOT NULL"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql1), &expected_name.to_string()); + assert_eq!(expected_operation, operation); + + let sql2 = "ALTER TABLE orders MODIFY description TEXT NOT NULL"; + let operation = alter_table_op_with_name( + mysql().one_statement_parses_to(sql2, sql1), + &expected_name.to_string(), + ); + assert_eq!(expected_operation, operation); + + let expected_operation = AlterTableOperation::ModifyColumn { + col_name: Ident::new("description"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::First), + }; + let sql3 = "ALTER TABLE orders MODIFY COLUMN description TEXT NOT NULL FIRST"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql3), &expected_name.to_string()); + assert_eq!(expected_operation, operation); + + let expected_operation = AlterTableOperation::ModifyColumn { + col_name: Ident::new("description"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("foo"), + quote_style: None, + })), + }; + let sql4 = "ALTER TABLE orders MODIFY COLUMN description TEXT NOT NULL AFTER foo"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql4), &expected_name.to_string()); + assert_eq!(expected_operation, operation); +} + +#[test] +fn parse_alter_table_modify_column_with_column_position() { + let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_operation_first = AlterTableOperation::ModifyColumn { + col_name: Ident::new("description"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::First), + }; + + let sql1 = "ALTER TABLE orders MODIFY COLUMN description TEXT NOT NULL FIRST"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql1), &expected_name.to_string()); + assert_eq!(expected_operation_first, operation); + + let sql2 = "ALTER TABLE orders MODIFY description TEXT NOT NULL FIRST"; + let operation = alter_table_op_with_name( + mysql().one_statement_parses_to(sql2, sql1), + &expected_name.to_string(), + ); + assert_eq!(expected_operation_first, operation); + + let expected_operation_after = AlterTableOperation::ModifyColumn { + col_name: Ident::new("description"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("total_count"), + quote_style: None, + })), + }; + + let sql1 = "ALTER TABLE orders MODIFY COLUMN description TEXT NOT NULL AFTER total_count"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql1), &expected_name.to_string()); + assert_eq!(expected_operation_after, operation); + + let sql2 = "ALTER TABLE orders MODIFY description TEXT NOT NULL AFTER total_count"; + let operation = alter_table_op_with_name( + mysql().one_statement_parses_to(sql2, sql1), + &expected_name.to_string(), + ); + assert_eq!(expected_operation_after, operation); +} + #[test] fn parse_substring_in_select() { let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; From d2c2b15f9e349e5aba679baed96af497c734d834 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Sun, 21 Apr 2024 15:07:56 +0200 Subject: [PATCH 415/806] Add support for quoted string backslash escaping (#1177) --- src/ast/mod.rs | 6 +- src/dialect/bigquery.rs | 5 + src/dialect/clickhouse.rs | 4 + src/dialect/mod.rs | 21 ++++ src/dialect/mysql.rs | 5 + src/dialect/snowflake.rs | 5 + src/parser/mod.rs | 4 +- src/tokenizer.rs | 130 ++++++++++++++++------- tests/sqlparser_bigquery.rs | 109 ------------------- tests/sqlparser_clickhouse.rs | 109 ------------------- tests/sqlparser_common.rs | 191 +++++++++++++++++++++++++++++++++- tests/sqlparser_hive.rs | 111 +------------------- tests/sqlparser_mssql.rs | 109 ------------------- tests/sqlparser_mysql.rs | 72 ------------- tests/sqlparser_postgres.rs | 109 ------------------- tests/sqlparser_redshift.rs | 109 ------------------- tests/sqlparser_snowflake.rs | 140 ++++--------------------- tests/sqlparser_sqlite.rs | 109 ------------------- 18 files changed, 352 insertions(+), 996 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 31924e051..b78a559a0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -512,21 +512,21 @@ pub enum Expr { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// `ILIKE` (case-insensitive `LIKE`) ILike { negated: 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 { diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index bcd27c3b5..d36910dbc 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -29,4 +29,9 @@ impl Dialect for BigQueryDialect { fn is_identifier_part(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_' } + + // See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#escape_sequences + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 50fbde99e..83cc4ae9a 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -25,4 +25,8 @@ impl Dialect for ClickHouseDialect { fn is_identifier_part(&self, ch: char) -> bool { self.is_identifier_start(ch) || ch.is_ascii_digit() } + + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 2463121e7..e409c716e 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -120,6 +120,23 @@ pub trait Dialect: Debug + Any { fn is_identifier_start(&self, ch: char) -> bool; /// Determine if a character is a valid unquoted identifier character fn is_identifier_part(&self, ch: char) -> bool; + /// Determine if the dialect supports escaping characters via '\' in string literals. + /// + /// Some dialects like BigQuery and Snowflake support this while others like + /// Postgres do not. Such that the following is accepted by the former but + /// rejected by the latter. + /// ```sql + /// SELECT 'ab\'cd'; + /// ``` + /// + /// Conversely, such dialects reject the following statement which + /// otherwise would be valid in the other dialects. + /// ```sql + /// SELECT '\'; + /// ``` + fn supports_string_literal_backslash_escape(&self) -> bool { + false + } /// Does the dialect support `FILTER (WHERE expr)` for aggregate queries? fn supports_filter_during_aggregation(&self) -> bool { false @@ -306,6 +323,10 @@ mod tests { self.0.identifier_quote_style(identifier) } + fn supports_string_literal_backslash_escape(&self) -> bool { + self.0.supports_string_literal_backslash_escape() + } + fn is_proper_identifier_inside_quotes( &self, chars: std::iter::Peekable>, diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index d0dbe923c..f7711b2b0 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -48,6 +48,11 @@ impl Dialect for MySqlDialect { Some('`') } + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } + fn parse_infix( &self, parser: &mut crate::parser::Parser, diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 1d9d983e5..28b18b78c 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -46,6 +46,11 @@ impl Dialect for SnowflakeDialect { || ch == '_' } + // See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#escape_sequences + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } + fn supports_within_after_array_aggregation(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9910c889a..9ad27b16a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2560,9 +2560,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_char()?)) + Ok(Some(self.parse_literal_string()?)) } else { Ok(None) } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b239d990e..b99eeba80 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -627,11 +627,11 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume match chars.peek() { Some('\'') => { - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string(chars, '\'', false)?; Ok(Some(Token::SingleQuotedByteStringLiteral(s))) } Some('\"') => { - let s = self.tokenize_quoted_string(chars, '\"')?; + let s = self.tokenize_quoted_string(chars, '\"', false)?; Ok(Some(Token::DoubleQuotedByteStringLiteral(s))) } _ => { @@ -646,11 +646,11 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume match chars.peek() { Some('\'') => { - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string(chars, '\'', false)?; Ok(Some(Token::RawStringLiteral(s))) } Some('\"') => { - let s = self.tokenize_quoted_string(chars, '\"')?; + let s = self.tokenize_quoted_string(chars, '\"', false)?; Ok(Some(Token::RawStringLiteral(s))) } _ => { @@ -666,7 +666,7 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('\'') => { // N'...' - a - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string(chars, '\'', true)?; Ok(Some(Token::NationalStringLiteral(s))) } _ => { @@ -700,7 +700,7 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('\'') => { // X'...' - a - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string(chars, '\'', true)?; Ok(Some(Token::HexStringLiteral(s))) } _ => { @@ -712,7 +712,11 @@ impl<'a> Tokenizer<'a> { } // single quoted string '\'' => { - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string( + chars, + '\'', + self.dialect.supports_string_literal_backslash_escape(), + )?; Ok(Some(Token::SingleQuotedString(s))) } @@ -720,7 +724,11 @@ impl<'a> Tokenizer<'a> { '\"' if !self.dialect.is_delimited_identifier_start(ch) && !self.dialect.is_identifier_start(ch) => { - let s = self.tokenize_quoted_string(chars, '"')?; + let s = self.tokenize_quoted_string( + chars, + '"', + self.dialect.supports_string_literal_backslash_escape(), + )?; Ok(Some(Token::DoubleQuotedString(s))) } @@ -1222,6 +1230,7 @@ impl<'a> Tokenizer<'a> { &self, chars: &mut State, quote_style: char, + allow_escape: bool, ) -> Result { let mut s = String::new(); let error_loc = chars.location(); @@ -1243,35 +1252,31 @@ impl<'a> Tokenizer<'a> { return Ok(s); } } - '\\' => { - // consume + '\\' if allow_escape => { + // consume backslash chars.next(); - // slash escaping is specific to MySQL dialect. - if dialect_of!(self is MySqlDialect) { - if let Some(next) = chars.peek() { - if !self.unescape { - // In no-escape mode, the given query has to be saved completely including backslashes. - s.push(ch); - s.push(*next); - chars.next(); // consume next - } else { - // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences - let n = match next { - '\'' | '\"' | '\\' | '%' | '_' => *next, - '0' => '\0', - 'b' => '\u{8}', - 'n' => '\n', - 'r' => '\r', - 't' => '\t', - 'Z' => '\u{1a}', - _ => *next, - }; - s.push(n); - chars.next(); // consume next - } + + if let Some(next) = chars.peek() { + if !self.unescape { + // In no-escape mode, the given query has to be saved completely including backslashes. + s.push(ch); + s.push(*next); + chars.next(); // consume next + } else { + let n = match next { + '0' => '\0', + 'a' => '\u{7}', + 'b' => '\u{8}', + 'f' => '\u{c}', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'Z' => '\u{1a}', + _ => *next, + }; + s.push(n); + chars.next(); // consume next } - } else { - s.push(ch); } } _ => { @@ -1517,7 +1522,7 @@ impl<'a: 'b, 'b> Unescape<'a, 'b> { #[cfg(test)] mod tests { use super::*; - use crate::dialect::{ClickHouseDialect, MsSqlDialect}; + use crate::dialect::{BigQueryDialect, ClickHouseDialect, MsSqlDialect}; #[test] fn tokenizer_error_impl() { @@ -2386,4 +2391,57 @@ mod tests { check_unescape(r"Hello\0", None); check_unescape(r"Hello\xCADRust", None); } + + #[test] + fn tokenize_quoted_string_escape() { + for (sql, expected, expected_unescaped) in [ + (r#"'%a\'%b'"#, r#"%a\'%b"#, r#"%a'%b"#), + (r#"'a\'\'b\'c\'d'"#, r#"a\'\'b\'c\'d"#, r#"a''b'c'd"#), + (r#"'\\'"#, r#"\\"#, r#"\"#), + ( + r#"'\0\a\b\f\n\r\t\Z'"#, + r#"\0\a\b\f\n\r\t\Z"#, + "\0\u{7}\u{8}\u{c}\n\r\t\u{1a}", + ), + (r#"'\"'"#, r#"\""#, "\""), + (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"#), + ] { + let dialect = BigQueryDialect {}; + + let tokens = Tokenizer::new(&dialect, sql) + .with_unescape(false) + .tokenize() + .unwrap(); + let expected = vec![Token::SingleQuotedString(expected.to_string())]; + compare(expected, tokens); + + let tokens = Tokenizer::new(&dialect, sql) + .with_unescape(true) + .tokenize() + .unwrap(); + let expected = vec![Token::SingleQuotedString(expected_unescaped.to_string())]; + compare(expected, tokens); + } + + for sql in [r#"'\'"#, r#"'ab\'"#] { + let dialect = BigQueryDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, sql); + assert_eq!( + "Unterminated string literal", + tokenizer.tokenize().unwrap_err().message.as_str(), + ); + } + + // Non-escape dialect + for (sql, expected) in [(r#"'\'"#, r#"\"#), (r#"'ab\'"#, r#"ab\"#)] { + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + + let expected = vec![Token::SingleQuotedString(expected.to_string())]; + + compare(expected, tokens); + } + } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 43e6a84b7..a01c09d96 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1128,115 +1128,6 @@ fn parse_cast_bytes_to_string_format() { bigquery_and_generic().verified_only_select(sql); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = bigquery().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = bigquery().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = bigquery().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - if negated { "NOT " } else { "" } - ); - let select = bigquery().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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = bigquery().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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = bigquery().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_array_agg_func() { for sql in [ diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 22396d064..4f1e67a17 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -230,115 +230,6 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = clickhouse().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = clickhouse().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = clickhouse().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - if negated { "NOT " } else { "" } - ); - let select = clickhouse().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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = clickhouse().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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = clickhouse().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_create_table() { clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bbc0f0b2f..dd447ebb8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1599,7 +1599,7 @@ fn parse_ilike() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('^'), + escape_char: Some('^'.to_string()), }, select.selection.unwrap() ); @@ -1625,6 +1625,115 @@ fn parse_ilike() { chk(true); } +#[test] +fn parse_like() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a'", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '^'", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('^'.to_string()), + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: None, + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + 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()))), + escape_char: None, + }, + select.selection.unwrap() + ); + + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '^'", + 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()))), + escape_char: Some('^'.to_string()), + }, + select.selection.unwrap() + ); + + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '^' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + escape_char: Some('^'.to_string()), + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + #[test] fn parse_in_list() { fn chk(negated: bool) { @@ -8166,6 +8275,86 @@ fn parse_with_recursion_limit() { assert!(res.is_ok(), "{res:?}"); } +#[test] +fn parse_escaped_string_with_unescape() { + fn assert_mysql_query_value(sql: &str, quoted: &str) { + let stmt = TestedDialects { + dialects: vec![ + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + ], + options: None, + } + .one_statement_parses_to(sql, ""); + + match stmt { + Statement::Query(query) => match *query.body { + SetExpr::Select(value) => { + let expr = expr_from_projection(only(&value.projection)); + assert_eq!( + *expr, + Expr::Value(Value::SingleQuotedString(quoted.to_string())) + ); + } + _ => unreachable!(), + }, + _ => unreachable!(), + }; + } + let sql = r"SELECT 'I\'m fine'"; + assert_mysql_query_value(sql, "I'm fine"); + + let sql = r#"SELECT 'I''m fine'"#; + assert_mysql_query_value(sql, "I'm fine"); + + let sql = r#"SELECT 'I\"m fine'"#; + assert_mysql_query_value(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 "); +} + +#[test] +fn parse_escaped_string_without_unescape() { + fn assert_mysql_query_value(sql: &str, quoted: &str) { + let stmt = TestedDialects { + dialects: vec![ + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + ], + options: Some(ParserOptions::new().with_unescape(false)), + } + .one_statement_parses_to(sql, ""); + + match stmt { + Statement::Query(query) => match *query.body { + SetExpr::Select(value) => { + let expr = expr_from_projection(only(&value.projection)); + assert_eq!( + *expr, + Expr::Value(Value::SingleQuotedString(quoted.to_string())) + ); + } + _ => unreachable!(), + }, + _ => unreachable!(), + }; + } + let sql = r"SELECT 'I\'m fine'"; + assert_mysql_query_value(sql, r"I\'m fine"); + + let sql = r#"SELECT 'I''m fine'"#; + assert_mysql_query_value(sql, r#"I''m fine"#); + + let sql = r#"SELECT 'I\"m fine'"#; + assert_mysql_query_value(sql, r#"I\"m fine"#); + + let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"; + assert_mysql_query_value(sql, r"Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ "); +} + #[test] fn parse_pivot_table() { let sql = concat!( diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 788c937a6..76fe961fe 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -17,7 +17,7 @@ use sqlparser::ast::{ CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionDefinition, Ident, ObjectName, - SelectItem, Statement, TableFactor, UnaryOperator, Value, + SelectItem, Statement, TableFactor, UnaryOperator, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -420,115 +420,6 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = hive().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = hive().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = hive().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - if negated { "NOT " } else { "" } - ); - let select = hive().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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = hive().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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = hive().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - fn hive() -> TestedDialects { TestedDialects { dialects: vec![Box::new(HiveDialect {})], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ff3e75569..ed4d69e6b 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -390,61 +390,6 @@ fn parse_table_name_in_square_brackets() { ); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = ms_and_generic().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = ms_and_generic().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = ms_and_generic().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_for_clause() { ms_and_generic().verified_stmt("SELECT a FROM t FOR JSON PATH"); @@ -495,60 +440,6 @@ fn parse_convert() { ms().verified_expr("CONVERT(DECIMAL(10,5), 12.55)"); } -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - if negated { "NOT " } else { "" } - ); - let select = ms_and_generic().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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = ms_and_generic().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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = ms_and_generic().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_substring_in_select() { let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e53f434d5..b2c164e3d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1061,78 +1061,6 @@ fn parse_unterminated_escape() { assert!(result.is_err()); } -#[test] -fn parse_escaped_string_with_escape() { - fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: None, - } - .one_statement_parses_to(sql, ""); - - match stmt { - Statement::Query(query) => match *query.body { - SetExpr::Select(value) => { - let expr = expr_from_projection(only(&value.projection)); - assert_eq!( - *expr, - Expr::Value(Value::SingleQuotedString(quoted.to_string())) - ); - } - _ => unreachable!(), - }, - _ => unreachable!(), - }; - } - let sql = r"SELECT 'I\'m fine'"; - assert_mysql_query_value(sql, "I'm fine"); - - let sql = r#"SELECT 'I''m fine'"#; - assert_mysql_query_value(sql, "I'm fine"); - - let sql = r#"SELECT 'I\"m fine'"#; - assert_mysql_query_value(sql, "I\"m fine"); - - let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"; - assert_mysql_query_value(sql, "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} a "); -} - -#[test] -fn parse_escaped_string_with_no_escape() { - fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: Some(ParserOptions::new().with_unescape(false)), - } - .one_statement_parses_to(sql, ""); - - match stmt { - Statement::Query(query) => match *query.body { - SetExpr::Select(value) => { - let expr = expr_from_projection(only(&value.projection)); - assert_eq!( - *expr, - Expr::Value(Value::SingleQuotedString(quoted.to_string())) - ); - } - _ => unreachable!(), - }, - _ => unreachable!(), - }; - } - let sql = r"SELECT 'I\'m fine'"; - assert_mysql_query_value(sql, r"I\'m fine"); - - let sql = r#"SELECT 'I''m fine'"#; - assert_mysql_query_value(sql, r#"I''m fine"#); - - let sql = r#"SELECT 'I\"m fine'"#; - assert_mysql_query_value(sql, r#"I\"m fine"#); - - let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '"; - assert_mysql_query_value(sql, r"Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ "); -} - #[test] fn check_roundtrip_of_escaped_string() { let options = Some(ParserOptions::new().with_unescape(false)); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 38e32780d..6bb4bc69b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3171,115 +3171,6 @@ 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 parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = pg().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = pg().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = pg().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - if negated { "NOT " } else { "" } - ); - let select = pg().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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = pg().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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = pg().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_create_function() { let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'"; diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 6fa647d38..3de229676 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -159,115 +159,6 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = redshift().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = redshift().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = redshift().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - if negated { "NOT " } else { "" } - ); - let select = redshift().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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = redshift().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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = redshift().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - fn redshift() -> TestedDialects { TestedDialects { dialects: vec![Box::new(RedshiftSqlDialect {})], diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 56060a0d7..469e6739f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -19,7 +19,7 @@ use sqlparser::ast::helpers::stmt_data_loading::{ }; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SnowflakeDialect}; -use sqlparser::parser::ParserError; +use sqlparser::parser::{ParserError, ParserOptions}; use sqlparser::tokenizer::*; use test_utils::*; @@ -309,115 +309,6 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = snowflake().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = snowflake().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = snowflake().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - if negated { "NOT " } else { "" } - ); - let select = snowflake().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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = snowflake().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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = snowflake().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn test_array_agg_func() { for sql in [ @@ -444,6 +335,13 @@ fn snowflake() -> TestedDialects { } } +fn snowflake_without_unescape() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(SnowflakeDialect {})], + options: Some(ParserOptions::new().with_unescape(false)), + } +} + fn snowflake_and_generic() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SnowflakeDialect {}), Box::new(GenericDialect {})], @@ -985,10 +883,10 @@ fn test_create_stage_with_file_format() { let sql = concat!( "CREATE OR REPLACE STAGE my_ext_stage ", "URL='s3://load/files/' ", - "FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')" + r#"FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')"# ); - match snowflake().verified_stmt(sql) { + match snowflake_without_unescape().verified_stmt(sql) { Statement::CreateStage { file_format, .. } => { assert!(file_format.options.contains(&DataLoadingOption { option_name: "COMPRESSION".to_string(), @@ -1003,12 +901,15 @@ fn test_create_stage_with_file_format() { assert!(file_format.options.contains(&DataLoadingOption { option_name: "ESCAPE".to_string(), option_type: DataLoadingOptionType::STRING, - value: "\\".to_string() + value: r#"\\"#.to_string() })); } _ => unreachable!(), }; - assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + assert_eq!( + snowflake_without_unescape().verified_stmt(sql).to_string(), + sql + ); } #[test] @@ -1243,10 +1144,10 @@ fn test_copy_into_file_format() { "FROM 'gcs://mybucket/./../a.csv' ", "FILES = ('file1.json', 'file2.json') ", "PATTERN = '.*employees0[1-5].csv.gz' ", - "FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')" + r#"FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')"# ); - match snowflake().verified_stmt(sql) { + match snowflake_without_unescape().verified_stmt(sql) { Statement::CopyIntoSnowflake { file_format, .. } => { assert!(file_format.options.contains(&DataLoadingOption { option_name: "COMPRESSION".to_string(), @@ -1261,12 +1162,15 @@ fn test_copy_into_file_format() { assert!(file_format.options.contains(&DataLoadingOption { option_name: "ESCAPE".to_string(), option_type: DataLoadingOptionType::STRING, - value: "\\".to_string() + value: r#"\\"#.to_string() })); } _ => unreachable!(), } - assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + assert_eq!( + snowflake_without_unescape().verified_stmt(sql).to_string(), + sql + ); } #[test] diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index c9d5d98cd..b90e45827 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -290,115 +290,6 @@ fn test_placeholder() { ); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = sqlite().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = sqlite().verified_only_select(sql); - assert_eq!( - Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = sqlite().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::Like { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None, - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - -#[test] -fn parse_similar_to() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", - if negated { "NOT " } else { "" } - ); - let select = sqlite().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()))), - escape_char: None, - }, - select.selection.unwrap() - ); - - // Test with escape char - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", - if negated { "NOT " } else { "" } - ); - let select = sqlite().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()))), - escape_char: Some('\\'), - }, - select.selection.unwrap() - ); - - // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. - let sql = &format!( - "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", - if negated { "NOT " } else { "" } - ); - let select = sqlite().verified_only_select(sql); - assert_eq!( - Expr::IsNull(Box::new(Expr::SimilarTo { - expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_create_table_with_strict() { let sql = "CREATE TABLE Fruits (id TEXT NOT NULL PRIMARY KEY) STRICT"; From bf89b7d808f331aaac5cdb281ae66995ffd06fdc Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 21 Apr 2024 21:13:18 +0800 Subject: [PATCH 416/806] Encapsulate `Insert` and `Delete` into specific structs (#1224) Signed-off-by: tison --- src/ast/dml.rs | 84 +++++++++++++++++++++++++++ src/ast/mod.rs | 110 +++++++++++------------------------- src/parser/mod.rs | 10 ++-- tests/sqlparser_bigquery.rs | 4 +- tests/sqlparser_common.rs | 42 +++++++------- tests/sqlparser_mysql.rs | 44 +++++++-------- tests/sqlparser_postgres.rs | 40 ++++++------- 7 files changed, 187 insertions(+), 147 deletions(-) create mode 100644 src/ast/dml.rs diff --git a/src/ast/dml.rs b/src/ast/dml.rs new file mode 100644 index 000000000..badc58a7d --- /dev/null +++ b/src/ast/dml.rs @@ -0,0 +1,84 @@ +// Licensed 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. + +#[cfg(not(feature = "std"))] +use alloc::{boxed::Box, vec::Vec}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +use super::{ + Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, + Query, SelectItem, SqliteOnConflict, TableWithJoins, +}; + +/// INSERT 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 Insert { + /// Only for Sqlite + pub or: Option, + /// Only for mysql + pub ignore: bool, + /// INTO - optional keyword + pub into: bool, + /// TABLE + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// table_name as foo (for PostgreSQL) + pub table_alias: Option, + /// COLUMNS + pub columns: Vec, + /// Overwrite (Hive) + pub overwrite: bool, + /// A SQL query that specifies what to insert + pub source: Option>, + /// partitioned insert (Hive) + pub partitioned: Option>, + /// Columns defined after PARTITION + pub after_columns: Vec, + /// whether the insert has the table keyword (Hive) + pub table: bool, + pub on: Option, + /// RETURNING + pub returning: Option>, + /// Only for mysql + pub replace_into: bool, + /// Only for mysql + pub priority: Option, + /// Only for mysql + pub insert_alias: Option, +} + +/// DELETE 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 Delete { + /// Multi tables delete are supported in mysql + pub tables: Vec, + /// FROM + pub from: FromTable, + /// USING (Snowflake, Postgres, MySQL) + pub using: Option>, + /// WHERE + pub selection: Option, + /// RETURNING + pub returning: Option>, + /// ORDER BY (MySQL) + pub order_by: Vec, + /// LIMIT (MySQL) + pub limit: Option, +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b78a559a0..f02461e0e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -37,6 +37,7 @@ pub use self::ddl::{ ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; +pub use self::dml::{Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ Cte, CteAsMaterialized, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, ForClause, @@ -60,6 +61,7 @@ pub use visitor::*; mod data_type; mod dcl; mod ddl; +mod dml; pub mod helpers; mod operator; mod query; @@ -1800,40 +1802,7 @@ pub enum Statement { /// ```sql /// INSERT /// ``` - Insert { - /// Only for Sqlite - or: Option, - /// Only for mysql - ignore: bool, - /// INTO - optional keyword - into: bool, - /// TABLE - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - /// table_name as foo (for PostgreSQL) - table_alias: Option, - /// COLUMNS - columns: Vec, - /// Overwrite (Hive) - overwrite: bool, - /// A SQL query that specifies what to insert - source: Option>, - /// partitioned insert (Hive) - partitioned: Option>, - /// Columns defined after PARTITION - after_columns: Vec, - /// whether the insert has the table keyword (Hive) - table: bool, - on: Option, - /// RETURNING - returning: Option>, - /// Only for mysql - replace_into: bool, - /// Only for mysql - priority: Option, - /// Only for mysql - insert_alias: Option, - }, + Insert(Insert), /// ```sql /// INSTALL /// ``` @@ -1923,22 +1892,7 @@ pub enum Statement { /// ```sql /// DELETE /// ``` - Delete { - /// Multi tables delete are supported in mysql - tables: Vec, - /// FROM - from: FromTable, - /// USING (Snowflake, Postgres, MySQL) - using: Option>, - /// WHERE - selection: Option, - /// RETURNING - returning: Option>, - /// ORDER BY (MySQL) - order_by: Vec, - /// LIMIT (MySQL) - limit: Option, - }, + Delete(Delete), /// ```sql /// CREATE VIEW /// ``` @@ -2912,24 +2866,25 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Insert { - or, - ignore, - into, - table_name, - table_alias, - overwrite, - partitioned, - columns, - after_columns, - source, - table, - on, - returning, - replace_into, - priority, - insert_alias, - } => { + Statement::Insert(insert) => { + let Insert { + or, + ignore, + into, + table_name, + table_alias, + overwrite, + partitioned, + columns, + after_columns, + source, + table, + on, + returning, + replace_into, + priority, + insert_alias, + } = insert; let table_name = if let Some(alias) = table_alias { format!("{table_name} AS {alias}") } else { @@ -3074,15 +3029,16 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Delete { - tables, - from, - using, - selection, - returning, - order_by, - limit, - } => { + Statement::Delete(delete) => { + let Delete { + tables, + from, + using, + selection, + returning, + order_by, + limit, + } = delete; write!(f, "DELETE ")?; if !tables.is_empty() { write!(f, "{} ", display_comma_separated(tables))?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9ad27b16a..c19019074 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7082,7 +7082,7 @@ impl<'a> Parser<'a> { None }; - Ok(Statement::Delete { + Ok(Statement::Delete(Delete { tables, from: if with_from_keyword { FromTable::WithFromKeyword(from) @@ -7094,7 +7094,7 @@ impl<'a> Parser<'a> { returning, order_by, limit, - }) + })) } // KILL [CONNECTION | QUERY | MUTATION] processlist_id @@ -8658,7 +8658,7 @@ impl<'a> Parser<'a> { } let insert = &mut self.parse_insert()?; - if let Statement::Insert { replace_into, .. } = insert { + if let Statement::Insert(Insert { replace_into, .. }) = insert { *replace_into = true; } @@ -8826,7 +8826,7 @@ impl<'a> Parser<'a> { None }; - Ok(Statement::Insert { + Ok(Statement::Insert(Insert { or, table_name, table_alias, @@ -8843,7 +8843,7 @@ impl<'a> Parser<'a> { replace_into, priority, insert_alias, - }) + })) } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index a01c09d96..170af820d 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -90,10 +90,10 @@ fn parse_raw_literal() { fn parse_delete_statement() { let sql = "DELETE \"table\" WHERE 1"; match bigquery_and_generic().verified_stmt(sql) { - Statement::Delete { + Statement::Delete(Delete { from: FromTable::WithoutKeyword(from), .. - } => { + }) => { assert_eq!( TableFactor::Table { name: ObjectName(vec![Ident::with_quote('"', "table")]), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index dd447ebb8..82cb600c5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -84,12 +84,12 @@ fn parse_insert_values() { expected_rows: &[Vec], ) { match verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source: Some(source), .. - } => { + }) => { assert_eq!(table_name.to_string(), expected_table_name); assert_eq!(columns.len(), expected_columns.len()); for (index, column) in columns.iter().enumerate() { @@ -125,7 +125,7 @@ fn parse_insert_default_values() { let insert_with_default_values = verified_stmt("INSERT INTO test_table DEFAULT VALUES"); match insert_with_default_values { - Statement::Insert { + Statement::Insert(Insert { after_columns, columns, on, @@ -134,7 +134,7 @@ fn parse_insert_default_values() { source, table_name, .. - } => { + }) => { assert_eq!(columns, vec![]); assert_eq!(after_columns, vec![]); assert_eq!(on, None); @@ -150,7 +150,7 @@ fn parse_insert_default_values() { verified_stmt("INSERT INTO test_table DEFAULT VALUES RETURNING test_column"); match insert_with_default_values_and_returning { - Statement::Insert { + Statement::Insert(Insert { after_columns, columns, on, @@ -159,7 +159,7 @@ fn parse_insert_default_values() { source, table_name, .. - } => { + }) => { assert_eq!(after_columns, vec![]); assert_eq!(columns, vec![]); assert_eq!(on, None); @@ -175,7 +175,7 @@ fn parse_insert_default_values() { verified_stmt("INSERT INTO test_table DEFAULT VALUES ON CONFLICT DO NOTHING"); match insert_with_default_values_and_on_conflict { - Statement::Insert { + Statement::Insert(Insert { after_columns, columns, on, @@ -184,7 +184,7 @@ fn parse_insert_default_values() { source, table_name, .. - } => { + }) => { assert_eq!(after_columns, vec![]); assert_eq!(columns, vec![]); assert!(on.is_some()); @@ -230,11 +230,11 @@ fn parse_insert_select_returning() { verified_stmt("INSERT INTO t SELECT 1 RETURNING 2"); let stmt = verified_stmt("INSERT INTO t SELECT x RETURNING x AS y"); match stmt { - Statement::Insert { + Statement::Insert(Insert { returning: Some(ret), source: Some(_), .. - } => assert_eq!(ret.len(), 1), + }) => assert_eq!(ret.len(), 1), _ => unreachable!(), } } @@ -255,7 +255,7 @@ fn parse_insert_sqlite() { .pop() .unwrap() { - Statement::Insert { or, .. } => assert_eq!(or, expected_action), + Statement::Insert(Insert { or, .. }) => assert_eq!(or, expected_action), _ => panic!("{}", sql), }; @@ -545,10 +545,10 @@ fn parse_no_table_name() { fn parse_delete_statement() { let sql = "DELETE FROM \"table\""; match verified_stmt(sql) { - Statement::Delete { + Statement::Delete(Delete { from: FromTable::WithFromKeyword(from), .. - } => { + }) => { assert_eq!( TableFactor::Table { name: ObjectName(vec![Ident::with_quote('"', "table")]), @@ -582,11 +582,11 @@ fn parse_delete_statement_for_multi_tables() { let sql = "DELETE schema1.table1, schema2.table2 FROM schema1.table1 JOIN schema2.table2 ON schema2.table2.col1 = schema1.table1.col1 WHERE schema2.table2.col2 = 1"; let dialects = all_dialects_except(|d| d.is::() || d.is::()); match dialects.verified_stmt(sql) { - Statement::Delete { + Statement::Delete(Delete { tables, from: FromTable::WithFromKeyword(from), .. - } => { + }) => { assert_eq!( ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), tables[0] @@ -626,11 +626,11 @@ fn parse_delete_statement_for_multi_tables() { fn parse_delete_statement_for_multi_tables_with_using() { let sql = "DELETE FROM schema1.table1, schema2.table2 USING schema1.table1 JOIN schema2.table2 ON schema2.table2.pk = schema1.table1.col1 WHERE schema2.table2.col2 = 1"; match verified_stmt(sql) { - Statement::Delete { + Statement::Delete(Delete { from: FromTable::WithFromKeyword(from), using: Some(using), .. - } => { + }) => { assert_eq!( TableFactor::Table { name: ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), @@ -686,14 +686,14 @@ fn parse_where_delete_statement() { let sql = "DELETE FROM foo WHERE name = 5"; match verified_stmt(sql) { - Statement::Delete { + Statement::Delete(Delete { tables: _, from: FromTable::WithFromKeyword(from), using, selection, returning, .. - } => { + }) => { assert_eq!( TableFactor::Table { name: ObjectName(vec![Ident::new("foo")]), @@ -727,14 +727,14 @@ fn parse_where_delete_with_alias_statement() { let sql = "DELETE FROM basket AS a USING basket AS b WHERE a.id < b.id"; match verified_stmt(sql) { - Statement::Delete { + Statement::Delete(Delete { tables: _, from: FromTable::WithFromKeyword(from), using, selection, returning, .. - } => { + }) => { assert_eq!( TableFactor::Table { name: ObjectName(vec![Ident::new("basket")]), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b2c164e3d..0fcf61d0b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1211,13 +1211,13 @@ fn parse_simple_insert() { let sql = r"INSERT INTO tasks (title, priority) VALUES ('Test Some Inserts', 1), ('Test Entry 2', 2), ('Test Entry 3', 3)"; match mysql().verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, on, .. - } => { + }) => { assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); @@ -1263,14 +1263,14 @@ fn parse_ignore_insert() { let sql = r"INSERT IGNORE INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; match mysql_and_generic().verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, on, ignore, .. - } => { + }) => { assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); @@ -1305,14 +1305,14 @@ fn parse_priority_insert() { let sql = r"INSERT HIGH_PRIORITY INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; match mysql_and_generic().verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, on, priority, .. - } => { + }) => { assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); @@ -1344,14 +1344,14 @@ fn parse_priority_insert() { let sql2 = r"INSERT LOW_PRIORITY INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; match mysql().verified_stmt(sql2) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, on, priority, .. - } => { + }) => { assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); @@ -1385,13 +1385,13 @@ fn parse_priority_insert() { fn parse_insert_as() { let sql = r"INSERT INTO `table` (`date`) VALUES ('2024-01-01') AS `alias`"; match mysql_and_generic().verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, insert_alias, .. - } => { + }) => { assert_eq!( ObjectName(vec![Ident::with_quote('`', "table")]), table_name @@ -1435,13 +1435,13 @@ fn parse_insert_as() { let sql = r"INSERT INTO `table` (`id`, `date`) VALUES (1, '2024-01-01') AS `alias` (`mek_id`, `mek_date`)"; match mysql_and_generic().verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, insert_alias, .. - } => { + }) => { assert_eq!( ObjectName(vec![Ident::with_quote('`', "table")]), table_name @@ -1491,7 +1491,7 @@ fn parse_insert_as() { fn parse_replace_insert() { let sql = r"REPLACE DELAYED INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; match mysql().verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, @@ -1499,7 +1499,7 @@ fn parse_replace_insert() { replace_into, priority, .. - } => { + }) => { assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); @@ -1535,13 +1535,13 @@ fn parse_empty_row_insert() { let sql = "INSERT INTO tb () VALUES (), ()"; match mysql().one_statement_parses_to(sql, "INSERT INTO tb VALUES (), ()") { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, on, .. - } => { + }) => { assert_eq!(ObjectName(vec![Ident::new("tb")]), table_name); assert!(columns.is_empty()); assert!(on.is_none()); @@ -1572,13 +1572,13 @@ fn parse_insert_with_on_duplicate_update() { let sql = "INSERT INTO permission_groups (name, description, perm_create, perm_read, perm_update, perm_delete) VALUES ('accounting_manager', 'Some description about the group', true, true, true, true) ON DUPLICATE KEY UPDATE description = VALUES(description), perm_create = VALUES(perm_create), perm_read = VALUES(perm_read), perm_update = VALUES(perm_update), perm_delete = VALUES(perm_delete)"; match mysql().verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source, on, .. - } => { + }) => { assert_eq!( ObjectName(vec![Ident::new("permission_groups")]), table_name @@ -1804,11 +1804,11 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { fn parse_insert_with_numeric_prefix_column_name() { let sql = "INSERT INTO s1.t1 (123col_$@length123) VALUES (67.654)"; match mysql().verified_stmt(sql) { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, .. - } => { + }) => { assert_eq!( ObjectName(vec![Ident::new("s1"), Ident::new("t1")]), table_name @@ -1898,7 +1898,7 @@ fn parse_update_with_joins() { fn parse_delete_with_order_by() { let sql = "DELETE FROM customers ORDER BY id DESC"; match mysql().verified_stmt(sql) { - Statement::Delete { order_by, .. } => { + Statement::Delete(Delete { order_by, .. }) => { assert_eq!( vec![OrderByExpr { expr: Expr::Identifier(Ident { @@ -1919,7 +1919,7 @@ fn parse_delete_with_order_by() { fn parse_delete_with_limit() { let sql = "DELETE FROM customers LIMIT 100"; match mysql().verified_stmt(sql) { - Statement::Delete { limit, .. } => { + Statement::Delete(Delete { limit, .. }) => { assert_eq!(Some(Expr::Value(number("100"))), limit); } _ => unreachable!(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6bb4bc69b..356651af4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1482,12 +1482,12 @@ fn parse_prepare() { _ => unreachable!(), }; match sub_stmt.as_ref() { - Statement::Insert { + Statement::Insert(Insert { table_name, columns, source: Some(source), .. - } => { + }) => { assert_eq!(table_name.to_string(), "customers"); assert!(columns.is_empty()); @@ -1539,14 +1539,14 @@ fn parse_pg_on_conflict() { DO UPDATE SET dname = EXCLUDED.dname", ); match stmt { - Statement::Insert { + Statement::Insert(Insert { on: Some(OnInsert::OnConflict(OnConflict { conflict_target: Some(ConflictTarget::Columns(cols)), action, })), .. - } => { + }) => { assert_eq!(vec![Ident::from("did")], cols); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { @@ -1569,14 +1569,14 @@ fn parse_pg_on_conflict() { DO UPDATE SET dname = EXCLUDED.dname, area = EXCLUDED.area", ); match stmt { - Statement::Insert { + Statement::Insert(Insert { on: Some(OnInsert::OnConflict(OnConflict { conflict_target: Some(ConflictTarget::Columns(cols)), action, })), .. - } => { + }) => { assert_eq!(vec![Ident::from("did"), Ident::from("area"),], cols); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { @@ -1607,14 +1607,14 @@ fn parse_pg_on_conflict() { ON CONFLICT DO NOTHING", ); match stmt { - Statement::Insert { + Statement::Insert(Insert { on: Some(OnInsert::OnConflict(OnConflict { conflict_target: None, action, })), .. - } => { + }) => { assert_eq!(OnConflictAction::DoNothing, action); } _ => unreachable!(), @@ -1627,14 +1627,14 @@ fn parse_pg_on_conflict() { DO UPDATE SET dname = $1 WHERE dsize > $2", ); match stmt { - Statement::Insert { + Statement::Insert(Insert { on: Some(OnInsert::OnConflict(OnConflict { conflict_target: Some(ConflictTarget::Columns(cols)), action, })), .. - } => { + }) => { assert_eq!(vec![Ident::from("did")], cols); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { @@ -1664,14 +1664,14 @@ fn parse_pg_on_conflict() { DO UPDATE SET dname = $1 WHERE dsize > $2", ); match stmt { - Statement::Insert { + Statement::Insert(Insert { on: Some(OnInsert::OnConflict(OnConflict { conflict_target: Some(ConflictTarget::OnConstraint(cname)), action, })), .. - } => { + }) => { assert_eq!(vec![Ident::from("distributors_did_pkey")], cname.0); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { @@ -1701,7 +1701,7 @@ fn parse_pg_returning() { "INSERT INTO distributors (did, dname) VALUES (DEFAULT, 'XYZ Widgets') RETURNING did", ); match stmt { - Statement::Insert { returning, .. } => { + Statement::Insert(Insert { returning, .. }) => { assert_eq!( Some(vec![SelectItem::UnnamedExpr(Expr::Identifier( "did".into() @@ -1739,7 +1739,7 @@ fn parse_pg_returning() { let stmt = pg_and_generic().verified_stmt("DELETE FROM tasks WHERE status = 'DONE' RETURNING *"); match stmt { - Statement::Delete { returning, .. } => { + Statement::Delete(Delete { returning, .. }) => { assert_eq!( Some(vec![SelectItem::Wildcard( WildcardAdditionalOptions::default() @@ -3570,7 +3570,7 @@ fn test_simple_postgres_insert_with_alias() { assert_eq!( statement, - Statement::Insert { + Statement::Insert(Insert { or: None, ignore: false, into: true, @@ -3621,7 +3621,7 @@ fn test_simple_postgres_insert_with_alias() { replace_into: false, priority: None, insert_alias: None - } + }) ) } @@ -3634,7 +3634,7 @@ fn test_simple_postgres_insert_with_alias() { assert_eq!( statement, - Statement::Insert { + Statement::Insert(Insert { or: None, ignore: false, into: true, @@ -3688,7 +3688,7 @@ fn test_simple_postgres_insert_with_alias() { replace_into: false, priority: None, insert_alias: None - } + }) ) } @@ -3700,7 +3700,7 @@ fn test_simple_insert_with_quoted_alias() { assert_eq!( statement, - Statement::Insert { + Statement::Insert(Insert { or: None, ignore: false, into: true, @@ -3751,7 +3751,7 @@ fn test_simple_insert_with_quoted_alias() { replace_into: false, priority: None, insert_alias: None, - } + }) ) } From 39980e89765041c194acfbb7e347291c9ed7f730 Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Mon, 22 Apr 2024 13:17:50 -0700 Subject: [PATCH 417/806] Support Snowflake `MATCH_RECOGNIZE` syntax (#1222) --- src/ast/mod.rs | 16 +- src/ast/query.rs | 266 ++++++++++++++++++++++++++++++++++ src/dialect/generic.rs | 4 + src/dialect/mod.rs | 4 + src/dialect/snowflake.rs | 4 + src/keywords.rs | 11 ++ src/parser/mod.rs | 237 ++++++++++++++++++++++++++++++ src/test_utils.rs | 17 +++ tests/sqlparser_common.rs | 297 +++++++++++++++++++++++++++++++++++++- 9 files changed, 847 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f02461e0e..32c7f6e9b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,13 +40,15 @@ pub use self::ddl::{ pub use self::dml::{Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ - Cte, CteAsMaterialized, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, ForClause, - ForJson, ForXml, GroupByExpr, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, - JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, - NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, - ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, - SetQuantifier, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, - ValueTableMode, Values, WildcardAdditionalOptions, With, + AfterMatchSkip, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, ExceptSelectItem, + ExcludeSelectItem, Fetch, ForClause, ForJson, ForXml, GroupByExpr, IdentWithAlias, + IlikeSelectItem, Join, JoinConstraint, JoinOperator, JsonTableColumn, + JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern, + MatchRecognizeSymbol, Measure, NamedWindowDefinition, NonBlock, Offset, OffsetRows, + OrderByExpr, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, + ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, + SetQuantifier, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, + Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, diff --git a/src/ast/query.rs b/src/ast/query.rs index 391ef51d8..5f5bca4cc 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -852,6 +852,238 @@ pub enum TableFactor { columns: Vec, alias: Option, }, + /// A `MATCH_RECOGNIZE` operation on a table. + /// + /// See . + MatchRecognize { + table: Box, + /// `PARTITION BY [, ... ]` + partition_by: Vec, + /// `ORDER BY [, ... ]` + order_by: Vec, + /// `MEASURES [AS] [, ... ]` + measures: Vec, + /// `ONE ROW PER MATCH | ALL ROWS PER MATCH [ , b BYTES(42)>, y ARRAY>)"; - match bigquery().one_statement_parses_to(sql, sql) { + match bigquery_and_generic().one_statement_parses_to(sql, sql) { Statement::CreateTable { name, columns, .. } => { assert_eq!(name, ObjectName(vec!["table".into()])); assert_eq!( @@ -395,19 +395,25 @@ fn parse_nested_data_types() { fn parse_invalid_brackets() { let sql = "SELECT STRUCT>(NULL)"; assert_eq!( - bigquery().parse_sql_statements(sql).unwrap_err(), + 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().parse_sql_statements(sql).unwrap_err(), + 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().parse_sql_statements(sql).unwrap_err(), + bigquery_and_generic() + .parse_sql_statements(sql) + .unwrap_err(), ParserError::ParserError( "Expected ',' or ')' after column definition, found: >".to_string() ) @@ -445,7 +451,7 @@ fn parse_typeless_struct_syntax() { // typeless struct syntax https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#typeless_struct_syntax // syntax: STRUCT( expr1 [AS field_name] [, ... ]) let sql = "SELECT STRUCT(1, 2, 3), STRUCT('abc'), STRUCT(1, t.str_col), STRUCT(1 AS a, 'abc' AS b), STRUCT(str_col AS abc)"; - let select = bigquery().verified_only_select(sql); + let select = bigquery_and_generic().verified_only_select(sql); assert_eq!(5, select.projection.len()); assert_eq!( &Expr::Struct { @@ -505,7 +511,7 @@ fn parse_typeless_struct_syntax() { } #[test] -fn parse_typed_struct_syntax() { +fn parse_typed_struct_syntax_bigquery() { // typed struct syntax https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#typed_struct_syntax // syntax: STRUCT<[field_name] field_type, ...>( expr1 [, ... ]) @@ -789,7 +795,291 @@ fn parse_typed_struct_syntax() { } #[test] -fn parse_typed_struct_with_field_name() { +fn parse_typed_struct_syntax_bigquery_and_generic() { + // typed struct syntax https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#typed_struct_syntax + // syntax: STRUCT<[field_name] field_type, ...>( expr1 [, ... ]) + + let sql = r#"SELECT STRUCT(5), STRUCT(1, t.str_col), STRUCT, str STRUCT>(nested_col)"#; + let select = bigquery_and_generic().verified_only_select(sql); + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(number("5")),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Int64, + }] + }, + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Struct { + values: vec![ + Expr::Value(number("1")), + Expr::CompoundIdentifier(vec![ + Ident { + value: "t".into(), + quote_style: None, + }, + Ident { + value: "str_col".into(), + quote_style: None, + }, + ]), + ], + fields: vec![ + StructField { + field_name: Some(Ident { + value: "x".into(), + quote_style: None, + }), + field_type: DataType::Int64 + }, + StructField { + field_name: Some(Ident { + value: "y".into(), + quote_style: None, + }), + field_type: DataType::String(None) + }, + ] + }, + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Identifier(Ident { + value: "nested_col".into(), + quote_style: None, + }),], + fields: vec![ + StructField { + field_name: Some("arr".into()), + field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( + DataType::Float64 + ))) + }, + StructField { + field_name: Some("str".into()), + field_type: DataType::Struct(vec![StructField { + field_name: None, + field_type: DataType::Bool + }]) + }, + ] + }, + expr_from_projection(&select.projection[2]) + ); + + let sql = r#"SELECT STRUCT>(nested_col)"#; + let select = bigquery_and_generic().verified_only_select(sql); + assert_eq!(1, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Identifier(Ident { + value: "nested_col".into(), + quote_style: None, + }),], + fields: vec![ + StructField { + field_name: Some("x".into()), + field_type: DataType::Struct(Default::default()) + }, + StructField { + field_name: Some("y".into()), + field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( + DataType::Struct(Default::default()) + ))) + }, + ] + }, + expr_from_projection(&select.projection[0]) + ); + + let sql = r#"SELECT STRUCT(true), STRUCT(B'abc')"#; + let select = bigquery_and_generic().verified_only_select(sql); + assert_eq!(2, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(Value::Boolean(true)),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Bool + }] + }, + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(Value::SingleQuotedByteStringLiteral( + "abc".into() + )),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Bytes(Some(42)) + }] + }, + expr_from_projection(&select.projection[1]) + ); + + let sql = r#"SELECT STRUCT('2011-05-05'), STRUCT(DATETIME '1999-01-01 01:23:34.45'), STRUCT(5.0), STRUCT(1)"#; + let select = bigquery_and_generic().verified_only_select(sql); + assert_eq!(4, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(Value::SingleQuotedString( + "2011-05-05".to_string() + )),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Date + }] + }, + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::TypedString { + data_type: DataType::Datetime(None), + value: "1999-01-01 01:23:34.45".to_string() + },], + fields: vec![StructField { + field_name: None, + field_type: DataType::Datetime(None) + }] + }, + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(number("5.0")),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Float64 + }] + }, + expr_from_projection(&select.projection[2]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Value(number("1")),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Int64 + }] + }, + expr_from_projection(&select.projection[3]) + ); + + let sql = r#"SELECT STRUCT(INTERVAL '1-2 3 4:5:6.789999'), STRUCT(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#; + let select = bigquery_and_generic().verified_only_select(sql); + assert_eq!(2, select.projection.len()); + assert_eq!( + &Expr::Struct { + values: vec![Expr::Interval(ast::Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString( + "1-2 3 4:5:6.789999".to_string() + ))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None + }),], + fields: vec![StructField { + field_name: None, + field_type: DataType::Interval + }] + }, + expr_from_projection(&select.projection[0]) + ); + assert_eq!( + &Expr::Struct { + values: vec![Expr::TypedString { + data_type: DataType::JSON, + value: r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.to_string() + },], + fields: vec![StructField { + field_name: None, + field_type: DataType::JSON + }] + }, + expr_from_projection(&select.projection[1]) + ); + + let sql = r#"SELECT STRUCT('foo'), STRUCT(TIMESTAMP '2008-12-25 15:30:00 America/Los_Angeles'), STRUCT
), } +impl SetExpr { + /// If this `SetExpr` is a `SELECT`, returns the [`Select`]. + pub fn as_select(&self) -> Option<&Select> { + if let Self::Select(select) = self { + Some(&**select) + } else { + None + } + } +} + impl fmt::Display for SetExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a7ec4d093..8132921f1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8594,8 +8594,19 @@ impl<'a> Parser<'a> { self.expected("joined table", self.peek_token()) } } else if dialect_of!(self is SnowflakeDialect | DatabricksDialect | GenericDialect) - && self.parse_keyword(Keyword::VALUES) + && matches!( + self.peek_tokens(), + [ + Token::Word(Word { + keyword: Keyword::VALUES, + .. + }), + Token::LParen + ] + ) { + self.expect_keyword(Keyword::VALUES)?; + // Snowflake and Databricks allow syntax like below: // SELECT * FROM VALUES (1, 'a'), (2, 'b') AS t (col1, col2) // where there are no parentheses around the VALUES clause. diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 8f0579fc9..430647ded 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -1,5 +1,5 @@ use sqlparser::ast::*; -use sqlparser::dialect::DatabricksDialect; +use sqlparser::dialect::{DatabricksDialect, GenericDialect}; use sqlparser::parser::ParserError; use test_utils::*; @@ -13,6 +13,13 @@ fn databricks() -> TestedDialects { } } +fn databricks_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(DatabricksDialect {}), Box::new(GenericDialect {})], + options: None, + } +} + #[test] fn test_databricks_identifiers() { // databricks uses backtick for delimited identifiers @@ -124,3 +131,60 @@ fn test_databricks_lambdas() { ); databricks().verified_expr("transform(array(1, 2, 3), x -> x + 1)"); } + +#[test] +fn test_values_clause() { + let values = Values { + explicit_row: false, + rows: vec![ + vec![ + Expr::Value(Value::DoubleQuotedString("one".to_owned())), + Expr::Value(number("1")), + ], + vec![ + Expr::Value(Value::SingleQuotedString("two".to_owned())), + Expr::Value(number("2")), + ], + ], + }; + + let query = databricks().verified_query(r#"VALUES ("one", 1), ('two', 2)"#); + assert_eq!(SetExpr::Values(values.clone()), *query.body); + + // VALUES is permitted in a FROM clause without a subquery + let query = databricks().verified_query_with_canonical( + r#"SELECT * FROM VALUES ("one", 1), ('two', 2)"#, + r#"SELECT * FROM (VALUES ("one", 1), ('two', 2))"#, + ); + let Some(TableFactor::Derived { subquery, .. }) = query + .body + .as_select() + .map(|select| &select.from[0].relation) + else { + panic!("expected subquery"); + }; + assert_eq!(SetExpr::Values(values), *subquery.body); + + // values is also a valid table name + let query = databricks_and_generic().verified_query(concat!( + "WITH values AS (SELECT 42) ", + "SELECT * FROM values", + )); + assert_eq!( + Some(&TableFactor::Table { + name: ObjectName(vec![Ident::new("values")]), + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![] + }), + query + .body + .as_select() + .map(|select| &select.from[0].relation) + ); + + // TODO: support this example from https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-qry-select-values.html#examples + // databricks().verified_query("VALUES 1, 2, 3"); +} From d5faf3c54bba16a0b617117a312eb96eee59706a Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Thu, 23 May 2024 10:30:05 -0700 Subject: [PATCH 452/806] Support expression in AT TIME ZONE and fix precedence (#1272) --- src/ast/mod.rs | 4 +-- src/parser/mod.rs | 62 +++++++++---------------------------- tests/sqlparser_common.rs | 12 +++++-- tests/sqlparser_postgres.rs | 40 ++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d937b7275..c9de747c7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -584,7 +584,7 @@ pub enum Expr { /// AT a timestamp to a different timezone e.g. `FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00'` AtTimeZone { timestamp: Box, - time_zone: String, + time_zone: Box, }, /// Extract a field from a timestamp e.g. `EXTRACT(MONTH FROM foo)` /// @@ -1270,7 +1270,7 @@ impl fmt::Display for Expr { timestamp, time_zone, } => { - write!(f, "{timestamp} AT TIME ZONE '{time_zone}'") + write!(f, "{timestamp} AT TIME ZONE {time_zone}") } Expr::Interval(interval) => { write!(f, "{interval}") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8132921f1..f88aefd10 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2469,26 +2469,11 @@ impl<'a> Parser<'a> { } } Keyword::AT => { - // if self.parse_keyword(Keyword::TIME) { - // self.expect_keyword(Keyword::ZONE)?; - if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) { - let time_zone = self.next_token(); - match time_zone.token { - Token::SingleQuotedString(time_zone) => { - log::trace!("Peek token: {:?}", self.peek_token()); - Ok(Expr::AtTimeZone { - timestamp: Box::new(expr), - time_zone, - }) - } - _ => self.expected( - "Expected Token::SingleQuotedString after AT TIME ZONE", - time_zone, - ), - } - } else { - self.expected("Expected Token::Word after AT", tok) - } + self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(Expr::AtTimeZone { + timestamp: Box::new(expr), + time_zone: Box::new(self.parse_subexpr(precedence)?), + }) } Keyword::NOT | Keyword::IN @@ -2545,35 +2530,12 @@ impl<'a> Parser<'a> { ), } } else if Token::DoubleColon == tok { - let data_type = self.parse_data_type()?; - - let cast_expr = Expr::Cast { + Ok(Expr::Cast { kind: CastKind::DoubleColon, expr: Box::new(expr), - data_type: data_type.clone(), + data_type: self.parse_data_type()?, format: None, - }; - - match data_type { - DataType::Date - | DataType::Datetime(_) - | DataType::Timestamp(_, _) - | DataType::Time(_, _) => { - let value = self.parse_optional_time_zone()?; - match value { - Some(Value::SingleQuotedString(tz)) => Ok(Expr::AtTimeZone { - timestamp: Box::new(cast_expr), - time_zone: tz, - }), - None => Ok(cast_expr), - _ => Err(ParserError::ParserError(format!( - "Expected Token::SingleQuotedString after AT TIME ZONE, but found: {}", - value.unwrap() - ))), - } - } - _ => Ok(cast_expr), - } + }) } else if Token::ExclamationMark == tok { // PostgreSQL factorial operation Ok(Expr::UnaryOp { @@ -2784,10 +2746,14 @@ impl<'a> Parser<'a> { // use https://www.postgresql.org/docs/7.0/operators.htm#AEN2026 as a reference // higher number = higher precedence + // + // NOTE: The pg documentation is incomplete, e.g. the AT TIME ZONE operator + // actually has higher precedence than addition. + // See https://postgrespro.com/list/thread-id/2673331. + const AT_TZ_PREC: u8 = 41; const MUL_DIV_MOD_OP_PREC: u8 = 40; const PLUS_MINUS_PREC: u8 = 30; const XOR_PREC: u8 = 24; - const TIME_ZONE_PREC: u8 = 20; const BETWEEN_PREC: u8 = 20; const LIKE_PREC: u8 = 19; const IS_PREC: u8 = 17; @@ -2817,7 +2783,7 @@ impl<'a> Parser<'a> { (Token::Word(w), Token::Word(w2)) if w.keyword == Keyword::TIME && w2.keyword == Keyword::ZONE => { - Ok(Self::TIME_ZONE_PREC) + Ok(Self::AT_TZ_PREC) } _ => Ok(0), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6668ce8f4..f8b7d0265 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4995,7 +4995,9 @@ fn parse_at_timezone() { assert_eq!( &Expr::AtTimeZone { timestamp: Box::new(call("FROM_UNIXTIME", [zero.clone()])), - time_zone: "UTC-06:00".to_string(), + time_zone: Box::new(Expr::Value(Value::SingleQuotedString( + "UTC-06:00".to_string() + ))), }, expr_from_projection(only(&select.projection)), ); @@ -5009,7 +5011,9 @@ fn parse_at_timezone() { [ Expr::AtTimeZone { timestamp: Box::new(call("FROM_UNIXTIME", [zero])), - time_zone: "UTC-06:00".to_string(), + time_zone: Box::new(Expr::Value(Value::SingleQuotedString( + "UTC-06:00".to_string() + ))), }, Expr::Value(Value::SingleQuotedString("%Y-%m-%dT%H".to_string()),) ] @@ -7037,7 +7041,9 @@ fn parse_double_colon_cast_at_timezone() { data_type: DataType::Timestamp(None, TimezoneInfo::None), format: None }), - time_zone: "Europe/Brussels".to_string() + time_zone: Box::new(Expr::Value(Value::SingleQuotedString( + "Europe/Brussels".to_string() + ))), }, expr_from_projection(only(&select.projection)), ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d68ebd556..5c3b653dd 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3882,3 +3882,43 @@ fn parse_mat_cte() { let sql2 = r#"WITH cte AS NOT MATERIALIZED (SELECT id FROM accounts) SELECT id FROM cte"#; pg().verified_stmt(sql2); } + +#[test] +fn parse_at_time_zone() { + pg_and_generic().verified_expr("CURRENT_TIMESTAMP AT TIME ZONE tz"); + pg_and_generic().verified_expr("CURRENT_TIMESTAMP AT TIME ZONE ('America/' || 'Los_Angeles')"); + + // check precedence + let expr = Expr::BinaryOp { + left: Box::new(Expr::AtTimeZone { + timestamp: Box::new(Expr::TypedString { + data_type: DataType::Timestamp(None, TimezoneInfo::None), + value: "2001-09-28 01:00".to_owned(), + }), + time_zone: Box::new(Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Value(Value::SingleQuotedString( + "America/Los_Angeles".to_owned(), + ))), + data_type: DataType::Text, + format: None, + }), + }), + op: BinaryOperator::Plus, + right: Box::new(Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString( + "23 hours".to_owned(), + ))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + })), + }; + pretty_assertions::assert_eq!( + pg_and_generic().verified_expr( + "TIMESTAMP '2001-09-28 01:00' AT TIME ZONE 'America/Los_Angeles'::TEXT + INTERVAL '23 hours'", + ), + expr + ); +} From 792e389baaee74d2c035e8467eda0440417e0523 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Thu, 30 May 2024 18:18:41 +0200 Subject: [PATCH 453/806] Support CREATE FUNCTION for BigQuery (#1253) --- src/ast/mod.rs | 213 +++++++++++++++++------- src/keywords.rs | 1 + src/parser/mod.rs | 322 ++++++++++++++++++++++++++---------- tests/sqlparser_bigquery.rs | 139 ++++++++++++++++ tests/sqlparser_hive.rs | 27 +-- tests/sqlparser_postgres.rs | 22 +-- 6 files changed, 554 insertions(+), 170 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c9de747c7..1227ce935 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2454,14 +2454,64 @@ 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) + /// 3. [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement) CreateFunction { or_replace: bool, temporary: bool, + if_not_exists: bool, name: ObjectName, args: Option>, return_type: Option, - /// Optional parameters. - params: CreateFunctionBody, + /// The expression that defines the function. + /// + /// Examples: + /// ```sql + /// AS ((SELECT 1)) + /// AS "console.log();" + /// ``` + function_body: Option, + /// Behavior attribute for the function + /// + /// IMMUTABLE | STABLE | VOLATILE + /// + /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + behavior: Option, + /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT + /// + /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + called_on_null: Option, + /// PARALLEL { UNSAFE | RESTRICTED | SAFE } + /// + /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + parallel: Option, + /// USING ... (Hive only) + using: Option, + /// Language used in a UDF definition. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION foo() LANGUAGE js AS "console.log();" + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_javascript_udf) + language: Option, + /// Determinism keyword used for non-sql UDF definitions. + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) + determinism_specifier: Option, + /// List of options for creating the function. + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) + options: Option>, + /// Connection resource for a remote function. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION foo() + /// RETURNS FLOAT64 + /// REMOTE WITH CONNECTION us.myconnection + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_remote_function) + remote_connection: Option, }, /// ```sql /// CREATE PROCEDURE @@ -3152,16 +3202,26 @@ impl fmt::Display for Statement { Statement::CreateFunction { or_replace, temporary, + if_not_exists, name, args, return_type, - params, + function_body, + language, + behavior, + called_on_null, + parallel, + using, + determinism_specifier, + options, + remote_connection, } => { write!( f, - "CREATE {or_replace}{temp}FUNCTION {name}", + "CREATE {or_replace}{temp}FUNCTION {if_not_exists}{name}", temp = if *temporary { "TEMPORARY " } else { "" }, or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, )?; if let Some(args) = args { write!(f, "({})", display_comma_separated(args))?; @@ -3169,7 +3229,43 @@ impl fmt::Display for Statement { if let Some(return_type) = return_type { write!(f, " RETURNS {return_type}")?; } - write!(f, "{params}")?; + if let Some(determinism_specifier) = determinism_specifier { + write!(f, " {determinism_specifier}")?; + } + if let Some(language) = language { + write!(f, " LANGUAGE {language}")?; + } + if let Some(behavior) = behavior { + write!(f, " {behavior}")?; + } + if let Some(called_on_null) = called_on_null { + write!(f, " {called_on_null}")?; + } + if let Some(parallel) = parallel { + write!(f, " {parallel}")?; + } + if let Some(remote_connection) = remote_connection { + write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; + } + if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = function_body { + write!(f, " AS {function_body}")?; + } + if let Some(CreateFunctionBody::Return(function_body)) = function_body { + write!(f, " RETURN {function_body}")?; + } + if let Some(using) = using { + write!(f, " {using}")?; + } + if let Some(options) = options { + write!( + f, + " OPTIONS({})", + display_comma_separated(options.as_slice()) + )?; + } + if let Some(CreateFunctionBody::AsAfterOptions(function_body)) = function_body { + write!(f, " AS {function_body}")?; + } Ok(()) } Statement::CreateProcedure { @@ -6143,75 +6239,74 @@ impl fmt::Display for FunctionParallel { } } +/// [BigQuery] Determinism specifier used in a UDF definition. +/// +/// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum FunctionDefinition { - SingleQuotedDef(String), - DoubleDollarDef(String), +pub enum FunctionDeterminismSpecifier { + Deterministic, + NotDeterministic, } -impl fmt::Display for FunctionDefinition { +impl fmt::Display for FunctionDeterminismSpecifier { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - FunctionDefinition::SingleQuotedDef(s) => write!(f, "'{s}'")?, - FunctionDefinition::DoubleDollarDef(s) => write!(f, "$${s}$$")?, + FunctionDeterminismSpecifier::Deterministic => { + write!(f, "DETERMINISTIC") + } + FunctionDeterminismSpecifier::NotDeterministic => { + write!(f, "NOT DETERMINISTIC") + } } - Ok(()) } } -/// Postgres specific feature. +/// Represent the expression body of a `CREATE FUNCTION` statement as well as +/// where within the statement, the body shows up. /// -/// See [Postgres docs](https://www.postgresql.org/docs/15/sql-createfunction.html) -/// for more details -#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +/// [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 +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct CreateFunctionBody { - /// LANGUAGE lang_name - pub language: Option, - /// IMMUTABLE | STABLE | VOLATILE - pub behavior: Option, - /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT - pub called_on_null: Option, - /// PARALLEL { UNSAFE | RESTRICTED | SAFE } - pub parallel: Option, - /// AS 'definition' +pub enum CreateFunctionBody { + /// A function body expression using the 'AS' keyword and shows up + /// before any `OPTIONS` clause. /// - /// Note that Hive's `AS class_name` is also parsed here. - pub as_: Option, - /// RETURN expression - pub return_: Option, - /// USING ... (Hive only) - pub using: Option, -} - -impl fmt::Display for CreateFunctionBody { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(language) = &self.language { - write!(f, " LANGUAGE {language}")?; - } - if let Some(behavior) = &self.behavior { - write!(f, " {behavior}")?; - } - if let Some(called_on_null) = &self.called_on_null { - write!(f, " {called_on_null}")?; - } - if let Some(parallel) = &self.parallel { - write!(f, " {parallel}")?; - } - if let Some(definition) = &self.as_ { - write!(f, " AS {definition}")?; - } - if let Some(expr) = &self.return_ { - write!(f, " RETURN {expr}")?; - } - if let Some(using) = &self.using { - write!(f, " {using}")?; - } - Ok(()) - } + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(x FLOAT64, y FLOAT64) RETURNS FLOAT64 + /// AS (x * y) + /// OPTIONS(description="desc"); + /// ``` + /// + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 + AsBeforeOptions(Expr), + /// A function body expression using the 'AS' keyword and shows up + /// after any `OPTIONS` clause. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(x FLOAT64, y FLOAT64) RETURNS FLOAT64 + /// OPTIONS(description="desc") + /// AS (x * y); + /// ``` + /// + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 + AsAfterOptions(Expr), + /// Function body expression using the 'RETURN' keyword. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION myfunc(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER + /// LANGUAGE SQL + /// RETURN a + b; + /// ``` + /// + /// [Postgres]: https://www.postgresql.org/docs/current/sql-createfunction.html + Return(Expr), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/keywords.rs b/src/keywords.rs index e67fffd97..06086297c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -572,6 +572,7 @@ define_keywords!( RELATIVE, RELAY, RELEASE, + REMOTE, RENAME, REORG, REPAIR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f88aefd10..f88f2c189 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3590,95 +3590,53 @@ impl<'a> Parser<'a> { temporary: bool, ) -> Result { if dialect_of!(self is HiveDialect) { - let name = self.parse_object_name(false)?; - self.expect_keyword(Keyword::AS)?; - let class_name = self.parse_function_definition()?; - let params = CreateFunctionBody { - as_: Some(class_name), - using: self.parse_optional_create_function_using()?, - ..Default::default() - }; - - Ok(Statement::CreateFunction { - or_replace, - temporary, - name, - args: None, - return_type: None, - params, - }) + self.parse_hive_create_function(or_replace, temporary) } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) { - let name = self.parse_object_name(false)?; - self.expect_token(&Token::LParen)?; - let args = if self.consume_token(&Token::RParen) { - self.prev_token(); - None - } else { - Some(self.parse_comma_separated(Parser::parse_function_arg)?) - }; - - self.expect_token(&Token::RParen)?; - - let return_type = if self.parse_keyword(Keyword::RETURNS) { - Some(self.parse_data_type()?) - } else { - None - }; - - let params = self.parse_create_function_body()?; - - Ok(Statement::CreateFunction { - or_replace, - temporary, - name, - args, - return_type, - params, - }) + self.parse_postgres_create_function(or_replace, temporary) } else if dialect_of!(self is DuckDbDialect) { self.parse_create_macro(or_replace, temporary) + } else if dialect_of!(self is BigQueryDialect) { + self.parse_bigquery_create_function(or_replace, temporary) } else { self.prev_token(); self.expected("an object type after CREATE", self.peek_token()) } } - fn parse_function_arg(&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 { + /// Parse `CREATE FUNCTION` for [Postgres] + /// + /// [Postgres]: https://www.postgresql.org/docs/15/sql-createfunction.html + fn parse_postgres_create_function( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; + let args = if self.consume_token(&Token::RParen) { + self.prev_token(); None + } else { + Some(self.parse_comma_separated(Parser::parse_function_arg)?) }; - // 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 - name = Some(n.0[0].clone()); - data_type = self.parse_data_type()?; - } + self.expect_token(&Token::RParen)?; - let default_expr = if self.parse_keyword(Keyword::DEFAULT) || self.consume_token(&Token::Eq) - { - Some(self.parse_expr()?) + let return_type = if self.parse_keyword(Keyword::RETURNS) { + Some(self.parse_data_type()?) } else { None }; - Ok(OperateFunctionArg { - mode, - name, - data_type, - default_expr, - }) - } - fn parse_create_function_body(&mut self) -> Result { - let mut body = CreateFunctionBody::default(); + #[derive(Default)] + struct Body { + language: Option, + behavior: Option, + function_body: Option, + called_on_null: Option, + parallel: Option, + } + let mut body = Body::default(); loop { fn ensure_not_set(field: &Option, name: &str) -> Result<(), ParserError> { if field.is_some() { @@ -3689,8 +3647,10 @@ impl<'a> Parser<'a> { Ok(()) } if self.parse_keyword(Keyword::AS) { - ensure_not_set(&body.as_, "AS")?; - body.as_ = Some(self.parse_function_definition()?); + ensure_not_set(&body.function_body, "AS")?; + body.function_body = Some(CreateFunctionBody::AsBeforeOptions( + self.parse_create_function_body_string()?, + )); } else if self.parse_keyword(Keyword::LANGUAGE) { ensure_not_set(&body.language, "LANGUAGE")?; body.language = Some(self.parse_identifier(false)?); @@ -3744,12 +3704,186 @@ impl<'a> Parser<'a> { return self.expected("one of UNSAFE | RESTRICTED | SAFE", self.peek_token()); } } else if self.parse_keyword(Keyword::RETURN) { - ensure_not_set(&body.return_, "RETURN")?; - body.return_ = Some(self.parse_expr()?); + ensure_not_set(&body.function_body, "RETURN")?; + body.function_body = Some(CreateFunctionBody::Return(self.parse_expr()?)); + } else { + break; + } + } + + Ok(Statement::CreateFunction { + or_replace, + temporary, + name, + args, + return_type, + behavior: body.behavior, + called_on_null: body.called_on_null, + parallel: body.parallel, + language: body.language, + function_body: body.function_body, + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + } + + /// Parse `CREATE FUNCTION` for [Hive] + /// + /// [Hive]: https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction + fn parse_hive_create_function( + &mut self, + or_replace: bool, + temporary: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + self.expect_keyword(Keyword::AS)?; + + let as_ = self.parse_create_function_body_string()?; + let using = self.parse_optional_create_function_using()?; + + Ok(Statement::CreateFunction { + or_replace, + temporary, + name, + function_body: Some(CreateFunctionBody::AsBeforeOptions(as_)), + using, + if_not_exists: false, + args: None, + return_type: None, + behavior: None, + called_on_null: None, + parallel: None, + language: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + } + + /// Parse `CREATE FUNCTION` for [BigQuery] + /// + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement + fn parse_bigquery_create_function( + &mut self, + or_replace: bool, + 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(false)?; + 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)?; + self.expect_token(&Token::RParen)?; + + let return_type = if self.parse_keyword(Keyword::RETURNS) { + Some(self.parse_data_type()?) + } else { + None + }; + + let determinism_specifier = if self.parse_keyword(Keyword::DETERMINISTIC) { + Some(FunctionDeterminismSpecifier::Deterministic) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::DETERMINISTIC]) { + Some(FunctionDeterminismSpecifier::NotDeterministic) + } else { + None + }; + + let language = if self.parse_keyword(Keyword::LANGUAGE) { + Some(self.parse_identifier(false)?) + } else { + None + }; + + let remote_connection = + if self.parse_keywords(&[Keyword::REMOTE, Keyword::WITH, Keyword::CONNECTION]) { + Some(self.parse_object_name(false)?) } else { - return Ok(body); + None + }; + + // `OPTIONS` may come before of after the function body but + // may be specified at most once. + let mut options = self.maybe_parse_options(Keyword::OPTIONS)?; + + let function_body = if remote_connection.is_none() { + self.expect_keyword(Keyword::AS)?; + let expr = self.parse_expr()?; + if options.is_none() { + options = self.maybe_parse_options(Keyword::OPTIONS)?; + Some(CreateFunctionBody::AsBeforeOptions(expr)) + } else { + Some(CreateFunctionBody::AsAfterOptions(expr)) } + } else { + None + }; + + Ok(Statement::CreateFunction { + or_replace, + temporary, + if_not_exists, + name, + args: Some(args), + return_type, + function_body, + language, + determinism_specifier, + options, + remote_connection, + using: None, + behavior: None, + called_on_null: None, + parallel: None, + }) + } + + fn parse_function_arg(&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 + }; + + // 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 + name = Some(n.0[0].clone()); + data_type = self.parse_data_type()?; } + + let default_expr = if self.parse_keyword(Keyword::DEFAULT) || self.consume_token(&Token::Eq) + { + Some(self.parse_expr()?) + } else { + None + }; + Ok(OperateFunctionArg { + mode, + name, + data_type, + default_expr, + }) } pub fn parse_create_macro( @@ -3893,12 +4027,9 @@ impl<'a> Parser<'a> { }; if dialect_of!(self is BigQueryDialect | GenericDialect) { - if let Token::Word(word) = self.peek_token().token { - if word.keyword == Keyword::OPTIONS { - let opts = self.parse_options(Keyword::OPTIONS)?; - if !opts.is_empty() { - options = CreateTableOptions::Options(opts); - } + if let Some(opts) = self.maybe_parse_options(Keyword::OPTIONS)? { + if !opts.is_empty() { + options = CreateTableOptions::Options(opts); } }; } @@ -5680,6 +5811,18 @@ impl<'a> Parser<'a> { } } + pub fn maybe_parse_options( + &mut self, + keyword: Keyword, + ) -> Result>, ParserError> { + if let Token::Word(word) = self.peek_token().token { + if word.keyword == keyword { + return Ok(Some(self.parse_options(keyword)?)); + } + }; + Ok(None) + } + pub fn parse_options(&mut self, keyword: Keyword) -> Result, ParserError> { if self.parse_keyword(keyword) { self.expect_token(&Token::LParen)?; @@ -6521,19 +6664,22 @@ impl<'a> Parser<'a> { } } - pub fn parse_function_definition(&mut self) -> Result { + /// Parse the body of a `CREATE FUNCTION` specified as a string. + /// e.g. `CREATE FUNCTION ... AS $$ body $$`. + fn parse_create_function_body_string(&mut self) -> Result { let peek_token = self.peek_token(); match peek_token.token { - Token::DollarQuotedString(value) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => + Token::DollarQuotedString(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { self.next_token(); - Ok(FunctionDefinition::DoubleDollarDef(value.value)) + Ok(Expr::Value(Value::DollarQuotedString(s))) } - _ => Ok(FunctionDefinition::SingleQuotedDef( + _ => Ok(Expr::Value(Value::SingleQuotedString( self.parse_literal_string()?, - )), + ))), } } + /// Parse a literal string pub fn parse_literal_string(&mut self) -> Result { let next_token = self.next_token(); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 179755e0c..0bb91cb36 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1950,6 +1950,145 @@ fn parse_map_access_expr() { bigquery().verified_only_select(sql); } +#[test] +fn test_bigquery_create_function() { + let sql = concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "project1.mydataset.myfunction(x FLOAT64) ", + "RETURNS FLOAT64 ", + "OPTIONS(x = 'y') ", + "AS 42" + ); + + let stmt = bigquery().verified_stmt(sql); + assert_eq!( + stmt, + Statement::CreateFunction { + or_replace: true, + temporary: true, + if_not_exists: false, + name: ObjectName(vec![ + Ident::new("project1"), + Ident::new("mydataset"), + Ident::new("myfunction"), + ]), + args: Some(vec![OperateFunctionArg::with_name("x", DataType::Float64),]), + return_type: Some(DataType::Float64), + function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value(number( + "42" + )))), + options: Some(vec![SqlOption { + name: Ident::new("x"), + value: Expr::Value(Value::SingleQuotedString("y".into())), + }]), + behavior: None, + using: None, + language: None, + determinism_specifier: None, + remote_connection: None, + called_on_null: None, + parallel: None, + } + ); + + let sqls = [ + // Arbitrary Options expressions. + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "RETURNS ARRAY ", + "OPTIONS(a = [1, 2], b = 'two', c = [('k1', 'v1'), ('k2', 'v2')]) ", + "AS ((SELECT 1 FROM mytable))" + ), + // Options after body. + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "RETURNS ARRAY ", + "AS ((SELECT 1 FROM mytable)) ", + "OPTIONS(a = [1, 2], b = 'two', c = [('k1', 'v1'), ('k2', 'v2')])", + ), + // IF NOT EXISTS + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION IF NOT EXISTS ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "RETURNS ARRAY ", + "OPTIONS(a = [1, 2]) ", + "AS ((SELECT 1 FROM mytable))" + ), + // No return type. + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "OPTIONS(a = [1, 2]) ", + "AS ((SELECT 1 FROM mytable))" + ), + // With language - body after options + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "DETERMINISTIC ", + "LANGUAGE js ", + "OPTIONS(a = [1, 2]) ", + "AS \"console.log('hello');\"" + ), + // With language - body before options + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "NOT DETERMINISTIC ", + "LANGUAGE js ", + "AS \"console.log('hello');\" ", + "OPTIONS(a = [1, 2])", + ), + // Remote + concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "myfunction(a FLOAT64, b INT64, c STRING) ", + "RETURNS INT64 ", + "REMOTE WITH CONNECTION us.myconnection ", + "OPTIONS(a = [1, 2])", + ), + ]; + for sql in sqls { + bigquery().verified_stmt(sql); + } + + let error_sqls = [ + ( + concat!( + "CREATE TEMPORARY FUNCTION myfunction() ", + "OPTIONS(a = [1, 2]) ", + "AS ((SELECT 1 FROM mytable)) ", + "OPTIONS(a = [1, 2])", + ), + "Expected end of statement, found: OPTIONS", + ), + ( + concat!( + "CREATE TEMPORARY FUNCTION myfunction() ", + "IMMUTABLE ", + "AS ((SELECT 1 FROM mytable)) ", + ), + "Expected AS, found: IMMUTABLE", + ), + ( + concat!( + "CREATE TEMPORARY FUNCTION myfunction() ", + "AS \"console.log('hello');\" ", + "LANGUAGE js ", + ), + "Expected end of statement, found: LANGUAGE", + ), + ]; + for (sql, error) in error_sqls { + assert_eq!( + ParserError::ParserError(error.to_owned()), + bigquery().parse_sql_statements(sql).unwrap_err() + ); + } +} + #[test] fn test_bigquery_trim() { let real_sql = r#"SELECT customer_id, TRIM(item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 78db48ec2..b661b6cd3 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -17,8 +17,8 @@ use sqlparser::ast::{ CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionArgumentList, - FunctionArguments, FunctionDefinition, Ident, ObjectName, OneOrManyWithParens, SelectItem, - Statement, TableFactor, UnaryOperator, + FunctionArguments, Ident, ObjectName, OneOrManyWithParens, SelectItem, Statement, TableFactor, + UnaryOperator, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -296,22 +296,23 @@ fn parse_create_function() { Statement::CreateFunction { temporary, name, - params, + function_body, + using, .. } => { assert!(temporary); assert_eq!(name.to_string(), "mydb.myfunc"); assert_eq!( - params, - CreateFunctionBody { - as_: Some(FunctionDefinition::SingleQuotedDef( - "org.random.class.Name".to_string() - )), - using: Some(CreateFunctionUsing::Jar( - "hdfs://somewhere.com:8020/very/far".to_string() - )), - ..Default::default() - } + function_body, + Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + Value::SingleQuotedString("org.random.class.Name".to_string()) + ))) + ); + assert_eq!( + using, + Some(CreateFunctionUsing::Jar( + "hdfs://somewhere.com:8020/very/far".to_string() + )), ) } _ => unreachable!(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5c3b653dd..ffcd783f0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3285,16 +3285,18 @@ fn parse_create_function() { OperateFunctionArg::unnamed(DataType::Integer(None)), ]), return_type: Some(DataType::Integer(None)), - params: CreateFunctionBody { - language: Some("SQL".into()), - behavior: Some(FunctionBehavior::Immutable), - called_on_null: Some(FunctionCalledOnNull::Strict), - parallel: Some(FunctionParallel::Safe), - as_: Some(FunctionDefinition::SingleQuotedDef( - "select $1 + $2;".into() - )), - ..Default::default() - }, + language: Some("SQL".into()), + behavior: Some(FunctionBehavior::Immutable), + called_on_null: Some(FunctionCalledOnNull::Strict), + parallel: Some(FunctionParallel::Safe), + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( + Value::SingleQuotedString("select $1 + $2;".into()) + ))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, } ); } From c2d84f568371d249d35c673432604a4b9a7988ba Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Thu, 30 May 2024 09:20:16 -0700 Subject: [PATCH 454/806] Support for Snowflake dynamic pivot (#1280) --- src/ast/mod.rs | 9 +++--- src/ast/query.rs | 49 +++++++++++++++++++++++++++++---- src/parser/mod.rs | 32 ++++++++++++++++++++-- tests/sqlparser_common.rs | 10 ++++--- tests/sqlparser_snowflake.rs | 53 ++++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 15 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1227ce935..ed2cba5e3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -46,10 +46,11 @@ pub use self::query::{ GroupByExpr, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, - NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, RepetitionQuantifier, - ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, - SetOperator, SetQuantifier, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion, - TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, + NonBlock, Offset, OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem, + RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, + SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, SymbolDefinition, Table, + TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, + Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, diff --git a/src/ast/query.rs b/src/ast/query.rs index 07863bd7c..c0fa738cd 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -957,7 +957,8 @@ pub enum TableFactor { table: Box, aggregate_functions: Vec, // Function expression value_column: Vec, - pivot_values: Vec, + value_source: PivotValueSource, + default_on_null: Option, alias: Option, }, /// An UNPIVOT operation on a table. @@ -998,6 +999,41 @@ pub enum TableFactor { }, } +/// The source of values in a `PIVOT` operation. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum PivotValueSource { + /// Pivot on a static list of values. + /// + /// See . + List(Vec), + /// Pivot on all distinct values of the pivot column. + /// + /// See . + Any(Vec), + /// Pivot on all values returned by a subquery. + /// + /// See . + Subquery(Query), +} + +impl fmt::Display for PivotValueSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PivotValueSource::List(values) => write!(f, "{}", display_comma_separated(values)), + PivotValueSource::Any(order_by) => { + write!(f, "ANY")?; + if !order_by.is_empty() { + write!(f, " ORDER BY {}", display_comma_separated(order_by))?; + } + Ok(()) + } + PivotValueSource::Subquery(query) => write!(f, "{query}"), + } + } +} + /// An item in the `MEASURES` subclause of a `MATCH_RECOGNIZE` operation. /// /// See . @@ -1324,17 +1360,20 @@ impl fmt::Display for TableFactor { table, aggregate_functions, value_column, - pivot_values, + value_source, + default_on_null, alias, } => { write!( f, - "{} PIVOT({} FOR {} IN ({}))", - table, + "{table} PIVOT({} FOR {} IN ({value_source})", display_comma_separated(aggregate_functions), Expr::CompoundIdentifier(value_column.to_vec()), - display_comma_separated(pivot_values) )?; + if let Some(expr) = default_on_null { + write!(f, " DEFAULT ON NULL ({expr})")?; + } + write!(f, ")")?; if alias.is_some() { write!(f, " AS {}", alias.as_ref().unwrap())?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f88f2c189..ea110ec34 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9191,16 +9191,44 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::FOR)?; let value_column = self.parse_object_name(false)?.0; self.expect_keyword(Keyword::IN)?; + self.expect_token(&Token::LParen)?; - let pivot_values = self.parse_comma_separated(Self::parse_expr_with_alias)?; + 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 + .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) + .is_some() + { + self.prev_token(); + PivotValueSource::Subquery(self.parse_query()?) + } else { + PivotValueSource::List(self.parse_comma_separated(Self::parse_expr_with_alias)?) + }; self.expect_token(&Token::RParen)?; + + let default_on_null = + if self.parse_keywords(&[Keyword::DEFAULT, Keyword::ON, Keyword::NULL]) { + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Some(expr) + } else { + None + }; + self.expect_token(&Token::RParen)?; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; Ok(TableFactor::Pivot { table: Box::new(table), aggregate_functions, value_column, - pivot_values, + value_source, + default_on_null, alias, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f8b7d0265..5ae867278 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8618,7 +8618,7 @@ fn parse_pivot_table() { expected_function("c", Some("u")), ], value_column: vec![Ident::new("a"), Ident::new("MONTH")], - pivot_values: vec![ + value_source: PivotValueSource::List(vec![ ExprWithAlias { expr: Expr::Value(number("1")), alias: Some(Ident::new("x")) @@ -8631,7 +8631,8 @@ fn parse_pivot_table() { expr: Expr::Identifier(Ident::new("three")), alias: Some(Ident::new("y")) }, - ], + ]), + default_on_null: None, alias: Some(TableAlias { name: Ident { value: "p".to_string(), @@ -8769,7 +8770,7 @@ fn parse_pivot_unpivot_table() { alias: None }], value_column: vec![Ident::new("year")], - pivot_values: vec![ + value_source: PivotValueSource::List(vec![ ExprWithAlias { expr: Expr::Value(Value::SingleQuotedString("population_2000".to_string())), alias: None @@ -8778,7 +8779,8 @@ fn parse_pivot_unpivot_table() { expr: Expr::Value(Value::SingleQuotedString("population_2010".to_string())), alias: None }, - ], + ]), + default_on_null: None, alias: Some(TableAlias { name: Ident::new("p"), columns: vec![] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 30f2cc601..e1dba252d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1585,3 +1585,56 @@ fn first_value_ignore_nulls() { "FROM some_table" )); } + +#[test] +fn test_pivot() { + // pivot on static list of values with default + #[rustfmt::skip] + snowflake().verified_only_select(concat!( + "SELECT * ", + "FROM quarterly_sales ", + "PIVOT(SUM(amount) ", + "FOR quarter IN (", + "'2023_Q1', ", + "'2023_Q2', ", + "'2023_Q3', ", + "'2023_Q4', ", + "'2024_Q1') ", + "DEFAULT ON NULL (0)", + ") ", + "ORDER BY empid", + )); + + // dynamic pivot from subquery + #[rustfmt::skip] + snowflake().verified_only_select(concat!( + "SELECT * ", + "FROM quarterly_sales ", + "PIVOT(SUM(amount) FOR quarter IN (", + "SELECT DISTINCT quarter ", + "FROM ad_campaign_types_by_quarter ", + "WHERE television = true ", + "ORDER BY quarter)", + ") ", + "ORDER BY empid", + )); + + // dynamic pivot on any value (with order by) + #[rustfmt::skip] + snowflake().verified_only_select(concat!( + "SELECT * ", + "FROM quarterly_sales ", + "PIVOT(SUM(amount) FOR quarter IN (ANY ORDER BY quarter)) ", + "ORDER BY empid", + )); + + // dynamic pivot on any value (without order by) + #[rustfmt::skip] + snowflake().verified_only_select(concat!( + "SELECT * ", + "FROM sales_data ", + "PIVOT(SUM(total_sales) FOR fis_quarter IN (ANY)) ", + "WHERE fis_year IN (2023) ", + "ORDER BY region", + )); +} From 029a9996459e23db987cbe521f86d1a9ec1315ea Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Thu, 30 May 2024 18:21:39 +0200 Subject: [PATCH 455/806] Add support for view comments for Snowflake (#1287) Co-authored-by: Joey Hain --- src/ast/mod.rs | 11 ++++++++ src/parser/mod.rs | 14 ++++++++++ tests/sqlparser_bigquery.rs | 2 ++ tests/sqlparser_common.rs | 14 ++++++++++ tests/sqlparser_snowflake.rs | 52 +++++++++++++++++++++++++++++++++++- tests/sqlparser_sqlite.rs | 2 ++ 6 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ed2cba5e3..ee39294a3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1959,6 +1959,9 @@ pub enum Statement { query: Box, options: CreateTableOptions, cluster_by: Vec, + /// Snowflake: Views can have comments in Snowflake. + /// + comment: Option, /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause with_no_schema_binding: bool, /// if true, has SQLite `IF NOT EXISTS` clause @@ -3323,6 +3326,7 @@ impl fmt::Display for Statement { materialized, options, cluster_by, + comment, with_no_schema_binding, if_not_exists, temporary, @@ -3336,6 +3340,13 @@ impl fmt::Display for Statement { temporary = if *temporary { "TEMPORARY " } else { "" }, if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } )?; + if let Some(comment) = comment { + write!( + f, + " COMMENT = '{}'", + value::escape_single_quote_string(comment) + )?; + } if matches!(options, CreateTableOptions::With(_)) { write!(f, " {options}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ea110ec34..f3e42de00 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4034,6 +4034,19 @@ impl<'a> Parser<'a> { }; } + let comment = if dialect_of!(self is SnowflakeDialect | GenericDialect) + && 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)?, + } + } else { + None + }; + self.expect_keyword(Keyword::AS)?; let query = self.parse_boxed_query()?; // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. @@ -4054,6 +4067,7 @@ impl<'a> Parser<'a> { or_replace, options, cluster_by, + comment, with_no_schema_binding, if_not_exists, temporary, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 0bb91cb36..ea40f67d1 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -309,6 +309,7 @@ fn parse_create_view_if_not_exists() { materialized, options, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -320,6 +321,7 @@ fn parse_create_view_if_not_exists() { assert!(!or_replace); assert_eq!(options, CreateTableOptions::None); assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); assert!(!late_binding); assert!(if_not_exists); assert!(!temporary); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5ae867278..c47538895 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6251,6 +6251,7 @@ fn parse_create_view() { materialized, options, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -6262,6 +6263,7 @@ fn parse_create_view() { assert!(!or_replace); assert_eq!(options, CreateTableOptions::None); assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); @@ -6305,6 +6307,7 @@ fn parse_create_view_with_columns() { query, materialized, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -6325,6 +6328,7 @@ fn parse_create_view_with_columns() { assert!(!materialized); assert!(!or_replace); assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); @@ -6345,6 +6349,7 @@ fn parse_create_view_temporary() { materialized, options, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -6356,6 +6361,7 @@ fn parse_create_view_temporary() { assert!(!or_replace); assert_eq!(options, CreateTableOptions::None); assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); assert!(!late_binding); assert!(!if_not_exists); assert!(temporary); @@ -6376,6 +6382,7 @@ fn parse_create_or_replace_view() { query, materialized, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -6387,6 +6394,7 @@ fn parse_create_or_replace_view() { assert!(!materialized); assert!(or_replace); assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); @@ -6411,6 +6419,7 @@ fn parse_create_or_replace_materialized_view() { query, materialized, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -6422,6 +6431,7 @@ fn parse_create_or_replace_materialized_view() { assert!(materialized); assert!(or_replace); assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); @@ -6442,6 +6452,7 @@ fn parse_create_materialized_view() { materialized, options, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -6453,6 +6464,7 @@ fn parse_create_materialized_view() { assert_eq!(options, CreateTableOptions::None); assert!(!or_replace); assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); @@ -6473,6 +6485,7 @@ fn parse_create_materialized_view_with_cluster_by() { materialized, options, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -6484,6 +6497,7 @@ fn parse_create_materialized_view_with_cluster_by() { assert_eq!(options, CreateTableOptions::None); assert!(!or_replace); assert_eq!(cluster_by, vec![Ident::new("foo")]); + assert!(comment.is_none()); assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e1dba252d..c11f60993 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -18,7 +18,7 @@ use sqlparser::ast::helpers::stmt_data_loading::{ DataLoadingOption, DataLoadingOptionType, StageLoadSelectItem, }; use sqlparser::ast::*; -use sqlparser::dialect::{GenericDialect, SnowflakeDialect}; +use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect}; use sqlparser::parser::{ParserError, ParserOptions}; use sqlparser::tokenizer::*; use test_utils::*; @@ -91,6 +91,56 @@ fn test_snowflake_single_line_tokenize() { assert_eq!(expected, tokens); } +#[test] +fn parse_sf_create_or_replace_view_with_comment_missing_equal() { + assert!(snowflake_and_generic() + .parse_sql_statements("CREATE OR REPLACE VIEW v COMMENT = 'hello, world' AS SELECT 1") + .is_ok()); + + assert!(snowflake_and_generic() + .parse_sql_statements("CREATE OR REPLACE VIEW v COMMENT 'hello, world' AS SELECT 1") + .is_err()); +} + +#[test] +fn parse_sf_create_or_replace_with_comment_for_snowflake() { + let sql = "CREATE OR REPLACE VIEW v COMMENT = 'hello, world' AS SELECT 1"; + let dialect = test_utils::TestedDialects { + dialects: vec![Box::new(SnowflakeDialect {}) as Box], + options: None, + }; + + match dialect.verified_stmt(sql) { + Statement::CreateView { + name, + columns, + or_replace, + options, + query, + materialized, + cluster_by, + comment, + with_no_schema_binding: late_binding, + if_not_exists, + temporary, + } => { + assert_eq!("v", name.to_string()); + assert_eq!(columns, vec![]); + assert_eq!(options, CreateTableOptions::None); + assert_eq!("SELECT 1", query.to_string()); + assert!(!materialized); + assert!(or_replace); + assert_eq!(cluster_by, vec![]); + assert!(comment.is_some()); + assert_eq!(comment.expect("expected comment"), "hello, world"); + assert!(!late_binding); + assert!(!if_not_exists); + assert!(!temporary); + } + _ => unreachable!(), + } +} + #[test] fn test_sf_derived_table_in_parenthesis() { // Nesting a subquery in an extra set of parentheses is non-standard, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 5742754c0..fe5346f14 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -167,6 +167,7 @@ fn parse_create_view_temporary_if_not_exists() { materialized, options, cluster_by, + comment, with_no_schema_binding: late_binding, if_not_exists, temporary, @@ -178,6 +179,7 @@ fn parse_create_view_temporary_if_not_exists() { assert!(!or_replace); assert_eq!(options, CreateTableOptions::None); assert_eq!(cluster_by, vec![]); + assert!(comment.is_none()); assert!(!late_binding); assert!(if_not_exists); assert!(temporary); From 375742d1fa2e3255078dd1c9ced0decc80d270e4 Mon Sep 17 00:00:00 2001 From: Aleksei Piianin Date: Thu, 30 May 2024 18:24:12 +0200 Subject: [PATCH 456/806] ClickHouse: create view with fields and data types (#1292) --- src/ast/ddl.rs | 7 +++++- src/parser/mod.rs | 11 +++++++++- tests/sqlparser_bigquery.rs | 2 ++ tests/sqlparser_clickhouse.rs | 41 +++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 1 + 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index de514550b..9c30999ab 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -815,7 +815,7 @@ impl fmt::Display for ColumnDef { /// /// Syntax /// ```markdown -/// [OPTIONS(option, ...)] +/// [data_type][OPTIONS(option, ...)] /// /// option: = /// ``` @@ -824,18 +824,23 @@ impl fmt::Display for ColumnDef { /// ```sql /// name /// age OPTIONS(description = "age column", tag = "prod") +/// created_at DateTime64 /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct ViewColumnDef { pub name: Ident, + pub data_type: Option, pub options: Option>, } 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)?; + } if let Some(options) = self.options.as_ref() { write!( f, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f3e42de00..fef307106 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7282,7 +7282,16 @@ impl<'a> Parser<'a> { } else { None }; - Ok(ViewColumnDef { name, options }) + let data_type = if dialect_of!(self is ClickHouseDialect) { + Some(self.parse_data_type()?) + } else { + None + }; + Ok(ViewColumnDef { + name, + data_type, + options, + }) } /// Parse a parenthesized comma-separated list of unqualified, possibly quoted identifiers diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index ea40f67d1..1cec15c30 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -261,10 +261,12 @@ fn parse_create_view_with_options() { vec![ ViewColumnDef { name: Ident::new("name"), + data_type: None, options: None, }, ViewColumnDef { name: Ident::new("age"), + data_type: None, options: Some(vec![SqlOption { name: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString("field age".to_string())), diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 7150a9489..a693936bc 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -220,6 +220,47 @@ fn parse_create_table() { ); } +#[test] +fn parse_create_view_with_fields_data_types() { + match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) { + Statement::CreateView { name, columns, .. } => { + assert_eq!(name, ObjectName(vec!["v".into()])); + assert_eq!( + columns, + vec![ + ViewColumnDef { + name: "i".into(), + data_type: Some(DataType::Custom( + ObjectName(vec![Ident { + value: "int".into(), + quote_style: Some('"') + }]), + vec![] + )), + options: None + }, + ViewColumnDef { + name: "f".into(), + data_type: Some(DataType::Custom( + ObjectName(vec![Ident { + value: "String".into(), + quote_style: Some('"') + }]), + vec![] + )), + options: None + }, + ] + ); + } + _ => unreachable!(), + } + + clickhouse() + .parse_sql_statements(r#"CREATE VIEW v (i, f) AS SELECT * FROM t"#) + .expect_err("CREATE VIEW with fields and without data types should be invalid"); +} + #[test] fn parse_double_equal() { clickhouse().one_statement_parses_to( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c47538895..5f2d2cc02 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6319,6 +6319,7 @@ fn parse_create_view_with_columns() { .into_iter() .map(|name| ViewColumnDef { name, + data_type: None, options: None }) .collect::>() From 80c03f5c6a42c2414d93dc411c013155dea676d4 Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Fri, 31 May 2024 02:45:07 -0700 Subject: [PATCH 457/806] Support for Snowflake ASOF joins (#1288) --- src/ast/query.rs | 17 +++++++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 26 ++++++++++++-- src/test_utils.rs | 14 ++++++++ tests/sqlparser_snowflake.rs | 67 ++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 3 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index c0fa738cd..fcd5b970d 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1562,6 +1562,15 @@ impl fmt::Display for Join { ), JoinOperator::CrossApply => write!(f, " CROSS APPLY {}", self.relation), JoinOperator::OuterApply => write!(f, " OUTER APPLY {}", self.relation), + JoinOperator::AsOf { + match_condition, + constraint, + } => write!( + f, + " ASOF JOIN {} MATCH_CONDITION ({match_condition}){}", + self.relation, + suffix(constraint) + ), } } } @@ -1587,6 +1596,14 @@ pub enum JoinOperator { CrossApply, /// OUTER APPLY (non-standard) OuterApply, + /// `ASOF` joins are used for joining tables containing time-series data + /// whose timestamp columns do not match exactly. + /// + /// See . + AsOf { + match_condition: Expr, + constraint: JoinConstraint, + }, } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/keywords.rs b/src/keywords.rs index 06086297c..6c6c642c3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -91,6 +91,7 @@ define_keywords!( AS, ASC, ASENSITIVE, + ASOF, ASSERT, ASYMMETRIC, AT, @@ -418,6 +419,7 @@ define_keywords!( MATCH, MATCHED, MATCHES, + MATCH_CONDITION, MATCH_RECOGNIZE, MATERIALIZED, MAX, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fef307106..123af045a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3196,6 +3196,16 @@ impl<'a> Parser<'a> { Ok(values) } + pub fn parse_parenthesized(&mut self, mut f: F) -> Result + where + F: FnMut(&mut Parser<'a>) -> Result, + { + self.expect_token(&Token::LParen)?; + let res = f(self)?; + self.expect_token(&Token::RParen)?; + Ok(res) + } + /// Parse a comma-separated list of 0+ items accepted by `F` pub fn parse_comma_separated0(&mut self, f: F) -> Result, ParserError> where @@ -8505,6 +8515,18 @@ impl<'a> Parser<'a> { relation: self.parse_table_factor()?, join_operator: JoinOperator::OuterApply, } + } else if self.parse_keyword(Keyword::ASOF) { + self.expect_keyword(Keyword::JOIN)?; + let relation = self.parse_table_factor()?; + self.expect_keyword(Keyword::MATCH_CONDITION)?; + let match_condition = self.parse_parenthesized(Self::parse_expr)?; + Join { + relation, + join_operator: JoinOperator::AsOf { + match_condition, + constraint: self.parse_join_constraint(false)?, + }, + } } else { let natural = self.parse_keyword(Keyword::NATURAL); let peek_keyword = if let Token::Word(w) = self.peek_token().token { @@ -8951,9 +8973,7 @@ impl<'a> Parser<'a> { }; self.expect_keyword(Keyword::PATTERN)?; - self.expect_token(&Token::LParen)?; - let pattern = self.parse_pattern()?; - self.expect_token(&Token::RParen)?; + let pattern = self.parse_parenthesized(Self::parse_pattern)?; self.expect_keyword(Keyword::DEFINE)?; diff --git a/src/test_utils.rs b/src/test_utils.rs index 464366ae4..9af9c8098 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -312,6 +312,20 @@ pub fn table(name: impl Into) -> TableFactor { } } +pub fn table_with_alias(name: impl Into, alias: impl Into) -> TableFactor { + TableFactor::Table { + name: ObjectName(vec![Ident::new(name)]), + alias: Some(TableAlias { + name: Ident::new(alias), + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + } +} + pub fn join(relation: TableFactor) -> Join { Join { relation, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c11f60993..7492802c7 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1688,3 +1688,70 @@ fn test_pivot() { "ORDER BY region", )); } + +#[test] +fn asof_joins() { + #[rustfmt::skip] + let query = snowflake_and_generic().verified_only_select(concat!( + "SELECT * ", + "FROM trades_unixtime AS tu ", + "ASOF JOIN quotes_unixtime AS qu ", + "MATCH_CONDITION (tu.trade_time >= qu.quote_time)", + )); + + assert_eq!( + query.from[0], + TableWithJoins { + relation: table_with_alias("trades_unixtime", "tu"), + joins: vec![Join { + relation: table_with_alias("quotes_unixtime", "qu"), + join_operator: JoinOperator::AsOf { + match_condition: Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("tu"), + Ident::new("trade_time"), + ])), + op: BinaryOperator::GtEq, + right: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("qu"), + Ident::new("quote_time"), + ])), + }, + constraint: JoinConstraint::None, + }, + }], + } + ); + + #[rustfmt::skip] + snowflake_and_generic().verified_query(concat!( + "SELECT t.stock_symbol, t.trade_time, t.quantity, q.quote_time, q.price ", + "FROM trades AS t ASOF JOIN quotes AS q ", + "MATCH_CONDITION (t.trade_time >= quote_time) ", + "ON t.stock_symbol = q.stock_symbol ", + "ORDER BY t.stock_symbol", + )); + + #[rustfmt::skip] + snowflake_and_generic().verified_query(concat!( + "SELECT t.stock_symbol, c.company_name, t.trade_time, t.quantity, q.quote_time, q.price ", + "FROM trades AS t ASOF JOIN quotes AS q ", + "MATCH_CONDITION (t.trade_time <= quote_time) ", + "USING(stock_symbol) ", + "JOIN companies AS c ON c.stock_symbol = t.stock_symbol ", + "ORDER BY t.stock_symbol", + )); + + #[rustfmt::skip] + snowflake_and_generic().verified_query(concat!( + "SELECT * ", + "FROM snowtime AS s ", + "ASOF JOIN raintime AS r ", + "MATCH_CONDITION (s.observed >= r.observed) ", + "ON s.state = r.state ", + "ASOF JOIN preciptime AS p ", + "MATCH_CONDITION (s.observed >= p.observed) ", + "ON s.state = p.state ", + "ORDER BY s.observed", + )); +} From afa5f08db9b1f3a4805f21fea6b1e72710cdb138 Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Fri, 31 May 2024 14:38:35 -0700 Subject: [PATCH 458/806] Support for Postgres array slice syntax (#1290) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 83 +++++++++++++-- src/parser/mod.rs | 94 +++++++++++++--- tests/sqlparser_duckdb.rs | 8 +- tests/sqlparser_postgres.rs | 200 +++++++++++++++++++++++++++++------ tests/sqlparser_snowflake.rs | 30 ++++++ 5 files changed, 355 insertions(+), 60 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ee39294a3..320dfc60e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -679,7 +679,7 @@ pub enum Expr { }, /// Access a map-like object by field (e.g. `column['field']` or `column[4]` /// Note that depending on the dialect, struct like accesses may be - /// parsed as [`ArrayIndex`](Self::ArrayIndex) or [`MapAccess`](Self::MapAccess) + /// parsed as [`Subscript`](Self::Subscript) or [`MapAccess`](Self::MapAccess) /// MapAccess { column: Box, @@ -746,10 +746,10 @@ pub enum Expr { /// ``` /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs Dictionary(Vec), - /// An array index expression e.g. `(ARRAY[1, 2])[1]` or `(current_schemas(FALSE))[1]` - ArrayIndex { - obj: Box, - indexes: Vec, + /// An access of nested data using subscript syntax, for example `array[2]`. + Subscript { + expr: Box, + subscript: Box, }, /// An array expression e.g. `ARRAY[1, 2]` Array(Array), @@ -805,6 +805,68 @@ pub enum Expr { Lambda(LambdaFunction), } +/// The contents inside the `[` and `]` in a subscript expression. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Subscript { + /// Accesses the element of the array at the given index. + Index { index: Expr }, + + /// Accesses a slice of an array on PostgreSQL, e.g. + /// + /// ```plaintext + /// => select (array[1,2,3,4,5,6])[2:5]; + /// ----------- + /// {2,3,4,5} + /// ``` + /// + /// The lower and/or upper bound can be omitted to slice from the start or + /// end of the array respectively. + /// + /// See . + /// + /// Also supports an optional "stride" as the last element (this is not + /// supported by postgres), e.g. + /// + /// ```plaintext + /// => select (array[1,2,3,4,5,6])[1:6:2]; + /// ----------- + /// {1,3,5} + /// ``` + Slice { + lower_bound: Option, + upper_bound: Option, + stride: Option, + }, +} + +impl fmt::Display for Subscript { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Subscript::Index { index } => write!(f, "{index}"), + Subscript::Slice { + lower_bound, + upper_bound, + stride, + } => { + if let Some(lower) = lower_bound { + write!(f, "{lower}")?; + } + write!(f, ":")?; + if let Some(upper) = upper_bound { + write!(f, "{upper}")?; + } + if let Some(stride) = stride { + write!(f, ":")?; + write!(f, "{stride}")?; + } + Ok(()) + } + } + } +} + /// A lambda function. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1251,12 +1313,11 @@ impl fmt::Display for Expr { Expr::Dictionary(fields) => { write!(f, "{{{}}}", display_comma_separated(fields)) } - Expr::ArrayIndex { obj, indexes } => { - write!(f, "{obj}")?; - for i in indexes { - write!(f, "[{i}]")?; - } - Ok(()) + Expr::Subscript { + expr, + subscript: key, + } => { + write!(f, "{expr}[{key}]") } Expr::Array(set) => { write!(f, "{set}") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 123af045a..c6750644c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2544,8 +2544,7 @@ impl<'a> Parser<'a> { }) } else if Token::LBracket == tok { if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) { - // parse index - self.parse_array_index(expr) + self.parse_subscript(expr) } else if dialect_of!(self is SnowflakeDialect) { self.prev_token(); self.parse_json_access(expr) @@ -2573,18 +2572,87 @@ impl<'a> Parser<'a> { } } - pub fn parse_array_index(&mut self, expr: Expr) -> Result { - let index = self.parse_expr()?; - self.expect_token(&Token::RBracket)?; - let mut indexes: Vec = vec![index]; - while self.consume_token(&Token::LBracket) { - let index = self.parse_expr()?; + /// Parses an array subscript like + /// * `[:]` + /// * `[l]` + /// * `[l:]` + /// * `[:u]` + /// * `[l:u]` + /// * `[l:u:s]` + /// + /// Parser is right after `[` + fn parse_subscript_inner(&mut self) -> Result { + // at either `:(rest)` or `:(rest)]` + let lower_bound = if self.consume_token(&Token::Colon) { + None + } else { + Some(self.parse_expr()?) + }; + + // check for end + if self.consume_token(&Token::RBracket) { + if let Some(lower_bound) = lower_bound { + return Ok(Subscript::Index { index: lower_bound }); + }; + return Ok(Subscript::Slice { + lower_bound, + upper_bound: None, + stride: None, + }); + } + + // consume the `:` + if lower_bound.is_some() { + self.expect_token(&Token::Colon)?; + } + + // we are now at either `]`, `(rest)]` + let upper_bound = if self.consume_token(&Token::RBracket) { + return Ok(Subscript::Slice { + lower_bound, + upper_bound: None, + stride: None, + }); + } else { + Some(self.parse_expr()?) + }; + + // check for end + if self.consume_token(&Token::RBracket) { + return Ok(Subscript::Slice { + lower_bound, + upper_bound, + stride: None, + }); + } + + // we are now at `:]` or `:stride]` + self.expect_token(&Token::Colon)?; + let stride = if self.consume_token(&Token::RBracket) { + None + } else { + Some(self.parse_expr()?) + }; + + if stride.is_some() { self.expect_token(&Token::RBracket)?; - indexes.push(index); } - Ok(Expr::ArrayIndex { - obj: Box::new(expr), - indexes, + + Ok(Subscript::Slice { + lower_bound, + upper_bound, + stride, + }) + } + + /// Parses an array subscript like `[1:3]` + /// + /// Parser is right after `[` + pub fn parse_subscript(&mut self, expr: Expr) -> Result { + let subscript = self.parse_subscript_inner()?; + Ok(Expr::Subscript { + expr: Box::new(expr), + subscript: Box::new(subscript), }) } @@ -2838,7 +2906,7 @@ impl<'a> Parser<'a> { Ok(Self::MUL_DIV_MOD_OP_PREC) } Token::DoubleColon => Ok(50), - Token::Colon => Ok(50), + Token::Colon if dialect_of!(self is SnowflakeDialect) => Ok(50), Token::ExclamationMark => Ok(50), Token::LBracket | Token::Overlap | Token::CaretAt => Ok(50), Token::Arrow diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a84da5378..8d12945dd 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -528,8 +528,8 @@ fn test_array_index() { _ => panic!("Expected an expression with alias"), }; assert_eq!( - &Expr::ArrayIndex { - obj: Box::new(Expr::Array(Array { + &Expr::Subscript { + expr: Box::new(Expr::Array(Array { elem: vec![ Expr::Value(Value::SingleQuotedString("a".to_owned())), Expr::Value(Value::SingleQuotedString("b".to_owned())), @@ -537,7 +537,9 @@ fn test_array_index() { ], named: false })), - indexes: vec![Expr::Value(number("3"))] + subscript: Box::new(Subscript::Index { + index: Expr::Value(number("3")) + }) }, expr ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ffcd783f0..677246a51 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1873,9 +1873,11 @@ fn parse_array_index_expr() { let sql = "SELECT foo[0] FROM foos"; let select = pg_and_generic().verified_only_select(sql); assert_eq!( - &Expr::ArrayIndex { - obj: Box::new(Expr::Identifier(Ident::new("foo"))), - indexes: vec![num[0].clone()], + &Expr::Subscript { + expr: Box::new(Expr::Identifier(Ident::new("foo"))), + subscript: Box::new(Subscript::Index { + index: num[0].clone() + }), }, expr_from_projection(only(&select.projection)), ); @@ -1883,9 +1885,16 @@ fn parse_array_index_expr() { let sql = "SELECT foo[0][0] FROM foos"; let select = pg_and_generic().verified_only_select(sql); assert_eq!( - &Expr::ArrayIndex { - obj: Box::new(Expr::Identifier(Ident::new("foo"))), - indexes: vec![num[0].clone(), num[0].clone()], + &Expr::Subscript { + expr: Box::new(Expr::Subscript { + expr: Box::new(Expr::Identifier(Ident::new("foo"))), + subscript: Box::new(Subscript::Index { + index: num[0].clone() + }), + }), + subscript: Box::new(Subscript::Index { + index: num[0].clone() + }), }, expr_from_projection(only(&select.projection)), ); @@ -1893,19 +1902,27 @@ fn parse_array_index_expr() { let sql = r#"SELECT bar[0]["baz"]["fooz"] FROM foos"#; let select = pg_and_generic().verified_only_select(sql); assert_eq!( - &Expr::ArrayIndex { - obj: Box::new(Expr::Identifier(Ident::new("bar"))), - indexes: vec![ - num[0].clone(), - Expr::Identifier(Ident { - value: "baz".to_string(), - quote_style: Some('"') + &Expr::Subscript { + expr: Box::new(Expr::Subscript { + expr: Box::new(Expr::Subscript { + expr: Box::new(Expr::Identifier(Ident::new("bar"))), + subscript: Box::new(Subscript::Index { + index: num[0].clone() + }) }), - Expr::Identifier(Ident { + subscript: Box::new(Subscript::Index { + index: Expr::Identifier(Ident { + value: "baz".to_string(), + quote_style: Some('"') + }) + }) + }), + subscript: Box::new(Subscript::Index { + index: Expr::Identifier(Ident { value: "fooz".to_string(), quote_style: Some('"') }) - ], + }) }, expr_from_projection(only(&select.projection)), ); @@ -1913,26 +1930,33 @@ fn parse_array_index_expr() { let sql = "SELECT (CAST(ARRAY[ARRAY[2, 3]] AS INT[][]))[1][2]"; let select = pg_and_generic().verified_only_select(sql); assert_eq!( - &Expr::ArrayIndex { - obj: Box::new(Expr::Nested(Box::new(Expr::Cast { - kind: CastKind::Cast, - expr: Box::new(Expr::Array(Array { - elem: vec![Expr::Array(Array { - elem: vec![num[2].clone(), num[3].clone(),], + &Expr::Subscript { + expr: Box::new(Expr::Subscript { + expr: Box::new(Expr::Nested(Box::new(Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::Array(Array { + elem: vec![Expr::Array(Array { + elem: vec![num[2].clone(), num[3].clone(),], + named: true, + })], named: true, - })], - named: true, - })), - data_type: DataType::Array(ArrayElemTypeDef::SquareBracket( - Box::new(DataType::Array(ArrayElemTypeDef::SquareBracket( - Box::new(DataType::Int(None)), + })), + data_type: DataType::Array(ArrayElemTypeDef::SquareBracket( + Box::new(DataType::Array(ArrayElemTypeDef::SquareBracket( + Box::new(DataType::Int(None)), + None + ))), None - ))), - None - )), - format: None, - }))), - indexes: vec![num[1].clone(), num[2].clone()], + )), + format: None, + }))), + subscript: Box::new(Subscript::Index { + index: num[1].clone() + }), + }), + subscript: Box::new(Subscript::Index { + index: num[2].clone() + }), }, expr_from_projection(only(&select.projection)), ); @@ -1948,6 +1972,116 @@ fn parse_array_index_expr() { ); } +#[test] +fn parse_array_subscript() { + let tests = [ + ( + "(ARRAY[1, 2, 3, 4, 5, 6])[2]", + Subscript::Index { + index: Expr::Value(number("2")), + }, + ), + ( + "(ARRAY[1, 2, 3, 4, 5, 6])[foo]", + Subscript::Index { + index: Expr::Identifier(Ident::new("foo")), + }, + ), + ( + "(ARRAY[1, 2, 3, 4, 5, 6])[2:5]", + Subscript::Slice { + lower_bound: Some(Expr::Value(number("2"))), + upper_bound: Some(Expr::Value(number("5"))), + stride: None, + }, + ), + ( + "(ARRAY[1, 2, 3, 4, 5, 6])[2:5:3]", + Subscript::Slice { + lower_bound: Some(Expr::Value(number("2"))), + upper_bound: Some(Expr::Value(number("5"))), + stride: Some(Expr::Value(number("3"))), + }, + ), + ( + "arr[array_length(arr) - 3:array_length(arr) - 1]", + Subscript::Slice { + lower_bound: Some(Expr::BinaryOp { + left: Box::new(call("array_length", [Expr::Identifier(Ident::new("arr"))])), + op: BinaryOperator::Minus, + right: Box::new(Expr::Value(number("3"))), + }), + upper_bound: Some(Expr::BinaryOp { + left: Box::new(call("array_length", [Expr::Identifier(Ident::new("arr"))])), + op: BinaryOperator::Minus, + right: Box::new(Expr::Value(number("1"))), + }), + stride: None, + }, + ), + ( + "(ARRAY[1, 2, 3, 4, 5, 6])[:5]", + Subscript::Slice { + lower_bound: None, + upper_bound: Some(Expr::Value(number("5"))), + stride: None, + }, + ), + ( + "(ARRAY[1, 2, 3, 4, 5, 6])[2:]", + Subscript::Slice { + lower_bound: Some(Expr::Value(number("2"))), + upper_bound: None, + stride: None, + }, + ), + ( + "(ARRAY[1, 2, 3, 4, 5, 6])[:]", + Subscript::Slice { + lower_bound: None, + upper_bound: None, + stride: None, + }, + ), + ]; + for (sql, expect) in tests { + let Expr::Subscript { subscript, .. } = pg_and_generic().verified_expr(sql) else { + panic!("expected subscript expr"); + }; + assert_eq!(expect, *subscript); + } + + pg_and_generic().verified_expr("schedule[:2][2:]"); +} + +#[test] +fn parse_array_multi_subscript() { + let expr = pg_and_generic().verified_expr("make_array(1, 2, 3)[1:2][2]"); + assert_eq!( + Expr::Subscript { + expr: Box::new(Expr::Subscript { + expr: Box::new(call( + "make_array", + vec![ + Expr::Value(number("1")), + Expr::Value(number("2")), + Expr::Value(number("3")) + ] + )), + subscript: Box::new(Subscript::Slice { + lower_bound: Some(Expr::Value(number("1"))), + upper_bound: Some(Expr::Value(number("2"))), + stride: None, + }), + }), + subscript: Box::new(Subscript::Index { + index: Expr::Value(number("2")), + }), + }, + expr, + ); +} + #[test] fn parse_create_index() { let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2)"; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7492802c7..d213efd7b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -394,6 +394,36 @@ fn parse_semi_structured_data_traversal() { })], select.projection ); + + // a json access used as a key to another json access + assert_eq!( + snowflake().verified_expr("a[b:c]"), + Expr::JsonAccess { + value: Box::new(Expr::Identifier(Ident::new("a"))), + path: JsonPath { + path: vec![JsonPathElem::Bracket { + key: Expr::JsonAccess { + value: Box::new(Expr::Identifier(Ident::new("b"))), + path: JsonPath { + path: vec![JsonPathElem::Dot { + key: "c".to_owned(), + quoted: false + }] + } + } + }] + } + } + ); + + // unquoted object keys cannot start with a digit + assert_eq!( + snowflake() + .parse_sql_statements("SELECT a:42") + .unwrap_err() + .to_string(), + "sql parser error: Expected variant object key name, found: 42" + ); } #[test] From 521a2c9e7aa4102db78eaf81ebbba941342976bb Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 1 Jun 2024 06:22:06 -0400 Subject: [PATCH 459/806] Add CHANGELOG for 0.47.0 (#1295) --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab654525f..18df2e33a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,33 @@ changes that break via addition as "Added". ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. + +## [0.47.0] 2024-06-01 + +### Fixed +* Re-support Postgres array slice syntax (#1290) - Thanks @jmhain +* Fix DoubleColon cast skipping AT TIME ZONE #1266 (#1267) - Thanks @dmitrybugakov +* Fix for values as table name in Databricks and generic (#1278) - Thanks @jmhain + +### Added +* Support `ASOF` joins in Snowflake (#1288) - Thanks @jmhain +* Support `CREATE VIEW` with fields and data types ClickHouse (#1292) - Thanks @7phs +* Support view comments for Snowflake (#1287) - Thanks @bombsimon +* Support dynamic pivot in Snowflake (#1280) - Thanks @jmhain +* Support `CREATE FUNCTION` for BigQuery, generalize AST (#1253) - Thanks @iffyio +* Support expression in `AT TIME ZONE` and fix precedence (#1272) - Thanks @jmhain +* Support `IGNORE/RESPECT NULLS` inside function argument list for Databricks (#1263) - Thanks @jmhain +* Support `SELECT * EXCEPT` Databricks (#1261) - Thanks @jmhain +* Support triple quoted strings (#1262) - Thanks @iffyio +* Support array indexing for duckdb (#1265) - Thanks @JichaoS +* Support multiple SET variables (#1252) - Thanks @iffyio +* Support `ANY_VALUE` `HAVING` clause (#1258) in BigQuery - Thanks @jmhain +* Support keywords as field names in BigQuery struct syntax (#1254) - Thanks @iffyio +* Support `GROUP_CONCAT()` in MySQL (#1256) - Thanks @jmhain +* Support lambda functions in Databricks (#1257) - Thanks @jmhain +* Add const generic peek_tokens method to parser (#1255) - Thanks @jmhain + + ## [0.46.0] 2024-05-03 ### Changed From f3f5de51e55cccdde9c10b4804cf790ccd532970 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 1 Jun 2024 06:23:18 -0400 Subject: [PATCH 460/806] chore: Release sqlparser version 0.47.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c9bf58bbb..8d015968b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.46.0" +version = "0.47.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From a0f511cb21efd9e992fb4c6ef273ef41802d308c Mon Sep 17 00:00:00 2001 From: Philip Cristiano Date: Wed, 5 Jun 2024 05:25:42 -0400 Subject: [PATCH 461/806] Encapsulate `CreateTable`, `CreateIndex` into specific structs (#1291) --- src/ast/dml.rs | 292 +++++++++++++++++++++++- src/ast/helpers/stmt_create_table.rs | 9 +- src/ast/mod.rs | 318 +-------------------------- src/parser/mod.rs | 4 +- tests/sqlparser_bigquery.rs | 8 +- tests/sqlparser_common.rs | 54 ++--- tests/sqlparser_mysql.rs | 42 ++-- tests/sqlparser_postgres.rs | 44 ++-- tests/sqlparser_snowflake.rs | 6 +- tests/sqlparser_sqlite.rs | 10 +- 10 files changed, 387 insertions(+), 400 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index badc58a7d..91232218f 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -11,18 +11,304 @@ // limitations under the License. #[cfg(not(feature = "std"))] -use alloc::{boxed::Box, vec::Vec}; +use alloc::{boxed::Box, string::String, vec::Vec}; +use core::fmt::{self, Display}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +pub use super::ddl::{ColumnDef, TableConstraint}; + use super::{ - Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, - Query, SelectItem, SqliteOnConflict, TableWithJoins, + display_comma_separated, display_separated, Expr, FileFormat, FromTable, HiveDistributionStyle, + HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, + OnCommit, OnInsert, OrderByExpr, Query, SelectItem, SqlOption, SqliteOnConflict, + TableWithJoins, }; +/// CREATE INDEX 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 CreateIndex { + /// index name + pub name: Option, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + pub using: Option, + pub columns: Vec, + pub unique: bool, + pub concurrently: bool, + pub if_not_exists: bool, + pub include: Vec, + pub nulls_distinct: Option, + pub predicate: Option, +} +/// CREATE TABLE 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 CreateTable { + pub or_replace: bool, + pub temporary: bool, + pub external: bool, + pub global: Option, + pub if_not_exists: bool, + pub transient: bool, + /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, + /// Optional schema + pub columns: Vec, + 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, + /// ClickHouse "ON CLUSTER" clause: + /// + pub on_cluster: Option, + /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different + /// than empty (represented as ()), the latter meaning "no sorting". + /// + pub order_by: Option>, + /// BigQuery: A partition expression for the table. + /// + pub partition_by: Option>, + /// BigQuery: Table clustering column list. + /// + pub cluster_by: Option>, + /// BigQuery: Table options list. + /// + pub options: 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. + pub strict: bool, +} + +impl Display for CreateTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // We want to allow the following options + // Empty column list, allowed by PostgreSQL: + // `CREATE TABLE t ()` + // No columns provided for CREATE TABLE AS: + // `CREATE TABLE t AS SELECT a from t2` + // Columns provided for CREATE TABLE AS: + // `CREATE TABLE t (a INT) AS SELECT a from t2` + write!( + f, + "CREATE {or_replace}{external}{global}{temporary}{transient}TABLE {if_not_exists}{name}", + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + external = if self.external { "EXTERNAL " } else { "" }, + global = self.global + .map(|global| { + if global { + "GLOBAL " + } else { + "LOCAL " + } + }) + .unwrap_or(""), + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + temporary = if self.temporary { "TEMPORARY " } else { "" }, + transient = if self.transient { "TRANSIENT " } else { "" }, + name = self.name, + )?; + if let Some(on_cluster) = &self.on_cluster { + write!( + f, + " ON CLUSTER {}", + on_cluster.replace('{', "'{").replace('}', "}'") + )?; + } + if !self.columns.is_empty() || !self.constraints.is_empty() { + write!(f, " ({}", display_comma_separated(&self.columns))?; + if !self.columns.is_empty() && !self.constraints.is_empty() { + write!(f, ", ")?; + } + write!(f, "{})", display_comma_separated(&self.constraints))?; + } 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, " ()")?; + } + // Only for SQLite + if self.without_rowid { + write!(f, " WITHOUT ROWID")?; + } + + // Only for Hive + if let Some(l) = &self.like { + write!(f, " LIKE {l}")?; + } + + if let Some(c) = &self.clone { + write!(f, " CLONE {c}")?; + } + + match &self.hive_distribution { + HiveDistributionStyle::PARTITIONED { columns } => { + write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?; + } + HiveDistributionStyle::CLUSTERED { + columns, + sorted_by, + num_buckets, + } => { + write!(f, " CLUSTERED BY ({})", display_comma_separated(columns))?; + if !sorted_by.is_empty() { + write!(f, " SORTED BY ({})", display_comma_separated(sorted_by))?; + } + if *num_buckets > 0 { + write!(f, " INTO {num_buckets} BUCKETS")?; + } + } + HiveDistributionStyle::SKEWED { + columns, + on, + stored_as_directories, + } => { + write!( + f, + " SKEWED BY ({})) ON ({})", + display_comma_separated(columns), + display_comma_separated(on) + )?; + if *stored_as_directories { + write!(f, " STORED AS DIRECTORIES")?; + } + } + _ => (), + } + + if let Some(HiveFormat { + row_format, + serde_properties, + storage, + location, + }) = &self.hive_formats + { + match row_format { + Some(HiveRowFormat::SERDE { class }) => write!(f, " ROW FORMAT SERDE '{class}'")?, + Some(HiveRowFormat::DELIMITED { delimiters }) => { + write!(f, " ROW FORMAT DELIMITED")?; + if !delimiters.is_empty() { + write!(f, " {}", display_separated(delimiters, " "))?; + } + } + None => (), + } + match storage { + Some(HiveIOFormat::IOF { + input_format, + output_format, + }) => write!( + f, + " STORED AS INPUTFORMAT {input_format} OUTPUTFORMAT {output_format}" + )?, + Some(HiveIOFormat::FileFormat { format }) if !self.external => { + write!(f, " STORED AS {format}")? + } + _ => (), + } + if let Some(serde_properties) = serde_properties.as_ref() { + write!( + f, + " WITH SERDEPROPERTIES ({})", + display_comma_separated(serde_properties) + )?; + } + if !self.external { + if let Some(loc) = location { + write!(f, " LOCATION '{loc}'")?; + } + } + } + if self.external { + if let Some(file_format) = self.file_format { + write!(f, " STORED AS {file_format}")?; + } + 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) = &self.comment { + write!(f, " COMMENT '{comment}'")?; + } + if let Some(auto_increment_offset) = self.auto_increment_offset { + write!(f, " AUTO_INCREMENT {auto_increment_offset}")?; + } + if let Some(order_by) = &self.order_by { + write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; + } + if let Some(partition_by) = self.partition_by.as_ref() { + write!(f, " PARTITION BY {partition_by}")?; + } + if let Some(cluster_by) = self.cluster_by.as_ref() { + write!( + f, + " CLUSTER BY {}", + display_comma_separated(cluster_by.as_slice()) + )?; + } + if let Some(options) = self.options.as_ref() { + write!( + f, + " OPTIONS({})", + display_comma_separated(options.as_slice()) + )?; + } + if let Some(query) = &self.query { + write!(f, " AS {query}")?; + } + 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", + Some(OnCommit::PreserveRows) => "ON COMMIT PRESERVE ROWS", + Some(OnCommit::Drop) => "ON COMMIT DROP", + None => "", + }; + write!(f, " {on_commit}")?; + } + if self.strict { + write!(f, " STRICT")?; + } + Ok(()) + } +} + /// INSERT statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 126542379..c50e7bbd9 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +use super::super::dml::CreateTable; use crate::ast::{ ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, Query, SqlOption, Statement, TableConstraint, @@ -263,7 +264,7 @@ impl CreateTableBuilder { } pub fn build(self) -> Statement { - Statement::CreateTable { + Statement::CreateTable(CreateTable { or_replace: self.or_replace, temporary: self.temporary, external: self.external, @@ -295,7 +296,7 @@ impl CreateTableBuilder { cluster_by: self.cluster_by, options: self.options, strict: self.strict, - } + }) } } @@ -306,7 +307,7 @@ impl TryFrom for CreateTableBuilder { // ownership. fn try_from(stmt: Statement) -> Result { match stmt { - Statement::CreateTable { + Statement::CreateTable(CreateTable { or_replace, temporary, external, @@ -338,7 +339,7 @@ impl TryFrom for CreateTableBuilder { cluster_by, options, strict, - } => Ok(Self { + }) => Ok(Self { or_replace, temporary, external, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 320dfc60e..e29a8df04 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -38,7 +38,7 @@ pub use self::ddl::{ ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; -pub use self::dml::{Delete, Insert}; +pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, @@ -75,7 +75,7 @@ mod value; #[cfg(feature = "visitor")] mod visitor; -struct DisplaySeparated<'a, T> +pub struct DisplaySeparated<'a, T> where T: fmt::Display, { @@ -98,14 +98,14 @@ where } } -fn display_separated<'a, T>(slice: &'a [T], sep: &'static str) -> DisplaySeparated<'a, T> +pub fn display_separated<'a, T>(slice: &'a [T], sep: &'static str) -> DisplaySeparated<'a, T> where T: fmt::Display, { DisplaySeparated { slice, sep } } -fn display_comma_separated(slice: &[T]) -> DisplaySeparated<'_, T> +pub fn display_comma_separated(slice: &[T]) -> DisplaySeparated<'_, T> where T: fmt::Display, { @@ -2033,56 +2033,7 @@ pub enum Statement { /// ```sql /// CREATE TABLE /// ``` - CreateTable { - or_replace: bool, - temporary: bool, - external: bool, - global: Option, - if_not_exists: bool, - transient: bool, - /// Table name - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - name: ObjectName, - /// Optional schema - columns: Vec, - constraints: Vec, - hive_distribution: HiveDistributionStyle, - hive_formats: Option, - table_properties: Vec, - with_options: Vec, - file_format: Option, - location: Option, - query: Option>, - without_rowid: bool, - like: Option, - clone: Option, - engine: Option, - comment: Option, - auto_increment_offset: Option, - default_charset: Option, - collation: Option, - on_commit: Option, - /// ClickHouse "ON CLUSTER" clause: - /// - on_cluster: Option, - /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different - /// than empty (represented as ()), the latter meaning "no sorting". - /// - order_by: Option>, - /// BigQuery: A partition expression for the table. - /// - partition_by: Option>, - /// BigQuery: Table clustering column list. - /// - cluster_by: Option>, - /// BigQuery: Table options list. - /// - options: 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. - strict: bool, - }, + CreateTable(CreateTable), /// ```sql /// CREATE VIRTUAL TABLE .. USING ()` /// ``` @@ -2097,20 +2048,7 @@ pub enum Statement { /// ```sql /// `CREATE INDEX` /// ``` - CreateIndex { - /// index name - name: Option, - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - using: Option, - columns: Vec, - unique: bool, - concurrently: bool, - if_not_exists: bool, - include: Vec, - nulls_distinct: Option, - predicate: Option, - }, + CreateIndex(CreateIndex), /// ```sql /// CREATE ROLE /// ``` @@ -3426,245 +3364,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::CreateTable { - name, - columns, - constraints, - table_properties, - with_options, - or_replace, - if_not_exists, - transient, - hive_distribution, - hive_formats, - external, - global, - temporary, - file_format, - location, - query, - without_rowid, - like, - clone, - default_charset, - engine, - comment, - auto_increment_offset, - collation, - on_commit, - on_cluster, - order_by, - partition_by, - cluster_by, - options, - strict, - } => { - // We want to allow the following options - // Empty column list, allowed by PostgreSQL: - // `CREATE TABLE t ()` - // No columns provided for CREATE TABLE AS: - // `CREATE TABLE t AS SELECT a from t2` - // Columns provided for CREATE TABLE AS: - // `CREATE TABLE t (a INT) AS SELECT a from t2` - write!( - f, - "CREATE {or_replace}{external}{global}{temporary}{transient}TABLE {if_not_exists}{name}", - or_replace = if *or_replace { "OR REPLACE " } else { "" }, - external = if *external { "EXTERNAL " } else { "" }, - global = global - .map(|global| { - if global { - "GLOBAL " - } else { - "LOCAL " - } - }) - .unwrap_or(""), - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - temporary = if *temporary { "TEMPORARY " } else { "" }, - transient = if *transient { "TRANSIENT " } else { "" }, - name = name, - )?; - if let Some(on_cluster) = on_cluster { - write!( - f, - " ON CLUSTER {}", - on_cluster.replace('{', "'{").replace('}', "}'") - )?; - } - if !columns.is_empty() || !constraints.is_empty() { - write!(f, " ({}", display_comma_separated(columns))?; - if !columns.is_empty() && !constraints.is_empty() { - write!(f, ", ")?; - } - write!(f, "{})", display_comma_separated(constraints))?; - } else if query.is_none() && like.is_none() && clone.is_none() { - // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens - write!(f, " ()")?; - } - // Only for SQLite - if *without_rowid { - write!(f, " WITHOUT ROWID")?; - } - - // Only for Hive - if let Some(l) = like { - write!(f, " LIKE {l}")?; - } - - if let Some(c) = clone { - write!(f, " CLONE {c}")?; - } - - match hive_distribution { - HiveDistributionStyle::PARTITIONED { columns } => { - write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?; - } - HiveDistributionStyle::CLUSTERED { - columns, - sorted_by, - num_buckets, - } => { - write!(f, " CLUSTERED BY ({})", display_comma_separated(columns))?; - if !sorted_by.is_empty() { - write!(f, " SORTED BY ({})", display_comma_separated(sorted_by))?; - } - if *num_buckets > 0 { - write!(f, " INTO {num_buckets} BUCKETS")?; - } - } - HiveDistributionStyle::SKEWED { - columns, - on, - stored_as_directories, - } => { - write!( - f, - " SKEWED BY ({})) ON ({})", - display_comma_separated(columns), - display_comma_separated(on) - )?; - if *stored_as_directories { - write!(f, " STORED AS DIRECTORIES")?; - } - } - _ => (), - } - - if let Some(HiveFormat { - row_format, - serde_properties, - storage, - location, - }) = hive_formats - { - match row_format { - Some(HiveRowFormat::SERDE { class }) => { - write!(f, " ROW FORMAT SERDE '{class}'")? - } - Some(HiveRowFormat::DELIMITED { delimiters }) => { - write!(f, " ROW FORMAT DELIMITED")?; - if !delimiters.is_empty() { - write!(f, " {}", display_separated(delimiters, " "))?; - } - } - None => (), - } - match storage { - Some(HiveIOFormat::IOF { - input_format, - output_format, - }) => write!( - f, - " STORED AS INPUTFORMAT {input_format} OUTPUTFORMAT {output_format}" - )?, - Some(HiveIOFormat::FileFormat { format }) if !*external => { - write!(f, " STORED AS {format}")? - } - _ => (), - } - if let Some(serde_properties) = serde_properties.as_ref() { - write!( - f, - " WITH SERDEPROPERTIES ({})", - display_comma_separated(serde_properties) - )?; - } - if !*external { - if let Some(loc) = location { - write!(f, " LOCATION '{loc}'")?; - } - } - } - if *external { - if let Some(file_format) = &file_format { - write!(f, " STORED AS {file_format}")?; - } - write!(f, " LOCATION '{}'", location.as_ref().unwrap())?; - } - if !table_properties.is_empty() { - write!( - f, - " TBLPROPERTIES ({})", - display_comma_separated(table_properties) - )?; - } - if !with_options.is_empty() { - write!(f, " WITH ({})", display_comma_separated(with_options))?; - } - if let Some(engine) = engine { - write!(f, " ENGINE={engine}")?; - } - if let Some(comment) = comment { - write!(f, " COMMENT '{comment}'")?; - } - if let Some(auto_increment_offset) = auto_increment_offset { - write!(f, " AUTO_INCREMENT {auto_increment_offset}")?; - } - if let Some(order_by) = order_by { - write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; - } - if let Some(partition_by) = partition_by.as_ref() { - write!(f, " PARTITION BY {partition_by}")?; - } - if let Some(cluster_by) = cluster_by.as_ref() { - write!( - f, - " CLUSTER BY {}", - display_comma_separated(cluster_by.as_slice()) - )?; - } - if let Some(options) = options.as_ref() { - write!( - f, - " OPTIONS({})", - display_comma_separated(options.as_slice()) - )?; - } - if let Some(query) = query { - write!(f, " AS {query}")?; - } - if let Some(default_charset) = default_charset { - write!(f, " DEFAULT CHARSET={default_charset}")?; - } - if let Some(collation) = collation { - write!(f, " COLLATE={collation}")?; - } - - if on_commit.is_some() { - let on_commit = match on_commit { - Some(OnCommit::DeleteRows) => "ON COMMIT DELETE ROWS", - Some(OnCommit::PreserveRows) => "ON COMMIT PRESERVE ROWS", - Some(OnCommit::Drop) => "ON COMMIT DROP", - None => "", - }; - write!(f, " {on_commit}")?; - } - if *strict { - write!(f, " STRICT")?; - } - Ok(()) - } + Statement::CreateTable(create_table) => create_table.fmt(f), Statement::CreateVirtualTable { name, if_not_exists, @@ -3683,7 +3383,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name, table_name, using, @@ -3694,7 +3394,7 @@ impl fmt::Display for Statement { include, nulls_distinct, predicate, - } => { + }) => { write!( f, "CREATE {unique}INDEX {concurrently}{if_not_exists}", diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c6750644c..a2468af3d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4963,7 +4963,7 @@ impl<'a> Parser<'a> { None }; - Ok(Statement::CreateIndex { + Ok(Statement::CreateIndex(CreateIndex { name: index_name, table_name, using, @@ -4974,7 +4974,7 @@ impl<'a> Parser<'a> { include, nulls_distinct, predicate, - }) + })) } pub fn parse_create_extension(&mut self) -> Result { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 1cec15c30..3b6d6bfcb 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -354,7 +354,7 @@ fn parse_create_view_with_unquoted_hyphen() { fn parse_create_table_with_unquoted_hyphen() { let sql = "CREATE TABLE my-pro-ject.mydataset.mytable (x INT64)"; match bigquery().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!( name, ObjectName(vec![ @@ -388,14 +388,14 @@ fn parse_create_table_with_options() { r#"OPTIONS(partition_expiration_days = 1, description = "table option description")"# ); match bigquery().verified_stmt(sql) { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, partition_by, cluster_by, options, .. - } => { + }) => { assert_eq!( name, ObjectName(vec!["mydataset".into(), "newtable".into()]) @@ -477,7 +477,7 @@ fn parse_create_table_with_options() { fn parse_nested_data_types() { let sql = "CREATE TABLE table (x STRUCT, b BYTES(42)>, y ARRAY>)"; match bigquery_and_generic().one_statement_parses_to(sql, sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name, ObjectName(vec!["table".into()])); assert_eq!( columns, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5f2d2cc02..580ae9867 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2747,7 +2747,7 @@ fn parse_create_table() { FOREIGN KEY (lng) REFERENCES othertable4(longitude) ON UPDATE SET NULL)", ); match ast { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, constraints, @@ -2757,7 +2757,7 @@ fn parse_create_table() { file_format: None, location: None, .. - } => { + }) => { assert_eq!("uk_cities", name.to_string()); assert_eq!( columns, @@ -2936,7 +2936,7 @@ fn parse_create_table_with_constraint_characteristics() { FOREIGN KEY (lng) REFERENCES othertable4(longitude) ON UPDATE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE ENFORCED)", ); match ast { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, constraints, @@ -2946,7 +2946,7 @@ fn parse_create_table_with_constraint_characteristics() { file_format: None, location: None, .. - } => { + }) => { assert_eq!("uk_cities", name.to_string()); assert_eq!( columns, @@ -3104,7 +3104,7 @@ fn parse_create_table_column_constraint_characteristics() { }; match ast { - Statement::CreateTable { columns, .. } => { + Statement::CreateTable(CreateTable { columns, .. }) => { assert_eq!( columns, vec![ColumnDef { @@ -3214,12 +3214,12 @@ fn parse_create_table_hive_array() { }; match dialects.one_statement_parses_to(sql.as_str(), sql.as_str()) { - Statement::CreateTable { + Statement::CreateTable(CreateTable { if_not_exists, name, columns, .. - } => { + }) => { assert!(if_not_exists); assert_eq!(name, ObjectName(vec!["something".into()])); assert_eq!( @@ -3373,7 +3373,7 @@ fn parse_create_table_as() { let sql = "CREATE TABLE t AS SELECT * FROM a"; match verified_stmt(sql) { - Statement::CreateTable { name, query, .. } => { + Statement::CreateTable(CreateTable { name, query, .. }) => { assert_eq!(name.to_string(), "t".to_string()); assert_eq!(query, Some(Box::new(verified_query("SELECT * FROM a")))); } @@ -3385,7 +3385,7 @@ fn parse_create_table_as() { // (without data types) in a CTAS, but we have yet to support that. let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a"; match verified_stmt(sql) { - Statement::CreateTable { columns, query, .. } => { + Statement::CreateTable(CreateTable { columns, query, .. }) => { assert_eq!(columns.len(), 2); assert_eq!(columns[0].to_string(), "a INT".to_string()); assert_eq!(columns[1].to_string(), "b INT".to_string()); @@ -3418,7 +3418,7 @@ fn parse_create_table_as_table() { }); match verified_stmt(sql1) { - Statement::CreateTable { query, name, .. } => { + Statement::CreateTable(CreateTable { query, name, .. }) => { assert_eq!(name, ObjectName(vec![Ident::new("new_table")])); assert_eq!(query.unwrap(), expected_query1); } @@ -3443,7 +3443,7 @@ fn parse_create_table_as_table() { }); match verified_stmt(sql2) { - Statement::CreateTable { query, name, .. } => { + Statement::CreateTable(CreateTable { query, name, .. }) => { assert_eq!(name, ObjectName(vec![Ident::new("new_table")])); assert_eq!(query.unwrap(), expected_query2); } @@ -3456,7 +3456,7 @@ fn parse_create_table_on_cluster() { // Using single-quote literal to define current cluster let sql = "CREATE TABLE t ON CLUSTER '{cluster}' (a INT, b INT)"; match verified_stmt(sql) { - Statement::CreateTable { on_cluster, .. } => { + Statement::CreateTable(CreateTable { on_cluster, .. }) => { assert_eq!(on_cluster.unwrap(), "{cluster}".to_string()); } _ => unreachable!(), @@ -3465,7 +3465,7 @@ fn parse_create_table_on_cluster() { // Using explicitly declared cluster name let sql = "CREATE TABLE t ON CLUSTER my_cluster (a INT, b INT)"; match verified_stmt(sql) { - Statement::CreateTable { on_cluster, .. } => { + Statement::CreateTable(CreateTable { on_cluster, .. }) => { assert_eq!(on_cluster.unwrap(), "my_cluster".to_string()); } _ => unreachable!(), @@ -3477,9 +3477,9 @@ fn parse_create_or_replace_table() { let sql = "CREATE OR REPLACE TABLE t (a INT)"; match verified_stmt(sql) { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, or_replace, .. - } => { + }) => { assert_eq!(name.to_string(), "t".to_string()); assert!(or_replace); } @@ -3488,7 +3488,7 @@ fn parse_create_or_replace_table() { let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a"; match verified_stmt(sql) { - Statement::CreateTable { columns, query, .. } => { + Statement::CreateTable(CreateTable { columns, query, .. }) => { assert_eq!(columns.len(), 2); assert_eq!(columns[0].to_string(), "a INT".to_string()); assert_eq!(columns[1].to_string(), "b INT".to_string()); @@ -3519,7 +3519,7 @@ fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), Par fn parse_create_table_with_options() { let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; match verified_stmt(sql) { - Statement::CreateTable { with_options, .. } => { + Statement::CreateTable(CreateTable { with_options, .. }) => { assert_eq!( vec![ SqlOption { @@ -3542,7 +3542,7 @@ fn parse_create_table_with_options() { fn parse_create_table_clone() { let sql = "CREATE OR REPLACE TABLE a CLONE a_tmp"; match verified_stmt(sql) { - Statement::CreateTable { name, clone, .. } => { + Statement::CreateTable(CreateTable { name, clone, .. }) => { assert_eq!(ObjectName(vec![Ident::new("a")]), name); assert_eq!(Some(ObjectName(vec![(Ident::new("a_tmp"))])), clone) } @@ -3572,7 +3572,7 @@ fn parse_create_external_table() { STORED AS TEXTFILE LOCATION '/tmp/example.csv'", ); match ast { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, constraints, @@ -3582,7 +3582,7 @@ fn parse_create_external_table() { file_format, location, .. - } => { + }) => { assert_eq!("uk_cities", name.to_string()); assert_eq!( columns, @@ -3643,7 +3643,7 @@ fn parse_create_or_replace_external_table() { STORED AS TEXTFILE LOCATION '/tmp/example.csv'", ); match ast { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, constraints, @@ -3654,7 +3654,7 @@ fn parse_create_or_replace_external_table() { location, or_replace, .. - } => { + }) => { assert_eq!("uk_cities", name.to_string()); assert_eq!( columns, @@ -3700,7 +3700,7 @@ fn parse_create_external_table_lowercase() { lng DOUBLE) \ STORED AS PARQUET LOCATION '/tmp/example.csv'", ); - assert_matches!(ast, Statement::CreateTable { .. }); + assert_matches!(ast, Statement::CreateTable(CreateTable { .. })); } #[test] @@ -7210,14 +7210,14 @@ fn parse_create_index() { }, ]; match verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name: Some(name), table_name, columns, unique, if_not_exists, .. - } => { + }) => { assert_eq!("idx_name", name.to_string()); assert_eq!("test", table_name.to_string()); assert_eq!(indexed_columns, columns); @@ -7244,7 +7244,7 @@ fn test_create_index_with_using_function() { }, ]; match verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name: Some(name), table_name, using, @@ -7255,7 +7255,7 @@ fn test_create_index_with_using_function() { include, nulls_distinct: None, predicate: None, - } => { + }) => { assert_eq!("idx_name", name.to_string()); assert_eq!("test", table_name.to_string()); assert_eq!("btree", using.unwrap().to_string()); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 1e185915b..3041b6001 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -470,7 +470,7 @@ fn parse_set_variables() { fn parse_create_table_auto_increment() { let sql = "CREATE TABLE foo (bar INT PRIMARY KEY AUTO_INCREMENT)"; match mysql().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ColumnDef { @@ -541,12 +541,12 @@ fn parse_create_table_primary_and_unique_key() { for (sql, index_type_display) in sqls.iter().zip(index_type_display) { match mysql().one_statement_parses_to(sql, "") { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, constraints, .. - } => { + }) => { assert_eq!(name.to_string(), "foo"); let expected_constraint = table_constraint_unique_primary_ctor( @@ -609,9 +609,9 @@ fn parse_create_table_primary_and_unique_key_with_index_options() { for (sql, index_type_display) in sqls.iter().zip(index_type_display) { match mysql_and_generic().one_statement_parses_to(sql, "") { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, constraints, .. - } => { + }) => { assert_eq!(name.to_string(), "foo"); let expected_constraint = table_constraint_unique_primary_ctor( @@ -647,9 +647,9 @@ fn parse_create_table_primary_and_unique_key_with_index_type() { for (sql, index_type_display) in sqls.iter().zip(index_type_display) { match mysql_and_generic().one_statement_parses_to(sql, "") { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, constraints, .. - } => { + }) => { assert_eq!(name.to_string(), "foo"); let expected_constraint = table_constraint_unique_primary_ctor( @@ -690,7 +690,7 @@ fn parse_create_table_comment() { for sql in [canonical, with_equal] { match mysql().one_statement_parses_to(sql, canonical) { - Statement::CreateTable { name, comment, .. } => { + Statement::CreateTable(CreateTable { name, comment, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!(comment.expect("Should exist").to_string(), "baz"); } @@ -708,11 +708,11 @@ fn parse_create_table_auto_increment_offset() { for sql in [canonical, with_equal] { match mysql().one_statement_parses_to(sql, canonical) { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, auto_increment_offset, .. - } => { + }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( auto_increment_offset.expect("Should exist").to_string(), @@ -728,7 +728,7 @@ fn parse_create_table_auto_increment_offset() { fn parse_create_table_set_enum() { let sql = "CREATE TABLE foo (bar SET('a', 'b'), baz ENUM('a', 'b'))"; match mysql().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ @@ -756,13 +756,13 @@ fn parse_create_table_set_enum() { fn parse_create_table_engine_default_charset() { let sql = "CREATE TABLE foo (id INT(11)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"; match mysql().verified_stmt(sql) { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, engine, default_charset, .. - } => { + }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ColumnDef { @@ -784,12 +784,12 @@ fn parse_create_table_engine_default_charset() { fn parse_create_table_collate() { let sql = "CREATE TABLE foo (id INT(11)) COLLATE=utf8mb4_0900_ai_ci"; match mysql().verified_stmt(sql) { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, collation, .. - } => { + }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ColumnDef { @@ -810,7 +810,7 @@ fn parse_create_table_collate() { fn parse_create_table_comment_character_set() { let sql = "CREATE TABLE foo (s TEXT CHARACTER SET utf8mb4 COMMENT 'comment')"; match mysql().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ColumnDef { @@ -857,7 +857,7 @@ fn parse_create_table_gencol() { fn parse_quote_identifiers() { let sql = "CREATE TABLE `PRIMARY` (`BEGIN` INT PRIMARY KEY)"; match mysql().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "`PRIMARY`"); assert_eq!( vec![ColumnDef { @@ -1126,7 +1126,7 @@ fn check_roundtrip_of_escaped_string() { fn parse_create_table_with_minimum_display_width() { let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3), bar_smallint SMALLINT(5), bar_mediumint MEDIUMINT(6), bar_int INT(11), bar_bigint BIGINT(20))"; match mysql().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ @@ -1172,7 +1172,7 @@ fn parse_create_table_with_minimum_display_width() { fn parse_create_table_unsigned() { let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3) UNSIGNED, bar_smallint SMALLINT(5) UNSIGNED, bar_mediumint MEDIUMINT(13) UNSIGNED, bar_int INT(11) UNSIGNED, bar_bigint BIGINT(20) UNSIGNED)"; match mysql().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ @@ -2321,7 +2321,7 @@ fn parse_kill() { fn parse_table_colum_option_on_update() { let sql1 = "CREATE TABLE foo (`modification_time` DATETIME ON UPDATE CURRENT_TIMESTAMP())"; match mysql().verified_stmt(sql1) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ColumnDef { @@ -2622,7 +2622,7 @@ fn parse_create_table_with_column_collate() { let sql = "CREATE TABLE tb (id TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci)"; let canonical = "CREATE TABLE tb (id TEXT COLLATE utf8mb4_0900_ai_ci CHARACTER SET utf8mb4)"; match mysql().one_statement_parses_to(sql, canonical) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "tb"); assert_eq!( vec![ColumnDef { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 677246a51..7118d650e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -317,7 +317,7 @@ fn parse_create_table_with_defaults() { active int NOT NULL ) WITH (fillfactor = 20, user_catalog_table = true, autovacuum_vacuum_threshold = 100)"; match pg_and_generic().one_statement_parses_to(sql, "") { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, constraints, @@ -327,7 +327,7 @@ fn parse_create_table_with_defaults() { file_format: None, location: None, .. - } => { + }) => { use pretty_assertions::assert_eq; assert_eq!("public.customer", name.to_string()); assert_eq!( @@ -537,12 +537,12 @@ fn parse_create_table_constraints_only() { let sql = "CREATE TABLE t (CONSTRAINT positive CHECK (2 > 1))"; let ast = pg_and_generic().verified_stmt(sql); match ast { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, constraints, .. - } => { + }) => { assert_eq!("t", name.to_string()); assert!(columns.is_empty()); assert_eq!( @@ -718,11 +718,11 @@ fn parse_create_table_if_not_exists() { let sql = "CREATE TABLE IF NOT EXISTS uk_cities ()"; let ast = pg_and_generic().verified_stmt(sql); match ast { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, if_not_exists: true, .. - } => { + }) => { assert_eq!("uk_cities", name.to_string()); } _ => unreachable!(), @@ -2086,7 +2086,7 @@ fn parse_array_multi_subscript() { fn parse_create_index() { let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2)"; match pg().verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), table_name: ObjectName(table_name), using, @@ -2097,7 +2097,7 @@ fn parse_create_index() { nulls_distinct: None, include, predicate: None, - } => { + }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); assert_eq!(None, using); @@ -2115,7 +2115,7 @@ fn parse_create_index() { fn parse_create_anonymous_index() { let sql = "CREATE INDEX ON my_table(col1,col2)"; match pg().verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name, table_name: ObjectName(table_name), using, @@ -2126,7 +2126,7 @@ fn parse_create_anonymous_index() { include, nulls_distinct: None, predicate: None, - } => { + }) => { assert_eq!(None, name); assert_eq_vec(&["my_table"], &table_name); assert_eq!(None, using); @@ -2144,7 +2144,7 @@ fn parse_create_anonymous_index() { fn parse_create_index_concurrently() { let sql = "CREATE INDEX CONCURRENTLY IF NOT EXISTS my_index ON my_table(col1,col2)"; match pg().verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), table_name: ObjectName(table_name), using, @@ -2155,7 +2155,7 @@ fn parse_create_index_concurrently() { include, nulls_distinct: None, predicate: None, - } => { + }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); assert_eq!(None, using); @@ -2173,7 +2173,7 @@ fn parse_create_index_concurrently() { fn parse_create_index_with_predicate() { let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) WHERE col3 IS NULL"; match pg().verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), table_name: ObjectName(table_name), using, @@ -2184,7 +2184,7 @@ fn parse_create_index_with_predicate() { include, nulls_distinct: None, predicate: Some(_), - } => { + }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); assert_eq!(None, using); @@ -2202,7 +2202,7 @@ fn parse_create_index_with_predicate() { fn parse_create_index_with_include() { let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) INCLUDE (col3)"; match pg().verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), table_name: ObjectName(table_name), using, @@ -2213,7 +2213,7 @@ fn parse_create_index_with_include() { include, nulls_distinct: None, predicate: None, - } => { + }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); assert_eq!(None, using); @@ -2231,7 +2231,7 @@ fn parse_create_index_with_include() { fn parse_create_index_with_nulls_distinct() { let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) NULLS NOT DISTINCT"; match pg().verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), table_name: ObjectName(table_name), using, @@ -2242,7 +2242,7 @@ fn parse_create_index_with_nulls_distinct() { include, nulls_distinct: Some(nulls_distinct), predicate: None, - } => { + }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); assert_eq!(None, using); @@ -2258,7 +2258,7 @@ fn parse_create_index_with_nulls_distinct() { let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) NULLS DISTINCT"; match pg().verified_stmt(sql) { - Statement::CreateIndex { + Statement::CreateIndex(CreateIndex { name: Some(ObjectName(name)), table_name: ObjectName(table_name), using, @@ -2269,7 +2269,7 @@ fn parse_create_index_with_nulls_distinct() { include, nulls_distinct: Some(nulls_distinct), predicate: None, - } => { + }) => { assert_eq_vec(&["my_index"], &name); assert_eq_vec(&["my_table"], &table_name); assert_eq!(None, using); @@ -3704,7 +3704,7 @@ fn parse_create_table_with_alias() { bool_col BOOL, );"; match pg_and_generic().one_statement_parses_to(sql, "") { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, columns, constraints, @@ -3714,7 +3714,7 @@ fn parse_create_table_with_alias() { file_format: None, location: None, .. - } => { + }) => { assert_eq!("public.datatype_aliases", name.to_string()); assert_eq!( columns, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index d213efd7b..a21e9d5d6 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -33,7 +33,7 @@ use pretty_assertions::assert_eq; fn test_snowflake_create_table() { let sql = "CREATE TABLE _my_$table (am00unt number)"; match snowflake_and_generic().verified_stmt(sql) { - Statement::CreateTable { name, .. } => { + Statement::CreateTable(CreateTable { name, .. }) => { assert_eq!("_my_$table", name.to_string()); } _ => unreachable!(), @@ -44,9 +44,9 @@ fn test_snowflake_create_table() { fn test_snowflake_create_transient_table() { let sql = "CREATE TRANSIENT TABLE CUSTOMER (id INT, name VARCHAR(255))"; match snowflake_and_generic().verified_stmt(sql) { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, transient, .. - } => { + }) => { assert_eq!("CUSTOMER", name.to_string()); assert!(transient) } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index fe5346f14..16ea9eb8c 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -122,11 +122,11 @@ fn pragma_eq_placeholder_style() { fn parse_create_table_without_rowid() { let sql = "CREATE TABLE t (a INT) WITHOUT ROWID"; match sqlite_and_generic().verified_stmt(sql) { - Statement::CreateTable { + Statement::CreateTable(CreateTable { name, without_rowid: true, .. - } => { + }) => { assert_eq!("t", name.to_string()); } _ => unreachable!(), @@ -200,7 +200,7 @@ fn double_equality_operator() { fn parse_create_table_auto_increment() { let sql = "CREATE TABLE foo (bar INT PRIMARY KEY AUTOINCREMENT)"; match sqlite_and_generic().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!( vec![ColumnDef { @@ -234,7 +234,7 @@ fn parse_create_table_auto_increment() { fn parse_create_sqlite_quote() { let sql = "CREATE TABLE `PRIMARY` (\"KEY\" INT, [INDEX] INT)"; match sqlite().verified_stmt(sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "`PRIMARY`"); assert_eq!( vec![ @@ -295,7 +295,7 @@ fn test_placeholder() { #[test] fn parse_create_table_with_strict() { let sql = "CREATE TABLE Fruits (id TEXT NOT NULL PRIMARY KEY) STRICT"; - if let Statement::CreateTable { name, strict, .. } = sqlite().verified_stmt(sql) { + if let Statement::CreateTable(CreateTable { name, strict, .. }) = sqlite().verified_stmt(sql) { assert_eq!(name.to_string(), "Fruits"); assert!(strict); } From 6d4776b4825dbc62f975a78e51b4ff69d8f49d34 Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:44:04 +0300 Subject: [PATCH 462/806] Enhancing Trailing Comma Option (#1212) --- src/dialect/bigquery.rs | 4 ++ src/dialect/duckdb.rs | 4 ++ src/dialect/mod.rs | 8 ++++ src/dialect/snowflake.rs | 4 ++ src/parser/mod.rs | 28 ++++++++++--- tests/sqlparser_common.rs | 80 ++++++++++++++++++++++++++++++++++--- tests/sqlparser_postgres.rs | 2 +- 7 files changed, 118 insertions(+), 12 deletions(-) diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index d65de3a47..d3673337f 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -22,6 +22,10 @@ impl Dialect for BigQueryDialect { ch == '`' } + fn supports_projection_trailing_commas(&self) -> bool { + true + } + fn is_identifier_start(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index e141f941f..c6edeac14 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -18,6 +18,10 @@ pub struct DuckDbDialect; // In most cases the redshift dialect is identical to [`PostgresSqlDialect`]. impl Dialect for DuckDbDialect { + fn supports_trailing_commas(&self) -> bool { + true + } + fn is_identifier_start(&self, ch: char) -> bool { ch.is_alphabetic() || ch == '_' } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index da5c8c5ac..e06c07a1c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -251,6 +251,14 @@ pub trait Dialect: Debug + Any { // return None to fall back to the default behavior None } + /// Does the dialect support trailing commas around the query? + fn supports_trailing_commas(&self) -> bool { + false + } + /// Does the dialect support trailing commas in the projection list? + fn supports_projection_trailing_commas(&self) -> bool { + self.supports_trailing_commas() + } /// Dialect-specific infix parser override fn parse_infix( &self, diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 21bc53554..894b00438 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -38,6 +38,10 @@ impl Dialect for SnowflakeDialect { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } + fn supports_projection_trailing_commas(&self) -> bool { + true + } + fn is_identifier_part(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a2468af3d..e0a5b86ab 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -305,7 +305,7 @@ impl<'a> Parser<'a> { state: ParserState::Normal, dialect, recursion_counter: RecursionCounter::new(DEFAULT_REMAINING_DEPTH), - options: ParserOptions::default(), + options: ParserOptions::new().with_trailing_commas(dialect.supports_trailing_commas()), } } @@ -3225,7 +3225,7 @@ impl<'a> Parser<'a> { // This pattern could be captured better with RAII type semantics, but it's quite a bit of // code to add for just one case, so we'll just do it manually here. let old_value = self.options.trailing_commas; - self.options.trailing_commas |= dialect_of!(self is BigQueryDialect | SnowflakeDialect); + self.options.trailing_commas |= self.dialect.supports_projection_trailing_commas(); let ret = self.parse_comma_separated(|p| p.parse_select_item()); self.options.trailing_commas = old_value; @@ -5413,12 +5413,17 @@ impl<'a> Parser<'a> { } else { return self.expected("column name or constraint definition", self.peek_token()); } + let comma = self.consume_token(&Token::Comma); - if self.consume_token(&Token::RParen) { - // allow a trailing comma, even though it's not in standard - break; - } else if !comma { + let rparen = self.peek_token().token == Token::RParen; + + if !comma && !rparen { return self.expected("',' or ')' after column definition", self.peek_token()); + }; + + if rparen && (!comma || self.options.trailing_commas) { + let _ = self.consume_token(&Token::RParen); + break; } } @@ -9411,6 +9416,9 @@ impl<'a> Parser<'a> { with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), } } else { + let old_value = self.options.trailing_commas; + self.options.trailing_commas = false; + let (actions, err): (Vec<_>, Vec<_>) = self .parse_comma_separated(Parser::parse_grant_permission)? .into_iter() @@ -9434,6 +9442,8 @@ impl<'a> Parser<'a> { }) .partition(Result::is_ok); + self.options.trailing_commas = old_value; + if !err.is_empty() { let errors: Vec = err.into_iter().filter_map(|x| x.err()).collect(); return Err(ParserError::ParserError(format!( @@ -9939,6 +9949,12 @@ impl<'a> Parser<'a> { Expr::Wildcard => Ok(SelectItem::Wildcard( self.parse_wildcard_additional_options()?, )), + Expr::Identifier(v) if v.value.to_lowercase() == "from" => { + parser_err!( + format!("Expected an expression, found: {}", v), + self.peek_token().location + ) + } expr => self .parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) .map(|alias| match alias { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 580ae9867..8fe7b862c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3552,8 +3552,13 @@ fn parse_create_table_clone() { #[test] fn parse_create_table_trailing_comma() { - let sql = "CREATE TABLE foo (bar int,)"; - all_dialects().one_statement_parses_to(sql, "CREATE TABLE foo (bar INT)"); + let dialect = TestedDialects { + dialects: vec![Box::new(DuckDbDialect {})], + options: None, + }; + + let sql = "CREATE TABLE foo (bar int,);"; + dialect.one_statement_parses_to(sql, "CREATE TABLE foo (bar INT)"); } #[test] @@ -4418,7 +4423,7 @@ fn parse_window_clause() { ORDER BY C3"; verified_only_select(sql); - let sql = "SELECT from mytable WINDOW window1 AS window2"; + let sql = "SELECT * from mytable WINDOW window1 AS window2"; let dialects = all_dialects_except(|d| d.is::() || d.is::()); let res = dialects.parse_sql_statements(sql); assert_eq!( @@ -8846,9 +8851,11 @@ fn parse_non_latin_identifiers() { #[test] fn parse_trailing_comma() { + // At the moment, Duck DB is the only dialect that allows + // trailing commas anywhere in the query let trailing_commas = TestedDialects { - dialects: vec![Box::new(GenericDialect {})], - options: Some(ParserOptions::new().with_trailing_commas(true)), + dialects: vec![Box::new(DuckDbDialect {})], + options: None, }; trailing_commas.one_statement_parses_to( @@ -8866,11 +8873,74 @@ fn parse_trailing_comma() { "SELECT DISTINCT ON (album_id) name FROM track", ); + trailing_commas.one_statement_parses_to( + "CREATE TABLE employees (name text, age int,)", + "CREATE TABLE employees (name TEXT, age INT)", + ); + trailing_commas.verified_stmt("SELECT album_id, name FROM track"); trailing_commas.verified_stmt("SELECT * FROM track ORDER BY milliseconds"); trailing_commas.verified_stmt("SELECT DISTINCT ON (album_id) name FROM track"); + + // doesn't allow any trailing commas + let trailing_commas = TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + options: None, + }; + + assert_eq!( + trailing_commas + .parse_sql_statements("SELECT name, age, from employees;") + .unwrap_err(), + ParserError::ParserError("Expected an expression, found: from".to_string()) + ); + + assert_eq!( + trailing_commas + .parse_sql_statements("CREATE TABLE employees (name text, age int,)") + .unwrap_err(), + ParserError::ParserError( + "Expected column name or constraint definition, found: )".to_string() + ) + ); +} + +#[test] +fn parse_projection_trailing_comma() { + // Some dialects allow trailing commas only in the projection + let trailing_commas = TestedDialects { + dialects: vec![Box::new(SnowflakeDialect {}), Box::new(BigQueryDialect {})], + options: None, + }; + + trailing_commas.one_statement_parses_to( + "SELECT album_id, name, FROM track", + "SELECT album_id, name FROM track", + ); + + trailing_commas.verified_stmt("SELECT album_id, name FROM track"); + + trailing_commas.verified_stmt("SELECT * FROM track ORDER BY milliseconds"); + + trailing_commas.verified_stmt("SELECT DISTINCT ON (album_id) name FROM track"); + + assert_eq!( + trailing_commas + .parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,") + .unwrap_err(), + ParserError::ParserError("Expected an expression:, found: EOF".to_string()) + ); + + assert_eq!( + trailing_commas + .parse_sql_statements("CREATE TABLE employees (name text, age int,)") + .unwrap_err(), + ParserError::ParserError( + "Expected column name or constraint definition, found: )".to_string() + ), + ); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7118d650e..1df94b100 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3701,7 +3701,7 @@ fn parse_create_table_with_alias() { int2_col INT2, float8_col FLOAT8, float4_col FLOAT4, - bool_col BOOL, + bool_col BOOL );"; match pg_and_generic().one_statement_parses_to(sql, "") { Statement::CreateTable(CreateTable { From 2fb919d8b21129bd8faf62fc4aadeb9629ac5e10 Mon Sep 17 00:00:00 2001 From: Aleksei Piianin Date: Fri, 7 Jun 2024 13:09:42 +0200 Subject: [PATCH 463/806] ClickHouse data types (#1285) --- src/ast/data_type.rs | 191 ++++++++++++++++++++++++++++++++-- src/ast/mod.rs | 2 +- src/keywords.rs | 18 ++++ src/parser/mod.rs | 151 ++++++++++++++++++++++++--- tests/sqlparser_clickhouse.rs | 190 +++++++++++++++++++++++++++++++++ 5 files changed, 528 insertions(+), 24 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index d71900bff..7d0aec8fc 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -22,7 +22,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{display_comma_separated, ObjectName, StructField}; -use super::value::escape_single_quote_string; +use super::{value::escape_single_quote_string, ColumnDef}; /// SQL data types #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -129,10 +129,39 @@ pub enum DataType { /// /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html Int4(Option), - /// Integer type in [bigquery] + /// 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] + /// + /// [postgresql]: https://www.postgresql.org/docs/15/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] + /// + /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + Int16, + /// Integer type in [clickhouse] + /// Note: Int16 mean 32 bits in [clickhouse] + /// + /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + Int32, + /// 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 Int64, + /// Integer type in [clickhouse] + /// Note: Int128 mean 128 bits in [clickhouse] + /// + /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + Int128, + /// Integer type in [clickhouse] + /// Note: Int256 mean 256 bits in [clickhouse] + /// + /// [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(Option), /// Unsigned int with optional display width e.g. INT UNSIGNED or INT(11) UNSIGNED @@ -141,25 +170,54 @@ pub enum DataType { UnsignedInt4(Option), /// Unsigned integer with optional display width e.g. INTGER UNSIGNED or INTEGER(11) UNSIGNED UnsignedInteger(Option), + /// Unsigned integer type in [clickhouse] + /// Note: UInt8 mean 8 bits in [clickhouse] + /// + /// [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] + /// + /// [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] + /// + /// [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] + /// + /// [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] + /// + /// [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] + /// + /// [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) BigInt(Option), /// Unsigned big integer with optional display width e.g. BIGINT UNSIGNED or BIGINT(20) UNSIGNED UnsignedBigInt(Option), - /// Int8 as alias for Bigint in [postgresql] - /// Note: Int8 mean 8 bytes in postgres (not 8 bits) - /// Int8 with optional display width e.g. INT8 or INT8(11) - /// - /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html - Int8(Option), /// Unsigned Int8 with optional display width e.g. INT8 UNSIGNED or INT8(11) UNSIGNED UnsignedInt8(Option), /// Float4 as alias for Real in [postgresql] /// /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html Float4, + /// Floating point in [clickhouse] + /// + /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/float + Float32, /// 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 Float64, /// Floating point e.g. REAL Real, @@ -182,6 +240,10 @@ pub enum DataType { Boolean, /// Date Date, + /// 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]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type @@ -190,6 +252,10 @@ pub enum DataType { /// /// [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]. + /// + /// [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]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type @@ -206,12 +272,28 @@ pub enum DataType { Text, /// String with optional length. String(Option), + /// A fixed-length string e.g [ClickHouse][1]. + /// + /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/fixedstring + FixedString(u64), /// Bytea Bytea, /// Custom type such as enums Custom(ObjectName, Vec), /// Arrays Array(ArrayElemTypeDef), + /// Map + /// + /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/map + Map(Box, Box), + /// Tuple + /// + /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple + Tuple(Vec), + /// Nested + /// + /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nested-data-structures/nested + Nested(Vec), /// Enums Enum(Vec), /// Set @@ -221,6 +303,14 @@ pub enum DataType { /// [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), + /// Nullable - special marker NULL represents in ClickHouse as a data type. + /// + /// [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 + LowCardinality(Box), /// No type specified - only used with /// [`SQLiteDialect`](crate::dialect::SQLiteDialect), from statements such /// as `CREATE TABLE t1 (a)`. @@ -296,9 +386,24 @@ impl fmt::Display for DataType { DataType::Int4(zerofill) => { format_type_with_optional_length(f, "INT4", zerofill, false) } + DataType::Int8(zerofill) => { + format_type_with_optional_length(f, "INT8", zerofill, false) + } + DataType::Int16 => { + write!(f, "Int16") + } + DataType::Int32 => { + write!(f, "Int32") + } DataType::Int64 => { write!(f, "INT64") } + DataType::Int128 => { + write!(f, "Int128") + } + DataType::Int256 => { + write!(f, "Int256") + } DataType::UnsignedInt4(zerofill) => { format_type_with_optional_length(f, "INT4", zerofill, true) } @@ -314,14 +419,30 @@ impl fmt::Display for DataType { DataType::UnsignedBigInt(zerofill) => { format_type_with_optional_length(f, "BIGINT", zerofill, true) } - DataType::Int8(zerofill) => { - format_type_with_optional_length(f, "INT8", zerofill, false) - } DataType::UnsignedInt8(zerofill) => { format_type_with_optional_length(f, "INT8", zerofill, true) } + DataType::UInt8 => { + write!(f, "UInt8") + } + DataType::UInt16 => { + write!(f, "UInt16") + } + DataType::UInt32 => { + write!(f, "UInt32") + } + DataType::UInt64 => { + write!(f, "UInt64") + } + DataType::UInt128 => { + write!(f, "UInt128") + } + DataType::UInt256 => { + write!(f, "UInt256") + } DataType::Real => write!(f, "REAL"), DataType::Float4 => write!(f, "FLOAT4"), + DataType::Float32 => write!(f, "Float32"), DataType::Float64 => write!(f, "FLOAT64"), DataType::Double => write!(f, "DOUBLE"), DataType::Float8 => write!(f, "FLOAT8"), @@ -329,6 +450,7 @@ impl fmt::Display for DataType { DataType::Bool => write!(f, "BOOL"), DataType::Boolean => write!(f, "BOOLEAN"), DataType::Date => write!(f, "DATE"), + DataType::Date32 => write!(f, "Date32"), DataType::Time(precision, timezone_info) => { format_datetime_precision_and_tz(f, "TIME", precision, timezone_info) } @@ -338,6 +460,14 @@ impl fmt::Display for DataType { DataType::Timestamp(precision, timezone_info) => { format_datetime_precision_and_tz(f, "TIMESTAMP", precision, timezone_info) } + DataType::Datetime64(precision, timezone) => { + format_clickhouse_datetime_precision_and_timezone( + f, + "DateTime64", + precision, + timezone, + ) + } DataType::Interval => write!(f, "INTERVAL"), DataType::JSON => write!(f, "JSON"), DataType::JSONB => write!(f, "JSONB"), @@ -350,6 +480,7 @@ impl fmt::Display for DataType { ArrayElemTypeDef::SquareBracket(t, None) => write!(f, "{t}[]"), ArrayElemTypeDef::SquareBracket(t, Some(size)) => write!(f, "{t}[{size}]"), ArrayElemTypeDef::AngleBracket(t) => write!(f, "ARRAY<{t}>"), + ArrayElemTypeDef::Parenthesis(t) => write!(f, "Array({t})"), }, DataType::Custom(ty, modifiers) => { if modifiers.is_empty() { @@ -385,6 +516,25 @@ impl fmt::Display for DataType { write!(f, "STRUCT") } } + // ClickHouse + DataType::Nullable(data_type) => { + write!(f, "Nullable({})", data_type) + } + DataType::FixedString(character_length) => { + write!(f, "FixedString({})", character_length) + } + DataType::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) + } + DataType::Tuple(fields) => { + write!(f, "Tuple({})", display_comma_separated(fields)) + } + DataType::Nested(fields) => { + write!(f, "Nested({})", display_comma_separated(fields)) + } DataType::Unspecified => Ok(()), } } @@ -439,6 +589,23 @@ fn format_datetime_precision_and_tz( Ok(()) } +fn format_clickhouse_datetime_precision_and_timezone( + f: &mut fmt::Formatter, + sql_type: &'static str, + len: &u64, + time_zone: &Option, +) -> fmt::Result { + write!(f, "{sql_type}({len}")?; + + if let Some(time_zone) = time_zone { + write!(f, ", '{time_zone}'")?; + } + + write!(f, ")")?; + + Ok(()) +} + /// Timestamp and Time data types information about TimeZone formatting. /// /// This is more related to a display information than real differences between each variant. To @@ -593,4 +760,6 @@ pub enum ArrayElemTypeDef { AngleBracket(Box), /// `INT[]` or `INT[2]` SquareBracket(Box, Option), + /// `Array(Int64)` + Parenthesis(Box), } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e29a8df04..0a0f8dd66 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -273,7 +273,7 @@ impl fmt::Display for Interval { } } -/// A field definition within a struct. +/// A field definition within a struct /// /// [bigquery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/keywords.rs b/src/keywords.rs index 6c6c642c3..1b204a8d5 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -202,7 +202,9 @@ define_keywords!( DATA, DATABASE, DATE, + DATE32, DATETIME, + DATETIME64, DAY, DAYOFWEEK, DAYOFYEAR, @@ -292,7 +294,9 @@ define_keywords!( FILTER, FIRST, FIRST_VALUE, + FIXEDSTRING, FLOAT, + FLOAT32, FLOAT4, FLOAT64, FLOAT8, @@ -362,7 +366,11 @@ define_keywords!( INSERT, INSTALL, INT, + INT128, + INT16, INT2, + INT256, + INT32, INT4, INT64, INT8, @@ -411,6 +419,7 @@ define_keywords!( LOCKED, LOGIN, LOGS, + LOWCARDINALITY, LOWER, LOW_PRIORITY, MACRO, @@ -455,6 +464,7 @@ define_keywords!( NATURAL, NCHAR, NCLOB, + NESTED, NEW, NEXT, NO, @@ -475,6 +485,7 @@ define_keywords!( NTH_VALUE, NTILE, NULL, + NULLABLE, NULLIF, NULLS, NUMERIC, @@ -713,8 +724,15 @@ define_keywords!( TRUE, TRUNCATE, TRY_CAST, + TUPLE, TYPE, UESCAPE, + UINT128, + UINT16, + UINT256, + UINT32, + UINT64, + UINT8, UNBOUNDED, UNCACHE, UNCOMMITTED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e0a5b86ab..c0a00c9fe 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2099,7 +2099,7 @@ impl<'a> Parser<'a> { /// ``` fn parse_bigquery_struct_literal(&mut self) -> Result { let (fields, trailing_bracket) = - self.parse_struct_type_def(Self::parse_big_query_struct_field_def)?; + self.parse_struct_type_def(Self::parse_struct_field_def)?; if trailing_bracket.0 { return parser_err!("unmatched > in STRUCT literal", self.peek_token().location); } @@ -2194,13 +2194,16 @@ impl<'a> Parser<'a> { )) } - /// Parse a field definition in a BigQuery struct. + /// Parse a field definition in a struct [1] or tuple [2]. /// Syntax: /// /// ```sql /// [field_name] field_type /// ``` - fn parse_big_query_struct_field_def( + /// + /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#declaring_a_struct_type + /// [2]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple + fn parse_struct_field_def( &mut self, ) -> Result<(StructField, MatchedTrailingBracket), ParserError> { // Look beyond the next item to infer whether both field name @@ -2266,6 +2269,47 @@ impl<'a> Parser<'a> { }) } + /// Parse clickhouse map [1] + /// Syntax + /// ```sql + /// Map(key_data_type, value_data_type) + /// ``` + /// + /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/map + fn parse_click_house_map_def(&mut self) -> Result<(DataType, DataType), ParserError> { + self.expect_keyword(Keyword::MAP)?; + self.expect_token(&Token::LParen)?; + let key_data_type = self.parse_data_type()?; + self.expect_token(&Token::Comma)?; + let value_data_type = self.parse_data_type()?; + self.expect_token(&Token::RParen)?; + + Ok((key_data_type, value_data_type)) + } + + /// Parse clickhouse tuple [1] + /// Syntax + /// ```sql + /// Tuple([field_name] field_type, ...) + /// ``` + /// + /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple + fn parse_click_house_tuple_def(&mut self) -> Result, ParserError> { + self.expect_keyword(Keyword::TUPLE)?; + self.expect_token(&Token::LParen)?; + let mut field_defs = vec![]; + loop { + let (def, _) = self.parse_struct_field_def()?; + field_defs.push(def); + if !self.consume_token(&Token::Comma) { + break; + } + } + self.expect_token(&Token::RParen)?; + + Ok(field_defs) + } + /// For nested types that use the angle bracket syntax, this matches either /// `>`, `>>` or nothing depending on which variant is expected (specified by the previously /// matched `trailing_bracket` argument). It returns whether there is a trailing @@ -6820,6 +6864,7 @@ impl<'a> Parser<'a> { Keyword::FLOAT => Ok(DataType::Float(self.parse_optional_precision()?)), Keyword::REAL => Ok(DataType::Real), Keyword::FLOAT4 => Ok(DataType::Float4), + Keyword::FLOAT32 => Ok(DataType::Float32), Keyword::FLOAT64 => Ok(DataType::Float64), Keyword::FLOAT8 => Ok(DataType::Float8), Keyword::DOUBLE => { @@ -6877,7 +6922,19 @@ impl<'a> Parser<'a> { Ok(DataType::Int4(optional_precision?)) } } + Keyword::INT8 => { + let optional_precision = self.parse_optional_precision(); + if self.parse_keyword(Keyword::UNSIGNED) { + Ok(DataType::UnsignedInt8(optional_precision?)) + } else { + Ok(DataType::Int8(optional_precision?)) + } + } + Keyword::INT16 => Ok(DataType::Int16), + Keyword::INT32 => Ok(DataType::Int32), Keyword::INT64 => Ok(DataType::Int64), + Keyword::INT128 => Ok(DataType::Int128), + Keyword::INT256 => Ok(DataType::Int256), Keyword::INTEGER => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { @@ -6894,14 +6951,12 @@ impl<'a> Parser<'a> { Ok(DataType::BigInt(optional_precision?)) } } - Keyword::INT8 => { - let optional_precision = self.parse_optional_precision(); - if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt8(optional_precision?)) - } else { - Ok(DataType::Int8(optional_precision?)) - } - } + Keyword::UINT8 => Ok(DataType::UInt8), + Keyword::UINT16 => Ok(DataType::UInt16), + Keyword::UINT32 => Ok(DataType::UInt32), + Keyword::UINT64 => Ok(DataType::UInt64), + Keyword::UINT128 => Ok(DataType::UInt128), + Keyword::UINT256 => Ok(DataType::UInt256), Keyword::VARCHAR => Ok(DataType::Varchar(self.parse_optional_character_length()?)), Keyword::NVARCHAR => { Ok(DataType::Nvarchar(self.parse_optional_character_length()?)) @@ -6937,7 +6992,13 @@ impl<'a> Parser<'a> { Keyword::BYTES => Ok(DataType::Bytes(self.parse_optional_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), + Keyword::DATE32 => Ok(DataType::Date32), Keyword::DATETIME => Ok(DataType::Datetime(self.parse_optional_precision()?)), + Keyword::DATETIME64 => { + self.prev_token(); + let (precision, time_zone) = self.parse_datetime_64()?; + Ok(DataType::Datetime64(precision, time_zone)) + } Keyword::TIMESTAMP => { let precision = self.parse_optional_precision()?; let tz = if self.parse_keyword(Keyword::WITH) { @@ -6980,6 +7041,12 @@ impl<'a> Parser<'a> { Keyword::JSONB => Ok(DataType::JSONB), Keyword::REGCLASS => Ok(DataType::Regclass), Keyword::STRING => Ok(DataType::String(self.parse_optional_precision()?)), + Keyword::FIXEDSTRING => { + self.expect_token(&Token::LParen)?; + let character_length = self.parse_literal_uint()?; + self.expect_token(&Token::RParen)?; + Ok(DataType::FixedString(character_length)) + } Keyword::TEXT => Ok(DataType::Text), Keyword::BYTEA => Ok(DataType::Bytea), Keyword::NUMERIC => Ok(DataType::Numeric( @@ -7002,6 +7069,10 @@ impl<'a> Parser<'a> { Keyword::ARRAY => { if dialect_of!(self is SnowflakeDialect) { Ok(DataType::Array(ArrayElemTypeDef::None)) + } else if dialect_of!(self is ClickHouseDialect) { + Ok(self.parse_sub_type(|internal_type| { + DataType::Array(ArrayElemTypeDef::Parenthesis(internal_type)) + })?) } else { self.expect_token(&Token::Lt)?; let (inside_type, _trailing_bracket) = self.parse_data_type_helper()?; @@ -7014,10 +7085,35 @@ impl<'a> Parser<'a> { Keyword::STRUCT if dialect_of!(self is BigQueryDialect | GenericDialect) => { self.prev_token(); let (field_defs, _trailing_bracket) = - self.parse_struct_type_def(Self::parse_big_query_struct_field_def)?; + self.parse_struct_type_def(Self::parse_struct_field_def)?; trailing_bracket = _trailing_bracket; Ok(DataType::Struct(field_defs)) } + Keyword::NULLABLE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + Ok(self.parse_sub_type(DataType::Nullable)?) + } + Keyword::LOWCARDINALITY if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + Ok(self.parse_sub_type(DataType::LowCardinality)?) + } + Keyword::MAP if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + self.prev_token(); + let (key_data_type, value_data_type) = self.parse_click_house_map_def()?; + Ok(DataType::Map( + Box::new(key_data_type), + Box::new(value_data_type), + )) + } + Keyword::NESTED if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + self.expect_token(&Token::LParen)?; + let field_defs = self.parse_comma_separated(Parser::parse_column_def)?; + self.expect_token(&Token::RParen)?; + Ok(DataType::Nested(field_defs)) + } + Keyword::TUPLE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + self.prev_token(); + let field_defs = self.parse_click_house_tuple_def()?; + Ok(DataType::Tuple(field_defs)) + } _ => { self.prev_token(); let type_name = self.parse_object_name(false)?; @@ -7416,6 +7512,26 @@ impl<'a> Parser<'a> { } } + /// Parse datetime64 [1] + /// Syntax + /// ```sql + /// DateTime64(precision[, timezone]) + /// ``` + /// + /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/datetime64 + pub fn parse_datetime_64(&mut self) -> Result<(u64, Option), ParserError> { + self.expect_keyword(Keyword::DATETIME64)?; + self.expect_token(&Token::LParen)?; + let precision = self.parse_literal_uint()?; + let time_zone = if self.consume_token(&Token::Comma) { + Some(self.parse_literal_string()?) + } else { + None + }; + self.expect_token(&Token::RParen)?; + Ok((precision, time_zone)) + } + pub fn parse_optional_character_length( &mut self, ) -> Result, ParserError> { @@ -7508,6 +7624,17 @@ impl<'a> Parser<'a> { } } + /// Parse a parenthesized sub data type + fn parse_sub_type(&mut self, parent_type: F) -> Result + where + F: FnOnce(Box) -> DataType, + { + self.expect_token(&Token::LParen)?; + let inside_type = self.parse_data_type()?; + self.expect_token(&Token::RParen)?; + Ok(parent_type(inside_type.into())) + } + 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. diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index a693936bc..20c3d0569 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -220,6 +220,196 @@ fn parse_create_table() { ); } +fn column_def(name: Ident, data_type: DataType) -> ColumnDef { + ColumnDef { + name, + data_type, + collation: None, + options: vec![], + } +} + +#[test] +fn parse_clickhouse_data_types() { + let sql = concat!( + "CREATE TABLE table (", + "a1 UInt8, a2 UInt16, a3 UInt32, a4 UInt64, a5 UInt128, a6 UInt256,", + " b1 Int8, b2 Int16, b3 Int32, b4 Int64, b5 Int128, b6 Int256,", + " c1 Float32, c2 Float64,", + " d1 Date32, d2 DateTime64(3), d3 DateTime64(3, 'UTC'),", + " e1 FixedString(255),", + " f1 LowCardinality(Int32)", + ") ORDER BY (a1)", + ); + // ClickHouse has a case-sensitive definition of data type, but canonical representation is not + let canonical_sql = sql + .replace(" Int8", " INT8") + .replace(" Int64", " INT64") + .replace(" Float64", " FLOAT64"); + + match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) { + Statement::CreateTable { name, columns, .. } => { + assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!( + columns, + vec![ + column_def("a1".into(), DataType::UInt8), + column_def("a2".into(), DataType::UInt16), + column_def("a3".into(), DataType::UInt32), + column_def("a4".into(), DataType::UInt64), + column_def("a5".into(), DataType::UInt128), + column_def("a6".into(), DataType::UInt256), + column_def("b1".into(), DataType::Int8(None)), + column_def("b2".into(), DataType::Int16), + column_def("b3".into(), DataType::Int32), + column_def("b4".into(), DataType::Int64), + column_def("b5".into(), DataType::Int128), + column_def("b6".into(), DataType::Int256), + column_def("c1".into(), DataType::Float32), + column_def("c2".into(), DataType::Float64), + column_def("d1".into(), DataType::Date32), + column_def("d2".into(), DataType::Datetime64(3, None)), + column_def("d3".into(), DataType::Datetime64(3, Some("UTC".into()))), + column_def("e1".into(), DataType::FixedString(255)), + column_def( + "f1".into(), + DataType::LowCardinality(Box::new(DataType::Int32)) + ), + ] + ); + } + _ => unreachable!(), + } +} + +#[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`)"#; + // ClickHouse has a case-sensitive definition of data type, but canonical representation is not + let canonical_sql = sql.replace("String", "STRING"); + + match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) { + Statement::CreateTable { name, columns, .. } => { + assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!( + columns, + vec![ + column_def("k".into(), DataType::UInt8), + column_def( + Ident::with_quote('`', "a"), + DataType::Nullable(Box::new(DataType::String(None))) + ), + column_def( + Ident::with_quote('`', "b"), + DataType::Nullable(Box::new(DataType::Datetime64( + 9, + Some("UTC".to_string()) + ))) + ), + column_def( + "c".into(), + DataType::Nullable(Box::new(DataType::Datetime64(9, None))) + ), + ColumnDef { + name: "d".into(), + data_type: DataType::Date32, + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Null + }], + } + ] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_table_with_nested_data_types() { + let sql = concat!( + "CREATE TABLE table (", + " i Nested(a Array(Int16), b LowCardinality(String)),", + " k Array(Tuple(FixedString(128), Int128)),", + " l Tuple(a DateTime64(9), b Array(UUID)),", + " m Map(String, UInt16)", + ") ENGINE=MergeTree ORDER BY (k)" + ); + + match clickhouse().one_statement_parses_to(sql, "") { + Statement::CreateTable { name, columns, .. } => { + assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!( + columns, + vec![ + ColumnDef { + name: Ident::new("i"), + data_type: DataType::Nested(vec![ + column_def( + "a".into(), + DataType::Array(ArrayElemTypeDef::Parenthesis(Box::new( + DataType::Int16 + ),)) + ), + column_def( + "b".into(), + DataType::LowCardinality(Box::new(DataType::String(None))) + ) + ]), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident::new("k"), + data_type: DataType::Array(ArrayElemTypeDef::Parenthesis(Box::new( + DataType::Tuple(vec![ + StructField { + field_name: None, + field_type: DataType::FixedString(128) + }, + StructField { + field_name: None, + field_type: DataType::Int128 + } + ]) + ))), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident::new("l"), + data_type: DataType::Tuple(vec![ + StructField { + field_name: Some("a".into()), + field_type: DataType::Datetime64(9, None), + }, + StructField { + field_name: Some("b".into()), + field_type: DataType::Array(ArrayElemTypeDef::Parenthesis( + Box::new(DataType::Uuid) + )) + }, + ]), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident::new("m"), + data_type: DataType::Map( + Box::new(DataType::String(None)), + Box::new(DataType::UInt16) + ), + collation: None, + options: vec![], + }, + ] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_view_with_fields_data_types() { match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) { From 4b60866bc7ae0c2cd44c4d35ca6cd2e625c1cd79 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Fri, 7 Jun 2024 13:12:18 +0200 Subject: [PATCH 464/806] add support for custom operators in postgres (#1302) Co-authored-by: Joey Hain --- src/ast/operator.rs | 2 +- src/dialect/mod.rs | 6 +++ src/dialect/postgresql.rs | 27 ++++++++++ src/parser/mod.rs | 9 ++-- src/tokenizer.rs | 103 +++++++++++++++++++++++++----------- tests/sqlparser_mssql.rs | 6 +++ tests/sqlparser_postgres.rs | 96 +++++++++++++++++++++++++++++---- 7 files changed, 203 insertions(+), 46 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 3c4f192e3..e70df344a 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -111,7 +111,7 @@ pub enum BinaryOperator { DuckIntegerDivide, /// MySQL [`DIV`](https://dev.mysql.com/doc/refman/8.0/en/arithmetic-functions.html) integer division MyIntegerDivide, - /// Support for custom operators (built by parsers outside this crate) + /// Support for custom operators (such as Postgres custom operators) Custom(String), /// Bitwise XOR, e.g. `a # b` (PostgreSQL-specific) PGBitwiseXor, diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index e06c07a1c..b223ead47 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -122,6 +122,12 @@ pub trait Dialect: Debug + Any { fn is_identifier_start(&self, ch: char) -> bool; /// Determine if a character is a valid unquoted identifier character fn is_identifier_part(&self, ch: char) -> bool; + + /// Most dialects do not have custom operators. Override this method to provide custom operators. + fn is_custom_operator_part(&self, _ch: char) -> bool { + false + } + /// Determine if the dialect supports escaping characters via '\' in string literals. /// /// Some dialects like BigQuery and Snowflake support this while others like diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index f179111e0..0e04bfa27 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -25,6 +25,10 @@ impl Dialect for PostgreSqlDialect { Some('"') } + fn is_delimited_identifier_start(&self, ch: char) -> bool { + ch == '"' // Postgres does not support backticks to quote identifiers + } + fn is_identifier_start(&self, ch: char) -> bool { // See https://www.postgresql.org/docs/11/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS // We don't yet support identifiers beginning with "letters with @@ -36,6 +40,29 @@ impl Dialect for PostgreSqlDialect { ch.is_alphabetic() || ch.is_ascii_digit() || ch == '$' || ch == '_' } + /// See + fn is_custom_operator_part(&self, ch: char) -> bool { + matches!( + ch, + '+' | '-' + | '*' + | '/' + | '<' + | '>' + | '=' + | '~' + | '!' + | '@' + | '#' + | '%' + | '^' + | '&' + | '|' + | '`' + | '?' + ) + } + fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.parse_keyword(Keyword::COMMENT) { Some(parse_comment(parser)) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c0a00c9fe..7aaef555e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2344,9 +2344,8 @@ impl<'a> Parser<'a> { return infix; } - let tok = self.next_token(); - - let regular_binary_operator = match &tok.token { + let mut tok = self.next_token(); + let regular_binary_operator = match &mut tok.token { Token::Spaceship => Some(BinaryOperator::Spaceship), Token::DoubleEq => Some(BinaryOperator::Eq), Token::Eq => Some(BinaryOperator::Eq), @@ -2410,6 +2409,7 @@ impl<'a> Parser<'a> { Token::Question => Some(BinaryOperator::Question), Token::QuestionAnd => Some(BinaryOperator::QuestionAnd), Token::QuestionPipe => Some(BinaryOperator::QuestionPipe), + Token::CustomBinaryOperator(s) => Some(BinaryOperator::Custom(core::mem::take(s))), Token::Word(w) => match w.keyword { Keyword::AND => Some(BinaryOperator::And), @@ -2964,7 +2964,8 @@ impl<'a> Parser<'a> { | Token::AtAt | Token::Question | Token::QuestionAnd - | Token::QuestionPipe => Ok(Self::PG_OTHER_PREC), + | Token::QuestionPipe + | Token::CustomBinaryOperator(_) => Ok(Self::PG_OTHER_PREC), _ => Ok(0), } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b6fed354d..bcc5478bc 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -231,6 +231,10 @@ pub enum Token { /// jsonb ?| text[] -> boolean: Check whether any member of the text array exists as top-level /// keys within the jsonb object QuestionPipe, + /// Custom binary operator + /// This is used to represent any custom binary operator that is not part of the SQL standard. + /// PostgreSQL allows defining custom binary operators using CREATE OPERATOR. + CustomBinaryOperator(String), } impl fmt::Display for Token { @@ -320,6 +324,7 @@ impl fmt::Display for Token { Token::Question => write!(f, "?"), Token::QuestionAnd => write!(f, "?&"), Token::QuestionPipe => write!(f, "?|"), + Token::CustomBinaryOperator(s) => f.write_str(s), } } } @@ -961,15 +966,12 @@ impl<'a> Tokenizer<'a> { Some('>') => { chars.next(); match chars.peek() { - Some('>') => { - chars.next(); - Ok(Some(Token::LongArrow)) - } - _ => Ok(Some(Token::Arrow)), + Some('>') => self.consume_for_binop(chars, "->>", Token::LongArrow), + _ => self.start_binop(chars, "->", Token::Arrow), } } // a regular '-' operator - _ => Ok(Some(Token::Minus)), + _ => self.start_binop(chars, "-", Token::Minus), } } '/' => { @@ -999,26 +1001,28 @@ impl<'a> Tokenizer<'a> { '%' => { chars.next(); // advance past '%' match chars.peek() { - Some(' ') => Ok(Some(Token::Mod)), + Some(s) if s.is_whitespace() => Ok(Some(Token::Mod)), Some(sch) if self.dialect.is_identifier_start('%') => { self.tokenize_identifier_or_keyword([ch, *sch], chars) } - _ => Ok(Some(Token::Mod)), + _ => self.start_binop(chars, "%", Token::Mod), } } '|' => { chars.next(); // consume the '|' match chars.peek() { - Some('/') => self.consume_and_return(chars, Token::PGSquareRoot), + Some('/') => self.consume_for_binop(chars, "|/", Token::PGSquareRoot), Some('|') => { chars.next(); // consume the second '|' match chars.peek() { - Some('/') => self.consume_and_return(chars, Token::PGCubeRoot), - _ => Ok(Some(Token::StringConcat)), + Some('/') => { + self.consume_for_binop(chars, "||/", Token::PGCubeRoot) + } + _ => self.start_binop(chars, "||", Token::StringConcat), } } // Bitshift '|' operator - _ => Ok(Some(Token::Pipe)), + _ => self.start_binop(chars, "|", Token::Pipe), } } '=' => { @@ -1061,22 +1065,22 @@ impl<'a> Tokenizer<'a> { Some('=') => { chars.next(); match chars.peek() { - Some('>') => self.consume_and_return(chars, Token::Spaceship), - _ => Ok(Some(Token::LtEq)), + Some('>') => self.consume_for_binop(chars, "<=>", Token::Spaceship), + _ => self.start_binop(chars, "<=", Token::LtEq), } } - Some('>') => self.consume_and_return(chars, Token::Neq), - Some('<') => self.consume_and_return(chars, Token::ShiftLeft), - Some('@') => self.consume_and_return(chars, Token::ArrowAt), - _ => Ok(Some(Token::Lt)), + Some('>') => self.consume_for_binop(chars, "<>", Token::Neq), + Some('<') => self.consume_for_binop(chars, "<<", Token::ShiftLeft), + Some('@') => self.consume_for_binop(chars, "<@", Token::ArrowAt), + _ => self.start_binop(chars, "<", Token::Lt), } } '>' => { chars.next(); // consume match chars.peek() { - Some('=') => self.consume_and_return(chars, Token::GtEq), - Some('>') => self.consume_and_return(chars, Token::ShiftRight), - _ => Ok(Some(Token::Gt)), + Some('=') => self.consume_for_binop(chars, ">=", Token::GtEq), + Some('>') => self.consume_for_binop(chars, ">>", Token::ShiftRight), + _ => self.start_binop(chars, ">", Token::Gt), } } ':' => { @@ -1094,9 +1098,12 @@ impl<'a> Tokenizer<'a> { '&' => { chars.next(); // consume the '&' match chars.peek() { - Some('&') => self.consume_and_return(chars, Token::Overlap), + Some('&') => { + chars.next(); // consume the second '&' + self.start_binop(chars, "&&", Token::Overlap) + } // Bitshift '&' operator - _ => Ok(Some(Token::Ampersand)), + _ => self.start_binop(chars, "&", Token::Ampersand), } } '^' => { @@ -1119,38 +1126,37 @@ impl<'a> Tokenizer<'a> { '~' => { chars.next(); // consume match chars.peek() { - Some('*') => self.consume_and_return(chars, Token::TildeAsterisk), + Some('*') => self.consume_for_binop(chars, "~*", Token::TildeAsterisk), Some('~') => { chars.next(); match chars.peek() { Some('*') => { - self.consume_and_return(chars, Token::DoubleTildeAsterisk) + self.consume_for_binop(chars, "~~*", Token::DoubleTildeAsterisk) } - _ => Ok(Some(Token::DoubleTilde)), + _ => self.start_binop(chars, "~~", Token::DoubleTilde), } } - _ => Ok(Some(Token::Tilde)), + _ => self.start_binop(chars, "~", Token::Tilde), } } '#' => { chars.next(); match chars.peek() { - Some('-') => self.consume_and_return(chars, Token::HashMinus), + Some('-') => self.consume_for_binop(chars, "#-", Token::HashMinus), Some('>') => { chars.next(); match chars.peek() { Some('>') => { - chars.next(); - Ok(Some(Token::HashLongArrow)) + self.consume_for_binop(chars, "#>>", Token::HashLongArrow) } - _ => Ok(Some(Token::HashArrow)), + _ => self.start_binop(chars, "#>", Token::HashArrow), } } Some(' ') => Ok(Some(Token::Sharp)), Some(sch) if self.dialect.is_identifier_start('#') => { self.tokenize_identifier_or_keyword([ch, *sch], chars) } - _ => Ok(Some(Token::Sharp)), + _ => self.start_binop(chars, "#", Token::Sharp), } } '@' => { @@ -1206,6 +1212,39 @@ impl<'a> Tokenizer<'a> { } } + /// Consume the next character, then parse a custom binary operator. The next character should be included in the prefix + fn consume_for_binop( + &self, + chars: &mut State, + prefix: &str, + default: Token, + ) -> Result, TokenizerError> { + chars.next(); // consume the first char + self.start_binop(chars, prefix, default) + } + + /// parse a custom binary operator + fn start_binop( + &self, + chars: &mut State, + prefix: &str, + default: Token, + ) -> Result, TokenizerError> { + let mut custom = None; + while let Some(&ch) = chars.peek() { + if !self.dialect.is_custom_operator_part(ch) { + break; + } + + custom.get_or_insert_with(|| prefix.to_string()).push(ch); + chars.next(); + } + + Ok(Some( + custom.map(Token::CustomBinaryOperator).unwrap_or(default), + )) + } + /// Tokenize dollar preceded value (i.e: a string/placeholder) fn tokenize_dollar_preceded_value(&self, chars: &mut State) -> Result { let mut s = String::new(); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 5d61c6ab9..86d3990f6 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -437,6 +437,12 @@ fn parse_for_json_expect_ast() { ); } +#[test] +fn parse_ampersand_arobase() { + // In SQL Server, a&@b means (a) & (@b), in PostgreSQL it means (a) &@ (b) + ms().expr_parses_to("a&@b", "a & @b"); +} + #[test] fn parse_cast_varchar_max() { ms_and_generic().verified_expr("CAST('foo' AS VARCHAR(MAX))"); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1df94b100..93b3c044a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1757,6 +1757,29 @@ fn parse_pg_returning() { }; } +fn test_operator(operator: &str, dialect: &TestedDialects, expected: BinaryOperator) { + let operator_tokens = + sqlparser::tokenizer::Tokenizer::new(&PostgreSqlDialect {}, &format!("a{operator}b")) + .tokenize() + .unwrap(); + assert_eq!( + operator_tokens.len(), + 3, + "binary op should be 3 tokens, not {operator_tokens:?}" + ); + let expected_expr = Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: expected, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }; + let str_expr_canonical = format!("a {operator} b"); + assert_eq!(expected_expr, dialect.verified_expr(&str_expr_canonical)); + assert_eq!( + expected_expr, + dialect.expr_parses_to(&format!("a{operator}b"), &str_expr_canonical) + ); +} + #[test] fn parse_pg_binary_ops() { let binary_ops = &[ @@ -1770,18 +1793,73 @@ fn parse_pg_binary_ops() { ]; for (str_op, op, dialects) in binary_ops { - let select = dialects.verified_only_select(&format!("SELECT a {} b", &str_op)); - assert_eq!( - SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("a"))), - op: op.clone(), - right: Box::new(Expr::Identifier(Ident::new("b"))), - }), - select.projection[0] - ); + test_operator(str_op, dialects, op.clone()); + } +} + +#[test] +fn parse_pg_custom_binary_ops() { + // Postgres supports declaring custom binary operators, using any character in the following set: + // + - * / < > = ~ ! @ # % ^ & | ` ? + + // Here, we test the ones used by common extensions + let operators = [ + // PostGIS + "&&&", // n-D bounding boxes intersect + "&<", // (is strictly to the left of) + "&>", // (is strictly to the right of) + "|=|", // distance between A and B trajectories at their closest point of approach + "<<#>>", // n-D distance between A and B bounding boxes + "|>>", // A's bounding box is strictly above B's. + "~=", // bounding box is the same + // PGroonga + "&@", // Full text search by a keyword + "&@~", // Full text search by easy to use query language + "&@*", // Similar search + "&`", // Advanced search by ECMAScript like query language + "&@|", // Full text search by an array of keywords + "&@~|", // Full text search by an array of queries in easy to use query language + // pgtrgm + "<<%", // second argument has a continuous extent of an ordered trigram set that matches word boundaries + "%>>", // commutator of <<% + "<<<->", // distance between arguments + // hstore + "#=", // Replace fields with matching values from hstore + // ranges + "-|-", // Is adjacent to + // pg_similarity + "~++", // L1 distance + "~##", // Cosine Distance + "~-~", // Dice Coefficient + "~!!", // Euclidean Distance + "~@~", // Hamming Distance + "~??", // Jaccard Coefficient + "~%%", // Jaro Distance + "~@@", // Jaro-Winkler Distance + "~==", // Levenshtein Distance + "~^^", // Matching Coefficient + "~||", // Monge-Elkan Coefficient + "~#~", // Needleman-Wunsch Coefficient + "~**", // Overlap Coefficient + "~~~", // Q-Gram Distance + "~=~", // Smith-Waterman Coefficient + "~!~", // Smith-Waterman-Gotoh Coefficient + "~*~", // Soundex Distance + // soundex_operator + ">@@<", // Soundex matches + "<@@>", // Soundex doesn't match + ]; + for op in &operators { + test_operator(op, &pg(), BinaryOperator::Custom(op.to_string())); } } +#[test] +fn parse_ampersand_arobase() { + // In SQL Server, a&@b means (a) & (@b), in PostgreSQL it means (a) &@ (b) + pg().expr_parses_to("a&@b", "a &@ b"); +} + #[test] fn parse_pg_unary_ops() { let pg_unary_ops = &[ From 3c33ac15bd9a33ff819d42bbeb2994049dd03fdf Mon Sep 17 00:00:00 2001 From: Aleksei Piianin Date: Fri, 7 Jun 2024 14:19:32 +0200 Subject: [PATCH 465/806] ClickHouse: support of create table query with primary key and parametrised table engine (#1289) --- src/ast/dml.rs | 16 +++-- src/ast/helpers/stmt_create_table.rs | 20 ++++-- src/ast/mod.rs | 23 +++++++ src/parser/mod.rs | 31 +++++++-- tests/sqlparser_clickhouse.rs | 95 +++++++++++++++++++++++++--- tests/sqlparser_mysql.rs | 8 ++- 6 files changed, 168 insertions(+), 25 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 91232218f..7238785ca 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -24,8 +24,8 @@ pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ display_comma_separated, display_separated, Expr, FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, - OnCommit, OnInsert, OrderByExpr, Query, SelectItem, SqlOption, SqliteOnConflict, - TableWithJoins, + OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, SelectItem, SqlOption, + SqliteOnConflict, TableEngine, TableWithJoins, }; /// CREATE INDEX statement. @@ -73,7 +73,7 @@ pub struct CreateTable { pub without_rowid: bool, pub like: Option, pub clone: Option, - pub engine: Option, + pub engine: Option, pub comment: Option, pub auto_increment_offset: Option, pub default_charset: Option, @@ -82,10 +82,13 @@ pub struct CreateTable { /// ClickHouse "ON CLUSTER" clause: /// pub on_cluster: Option, + /// ClickHouse "PRIMARY KEY " clause. + /// + pub primary_key: Option>, /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different /// than empty (represented as ()), the latter meaning "no sorting". /// - pub order_by: Option>, + pub order_by: Option>, /// BigQuery: A partition expression for the table. /// pub partition_by: Option>, @@ -263,8 +266,11 @@ impl Display for CreateTable { 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)?; + } if let Some(order_by) = &self.order_by { - write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; + write!(f, " ORDER BY {}", order_by)?; } 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 c50e7bbd9..b2b3f5688 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -10,7 +10,7 @@ use sqlparser_derive::{Visit, VisitMut}; use super::super::dml::CreateTable; use crate::ast::{ ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, - Query, SqlOption, Statement, TableConstraint, + OneOrManyWithParens, Query, SqlOption, Statement, TableConstraint, TableEngine, }; use crate::parser::ParserError; @@ -65,14 +65,15 @@ pub struct CreateTableBuilder { pub without_rowid: bool, pub like: Option, pub clone: Option, - pub engine: 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 order_by: Option>, + pub primary_key: Option>, + pub order_by: Option>, pub partition_by: Option>, pub cluster_by: Option>, pub options: Option>, @@ -108,6 +109,7 @@ impl CreateTableBuilder { collation: None, on_commit: None, on_cluster: None, + primary_key: None, order_by: None, partition_by: None, cluster_by: None, @@ -203,7 +205,7 @@ impl CreateTableBuilder { self } - pub fn engine(mut self, engine: Option) -> Self { + pub fn engine(mut self, engine: Option) -> Self { self.engine = engine; self } @@ -238,7 +240,12 @@ impl CreateTableBuilder { self } - pub fn order_by(mut self, order_by: Option>) -> Self { + pub fn primary_key(mut self, primary_key: Option>) -> Self { + self.primary_key = primary_key; + self + } + + pub fn order_by(mut self, order_by: Option>) -> Self { self.order_by = order_by; self } @@ -291,6 +298,7 @@ impl CreateTableBuilder { collation: self.collation, on_commit: self.on_commit, on_cluster: self.on_cluster, + primary_key: self.primary_key, order_by: self.order_by, partition_by: self.partition_by, cluster_by: self.cluster_by, @@ -334,6 +342,7 @@ impl TryFrom for CreateTableBuilder { collation, on_commit, on_cluster, + primary_key, order_by, partition_by, cluster_by, @@ -366,6 +375,7 @@ impl TryFrom for CreateTableBuilder { collation, on_commit, on_cluster, + primary_key, order_by, partition_by, cluster_by, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0a0f8dd66..1747d677e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6315,6 +6315,29 @@ impl Display for MySQLColumnPosition { } } +/// 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(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7aaef555e..6406bd4e5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5262,7 +5262,15 @@ impl<'a> Parser<'a> { self.expect_token(&Token::Eq)?; let next_token = self.next_token(); match next_token.token { - Token::Word(w) => Some(w.value), + 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 { @@ -5280,17 +5288,27 @@ impl<'a> Parser<'a> { None }; + // ClickHouse supports `PRIMARY KEY`, before `ORDER BY` + // https://clickhouse.com/docs/en/sql-reference/statements/create/table#primary-key + let primary_key = if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) + { + Some(Box::new(self.parse_expr()?)) + } else { + None + }; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { if self.consume_token(&Token::LParen) { let columns = if self.peek_token() != Token::RParen { - self.parse_comma_separated(|p| p.parse_identifier(false))? + self.parse_comma_separated(|p| p.parse_expr())? } else { vec![] }; self.expect_token(&Token::RParen)?; - Some(columns) + Some(OneOrManyWithParens::Many(columns)) } else { - Some(vec![self.parse_identifier(false)?]) + Some(OneOrManyWithParens::One(self.parse_expr()?)) } } else { None @@ -5388,6 +5406,7 @@ impl<'a> Parser<'a> { .partition_by(big_query_config.partition_by) .cluster_by(big_query_config.cluster_by) .options(big_query_config.options) + .primary_key(primary_key) .strict(strict) .build()) } @@ -9041,7 +9060,7 @@ impl<'a> Parser<'a> { let partitions: Vec = if dialect_of!(self is MySqlDialect | GenericDialect) && self.parse_keyword(Keyword::PARTITION) { - self.parse_partitions()? + self.parse_parenthesized_identifiers()? } else { vec![] }; @@ -10969,7 +10988,7 @@ impl<'a> Parser<'a> { }) } - fn parse_partitions(&mut self) -> Result, ParserError> { + fn parse_parenthesized_identifiers(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let partitions = self.parse_comma_separated(|p| p.parse_identifier(false))?; self.expect_token(&Token::RParen)?; diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 20c3d0569..ed3b2de22 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -211,12 +211,9 @@ fn parse_delimited_identifiers() { #[test] fn parse_create_table() { clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#); - clickhouse().one_statement_parses_to( - r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY "x""#, - 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"#, ); } @@ -248,7 +245,7 @@ fn parse_clickhouse_data_types() { .replace(" Float64", " FLOAT64"); match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name, ObjectName(vec!["table".into()])); assert_eq!( columns, @@ -289,7 +286,7 @@ fn parse_create_table_with_nullable() { let canonical_sql = sql.replace("String", "STRING"); match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name, ObjectName(vec!["table".into()])); assert_eq!( columns, @@ -338,7 +335,7 @@ fn parse_create_table_with_nested_data_types() { ); match clickhouse().one_statement_parses_to(sql, "") { - Statement::CreateTable { name, columns, .. } => { + Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name, ObjectName(vec!["table".into()])); assert_eq!( columns, @@ -410,6 +407,88 @@ fn parse_create_table_with_nested_data_types() { } } +#[test] +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}')", + " PRIMARY KEY tuple(i)", + " ORDER BY tuple(i)", + )) { + Statement::CreateTable(CreateTable { + name, + columns, + engine, + primary_key, + order_by, + .. + }) => { + assert_eq!(name.to_string(), "db.table"); + assert_eq!( + vec![ + ColumnDef { + name: Ident::with_quote('`', "i"), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident::with_quote('`', "k"), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + ], + columns + ); + assert_eq!( + engine, + Some(TableEngine { + name: "SharedMergeTree".to_string(), + parameters: Some(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(vec![Ident::new(name)])); + assert_eq!( + actual.args, + FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Identifier( + Ident::new(arg) + )),)], + duplicate_treatment: None, + clauses: vec![], + }) + ); + true + } + match primary_key.unwrap().as_ref() { + Expr::Function(primary_key) => { + assert!(assert_function(primary_key, "tuple", "i")); + } + _ => panic!("unexpected primary key type"), + } + match order_by { + Some(OneOrManyWithParens::One(Expr::Function(order_by))) => { + assert!(assert_function(&order_by, "tuple", "i")); + } + _ => panic!("unexpected order by type"), + }; + } + _ => unreachable!(), + } + + clickhouse_and_generic() + .parse_sql_statements(concat!( + r#"CREATE TABLE db.table (`i` Int, `k` Int)"#, + " ORDER BY tuple(i), tuple(k)", + )) + .expect_err("ORDER BY supports one expression with tuple"); +} + #[test] fn parse_create_view_with_fields_data_types() { match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 3041b6001..e65fc181b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -773,7 +773,13 @@ fn parse_create_table_engine_default_charset() { },], columns ); - assert_eq!(engine, Some("InnoDB".to_string())); + assert_eq!( + engine, + Some(TableEngine { + name: "InnoDB".to_string(), + parameters: None + }) + ); assert_eq!(default_charset, Some("utf8mb3".to_string())); } _ => unreachable!(), From be77ce50ca34958f94bc05d92795b38ca286614a Mon Sep 17 00:00:00 2001 From: Ilson Balliego Date: Sun, 9 Jun 2024 23:47:21 +0200 Subject: [PATCH 466/806] Add support for snowflake exclusive create table options (#1233) Co-authored-by: Ilson Roberto Balliego Junior --- src/ast/dml.rs | 114 ++++++- src/ast/helpers/stmt_create_table.rs | 121 +++++++- src/ast/mod.rs | 111 +++++++ src/dialect/snowflake.rs | 223 +++++++++++++- src/keywords.rs | 10 + src/parser/mod.rs | 8 +- tests/sqlparser_bigquery.rs | 5 +- tests/sqlparser_common.rs | 16 +- tests/sqlparser_postgres.rs | 23 ++ tests/sqlparser_snowflake.rs | 429 +++++++++++++++++++++++++++ 10 files changed, 1029 insertions(+), 31 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 7238785ca..74bb5435c 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -22,10 +22,11 @@ use sqlparser_derive::{Visit, VisitMut}; pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ - display_comma_separated, display_separated, Expr, FileFormat, FromTable, HiveDistributionStyle, - HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, - OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, SelectItem, SqlOption, - SqliteOnConflict, TableEngine, TableWithJoins, + display_comma_separated, display_separated, CommentDef, Expr, FileFormat, FromTable, + HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, + MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, + RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, TableWithJoins, Tag, + WrappedCollection, }; /// CREATE INDEX statement. @@ -57,6 +58,7 @@ pub struct CreateTable { pub global: Option, pub if_not_exists: bool, pub transient: bool, + pub volatile: bool, /// Table name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub name: ObjectName, @@ -74,7 +76,7 @@ pub struct CreateTable { pub like: Option, pub clone: Option, pub engine: Option, - pub comment: Option, + pub comment: Option, pub auto_increment_offset: Option, pub default_charset: Option, pub collation: Option, @@ -94,7 +96,7 @@ pub struct CreateTable { pub partition_by: Option>, /// BigQuery: Table clustering column list. /// - pub cluster_by: Option>, + pub cluster_by: Option>>, /// BigQuery: Table options list. /// pub options: Option>, @@ -102,6 +104,33 @@ pub struct CreateTable { /// if the "STRICT" table-option keyword is added to the end, after the closing ")", /// then strict typing rules apply to that table. pub strict: bool, + /// Snowflake "COPY GRANTS" clause + /// + pub copy_grants: bool, + /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause + /// + pub enable_schema_evolution: Option, + /// Snowflake "CHANGE_TRACKING" clause + /// + pub change_tracking: Option, + /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause + /// + pub data_retention_time_in_days: Option, + /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause + /// + pub max_data_extension_time_in_days: Option, + /// Snowflake "DEFAULT_DDL_COLLATION" clause + /// + pub default_ddl_collation: Option, + /// Snowflake "WITH AGGREGATION POLICY" clause + /// + pub with_aggregation_policy: Option, + /// Snowflake "WITH ROW ACCESS POLICY" clause + /// + pub with_row_access_policy: Option, + /// Snowflake "WITH TAG" clause + /// + pub with_tags: Option>, } impl Display for CreateTable { @@ -115,7 +144,7 @@ impl Display for CreateTable { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {or_replace}{external}{global}{temporary}{transient}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}TABLE {if_not_exists}{name}", or_replace = if self.or_replace { "OR REPLACE " } else { "" }, external = if self.external { "EXTERNAL " } else { "" }, global = self.global @@ -130,6 +159,7 @@ impl Display for CreateTable { if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, temporary = if self.temporary { "TEMPORARY " } else { "" }, transient = if self.transient { "TRANSIENT " } else { "" }, + volatile = if self.volatile { "VOLATILE " } else { "" }, name = self.name, )?; if let Some(on_cluster) = &self.on_cluster { @@ -260,9 +290,17 @@ impl Display for CreateTable { if let Some(engine) = &self.engine { write!(f, " ENGINE={engine}")?; } - if let Some(comment) = &self.comment { - write!(f, " COMMENT '{comment}'")?; + if let Some(comment_def) = &self.comment { + match comment_def { + CommentDef::WithEq(comment) => { + write!(f, " COMMENT = '{comment}'")?; + } + CommentDef::WithoutEq(comment) => { + write!(f, " COMMENT '{comment}'")?; + } + } } + if let Some(auto_increment_offset) = self.auto_increment_offset { write!(f, " AUTO_INCREMENT {auto_increment_offset}")?; } @@ -276,12 +314,9 @@ impl Display for CreateTable { write!(f, " PARTITION BY {partition_by}")?; } if let Some(cluster_by) = self.cluster_by.as_ref() { - write!( - f, - " CLUSTER BY {}", - display_comma_separated(cluster_by.as_slice()) - )?; + write!(f, " CLUSTER BY {cluster_by}")?; } + if let Some(options) = self.options.as_ref() { write!( f, @@ -289,6 +324,57 @@ impl Display for CreateTable { display_comma_separated(options.as_slice()) )?; } + + if self.copy_grants { + write!(f, " COPY GRANTS")?; + } + + if let Some(is_enabled) = self.enable_schema_evolution { + write!( + f, + " ENABLE_SCHEMA_EVOLUTION={}", + if is_enabled { "TRUE" } else { "FALSE" } + )?; + } + + if let Some(is_enabled) = self.change_tracking { + write!( + f, + " CHANGE_TRACKING={}", + if is_enabled { "TRUE" } else { "FALSE" } + )?; + } + + if let Some(data_retention_time_in_days) = self.data_retention_time_in_days { + write!( + f, + " DATA_RETENTION_TIME_IN_DAYS={data_retention_time_in_days}", + )?; + } + + if let Some(max_data_extension_time_in_days) = self.max_data_extension_time_in_days { + write!( + f, + " MAX_DATA_EXTENSION_TIME_IN_DAYS={max_data_extension_time_in_days}", + )?; + } + + if let Some(default_ddl_collation) = &self.default_ddl_collation { + write!(f, " DEFAULT_DDL_COLLATION='{default_ddl_collation}'",)?; + } + + if let Some(with_aggregation_policy) = &self.with_aggregation_policy { + write!(f, " WITH AGGREGATION POLICY {with_aggregation_policy}",)?; + } + + if let Some(row_access_policy) = &self.with_row_access_policy { + write!(f, " {row_access_policy}",)?; + } + + if let Some(tag) = &self.with_tags { + write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; + } + if let Some(query) = &self.query { write!(f, " AS {query}")?; } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index b2b3f5688..d862a36ae 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -9,8 +9,9 @@ use sqlparser_derive::{Visit, VisitMut}; use super::super::dml::CreateTable; use crate::ast::{ - ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, - OneOrManyWithParens, Query, SqlOption, Statement, TableConstraint, TableEngine, + ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, + OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, TableConstraint, + TableEngine, Tag, WrappedCollection, }; use crate::parser::ParserError; @@ -52,6 +53,7 @@ pub struct CreateTableBuilder { pub global: Option, pub if_not_exists: bool, pub transient: bool, + pub volatile: bool, pub name: ObjectName, pub columns: Vec, pub constraints: Vec, @@ -66,7 +68,7 @@ pub struct CreateTableBuilder { pub like: Option, pub clone: Option, pub engine: Option, - pub comment: Option, + pub comment: Option, pub auto_increment_offset: Option, pub default_charset: Option, pub collation: Option, @@ -75,9 +77,18 @@ pub struct CreateTableBuilder { pub primary_key: Option>, pub order_by: Option>, pub partition_by: Option>, - pub cluster_by: Option>, + pub cluster_by: Option>>, pub options: Option>, pub strict: bool, + pub copy_grants: bool, + pub enable_schema_evolution: Option, + pub change_tracking: Option, + pub data_retention_time_in_days: Option, + pub max_data_extension_time_in_days: Option, + pub default_ddl_collation: Option, + pub with_aggregation_policy: Option, + pub with_row_access_policy: Option, + pub with_tags: Option>, } impl CreateTableBuilder { @@ -89,6 +100,7 @@ impl CreateTableBuilder { global: None, if_not_exists: false, transient: false, + volatile: false, name, columns: vec![], constraints: vec![], @@ -115,6 +127,15 @@ impl CreateTableBuilder { cluster_by: None, options: None, strict: false, + copy_grants: false, + enable_schema_evolution: None, + change_tracking: None, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + default_ddl_collation: None, + with_aggregation_policy: None, + with_row_access_policy: None, + with_tags: None, } } pub fn or_replace(mut self, or_replace: bool) -> Self { @@ -147,6 +168,11 @@ impl CreateTableBuilder { self } + pub fn volatile(mut self, volatile: bool) -> Self { + self.volatile = volatile; + self + } + pub fn columns(mut self, columns: Vec) -> Self { self.columns = columns; self @@ -210,7 +236,7 @@ impl CreateTableBuilder { self } - pub fn comment(mut self, comment: Option) -> Self { + pub fn comment(mut self, comment: Option) -> Self { self.comment = comment; self } @@ -255,7 +281,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 } @@ -270,6 +296,57 @@ impl CreateTableBuilder { self } + pub fn copy_grants(mut self, copy_grants: bool) -> Self { + self.copy_grants = copy_grants; + self + } + + pub fn enable_schema_evolution(mut self, enable_schema_evolution: Option) -> Self { + self.enable_schema_evolution = enable_schema_evolution; + self + } + + pub fn change_tracking(mut self, change_tracking: Option) -> Self { + self.change_tracking = change_tracking; + self + } + + pub fn data_retention_time_in_days(mut self, data_retention_time_in_days: Option) -> Self { + self.data_retention_time_in_days = data_retention_time_in_days; + self + } + + pub fn max_data_extension_time_in_days( + mut self, + max_data_extension_time_in_days: Option, + ) -> Self { + self.max_data_extension_time_in_days = max_data_extension_time_in_days; + self + } + + pub fn default_ddl_collation(mut self, default_ddl_collation: Option) -> Self { + self.default_ddl_collation = default_ddl_collation; + self + } + + pub fn with_aggregation_policy(mut self, with_aggregation_policy: Option) -> Self { + self.with_aggregation_policy = with_aggregation_policy; + self + } + + pub fn with_row_access_policy( + mut self, + with_row_access_policy: Option, + ) -> Self { + self.with_row_access_policy = with_row_access_policy; + self + } + + pub fn with_tags(mut self, with_tags: Option>) -> Self { + self.with_tags = with_tags; + self + } + pub fn build(self) -> Statement { Statement::CreateTable(CreateTable { or_replace: self.or_replace, @@ -278,6 +355,7 @@ impl CreateTableBuilder { global: self.global, if_not_exists: self.if_not_exists, transient: self.transient, + volatile: self.volatile, name: self.name, columns: self.columns, constraints: self.constraints, @@ -304,6 +382,15 @@ impl CreateTableBuilder { cluster_by: self.cluster_by, options: self.options, strict: self.strict, + copy_grants: self.copy_grants, + enable_schema_evolution: self.enable_schema_evolution, + change_tracking: self.change_tracking, + data_retention_time_in_days: self.data_retention_time_in_days, + max_data_extension_time_in_days: self.max_data_extension_time_in_days, + default_ddl_collation: self.default_ddl_collation, + with_aggregation_policy: self.with_aggregation_policy, + with_row_access_policy: self.with_row_access_policy, + with_tags: self.with_tags, }) } } @@ -322,6 +409,7 @@ impl TryFrom for CreateTableBuilder { global, if_not_exists, transient, + volatile, name, columns, constraints, @@ -348,6 +436,15 @@ impl TryFrom for CreateTableBuilder { cluster_by, options, strict, + copy_grants, + enable_schema_evolution, + change_tracking, + data_retention_time_in_days, + max_data_extension_time_in_days, + default_ddl_collation, + with_aggregation_policy, + with_row_access_policy, + with_tags, }) => Ok(Self { or_replace, temporary, @@ -381,6 +478,16 @@ impl TryFrom for CreateTableBuilder { cluster_by, options, strict, + copy_grants, + enable_schema_evolution, + change_tracking, + data_retention_time_in_days, + max_data_extension_time_in_days, + default_ddl_collation, + with_aggregation_policy, + with_row_access_policy, + with_tags, + volatile, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" @@ -393,7 +500,7 @@ impl TryFrom for CreateTableBuilder { #[derive(Default)] pub(crate) struct BigQueryTableConfiguration { pub partition_by: Option>, - pub cluster_by: Option>, + pub cluster_by: Option>>, pub options: Option>, } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1747d677e..49d6499c5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6338,6 +6338,117 @@ impl Display for TableEngine { } } +/// Snowflake `WITH ROW ACCESS POLICY policy_name ON (identifier, ...)` +/// +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct RowAccessPolicy { + pub policy: ObjectName, + pub on: Vec, +} + +impl RowAccessPolicy { + pub fn new(policy: ObjectName, on: Vec) -> Self { + Self { policy, on } + } +} + +impl Display for RowAccessPolicy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "WITH ROW ACCESS POLICY {} ON ({})", + self.policy, + display_comma_separated(self.on.as_slice()) + ) + } +} + +/// Snowflake `WITH TAG ( tag_name = '', ...)` +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Tag { + pub key: Ident, + pub value: String, +} + +impl Tag { + pub fn new(key: Ident, value: String) -> Self { + Self { key, value } + } +} + +impl Display for Tag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}='{}'", self.key, self.value) + } +} + +/// Helper to indicate if a comment includes the `=` in the display form +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CommentDef { + /// Includes `=` when printing the comment, as `COMMENT = 'comment'` + /// Does not include `=` when printing the comment, as `COMMENT 'comment'` + WithEq(String), + WithoutEq(String), +} + +impl Display for CommentDef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CommentDef::WithEq(comment) | CommentDef::WithoutEq(comment) => write!(f, "{comment}"), + } + } +} + +/// Helper to indicate if a collection should be wrapped by a symbol in the display form +/// +/// [`Display`] is implemented for every [`Vec`] where `T: Display`. +/// The string output is a comma separated list for the vec items +/// +/// # Examples +/// ``` +/// # use sqlparser::ast::WrappedCollection; +/// let items = WrappedCollection::Parentheses(vec!["one", "two", "three"]); +/// assert_eq!("(one, two, three)", items.to_string()); +/// +/// let items = WrappedCollection::NoWrapping(vec!["one", "two", "three"]); +/// assert_eq!("one, two, three", items.to_string()); +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum WrappedCollection { + /// Print the collection without wrapping symbols, as `item, item, item` + NoWrapping(T), + /// Wraps the collection in Parentheses, as `(item, item, item)` + Parentheses(T), +} + +impl Display for WrappedCollection> +where + T: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WrappedCollection::NoWrapping(inner) => { + write!(f, "{}", display_comma_separated(inner.as_slice())) + } + WrappedCollection::Parentheses(inner) => { + write!(f, "({})", display_comma_separated(inner.as_slice())) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 894b00438..9f1d7f27b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -12,11 +12,14 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; +use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageLoadSelectItem, StageParamsObject, }; -use crate::ast::{Ident, ObjectName, Statement}; +use crate::ast::{ + CommentDef, Ident, ObjectName, RowAccessPolicy, Statement, Tag, WrappedCollection, +}; use crate::dialect::Dialect; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -91,12 +94,36 @@ impl Dialect for SnowflakeDialect { // possibly CREATE STAGE //[ OR REPLACE ] let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); - //[ TEMPORARY ] - let temporary = parser.parse_keyword(Keyword::TEMPORARY); + // LOCAL | GLOBAL + let global = match parser.parse_one_of_keywords(&[Keyword::LOCAL, Keyword::GLOBAL]) { + Some(Keyword::LOCAL) => Some(false), + Some(Keyword::GLOBAL) => Some(true), + _ => None, + }; + + let mut temporary = false; + let mut volatile = false; + let mut transient = false; + + match parser.parse_one_of_keywords(&[ + Keyword::TEMP, + Keyword::TEMPORARY, + Keyword::VOLATILE, + Keyword::TRANSIENT, + ]) { + Some(Keyword::TEMP | Keyword::TEMPORARY) => temporary = true, + Some(Keyword::VOLATILE) => volatile = true, + Some(Keyword::TRANSIENT) => transient = true, + _ => {} + } if parser.parse_keyword(Keyword::STAGE) { // OK - this is CREATE STAGE statement return Some(parse_create_stage(or_replace, temporary, parser)); + } else if parser.parse_keyword(Keyword::TABLE) { + return Some(parse_create_table( + or_replace, global, temporary, volatile, transient, parser, + )); } else { // need to go back with the cursor let mut back = 1; @@ -120,6 +147,196 @@ impl Dialect for SnowflakeDialect { } } +/// Parse snowflake create table statement. +/// +pub fn parse_create_table( + or_replace: bool, + global: Option, + temporary: bool, + volatile: bool, + transient: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let table_name = parser.parse_object_name(false)?; + + let mut builder = CreateTableBuilder::new(table_name) + .or_replace(or_replace) + .if_not_exists(if_not_exists) + .temporary(temporary) + .transient(transient) + .volatile(volatile) + .global(global) + .hive_formats(Some(Default::default())); + + // Snowflake does not enforce order of the parameters in the statement. The parser needs to + // parse the statement in a loop. + // + // "CREATE TABLE x COPY GRANTS (c INT)" and "CREATE TABLE x (c INT) COPY GRANTS" are both + // accepted by Snowflake + + loop { + let next_token = parser.next_token(); + match &next_token.token { + Token::Word(word) => match word.keyword { + Keyword::COPY => { + parser.expect_keyword(Keyword::GRANTS)?; + builder = builder.copy_grants(true); + } + Keyword::COMMENT => { + parser.expect_token(&Token::Eq)?; + let next_token = parser.next_token(); + let comment = match next_token.token { + Token::SingleQuotedString(str) => Some(CommentDef::WithEq(str)), + _ => parser.expected("comment", next_token)?, + }; + builder = builder.comment(comment); + } + Keyword::AS => { + let query = parser.parse_boxed_query()?; + builder = builder.query(Some(query)); + break; + } + Keyword::CLONE => { + let clone = parser.parse_object_name(false).ok(); + builder = builder.clone_clause(clone); + break; + } + Keyword::LIKE => { + let like = parser.parse_object_name(false).ok(); + builder = builder.like(like); + break; + } + Keyword::CLUSTER => { + parser.expect_keyword(Keyword::BY)?; + parser.expect_token(&Token::LParen)?; + let cluster_by = Some(WrappedCollection::Parentheses( + parser.parse_comma_separated(|p| p.parse_identifier(false))?, + )); + parser.expect_token(&Token::RParen)?; + + builder = builder.cluster_by(cluster_by) + } + Keyword::ENABLE_SCHEMA_EVOLUTION => { + parser.expect_token(&Token::Eq)?; + let enable_schema_evolution = + match parser.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) { + Some(Keyword::TRUE) => true, + Some(Keyword::FALSE) => false, + _ => { + return parser.expected("TRUE or FALSE", next_token); + } + }; + + builder = builder.enable_schema_evolution(Some(enable_schema_evolution)); + } + Keyword::CHANGE_TRACKING => { + parser.expect_token(&Token::Eq)?; + let change_tracking = + match parser.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) { + Some(Keyword::TRUE) => true, + Some(Keyword::FALSE) => false, + _ => { + return parser.expected("TRUE or FALSE", next_token); + } + }; + + builder = builder.change_tracking(Some(change_tracking)); + } + Keyword::DATA_RETENTION_TIME_IN_DAYS => { + parser.expect_token(&Token::Eq)?; + let data_retention_time_in_days = parser.parse_literal_uint()?; + builder = + builder.data_retention_time_in_days(Some(data_retention_time_in_days)); + } + Keyword::MAX_DATA_EXTENSION_TIME_IN_DAYS => { + parser.expect_token(&Token::Eq)?; + let max_data_extension_time_in_days = parser.parse_literal_uint()?; + builder = builder + .max_data_extension_time_in_days(Some(max_data_extension_time_in_days)); + } + Keyword::DEFAULT_DDL_COLLATION => { + parser.expect_token(&Token::Eq)?; + let default_ddl_collation = parser.parse_literal_string()?; + builder = builder.default_ddl_collation(Some(default_ddl_collation)); + } + // WITH is optional, we just verify that next token is one of the expected ones and + // fallback to the default match statement + Keyword::WITH => { + parser.expect_one_of_keywords(&[ + Keyword::AGGREGATION, + Keyword::TAG, + Keyword::ROW, + ])?; + parser.prev_token(); + } + Keyword::AGGREGATION => { + parser.expect_keyword(Keyword::POLICY)?; + let aggregation_policy = parser.parse_object_name(false)?; + builder = builder.with_aggregation_policy(Some(aggregation_policy)); + } + Keyword::ROW => { + parser.expect_keywords(&[Keyword::ACCESS, Keyword::POLICY])?; + let policy = parser.parse_object_name(false)?; + parser.expect_keyword(Keyword::ON)?; + parser.expect_token(&Token::LParen)?; + let columns = parser.parse_comma_separated(|p| p.parse_identifier(false))?; + parser.expect_token(&Token::RParen)?; + + builder = + builder.with_row_access_policy(Some(RowAccessPolicy::new(policy, columns))) + } + Keyword::TAG => { + fn parse_tag(parser: &mut Parser) -> Result { + let name = parser.parse_identifier(false)?; + parser.expect_token(&Token::Eq)?; + let value = parser.parse_literal_string()?; + + Ok(Tag::new(name, value)) + } + + parser.expect_token(&Token::LParen)?; + let tags = parser.parse_comma_separated(parse_tag)?; + parser.expect_token(&Token::RParen)?; + builder = builder.with_tags(Some(tags)); + } + _ => { + return parser.expected("end of statement", next_token); + } + }, + Token::LParen => { + parser.prev_token(); + let (columns, constraints) = parser.parse_columns()?; + builder = builder.columns(columns).constraints(constraints); + } + Token::EOF => { + if builder.columns.is_empty() { + return Err(ParserError::ParserError( + "unexpected end of input".to_string(), + )); + } + + break; + } + Token::SemiColon => { + if builder.columns.is_empty() { + return Err(ParserError::ParserError( + "unexpected end of input".to_string(), + )); + } + + parser.prev_token(); + break; + } + _ => { + return parser.expected("end of statement", next_token); + } + } + } + + Ok(builder.build()) +} + pub fn parse_create_stage( or_replace: bool, temporary: bool, diff --git a/src/keywords.rs b/src/keywords.rs index 1b204a8d5..e75d45e44 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -70,11 +70,13 @@ define_keywords!( ABORT, ABS, ABSOLUTE, + ACCESS, ACTION, ADD, ADMIN, AFTER, AGAINST, + AGGREGATION, ALL, ALLOCATE, ALTER, @@ -138,6 +140,7 @@ define_keywords!( CENTURY, CHAIN, CHANGE, + CHANGE_TRACKING, CHANNEL, CHAR, CHARACTER, @@ -201,6 +204,7 @@ define_keywords!( CYCLE, DATA, DATABASE, + DATA_RETENTION_TIME_IN_DAYS, DATE, DATE32, DATETIME, @@ -214,6 +218,7 @@ define_keywords!( DECIMAL, DECLARE, DEFAULT, + DEFAULT_DDL_COLLATION, DEFERRABLE, DEFERRED, DEFINE, @@ -251,6 +256,7 @@ define_keywords!( ELSE, EMPTY, ENABLE, + ENABLE_SCHEMA_EVOLUTION, ENCODING, ENCRYPTION, END, @@ -330,6 +336,7 @@ define_keywords!( GLOBAL, GRANT, GRANTED, + GRANTS, GRAPHVIZ, GROUP, GROUPING, @@ -433,6 +440,7 @@ define_keywords!( MATERIALIZED, MAX, MAXVALUE, + MAX_DATA_EXTENSION_TIME_IN_DAYS, MEASURES, MEDIUMINT, MEMBER, @@ -539,6 +547,7 @@ define_keywords!( PIVOT, PLACING, PLANS, + POLICY, PORTION, POSITION, POSITION_REGEX, @@ -690,6 +699,7 @@ define_keywords!( TABLE, TABLES, TABLESAMPLE, + TAG, TARGET, TBLPROPERTIES, TEMP, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6406bd4e5..c591b8116 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5372,7 +5372,7 @@ impl<'a> Parser<'a> { let _ = self.consume_token(&Token::Eq); let next_token = self.next_token(); match next_token.token { - Token::SingleQuotedString(str) => Some(str), + Token::SingleQuotedString(str) => Some(CommentDef::WithoutEq(str)), _ => self.expected("comment", next_token)?, } } else { @@ -5423,7 +5423,9 @@ impl<'a> Parser<'a> { let mut cluster_by = None; if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { - cluster_by = Some(self.parse_comma_separated(|p| p.parse_identifier(false))?); + cluster_by = Some(WrappedCollection::NoWrapping( + self.parse_comma_separated(|p| p.parse_identifier(false))?, + )); }; let mut options = None; @@ -7783,7 +7785,7 @@ impl<'a> Parser<'a> { /// This function can be used to reduce the stack size required in debug /// builds. Instead of `sizeof(Query)` only a pointer (`Box`) /// is used. - fn parse_boxed_query(&mut self) -> Result, ParserError> { + pub fn parse_boxed_query(&mut self) -> Result, ParserError> { self.parse_query().map(Box::new) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3b6d6bfcb..171439d19 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -442,7 +442,10 @@ fn parse_create_table_with_options() { assert_eq!( ( Some(Box::new(Expr::Identifier(Ident::new("_PARTITIONDATE")))), - Some(vec![Ident::new("userid"), Ident::new("age"),]), + Some(WrappedCollection::NoWrapping(vec![ + Ident::new("userid"), + Ident::new("age"), + ])), Some(vec![ SqlOption { name: Ident::new("partition_expiration_days"), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8fe7b862c..f6518e276 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3453,9 +3453,14 @@ fn parse_create_table_as_table() { #[test] fn parse_create_table_on_cluster() { + let generic = TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + options: None, + }; + // Using single-quote literal to define current cluster let sql = "CREATE TABLE t ON CLUSTER '{cluster}' (a INT, b INT)"; - match verified_stmt(sql) { + match generic.verified_stmt(sql) { Statement::CreateTable(CreateTable { on_cluster, .. }) => { assert_eq!(on_cluster.unwrap(), "{cluster}".to_string()); } @@ -3464,7 +3469,7 @@ fn parse_create_table_on_cluster() { // Using explicitly declared cluster name let sql = "CREATE TABLE t ON CLUSTER my_cluster (a INT, b INT)"; - match verified_stmt(sql) { + match generic.verified_stmt(sql) { Statement::CreateTable(CreateTable { on_cluster, .. }) => { assert_eq!(on_cluster.unwrap(), "my_cluster".to_string()); } @@ -3517,8 +3522,13 @@ fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), Par #[test] fn parse_create_table_with_options() { + let generic = TestedDialects { + dialects: vec![Box::new(GenericDialect {})], + options: None, + }; + let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; - match verified_stmt(sql) { + match generic.verified_stmt(sql) { Statement::CreateTable(CreateTable { with_options, .. }) => { assert_eq!( vec![ diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 93b3c044a..5343fe5e0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4136,3 +4136,26 @@ fn parse_at_time_zone() { expr ); } + +#[test] +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, .. }) => { + assert_eq!( + vec![ + SqlOption { + name: "foo".into(), + value: Expr::Value(Value::SingleQuotedString("bar".into())), + }, + SqlOption { + name: "a".into(), + value: Expr::Value(number("123")), + }, + ], + with_options + ); + } + _ => unreachable!(), + } +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a21e9d5d6..f0a7c7735 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -40,6 +40,279 @@ fn test_snowflake_create_table() { } } +#[test] +fn test_snowflake_create_or_replace_table() { + let sql = "CREATE OR REPLACE TABLE my_table (a number)"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, or_replace, .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(or_replace); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_or_replace_table_copy_grants() { + let sql = "CREATE OR REPLACE TABLE my_table (a number) COPY GRANTS"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + or_replace, + copy_grants, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(or_replace); + assert!(copy_grants); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_or_replace_table_copy_grants_at_end() { + let sql = "CREATE OR REPLACE TABLE my_table COPY GRANTS (a number) "; + let parsed = "CREATE OR REPLACE TABLE my_table (a number) COPY GRANTS"; + match snowflake().one_statement_parses_to(sql, parsed) { + Statement::CreateTable(CreateTable { + name, + or_replace, + copy_grants, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(or_replace); + assert!(copy_grants); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_or_replace_table_copy_grants_cta() { + let sql = "CREATE OR REPLACE TABLE my_table COPY GRANTS AS SELECT 1 AS a"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + or_replace, + copy_grants, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(or_replace); + assert!(copy_grants); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_enable_schema_evolution() { + let sql = "CREATE TABLE my_table (a number) ENABLE_SCHEMA_EVOLUTION=TRUE"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + enable_schema_evolution, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(true), enable_schema_evolution); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_change_tracking() { + let sql = "CREATE TABLE my_table (a number) CHANGE_TRACKING=TRUE"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + change_tracking, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(true), change_tracking); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_data_retention_time_in_days() { + let sql = "CREATE TABLE my_table (a number) DATA_RETENTION_TIME_IN_DAYS=5"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + data_retention_time_in_days, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(5), data_retention_time_in_days); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_max_data_extension_time_in_days() { + let sql = "CREATE TABLE my_table (a number) MAX_DATA_EXTENSION_TIME_IN_DAYS=5"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + max_data_extension_time_in_days, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(5), max_data_extension_time_in_days); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_with_aggregation_policy() { + match snowflake() + .verified_stmt("CREATE TABLE my_table (a number) WITH AGGREGATION POLICY policy_name") + { + Statement::CreateTable(CreateTable { + name, + with_aggregation_policy, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some("policy_name".to_string()), + with_aggregation_policy.map(|name| name.to_string()) + ); + } + _ => unreachable!(), + } + + match snowflake() + .parse_sql_statements("CREATE TABLE my_table (a number) AGGREGATION POLICY policy_name") + .unwrap() + .pop() + .unwrap() + { + Statement::CreateTable(CreateTable { + name, + with_aggregation_policy, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some("policy_name".to_string()), + with_aggregation_policy.map(|name| name.to_string()) + ); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_with_row_access_policy() { + match snowflake().verified_stmt( + "CREATE TABLE my_table (a number, b number) WITH ROW ACCESS POLICY policy_name ON (a)", + ) { + Statement::CreateTable(CreateTable { + name, + with_row_access_policy, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some("WITH ROW ACCESS POLICY policy_name ON (a)".to_string()), + with_row_access_policy.map(|policy| policy.to_string()) + ); + } + _ => unreachable!(), + } + + match snowflake() + .parse_sql_statements( + "CREATE TABLE my_table (a number, b number) ROW ACCESS POLICY policy_name ON (a)", + ) + .unwrap() + .pop() + .unwrap() + { + Statement::CreateTable(CreateTable { + name, + with_row_access_policy, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some("WITH ROW ACCESS POLICY policy_name ON (a)".to_string()), + with_row_access_policy.map(|policy| policy.to_string()) + ); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_with_tag() { + match snowflake() + .verified_stmt("CREATE TABLE my_table (a number) WITH TAG (A='TAG A', B='TAG B')") + { + Statement::CreateTable(CreateTable { + name, with_tags, .. + }) => { + 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()) + ]), + with_tags + ); + } + _ => unreachable!(), + } + + match snowflake() + .parse_sql_statements("CREATE TABLE my_table (a number) TAG (A='TAG A', B='TAG B')") + .unwrap() + .pop() + .unwrap() + { + Statement::CreateTable(CreateTable { + name, with_tags, .. + }) => { + 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()) + ]), + with_tags + ); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_default_ddl_collation() { + let sql = "CREATE TABLE my_table (a number) DEFAULT_DDL_COLLATION='de'"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + default_ddl_collation, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some("de".to_string()), default_ddl_collation); + } + _ => unreachable!(), + } +} + #[test] fn test_snowflake_create_transient_table() { let sql = "CREATE TRANSIENT TABLE CUSTOMER (id INT, name VARCHAR(255))"; @@ -54,6 +327,162 @@ fn test_snowflake_create_transient_table() { } } +#[test] +fn test_snowflake_create_table_column_comment() { + let sql = "CREATE TABLE my_table (a STRING COMMENT 'some comment')"; + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + vec![ColumnDef { + name: "a".into(), + data_type: DataType::String(None), + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Comment("some comment".to_string()) + }], + collation: None + }], + columns + ) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_local_table() { + match snowflake().verified_stmt("CREATE TABLE my_table (a INT)") { + Statement::CreateTable(CreateTable { name, global, .. }) => { + assert_eq!("my_table", name.to_string()); + assert!(global.is_none()) + } + _ => unreachable!(), + } + + match snowflake().verified_stmt("CREATE LOCAL TABLE my_table (a INT)") { + Statement::CreateTable(CreateTable { name, global, .. }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(false), global) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_global_table() { + match snowflake().verified_stmt("CREATE GLOBAL TABLE my_table (a INT)") { + Statement::CreateTable(CreateTable { name, global, .. }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!(Some(true), global) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_invalid_local_global_table() { + assert_eq!( + snowflake().parse_sql_statements("CREATE LOCAL GLOBAL TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an SQL statement, found: LOCAL".to_string() + )) + ); + + assert_eq!( + snowflake().parse_sql_statements("CREATE GLOBAL LOCAL TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an SQL statement, found: GLOBAL".to_string() + )) + ); +} + +#[test] +fn test_snowflake_create_invalid_temporal_table() { + assert_eq!( + snowflake().parse_sql_statements("CREATE TEMP TEMPORARY TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an object type after CREATE, found: TEMPORARY".to_string() + )) + ); + + assert_eq!( + snowflake().parse_sql_statements("CREATE TEMP VOLATILE TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an object type after CREATE, found: VOLATILE".to_string() + )) + ); + + assert_eq!( + snowflake().parse_sql_statements("CREATE TEMP TRANSIENT TABLE my_table (a INT)"), + Err(ParserError::ParserError( + "Expected an object type after CREATE, found: TRANSIENT".to_string() + )) + ); +} + +#[test] +fn test_snowflake_create_table_if_not_exists() { + match snowflake().verified_stmt("CREATE TABLE IF NOT EXISTS my_table (a INT)") { + Statement::CreateTable(CreateTable { + name, + if_not_exists, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert!(if_not_exists) + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_cluster_by() { + match snowflake().verified_stmt("CREATE TABLE my_table (a INT) CLUSTER BY (a, b)") { + 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"), + ])), + cluster_by + ) + } + _ => unreachable!(), + } +} + +#[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, .. }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!("some comment", comment.unwrap().to_string()); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_incomplete_statement() { + assert_eq!( + snowflake().parse_sql_statements("CREATE TABLE my_table"), + Err(ParserError::ParserError( + "unexpected end of input".to_string() + )) + ); + + assert_eq!( + snowflake().parse_sql_statements("CREATE TABLE my_table; (c int)"), + Err(ParserError::ParserError( + "unexpected end of input".to_string() + )) + ); +} + #[test] fn test_snowflake_single_line_tokenize() { let sql = "CREATE TABLE# this is a comment \ntable_1"; From deac26971084d0790e718a3352a43ecbbc868e64 Mon Sep 17 00:00:00 2001 From: Philip Cristiano Date: Mon, 17 Jun 2024 14:10:40 -0400 Subject: [PATCH 467/806] CreateIndex: Move Display fmt to struct (#1307) --- src/ast/dml.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 43 +------------------------------------------ 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 74bb5435c..b35b2b970 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -47,6 +47,49 @@ pub struct CreateIndex { pub nulls_distinct: Option, pub predicate: Option, } + +impl Display for CreateIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {unique}INDEX {concurrently}{if_not_exists}", + unique = if self.unique { "UNIQUE " } else { "" }, + concurrently = if self.concurrently { + "CONCURRENTLY " + } else { + "" + }, + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + )?; + if let Some(value) = &self.name { + write!(f, "{value} ")?; + } + write!(f, "ON {}", self.table_name)?; + if let Some(value) = &self.using { + write!(f, " USING {value} ")?; + } + write!(f, "({})", display_separated(&self.columns, ","))?; + if !self.include.is_empty() { + write!(f, " INCLUDE ({})", display_separated(&self.include, ","))?; + } + if let Some(value) = self.nulls_distinct { + if value { + write!(f, " NULLS DISTINCT")?; + } else { + write!(f, " NULLS NOT DISTINCT")?; + } + } + if let Some(predicate) = &self.predicate { + write!(f, " WHERE {predicate}")?; + } + Ok(()) + } +} + /// CREATE TABLE statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 49d6499c5..6e306b1e3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3383,48 +3383,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::CreateIndex(CreateIndex { - name, - table_name, - using, - columns, - unique, - concurrently, - if_not_exists, - include, - nulls_distinct, - predicate, - }) => { - write!( - f, - "CREATE {unique}INDEX {concurrently}{if_not_exists}", - unique = if *unique { "UNIQUE " } else { "" }, - concurrently = if *concurrently { "CONCURRENTLY " } else { "" }, - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - )?; - if let Some(value) = name { - write!(f, "{value} ")?; - } - write!(f, "ON {table_name}")?; - if let Some(value) = using { - write!(f, " USING {value} ")?; - } - write!(f, "({})", display_separated(columns, ","))?; - if !include.is_empty() { - write!(f, " INCLUDE ({})", display_separated(include, ","))?; - } - if let Some(value) = nulls_distinct { - if *value { - write!(f, " NULLS DISTINCT")?; - } else { - write!(f, " NULLS NOT DISTINCT")?; - } - } - if let Some(predicate) = predicate { - write!(f, " WHERE {predicate}")?; - } - Ok(()) - } + Statement::CreateIndex(create_index) => create_index.fmt(f), Statement::CreateExtension { name, if_not_exists, From 0330f9def5ebd6b7813dc4656f40edc717dbd0a3 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Mon, 17 Jun 2024 22:14:40 +0400 Subject: [PATCH 468/806] Support use of `BY NAME` quantifier across all set ops (#1309) Co-authored-by: Alexander Beedie Co-authored-by: Joey Hain --- README.md | 6 +++--- src/ast/data_type.rs | 4 ++-- src/ast/mod.rs | 6 +++--- src/parser/mod.rs | 15 +++------------ src/tokenizer.rs | 2 +- tests/sqlparser_common.rs | 6 ++++++ 6 files changed, 18 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 512f5f6c0..3226b9549 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,12 @@ $ 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], [PRQL], [Qrlew], [JumpWire], and [ParadeDB]. +This parser is currently being used by the [DataFusion] query engine, [LocustDB], +[Ballista], [GlueSQL], [Opteryx], [Polars], [PRQL], [Qrlew], [JumpWire], and [ParadeDB]. If your project is using sqlparser-rs feel free to make a PR to add it to this list. - ## Design The core expression parser uses the [Pratt Parser] design, which is a top-down @@ -210,6 +209,7 @@ licensed as above, without any additional terms or conditions. [Ballista]: https://github.com/apache/arrow-ballista [GlueSQL]: https://github.com/gluesql/gluesql [Opteryx]: https://github.com/mabel-dev/opteryx +[Polars]: https://pola.rs/ [PRQL]: https://github.com/PRQL/prql [Qrlew]: https://github.com/Qrlew/qrlew [JumpWire]: https://github.com/extragoodlabs/jumpwire diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 7d0aec8fc..6b1a542f4 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -168,7 +168,7 @@ pub enum DataType { UnsignedInt(Option), /// Unsigned int4 with optional display width e.g. INT4 UNSIGNED or INT4(11) UNSIGNED UnsignedInt4(Option), - /// Unsigned integer with optional display width e.g. INTGER UNSIGNED or INTEGER(11) UNSIGNED + /// Unsigned integer with optional display width e.g. INTEGER UNSIGNED or INTEGER(11) UNSIGNED UnsignedInteger(Option), /// Unsigned integer type in [clickhouse] /// Note: UInt8 mean 8 bits in [clickhouse] @@ -699,7 +699,7 @@ pub enum CharacterLength { /// Optional unit. If not informed, the ANSI handles it as CHARACTERS implicitly unit: Option, }, - /// VARCHAR(MAX) or NVARCHAR(MAX), used in T-SQL (Miscrosoft SQL Server) + /// VARCHAR(MAX) or NVARCHAR(MAX), used in T-SQL (Microsoft SQL Server) Max, } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6e306b1e3..7af8efaec 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2265,7 +2265,7 @@ pub enum Statement { /// SET [ SESSION | LOCAL ] ROLE role_name /// ``` /// - /// Sets sesssion state. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4] + /// 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 @@ -2283,7 +2283,7 @@ pub enum Statement { /// ``` /// /// Note: this is not a standard SQL statement, but it is supported by at - /// least MySQL and PostgreSQL. Not all MySQL-specific syntatic forms are + /// least MySQL and PostgreSQL. Not all MySQL-specific syntactic forms are /// supported yet. SetVariable { local: bool, @@ -4750,7 +4750,7 @@ impl fmt::Display for FunctionArguments { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct FunctionArgumentList { - /// `[ ALL | DISTINCT ] + /// `[ ALL | DISTINCT ]` pub duplicate_treatment: Option, /// The function arguments. pub args: Vec, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c591b8116..e240441b9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8138,7 +8138,7 @@ impl<'a> Parser<'a> { pub fn parse_set_quantifier(&mut self, op: &Option) -> SetQuantifier { match op { - Some(SetOperator::Union) => { + Some(SetOperator::Except | SetOperator::Intersect | SetOperator::Union) => { if self.parse_keywords(&[Keyword::DISTINCT, Keyword::BY, Keyword::NAME]) { SetQuantifier::DistinctByName } else if self.parse_keywords(&[Keyword::BY, Keyword::NAME]) { @@ -8155,15 +8155,6 @@ impl<'a> Parser<'a> { SetQuantifier::None } } - Some(SetOperator::Except) | Some(SetOperator::Intersect) => { - if self.parse_keyword(Keyword::ALL) { - SetQuantifier::All - } else if self.parse_keyword(Keyword::DISTINCT) { - SetQuantifier::Distinct - } else { - SetQuantifier::None - } - } _ => SetQuantifier::None, } } @@ -8547,10 +8538,10 @@ impl<'a> Parser<'a> { }) } else if variable.to_string() == "TRANSACTION" && modifier.is_none() { if self.parse_keyword(Keyword::SNAPSHOT) { - let snaphot_id = self.parse_value()?; + let snapshot_id = self.parse_value()?; return Ok(Statement::SetTransaction { modes: vec![], - snapshot: Some(snaphot_id), + snapshot: Some(snapshot_id), session: false, }); } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index bcc5478bc..4e64e0712 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -654,7 +654,7 @@ impl<'a> Tokenizer<'a> { Ok(()) } - // Tokenize the identifer or keywords in `ch` + // Tokenize the identifier or keywords in `ch` fn tokenize_identifier_or_keyword( &self, ch: impl IntoIterator, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f6518e276..a86858129 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6010,6 +6010,12 @@ fn parse_union_except_intersect() { verified_stmt("SELECT foo FROM tab UNION SELECT bar FROM TAB"); verified_stmt("(SELECT * FROM new EXCEPT SELECT * FROM old) UNION ALL (SELECT * FROM old EXCEPT SELECT * FROM new) ORDER BY 1"); verified_stmt("(SELECT * FROM new EXCEPT DISTINCT SELECT * FROM old) UNION DISTINCT (SELECT * FROM old EXCEPT DISTINCT SELECT * FROM new) ORDER BY 1"); + verified_stmt("SELECT 1 AS x, 2 AS y EXCEPT BY NAME SELECT 9 AS y, 8 AS x"); + verified_stmt("SELECT 1 AS x, 2 AS y EXCEPT ALL BY NAME SELECT 9 AS y, 8 AS x"); + verified_stmt("SELECT 1 AS x, 2 AS y EXCEPT DISTINCT BY NAME SELECT 9 AS y, 8 AS x"); + verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT BY NAME SELECT 9 AS y, 8 AS x"); + verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT ALL BY NAME SELECT 9 AS y, 8 AS x"); + verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT DISTINCT BY NAME SELECT 9 AS y, 8 AS x"); } #[test] From 345e2098fb3cdd720f99e8183720529c4fd0acc5 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 18 Jun 2024 15:28:39 +0200 Subject: [PATCH 469/806] add support for update statements that contain tuple assignments (#1317) --- src/ast/mod.rs | 26 ++++++++++++++++++++++++-- src/parser/mod.rs | 16 ++++++++++++++-- tests/sqlparser_bigquery.rs | 4 ++-- tests/sqlparser_common.rs | 23 ++++++++++++++++------- tests/sqlparser_mysql.rs | 25 +++++++++++++++++++------ tests/sqlparser_postgres.rs | 10 +++++----- tests/sqlparser_sqlite.rs | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 24 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7af8efaec..769bda598 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4553,13 +4553,35 @@ impl fmt::Display for GrantObjects { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Assignment { - pub id: Vec, + pub target: AssignmentTarget, pub value: Expr, } impl fmt::Display for Assignment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} = {}", display_separated(&self.id, "."), self.value) + write!(f, "{} = {}", self.target, self.value) + } +} + +/// Left-hand side of an assignment in an UPDATE statement, +/// e.g. `foo` in `foo = 5` (ColumnName assignment) or +/// `(a, b)` in `(a, b) = (1, 2)` (Tuple assignment). +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AssignmentTarget { + /// A single column + ColumnName(ObjectName), + /// A tuple of columns + Tuple(Vec), +} + +impl fmt::Display for AssignmentTarget { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AssignmentTarget::ColumnName(column) => write!(f, "{}", column), + AssignmentTarget::Tuple(columns) => write!(f, "({})", display_comma_separated(columns)), + } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e240441b9..62222c6fb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9937,10 +9937,22 @@ impl<'a> Parser<'a> { /// Parse a `var = expr` assignment, used in an UPDATE statement pub fn parse_assignment(&mut self) -> Result { - let id = self.parse_identifiers()?; + let target = self.parse_assignment_target()?; self.expect_token(&Token::Eq)?; let value = self.parse_expr()?; - Ok(Assignment { id, value }) + Ok(Assignment { target, value }) + } + + /// Parse the left-hand side of an assignment, used in an UPDATE statement + pub fn parse_assignment_target(&mut self) -> Result { + if self.consume_token(&Token::LParen) { + let columns = self.parse_comma_separated(|p| p.parse_object_name(false))?; + self.expect_token(&Token::RParen)?; + Ok(AssignmentTarget::Tuple(columns)) + } else { + let column = self.parse_object_name(false)?; + Ok(AssignmentTarget::ColumnName(column)) + } } pub fn parse_function_args(&mut self) -> Result { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 171439d19..fb6e3b88a 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1590,11 +1590,11 @@ fn parse_merge() { let update_action = MergeAction::Update { assignments: vec![ Assignment { - id: vec![Ident::new("a")], + target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("a")])), value: Expr::Value(number("1")), }, Assignment { - id: vec![Ident::new("b")], + target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("b")])), value: Expr::Value(number("2")), }, ], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a86858129..15b3b69dd 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -296,15 +296,15 @@ fn parse_update() { assignments, vec![ Assignment { - id: vec!["a".into()], + target: AssignmentTarget::ColumnName(ObjectName(vec!["a".into()])), value: Expr::Value(number("1")), }, Assignment { - id: vec!["b".into()], + target: AssignmentTarget::ColumnName(ObjectName(vec!["b".into()])), value: Expr::Value(number("2")), }, Assignment { - id: vec!["c".into()], + target: AssignmentTarget::ColumnName(ObjectName(vec!["c".into()])), value: Expr::Value(number("3")), }, ] @@ -363,7 +363,7 @@ fn parse_update_set_from() { joins: vec![], }, assignments: vec![Assignment { - id: vec![Ident::new("name")], + target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("name")])), value: Expr::CompoundIdentifier(vec![Ident::new("t2"), Ident::new("name")]) }], from: Some(TableWithJoins { @@ -466,7 +466,10 @@ fn parse_update_with_table_alias() { ); assert_eq!( vec![Assignment { - id: vec![Ident::new("u"), Ident::new("username")], + target: AssignmentTarget::ColumnName(ObjectName(vec![ + Ident::new("u"), + Ident::new("username") + ])), value: Expr::Value(Value::SingleQuotedString("new_user".to_string())), }], assignments @@ -7702,14 +7705,20 @@ fn parse_merge() { action: MergeAction::Update { assignments: vec![ Assignment { - id: vec![Ident::new("dest"), Ident::new("F")], + target: AssignmentTarget::ColumnName(ObjectName(vec![ + Ident::new("dest"), + Ident::new("F") + ])), value: Expr::CompoundIdentifier(vec![ Ident::new("stg"), Ident::new("F"), ]), }, Assignment { - id: vec![Ident::new("dest"), Ident::new("G")], + target: AssignmentTarget::ColumnName(ObjectName(vec![ + Ident::new("dest"), + Ident::new("G") + ])), value: Expr::CompoundIdentifier(vec![ Ident::new("stg"), Ident::new("G"), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e65fc181b..ff8a49de7 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1639,23 +1639,33 @@ fn parse_insert_with_on_duplicate_update() { assert_eq!( Some(OnInsert::DuplicateKeyUpdate(vec![ Assignment { - id: vec![Ident::new("description".to_string())], + target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + "description".to_string() + )])), value: call("VALUES", [Expr::Identifier(Ident::new("description"))]), }, Assignment { - id: vec![Ident::new("perm_create".to_string())], + target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + "perm_create".to_string() + )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_create"))]), }, Assignment { - id: vec![Ident::new("perm_read".to_string())], + target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + "perm_read".to_string() + )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_read"))]), }, Assignment { - id: vec![Ident::new("perm_update".to_string())], + target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + "perm_update".to_string() + )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_update"))]), }, Assignment { - id: vec![Ident::new("perm_delete".to_string())], + target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + "perm_delete".to_string() + )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_delete"))]), }, ])), @@ -1835,7 +1845,10 @@ fn parse_update_with_joins() { ); assert_eq!( vec![Assignment { - id: vec![Ident::new("o"), Ident::new("completed")], + target: AssignmentTarget::ColumnName(ObjectName(vec![ + Ident::new("o"), + Ident::new("completed") + ])), value: Expr::Value(Value::Boolean(true)) }], assignments diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5343fe5e0..fe735b8b2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1557,7 +1557,7 @@ fn parse_pg_on_conflict() { assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - id: vec!["dname".into()], + target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "dname".into()]) },], selection: None @@ -1588,14 +1588,14 @@ fn parse_pg_on_conflict() { OnConflictAction::DoUpdate(DoUpdate { assignments: vec![ Assignment { - id: vec!["dname".into()], + target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), value: Expr::CompoundIdentifier(vec![ "EXCLUDED".into(), "dname".into() ]) }, Assignment { - id: vec!["area".into()], + target: AssignmentTarget::ColumnName(ObjectName(vec!["area".into()])), value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "area".into()]) }, ], @@ -1645,7 +1645,7 @@ fn parse_pg_on_conflict() { assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - id: vec!["dname".into()], + target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), value: Expr::Value(Value::Placeholder("$1".to_string())) },], selection: Some(Expr::BinaryOp { @@ -1682,7 +1682,7 @@ fn parse_pg_on_conflict() { assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - id: vec!["dname".into()], + target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), value: Expr::Value(Value::Placeholder("$1".to_string())) },], selection: Some(Expr::BinaryOp { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 16ea9eb8c..1181c480b 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -373,6 +373,40 @@ fn parse_attach_database() { } } +#[test] +fn parse_update_tuple_row_values() { + // See https://github.com/sqlparser-rs/sqlparser-rs/issues/1311 + assert_eq!( + sqlite().verified_stmt("UPDATE x SET (a, b) = (1, 2)"), + Statement::Update { + assignments: vec![Assignment { + target: AssignmentTarget::Tuple(vec![ + ObjectName(vec![Ident::new("a"),]), + ObjectName(vec![Ident::new("b"),]), + ]), + value: Expr::Tuple(vec![ + Expr::Value(Value::Number("1".parse().unwrap(), false)), + Expr::Value(Value::Number("2".parse().unwrap(), false)) + ]) + }], + selection: None, + table: TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("x")]), + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![] + }, + joins: vec![], + }, + from: None, + returning: None + } + ); +} + #[test] fn parse_where_in_empty_list() { let sql = "SELECT * FROM t1 WHERE a IN ()"; From 79af31b6727fbe60e21705f4bbf8dafc59516e42 Mon Sep 17 00:00:00 2001 From: Emil Ejbyfeldt Date: Tue, 18 Jun 2024 15:30:24 +0200 Subject: [PATCH 470/806] Return errors, not panic, when integers fail to parse in `AUTO_INCREMENT` and `TOP` (#1305) --- src/parser/mod.rs | 40 ++++++++++++++++++++++++--------------- tests/sqlparser_common.rs | 15 +++++++++++++++ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 62222c6fb..67aebcb33 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -20,7 +20,10 @@ use alloc::{ vec, vec::Vec, }; -use core::fmt; +use core::{ + fmt::{self, Display}, + str::FromStr, +}; use log::debug; @@ -3260,6 +3263,18 @@ impl<'a> Parser<'a> { } } + fn parse(s: String, loc: Location) -> Result + where + ::Err: Display, + { + s.parse::().map_err(|e| { + ParserError::ParserError(format!( + "Could not parse '{s}' as {}: {e}{loc}", + core::any::type_name::() + )) + }) + } + /// Parse a comma-separated list of 1+ SelectItem pub fn parse_projection(&mut self) -> Result, ParserError> { // BigQuery and Snowflake allow trailing commas, but only in project lists @@ -5281,7 +5296,7 @@ impl<'a> Parser<'a> { let _ = self.consume_token(&Token::Eq); let next_token = self.next_token(); match next_token.token { - Token::Number(s, _) => Some(s.parse::().expect("literal int")), + Token::Number(s, _) => Some(Self::parse::(s, next_token.location)?), _ => self.expected("literal int", next_token)?, } } else { @@ -6725,10 +6740,7 @@ impl<'a> Parser<'a> { // The call to n.parse() returns a bigdecimal when the // bigdecimal feature is enabled, and is otherwise a no-op // (i.e., it returns the input string). - Token::Number(ref n, l) => match n.parse() { - Ok(n) => Ok(Value::Number(n, l)), - Err(e) => parser_err!(format!("Could not parse '{n}' as number: {e}"), location), - }, + Token::Number(n, l) => Ok(Value::Number(Self::parse(n, location)?, l)), Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), Token::TripleSingleQuotedString(ref s) => { @@ -6820,9 +6832,7 @@ impl<'a> Parser<'a> { pub fn parse_literal_uint(&mut self) -> Result { let next_token = self.next_token(); match next_token.token { - Token::Number(s, _) => s.parse::().map_err(|e| { - ParserError::ParserError(format!("Could not parse '{s}' as u64: {e}")) - }), + Token::Number(s, _) => Self::parse::(s, next_token.location), _ => self.expected("literal int", next_token), } } @@ -9273,7 +9283,7 @@ impl<'a> Parser<'a> { return self.expected("literal number", next_token); }; self.expect_token(&Token::RBrace)?; - RepetitionQuantifier::AtMost(n.parse().expect("literal int")) + RepetitionQuantifier::AtMost(Self::parse(n, token.location)?) } Token::Number(n, _) if self.consume_token(&Token::Comma) => { let next_token = self.next_token(); @@ -9281,12 +9291,12 @@ impl<'a> Parser<'a> { Token::Number(m, _) => { self.expect_token(&Token::RBrace)?; RepetitionQuantifier::Range( - n.parse().expect("literal int"), - m.parse().expect("literal int"), + Self::parse(n, token.location)?, + Self::parse(m, token.location)?, ) } Token::RBrace => { - RepetitionQuantifier::AtLeast(n.parse().expect("literal int")) + RepetitionQuantifier::AtLeast(Self::parse(n, token.location)?) } _ => { return self.expected("} or upper bound", next_token); @@ -9295,7 +9305,7 @@ impl<'a> Parser<'a> { } Token::Number(n, _) => { self.expect_token(&Token::RBrace)?; - RepetitionQuantifier::Exactly(n.parse().expect("literal int")) + RepetitionQuantifier::Exactly(Self::parse(n, token.location)?) } _ => return self.expected("quantifier range", token), } @@ -10329,7 +10339,7 @@ impl<'a> Parser<'a> { } else { let next_token = self.next_token(); let quantity = match next_token.token { - Token::Number(s, _) => s.parse::().expect("literal int"), + Token::Number(s, _) => Self::parse::(s, next_token.location)?, _ => self.expected("literal int", next_token)?, }; Some(TopQuantity::Constant(quantity)) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 15b3b69dd..a87883908 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10006,3 +10006,18 @@ fn parse_select_wildcard_with_except() { "sql parser error: Expected identifier, found: )" ); } + +#[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:?}"); +} From f16c1afed0fa273228e74a633f3885c9c6609911 Mon Sep 17 00:00:00 2001 From: Lorrens Pantelis <100197010+LorrensP-2158466@users.noreply.github.com> Date: Sat, 22 Jun 2024 00:26:23 +0200 Subject: [PATCH 471/806] Improve error messages with additional colons (#1319) --- src/parser/mod.rs | 6 +- src/tokenizer.rs | 4 +- tests/sqlparser_bigquery.rs | 20 ++-- tests/sqlparser_common.rs | 168 +++++++++++++++++----------------- tests/sqlparser_databricks.rs | 2 +- tests/sqlparser_hive.rs | 8 +- tests/sqlparser_mssql.rs | 2 +- tests/sqlparser_mysql.rs | 2 +- tests/sqlparser_postgres.rs | 28 +++--- tests/sqlparser_snowflake.rs | 40 ++++---- tests/sqlparser_sqlite.rs | 8 +- 11 files changed, 144 insertions(+), 144 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 67aebcb33..27520a6c4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3116,7 +3116,7 @@ impl<'a> Parser<'a> { /// Report `found` was encountered instead of `expected` pub fn expected(&self, expected: &str, found: TokenWithLocation) -> Result { parser_err!( - format!("Expected {expected}, found: {found}"), + format!("Expected: {expected}, found: {found}"), found.location ) } @@ -11581,7 +11581,7 @@ mod tests { assert_eq!( ast, Err(ParserError::TokenizerError( - "Unterminated string literal at Line: 1, Column 5".to_string() + "Unterminated string literal at Line: 1, Column: 5".to_string() )) ); } @@ -11593,7 +11593,7 @@ mod tests { assert_eq!( ast, Err(ParserError::ParserError( - "Expected [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: a at Line: 1, Column 16" + "Expected: [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: a at Line: 1, Column: 16" .to_string() )) ); diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 4e64e0712..b8336cec8 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -429,7 +429,7 @@ impl fmt::Display for Location { write!( f, // TODO: use standard compiler location syntax (::) - " at Line: {}, Column {}", + " at Line: {}, Column: {}", self.line, self.column, ) } @@ -1816,7 +1816,7 @@ mod tests { use std::error::Error; assert!(err.source().is_none()); } - assert_eq!(err.to_string(), "test at Line: 1, Column 1"); + assert_eq!(err.to_string(), "test at Line: 1, Column: 1"); } #[test] diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index fb6e3b88a..ec4ddca96 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -535,7 +535,7 @@ fn parse_invalid_brackets() { bigquery_and_generic() .parse_sql_statements(sql) .unwrap_err(), - ParserError::ParserError("Expected (, found: >".to_string()) + ParserError::ParserError("Expected: (, found: >".to_string()) ); let sql = "CREATE TABLE table (x STRUCT>>)"; @@ -544,7 +544,7 @@ fn parse_invalid_brackets() { .parse_sql_statements(sql) .unwrap_err(), ParserError::ParserError( - "Expected ',' or ')' after column definition, found: >".to_string() + "Expected: ',' or ')' after column definition, found: >".to_string() ) ); } @@ -1753,11 +1753,11 @@ fn parse_merge_invalid_statements() { for (sql, err_msg) in [ ( "MERGE T USING U ON TRUE WHEN MATCHED BY TARGET AND 1 THEN DELETE", - "Expected THEN, found: BY", + "Expected: THEN, found: BY", ), ( "MERGE T USING U ON TRUE WHEN MATCHED BY SOURCE AND 1 THEN DELETE", - "Expected THEN, found: BY", + "Expected: THEN, found: BY", ), ( "MERGE T USING U ON TRUE WHEN NOT MATCHED BY SOURCE THEN INSERT(a) VALUES (b)", @@ -1898,13 +1898,13 @@ fn parse_big_query_declare() { let error_sql = "DECLARE x"; assert_eq!( - ParserError::ParserError("Expected a data type name, found: EOF".to_owned()), + ParserError::ParserError("Expected: a data type name, found: EOF".to_owned()), bigquery().parse_sql_statements(error_sql).unwrap_err() ); let error_sql = "DECLARE x 42"; assert_eq!( - ParserError::ParserError("Expected a data type name, found: 42".to_owned()), + ParserError::ParserError("Expected: a data type name, found: 42".to_owned()), bigquery().parse_sql_statements(error_sql).unwrap_err() ); } @@ -2069,7 +2069,7 @@ fn test_bigquery_create_function() { "AS ((SELECT 1 FROM mytable)) ", "OPTIONS(a = [1, 2])", ), - "Expected end of statement, found: OPTIONS", + "Expected: end of statement, found: OPTIONS", ), ( concat!( @@ -2077,7 +2077,7 @@ fn test_bigquery_create_function() { "IMMUTABLE ", "AS ((SELECT 1 FROM mytable)) ", ), - "Expected AS, found: IMMUTABLE", + "Expected: AS, found: IMMUTABLE", ), ( concat!( @@ -2085,7 +2085,7 @@ fn test_bigquery_create_function() { "AS \"console.log('hello');\" ", "LANGUAGE js ", ), - "Expected end of statement, found: LANGUAGE", + "Expected: end of statement, found: LANGUAGE", ), ]; for (sql, error) in error_sqls { @@ -2116,7 +2116,7 @@ fn test_bigquery_trim() { // missing comma separation let error_sql = "SELECT TRIM('xyz' 'a')"; assert_eq!( - ParserError::ParserError("Expected ), found: 'a'".to_owned()), + ParserError::ParserError("Expected: ), found: 'a'".to_owned()), bigquery().parse_sql_statements(error_sql).unwrap_err() ); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a87883908..0149bad5d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -115,7 +115,7 @@ fn parse_replace_into() { let sql = "REPLACE INTO public.customer (id, name, active) VALUES (1, 2, 3)"; assert_eq!( - ParserError::ParserError("Unsupported statement REPLACE at Line: 1, Column 9".to_string()), + ParserError::ParserError("Unsupported statement REPLACE at Line: 1, Column: 9".to_string()), Parser::parse_sql(&dialect, sql,).unwrap_err(), ) } @@ -199,7 +199,7 @@ fn parse_insert_default_values() { let insert_with_columns_and_default_values = "INSERT INTO test_table (test_col) DEFAULT VALUES"; assert_eq!( ParserError::ParserError( - "Expected SELECT, VALUES, or a subquery in the query body, found: DEFAULT".to_string() + "Expected: SELECT, VALUES, or a subquery in the query body, found: DEFAULT".to_string() ), parse_sql_statements(insert_with_columns_and_default_values).unwrap_err() ); @@ -207,20 +207,20 @@ fn parse_insert_default_values() { let insert_with_default_values_and_hive_after_columns = "INSERT INTO test_table DEFAULT VALUES (some_column)"; assert_eq!( - ParserError::ParserError("Expected end of statement, found: (".to_string()), + ParserError::ParserError("Expected: end of statement, found: (".to_string()), parse_sql_statements(insert_with_default_values_and_hive_after_columns).unwrap_err() ); let insert_with_default_values_and_hive_partition = "INSERT INTO test_table DEFAULT VALUES PARTITION (some_column)"; assert_eq!( - ParserError::ParserError("Expected end of statement, found: PARTITION".to_string()), + ParserError::ParserError("Expected: end of statement, found: PARTITION".to_string()), parse_sql_statements(insert_with_default_values_and_hive_partition).unwrap_err() ); let insert_with_default_values_and_values_list = "INSERT INTO test_table DEFAULT VALUES (1)"; assert_eq!( - ParserError::ParserError("Expected end of statement, found: (".to_string()), + ParserError::ParserError("Expected: end of statement, found: (".to_string()), parse_sql_statements(insert_with_default_values_and_values_list).unwrap_err() ); } @@ -319,14 +319,14 @@ fn parse_update() { let sql = "UPDATE t WHERE 1"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected SET, found: WHERE".to_string()), + ParserError::ParserError("Expected: SET, found: WHERE".to_string()), res.unwrap_err() ); let sql = "UPDATE t SET a = 1 extrabadstuff"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected end of statement, found: extrabadstuff".to_string()), + ParserError::ParserError("Expected: end of statement, found: extrabadstuff".to_string()), res.unwrap_err() ); } @@ -577,7 +577,7 @@ fn parse_delete_without_from_error() { let dialects = all_dialects_except(|d| d.is::() || d.is::()); let res = dialects.parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected FROM, found: WHERE".to_string()), + ParserError::ParserError("Expected: FROM, found: WHERE".to_string()), res.unwrap_err() ); } @@ -892,7 +892,7 @@ fn parse_select_distinct_on() { fn parse_select_distinct_missing_paren() { let result = parse_sql_statements("SELECT DISTINCT (name, id FROM customer"); assert_eq!( - ParserError::ParserError("Expected ), found: FROM".to_string()), + ParserError::ParserError("Expected: ), found: FROM".to_string()), result.unwrap_err(), ); } @@ -936,7 +936,7 @@ fn parse_select_into() { let sql = "SELECT * INTO table0 asdf FROM table1"; let result = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected end of statement, found: asdf".to_string()), + ParserError::ParserError("Expected: end of statement, found: asdf".to_string()), result.unwrap_err() ) } @@ -973,7 +973,7 @@ fn parse_select_wildcard() { let sql = "SELECT * + * FROM foo;"; let result = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected end of statement, found: +".to_string()), + ParserError::ParserError("Expected: end of statement, found: +".to_string()), result.unwrap_err(), ); } @@ -1002,7 +1002,7 @@ fn parse_column_aliases() { assert_eq!(&Expr::Value(number("1")), right.as_ref()); assert_eq!(&Ident::new("newname"), alias); } else { - panic!("Expected ExprWithAlias") + panic!("Expected: ExprWithAlias") } // alias without AS is parsed correctly: @@ -1013,13 +1013,13 @@ fn parse_column_aliases() { fn test_eof_after_as() { let res = parse_sql_statements("SELECT foo AS"); assert_eq!( - ParserError::ParserError("Expected an identifier after AS, found: EOF".to_string()), + ParserError::ParserError("Expected: an identifier after AS, found: EOF".to_string()), res.unwrap_err() ); let res = parse_sql_statements("SELECT 1 FROM foo AS"); assert_eq!( - ParserError::ParserError("Expected an identifier after AS, found: EOF".to_string()), + ParserError::ParserError("Expected: an identifier after AS, found: EOF".to_string()), res.unwrap_err() ); } @@ -1104,7 +1104,7 @@ fn parse_not() { fn parse_invalid_infix_not() { let res = parse_sql_statements("SELECT c FROM t WHERE c NOT ("); assert_eq!( - ParserError::ParserError("Expected end of statement, found: NOT".to_string()), + ParserError::ParserError("Expected: end of statement, found: NOT".to_string()), res.unwrap_err(), ); } @@ -1177,11 +1177,11 @@ fn parse_exponent_in_select() -> Result<(), ParserError> { let select = match select.pop().unwrap() { Statement::Query(inner) => *inner, - _ => panic!("Expected Query"), + _ => panic!("Expected: Query"), }; let select = match *select.body { SetExpr::Select(inner) => *inner, - _ => panic!("Expected SetExpr::Select"), + _ => panic!("Expected: SetExpr::Select"), }; assert_eq!( @@ -1810,7 +1810,7 @@ fn parse_in_error() { let sql = "SELECT * FROM customers WHERE segment in segment"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected (, found: segment".to_string()), + ParserError::ParserError("Expected: (, found: segment".to_string()), res.unwrap_err() ); } @@ -2023,14 +2023,14 @@ fn parse_tuple_invalid() { let sql = "select (1"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected ), found: EOF".to_string()), + ParserError::ParserError("Expected: ), found: EOF".to_string()), res.unwrap_err() ); let sql = "select (), 2"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected an expression:, found: )".to_string()), + ParserError::ParserError("Expected: an expression:, found: )".to_string()), res.unwrap_err() ); } @@ -2442,7 +2442,7 @@ fn parse_extract() { let dialects = all_dialects_except(|d| d.is::() || d.is::()); let res = dialects.parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)"); assert_eq!( - ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), + ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()), res.unwrap_err() ); } @@ -2481,7 +2481,7 @@ fn parse_ceil_datetime() { let dialects = all_dialects_except(|d| d.is::() || d.is::()); let res = dialects.parse_sql_statements("SELECT CEIL(d TO JIFFY) FROM df"); assert_eq!( - ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), + ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()), res.unwrap_err() ); } @@ -2508,7 +2508,7 @@ fn parse_floor_datetime() { let dialects = all_dialects_except(|d| d.is::() || d.is::()); let res = dialects.parse_sql_statements("SELECT FLOOR(d TO JIFFY) FROM df"); assert_eq!( - ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()), + ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()), res.unwrap_err() ); } @@ -2709,7 +2709,7 @@ fn parse_window_function_null_treatment_arg() { let sql = "SELECT LAG(1 IGNORE NULLS) IGNORE NULLS OVER () FROM t1"; assert_eq!( dialects.parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError("Expected end of statement, found: NULLS".to_string()) + ParserError::ParserError("Expected: end of statement, found: NULLS".to_string()) ); let sql = "SELECT LAG(1 IGNORE NULLS) IGNORE NULLS OVER () FROM t1"; @@ -2717,7 +2717,7 @@ fn parse_window_function_null_treatment_arg() { all_dialects_where(|d| !d.supports_window_function_null_treatment_arg()) .parse_sql_statements(sql) .unwrap_err(), - ParserError::ParserError("Expected ), found: IGNORE".to_string()) + ParserError::ParserError("Expected: ), found: IGNORE".to_string()) ); } @@ -2907,13 +2907,13 @@ fn parse_create_table() { assert!(res .unwrap_err() .to_string() - .contains("Expected \',\' or \')\' after column definition, found: GARBAGE")); + .contains("Expected: \',\' or \')\' after column definition, found: GARBAGE")); let res = parse_sql_statements("CREATE TABLE t (a int NOT NULL CONSTRAINT foo)"); assert!(res .unwrap_err() .to_string() - .contains("Expected constraint details after CONSTRAINT ")); + .contains("Expected: constraint details after CONSTRAINT ")); } #[test] @@ -3052,7 +3052,7 @@ fn parse_create_table_with_constraint_characteristics() { assert!(res .unwrap_err() .to_string() - .contains("Expected \',\' or \')\' after column definition, found: NOT")); + .contains("Expected: \',\' or \')\' after column definition, found: NOT")); let res = parse_sql_statements("CREATE TABLE t ( a int NOT NULL, @@ -3061,7 +3061,7 @@ fn parse_create_table_with_constraint_characteristics() { assert!(res .unwrap_err() .to_string() - .contains("Expected \',\' or \')\' after column definition, found: ENFORCED")); + .contains("Expected: \',\' or \')\' after column definition, found: ENFORCED")); let res = parse_sql_statements("CREATE TABLE t ( a int NOT NULL, @@ -3070,7 +3070,7 @@ fn parse_create_table_with_constraint_characteristics() { assert!(res .unwrap_err() .to_string() - .contains("Expected \',\' or \')\' after column definition, found: INITIALLY")); + .contains("Expected: \',\' or \')\' after column definition, found: INITIALLY")); } #[test] @@ -3161,7 +3161,7 @@ fn parse_create_table_column_constraint_characteristics() { assert!(res .unwrap_err() .to_string() - .contains("Expected one of DEFERRED or IMMEDIATE, found: BADVALUE")); + .contains("Expected: one of DEFERRED or IMMEDIATE, found: BADVALUE")); let res = parse_sql_statements( "CREATE TABLE t (a int NOT NULL UNIQUE INITIALLY IMMEDIATE DEFERRABLE INITIALLY DEFERRED)", @@ -3260,7 +3260,7 @@ fn parse_create_table_hive_array() { assert_eq!( dialects.parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError("Expected >, found: )".to_string()) + ParserError::ParserError("Expected: >, found: )".to_string()) ); } @@ -4035,7 +4035,7 @@ fn parse_alter_table_alter_column_type() { 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()), + ParserError::ParserError("Expected: SET/DROP NOT NULL, SET DEFAULT, or SET DATA TYPE after ALTER COLUMN, found: TYPE".to_string()), res.unwrap_err() ); @@ -4043,7 +4043,7 @@ fn parse_alter_table_alter_column_type() { "{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'" )); assert_eq!( - ParserError::ParserError("Expected end of statement, found: USING".to_string()), + ParserError::ParserError("Expected: end of statement, found: USING".to_string()), res.unwrap_err() ); } @@ -4082,7 +4082,7 @@ fn parse_alter_table_drop_constraint() { let res = parse_sql_statements(&format!("{alter_stmt} DROP CONSTRAINT is_active TEXT")); assert_eq!( - ParserError::ParserError("Expected end of statement, found: TEXT".to_string()), + ParserError::ParserError("Expected: end of statement, found: TEXT".to_string()), res.unwrap_err() ); } @@ -4091,14 +4091,14 @@ fn parse_alter_table_drop_constraint() { fn parse_bad_constraint() { let res = parse_sql_statements("ALTER TABLE tab ADD"); assert_eq!( - ParserError::ParserError("Expected identifier, found: EOF".to_string()), + ParserError::ParserError("Expected: identifier, found: EOF".to_string()), res.unwrap_err() ); let res = parse_sql_statements("CREATE TABLE tab (foo int,"); assert_eq!( ParserError::ParserError( - "Expected column name or constraint definition, found: EOF".to_string() + "Expected: column name or constraint definition, found: EOF".to_string() ), res.unwrap_err() ); @@ -4440,7 +4440,7 @@ fn parse_window_clause() { let dialects = all_dialects_except(|d| d.is::() || d.is::()); let res = dialects.parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected (, found: window2".to_string()), + ParserError::ParserError("Expected: (, found: window2".to_string()), res.unwrap_err() ); } @@ -4851,13 +4851,13 @@ fn parse_interval() { let result = parse_sql_statements("SELECT INTERVAL '1' SECOND TO SECOND"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: SECOND".to_string()), + ParserError::ParserError("Expected: end of statement, found: SECOND".to_string()), result.unwrap_err(), ); let result = parse_sql_statements("SELECT INTERVAL '10' HOUR (1) TO HOUR (2)"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: (".to_string()), + ParserError::ParserError("Expected: end of statement, found: (".to_string()), result.unwrap_err(), ); @@ -5198,13 +5198,13 @@ fn parse_table_function() { let res = parse_sql_statements("SELECT * FROM TABLE '1' AS a"); assert_eq!( - ParserError::ParserError("Expected (, found: \'1\'".to_string()), + ParserError::ParserError("Expected: (, found: \'1\'".to_string()), res.unwrap_err() ); let res = parse_sql_statements("SELECT * FROM TABLE (FUN(a) AS a"); assert_eq!( - ParserError::ParserError("Expected ), found: AS".to_string()), + ParserError::ParserError("Expected: ), found: AS".to_string()), res.unwrap_err() ); } @@ -5752,7 +5752,7 @@ fn parse_natural_join() { let sql = "SELECT * FROM t1 natural"; assert_eq!( - ParserError::ParserError("Expected a join type after NATURAL, found: EOF".to_string()), + ParserError::ParserError("Expected: a join type after NATURAL, found: EOF".to_string()), parse_sql_statements(sql).unwrap_err(), ); } @@ -5833,7 +5833,7 @@ fn parse_join_syntax_variants() { let res = parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1"); assert_eq!( - ParserError::ParserError("Expected APPLY, found: JOIN".to_string()), + ParserError::ParserError("Expected: APPLY, found: JOIN".to_string()), res.unwrap_err() ); } @@ -5871,7 +5871,7 @@ fn parse_ctes() { Expr::Subquery(ref subquery) => { assert_ctes_in_select(&cte_sqls, subquery.as_ref()); } - _ => panic!("Expected subquery"), + _ => panic!("Expected: subquery"), } // CTE in a derived table let sql = &format!("SELECT * FROM ({with})"); @@ -5880,13 +5880,13 @@ fn parse_ctes() { TableFactor::Derived { subquery, .. } => { assert_ctes_in_select(&cte_sqls, subquery.as_ref()) } - _ => panic!("Expected derived table"), + _ => panic!("Expected: derived table"), } // CTE in a view let sql = &format!("CREATE VIEW v AS {with}"); match verified_stmt(sql) { Statement::CreateView { query, .. } => assert_ctes_in_select(&cte_sqls, &query), - _ => panic!("Expected CREATE VIEW"), + _ => panic!("Expected: CREATE VIEW"), } // CTE in a CTE... let sql = &format!("WITH outer_cte AS ({with}) SELECT * FROM outer_cte"); @@ -6047,7 +6047,7 @@ fn parse_multiple_statements() { // Check that forgetting the semicolon results in an error: let res = parse_sql_statements(&(sql1.to_owned() + " " + sql2_kw + sql2_rest)); assert_eq!( - ParserError::ParserError("Expected end of statement, found: ".to_string() + sql2_kw), + ParserError::ParserError("Expected: end of statement, found: ".to_string() + sql2_kw), res.unwrap_err() ); } @@ -6102,7 +6102,7 @@ fn parse_overlay() { "SELECT OVERLAY('abccccde' PLACING 'abc' FROM 3 FOR 12)", ); assert_eq!( - ParserError::ParserError("Expected PLACING, found: FROM".to_owned()), + ParserError::ParserError("Expected: PLACING, found: FROM".to_owned()), parse_sql_statements("SELECT OVERLAY('abccccde' FROM 3)").unwrap_err(), ); @@ -6151,7 +6151,7 @@ fn parse_trim() { ); assert_eq!( - ParserError::ParserError("Expected ), found: 'xyz'".to_owned()), + ParserError::ParserError("Expected: ), found: 'xyz'".to_owned()), parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')").unwrap_err() ); @@ -6173,7 +6173,7 @@ fn parse_trim() { options: None, }; assert_eq!( - ParserError::ParserError("Expected ), found: 'a'".to_owned()), + ParserError::ParserError("Expected: ), found: 'a'".to_owned()), all_expected_snowflake .parse_sql_statements("SELECT TRIM('xyz', 'a')") .unwrap_err() @@ -6210,7 +6210,7 @@ fn parse_exists_subquery() { .parse_sql_statements("SELECT EXISTS ("); assert_eq!( ParserError::ParserError( - "Expected SELECT, VALUES, or a subquery in the query body, found: EOF".to_string() + "Expected: SELECT, VALUES, or a subquery in the query body, found: EOF".to_string() ), res.unwrap_err(), ); @@ -6219,7 +6219,7 @@ fn parse_exists_subquery() { .parse_sql_statements("SELECT EXISTS (NULL)"); assert_eq!( ParserError::ParserError( - "Expected SELECT, VALUES, or a subquery in the query body, found: NULL".to_string() + "Expected: SELECT, VALUES, or a subquery in the query body, found: NULL".to_string() ), res.unwrap_err(), ); @@ -6581,7 +6581,7 @@ fn parse_drop_table() { let sql = "DROP TABLE"; assert_eq!( - ParserError::ParserError("Expected identifier, found: EOF".to_string()), + ParserError::ParserError("Expected: identifier, found: EOF".to_string()), parse_sql_statements(sql).unwrap_err(), ); @@ -6613,7 +6613,7 @@ fn parse_drop_view() { fn parse_invalid_subquery_without_parens() { let res = parse_sql_statements("SELECT SELECT 1 FROM bar WHERE 1=1 FROM baz"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: 1".to_string()), + ParserError::ParserError("Expected: end of statement, found: 1".to_string()), res.unwrap_err() ); } @@ -6826,7 +6826,7 @@ fn lateral_derived() { let sql = "SELECT * FROM LATERAL UNNEST ([10,20,30]) as numbers WITH OFFSET;"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected end of statement, found: WITH".to_string()), + ParserError::ParserError("Expected: end of statement, found: WITH".to_string()), res.unwrap_err() ); @@ -6834,7 +6834,7 @@ fn lateral_derived() { let res = parse_sql_statements(sql); assert_eq!( ParserError::ParserError( - "Expected SELECT, VALUES, or a subquery in the query body, found: b".to_string() + "Expected: SELECT, VALUES, or a subquery in the query body, found: b".to_string() ), res.unwrap_err() ); @@ -6952,19 +6952,19 @@ fn parse_start_transaction() { let res = parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD"); assert_eq!( - ParserError::ParserError("Expected isolation level, found: BAD".to_string()), + ParserError::ParserError("Expected: isolation level, found: BAD".to_string()), res.unwrap_err() ); let res = parse_sql_statements("START TRANSACTION BAD"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: BAD".to_string()), + ParserError::ParserError("Expected: end of statement, found: BAD".to_string()), res.unwrap_err() ); let res = parse_sql_statements("START TRANSACTION READ ONLY,"); assert_eq!( - ParserError::ParserError("Expected transaction mode, found: EOF".to_string()), + ParserError::ParserError("Expected: transaction mode, found: EOF".to_string()), res.unwrap_err() ); } @@ -7050,8 +7050,8 @@ fn parse_set_variable() { } let error_sqls = [ - ("SET (a, b, c) = (1, 2, 3", "Expected ), found: EOF"), - ("SET (a, b, c) = 1, 2, 3", "Expected (, found: 1"), + ("SET (a, b, c) = (1, 2, 3", "Expected: ), found: EOF"), + ("SET (a, b, c) = 1, 2, 3", "Expected: (, found: 1"), ]; for (sql, error) in error_sqls { assert_eq!( @@ -8051,19 +8051,19 @@ fn parse_offset_and_limit() { // Can't repeat OFFSET / LIMIT let res = parse_sql_statements("SELECT foo FROM bar OFFSET 2 OFFSET 2"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: OFFSET".to_string()), + ParserError::ParserError("Expected: end of statement, found: OFFSET".to_string()), res.unwrap_err() ); let res = parse_sql_statements("SELECT foo FROM bar LIMIT 2 LIMIT 2"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: LIMIT".to_string()), + ParserError::ParserError("Expected: end of statement, found: LIMIT".to_string()), res.unwrap_err() ); let res = parse_sql_statements("SELECT foo FROM bar OFFSET 2 LIMIT 2 OFFSET 2"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: OFFSET".to_string()), + ParserError::ParserError("Expected: end of statement, found: OFFSET".to_string()), res.unwrap_err() ); } @@ -8132,7 +8132,7 @@ fn parse_position_negative() { let sql = "SELECT POSITION(foo IN) from bar"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected an expression:, found: )".to_string()), + ParserError::ParserError("Expected: an expression:, found: )".to_string()), res.unwrap_err() ); } @@ -8190,7 +8190,7 @@ fn parse_is_boolean() { let res = parse_sql_statements(sql); assert_eq!( ParserError::ParserError( - "Expected [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: 0" + "Expected: [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: 0" .to_string() ), res.unwrap_err() @@ -8383,7 +8383,7 @@ fn parse_cache_table() { let res = parse_sql_statements("CACHE TABLE 'table_name' foo"); assert_eq!( ParserError::ParserError( - "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + "Expected: SELECT, VALUES, or a subquery in the query body, found: foo".to_string() ), res.unwrap_err() ); @@ -8391,7 +8391,7 @@ fn parse_cache_table() { let res = parse_sql_statements("CACHE flag TABLE 'table_name' OPTIONS('K1'='V1') foo"); assert_eq!( ParserError::ParserError( - "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + "Expected: SELECT, VALUES, or a subquery in the query body, found: foo".to_string() ), res.unwrap_err() ); @@ -8399,7 +8399,7 @@ fn parse_cache_table() { let res = parse_sql_statements("CACHE TABLE 'table_name' AS foo"); assert_eq!( ParserError::ParserError( - "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + "Expected: SELECT, VALUES, or a subquery in the query body, found: foo".to_string() ), res.unwrap_err() ); @@ -8407,26 +8407,26 @@ fn parse_cache_table() { let res = parse_sql_statements("CACHE flag TABLE 'table_name' OPTIONS('K1'='V1') AS foo"); assert_eq!( ParserError::ParserError( - "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + "Expected: SELECT, VALUES, or a subquery in the query body, found: foo".to_string() ), res.unwrap_err() ); let res = parse_sql_statements("CACHE 'table_name'"); assert_eq!( - ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + ParserError::ParserError("Expected: a `TABLE` keyword, found: 'table_name'".to_string()), res.unwrap_err() ); let res = parse_sql_statements("CACHE 'table_name' OPTIONS('K1'='V1')"); assert_eq!( - ParserError::ParserError("Expected a `TABLE` keyword, found: OPTIONS".to_string()), + ParserError::ParserError("Expected: a `TABLE` keyword, found: OPTIONS".to_string()), res.unwrap_err() ); let res = parse_sql_statements("CACHE flag 'table_name' OPTIONS('K1'='V1')"); assert_eq!( - ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + ParserError::ParserError("Expected: a `TABLE` keyword, found: 'table_name'".to_string()), res.unwrap_err() ); } @@ -8451,19 +8451,19 @@ fn parse_uncache_table() { let res = parse_sql_statements("UNCACHE TABLE 'table_name' foo"); assert_eq!( - ParserError::ParserError("Expected an `EOF`, found: foo".to_string()), + ParserError::ParserError("Expected: an `EOF`, found: foo".to_string()), res.unwrap_err() ); let res = parse_sql_statements("UNCACHE 'table_name' foo"); assert_eq!( - ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + ParserError::ParserError("Expected: a `TABLE` keyword, found: 'table_name'".to_string()), res.unwrap_err() ); let res = parse_sql_statements("UNCACHE IF EXISTS 'table_name' foo"); assert_eq!( - ParserError::ParserError("Expected a `TABLE` keyword, found: IF".to_string()), + ParserError::ParserError("Expected: a `TABLE` keyword, found: IF".to_string()), res.unwrap_err() ); } @@ -8927,7 +8927,7 @@ fn parse_trailing_comma() { .parse_sql_statements("CREATE TABLE employees (name text, age int,)") .unwrap_err(), ParserError::ParserError( - "Expected column name or constraint definition, found: )".to_string() + "Expected: column name or constraint definition, found: )".to_string() ) ); } @@ -8955,7 +8955,7 @@ fn parse_projection_trailing_comma() { trailing_commas .parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,") .unwrap_err(), - ParserError::ParserError("Expected an expression:, found: EOF".to_string()) + ParserError::ParserError("Expected: an expression:, found: EOF".to_string()) ); assert_eq!( @@ -8963,7 +8963,7 @@ fn parse_projection_trailing_comma() { .parse_sql_statements("CREATE TABLE employees (name text, age int,)") .unwrap_err(), ParserError::ParserError( - "Expected column name or constraint definition, found: )".to_string() + "Expected: column name or constraint definition, found: )".to_string() ), ); } @@ -9962,14 +9962,14 @@ fn tests_select_values_without_parens_and_set_op() { assert_eq!(SetOperator::Union, op); match *left { SetExpr::Select(_) => {} - _ => panic!("Expected a SELECT statement"), + _ => panic!("Expected: a SELECT statement"), } match *right { SetExpr::Select(_) => {} - _ => panic!("Expected a SELECT statement"), + _ => panic!("Expected: a SELECT statement"), } } - _ => panic!("Expected a SET OPERATION"), + _ => panic!("Expected: a SET OPERATION"), } } @@ -10003,7 +10003,7 @@ fn parse_select_wildcard_with_except() { .parse_sql_statements("SELECT * EXCEPT () FROM employee_table") .unwrap_err() .to_string(), - "sql parser error: Expected identifier, found: )" + "sql parser error: Expected: identifier, found: )" ); } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 430647ded..90056f0f7 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -64,7 +64,7 @@ fn test_databricks_exists() { let res = databricks().parse_sql_statements("SELECT EXISTS ("); assert_eq!( // TODO: improve this error message... - ParserError::ParserError("Expected an expression:, found: EOF".to_string()), + ParserError::ParserError("Expected: an expression:, found: EOF".to_string()), res.unwrap_err(), ); } diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index b661b6cd3..a5a6e2435 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -284,7 +284,7 @@ fn set_statement_with_minus() { assert_eq!( hive().parse_sql_statements("SET hive.tez.java.opts = -"), Err(ParserError::ParserError( - "Expected variable value, found: EOF".to_string() + "Expected: variable value, found: EOF".to_string() )) ) } @@ -327,14 +327,14 @@ fn parse_create_function() { assert_eq!( unsupported_dialects.parse_sql_statements(sql).unwrap_err(), ParserError::ParserError( - "Expected an object type after CREATE, found: FUNCTION".to_string() + "Expected: an object type after CREATE, found: FUNCTION".to_string() ) ); let sql = "CREATE TEMPORARY FUNCTION mydb.myfunc AS 'org.random.class.Name' USING JAR"; assert_eq!( hive().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError("Expected literal string, found: EOF".to_string()), + ParserError::ParserError("Expected: literal string, found: EOF".to_string()), ); } @@ -398,7 +398,7 @@ fn parse_delimited_identifiers() { assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); assert_eq!(&Ident::with_quote('"', "column alias"), alias); } - _ => panic!("Expected ExprWithAlias"), + _ => panic!("Expected: ExprWithAlias"), } hive().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 86d3990f6..f570de11d 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -481,7 +481,7 @@ fn parse_convert() { let error_sql = "SELECT CONVERT(INT, 'foo',) FROM T"; assert_eq!( - ParserError::ParserError("Expected an expression:, found: )".to_owned()), + ParserError::ParserError("Expected: an expression:, found: )".to_owned()), ms().parse_sql_statements(error_sql).unwrap_err() ); } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ff8a49de7..a25f4c208 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2518,7 +2518,7 @@ fn parse_fulltext_expression() { } #[test] -#[should_panic = "Expected FULLTEXT or SPATIAL option without constraint name, found: cons"] +#[should_panic = "Expected: FULLTEXT or SPATIAL option without constraint name, found: cons"] fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name() { mysql_and_generic().verified_stmt("CREATE TABLE tb (c1 INT, CONSTRAINT cons FULLTEXT (c1))"); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fe735b8b2..63c53227a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -648,7 +648,7 @@ fn parse_alter_table_alter_column_add_generated() { "ALTER TABLE t ALTER COLUMN id ADD GENERATED ( INCREMENT 1 MINVALUE 1 )", ); assert_eq!( - ParserError::ParserError("Expected AS, found: (".to_string()), + ParserError::ParserError("Expected: AS, found: (".to_string()), res.unwrap_err() ); @@ -656,14 +656,14 @@ fn parse_alter_table_alter_column_add_generated() { "ALTER TABLE t ALTER COLUMN id ADD GENERATED AS IDENTITY ( INCREMENT )", ); assert_eq!( - ParserError::ParserError("Expected a value, found: )".to_string()), + ParserError::ParserError("Expected: a value, found: )".to_string()), res.unwrap_err() ); let res = pg().parse_sql_statements("ALTER TABLE t ALTER COLUMN id ADD GENERATED AS IDENTITY ("); assert_eq!( - ParserError::ParserError("Expected ), found: EOF".to_string()), + ParserError::ParserError("Expected: ), found: EOF".to_string()), res.unwrap_err() ); } @@ -733,25 +733,25 @@ fn parse_create_table_if_not_exists() { fn parse_bad_if_not_exists() { let res = pg().parse_sql_statements("CREATE TABLE NOT EXISTS uk_cities ()"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: EXISTS".to_string()), + ParserError::ParserError("Expected: end of statement, found: EXISTS".to_string()), res.unwrap_err() ); let res = pg().parse_sql_statements("CREATE TABLE IF EXISTS uk_cities ()"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: EXISTS".to_string()), + ParserError::ParserError("Expected: end of statement, found: EXISTS".to_string()), res.unwrap_err() ); let res = pg().parse_sql_statements("CREATE TABLE IF uk_cities ()"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: uk_cities".to_string()), + ParserError::ParserError("Expected: end of statement, found: uk_cities".to_string()), res.unwrap_err() ); let res = pg().parse_sql_statements("CREATE TABLE IF NOT uk_cities ()"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: NOT".to_string()), + ParserError::ParserError("Expected: end of statement, found: NOT".to_string()), res.unwrap_err() ); } @@ -1300,21 +1300,21 @@ fn parse_set() { assert_eq!( pg_and_generic().parse_sql_statements("SET"), Err(ParserError::ParserError( - "Expected identifier, found: EOF".to_string() + "Expected: identifier, found: EOF".to_string() )), ); assert_eq!( pg_and_generic().parse_sql_statements("SET a b"), Err(ParserError::ParserError( - "Expected equals sign or TO, found: b".to_string() + "Expected: equals sign or TO, found: b".to_string() )), ); assert_eq!( pg_and_generic().parse_sql_statements("SET a ="), Err(ParserError::ParserError( - "Expected variable value, found: EOF".to_string() + "Expected: variable value, found: EOF".to_string() )), ); } @@ -2685,7 +2685,7 @@ fn parse_json_table_is_not_reserved() { name: ObjectName(name), .. } => assert_eq!("JSON_TABLE", name[0].value), - other => panic!("Expected JSON_TABLE to be parsed as a table name, but got {other:?}"), + other => panic!("Expected: JSON_TABLE to be parsed as a table name, but got {other:?}"), } } @@ -2874,7 +2874,7 @@ fn parse_escaped_literal_string() { .parse_sql_statements(sql) .unwrap_err() .to_string(), - "sql parser error: Unterminated encoded string literal at Line: 1, Column 8" + "sql parser error: Unterminated encoded string literal at Line: 1, Column: 8" ); let sql = r"SELECT E'\u0001', E'\U0010FFFF', E'\xC', E'\x25', E'\2', E'\45', E'\445'"; @@ -2917,7 +2917,7 @@ fn parse_escaped_literal_string() { .parse_sql_statements(sql) .unwrap_err() .to_string(), - "sql parser error: Unterminated encoded string literal at Line: 1, Column 8" + "sql parser error: Unterminated encoded string literal at Line: 1, Column: 8" ); } } @@ -3455,7 +3455,7 @@ fn parse_delimited_identifiers() { assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); assert_eq!(&Ident::with_quote('"', "column alias"), alias); } - _ => panic!("Expected ExprWithAlias"), + _ => panic!("Expected: ExprWithAlias"), } pg().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index f0a7c7735..160bbcbd5 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -385,14 +385,14 @@ fn test_snowflake_create_invalid_local_global_table() { assert_eq!( snowflake().parse_sql_statements("CREATE LOCAL GLOBAL TABLE my_table (a INT)"), Err(ParserError::ParserError( - "Expected an SQL statement, found: LOCAL".to_string() + "Expected: an SQL statement, found: LOCAL".to_string() )) ); assert_eq!( snowflake().parse_sql_statements("CREATE GLOBAL LOCAL TABLE my_table (a INT)"), Err(ParserError::ParserError( - "Expected an SQL statement, found: GLOBAL".to_string() + "Expected: an SQL statement, found: GLOBAL".to_string() )) ); } @@ -402,21 +402,21 @@ fn test_snowflake_create_invalid_temporal_table() { assert_eq!( snowflake().parse_sql_statements("CREATE TEMP TEMPORARY TABLE my_table (a INT)"), Err(ParserError::ParserError( - "Expected an object type after CREATE, found: TEMPORARY".to_string() + "Expected: an object type after CREATE, found: TEMPORARY".to_string() )) ); assert_eq!( snowflake().parse_sql_statements("CREATE TEMP VOLATILE TABLE my_table (a INT)"), Err(ParserError::ParserError( - "Expected an object type after CREATE, found: VOLATILE".to_string() + "Expected: an object type after CREATE, found: VOLATILE".to_string() )) ); assert_eq!( snowflake().parse_sql_statements("CREATE TEMP TRANSIENT TABLE my_table (a INT)"), Err(ParserError::ParserError( - "Expected an object type after CREATE, found: TRANSIENT".to_string() + "Expected: an object type after CREATE, found: TRANSIENT".to_string() )) ); } @@ -851,7 +851,7 @@ fn parse_semi_structured_data_traversal() { .parse_sql_statements("SELECT a:42") .unwrap_err() .to_string(), - "sql parser error: Expected variant object key name, found: 42" + "sql parser error: Expected: variant object key name, found: 42" ); } @@ -908,7 +908,7 @@ fn parse_delimited_identifiers() { assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr); assert_eq!(&Ident::with_quote('"', "column alias"), alias); } - _ => panic!("Expected ExprWithAlias"), + _ => panic!("Expected: ExprWithAlias"), } snowflake().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); @@ -1034,7 +1034,7 @@ fn test_select_wildcard_with_exclude_and_rename() { .parse_sql_statements("SELECT * RENAME col_a AS col_b EXCLUDE col_z FROM data") .unwrap_err() .to_string(), - "sql parser error: Expected end of statement, found: EXCLUDE" + "sql parser error: Expected: end of statement, found: EXCLUDE" ); } @@ -1134,13 +1134,13 @@ fn parse_snowflake_declare_cursor() { let error_sql = "DECLARE c1 CURSOR SELECT id FROM invoices"; assert_eq!( - ParserError::ParserError("Expected FOR, found: SELECT".to_owned()), + ParserError::ParserError("Expected: FOR, found: SELECT".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); let error_sql = "DECLARE c1 CURSOR res"; assert_eq!( - ParserError::ParserError("Expected FOR, found: res".to_owned()), + ParserError::ParserError("Expected: FOR, found: res".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); } @@ -1188,13 +1188,13 @@ fn parse_snowflake_declare_result_set() { let error_sql = "DECLARE res RESULTSET DEFAULT"; assert_eq!( - ParserError::ParserError("Expected an expression:, found: EOF".to_owned()), + ParserError::ParserError("Expected: an expression:, found: EOF".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); let error_sql = "DECLARE res RESULTSET :="; assert_eq!( - ParserError::ParserError("Expected an expression:, found: EOF".to_owned()), + ParserError::ParserError("Expected: an expression:, found: EOF".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); } @@ -1280,19 +1280,19 @@ fn parse_snowflake_declare_variable() { let error_sql = "DECLARE profit INT 2"; assert_eq!( - ParserError::ParserError("Expected end of statement, found: 2".to_owned()), + ParserError::ParserError("Expected: end of statement, found: 2".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); let error_sql = "DECLARE profit INT DEFAULT"; assert_eq!( - ParserError::ParserError("Expected an expression:, found: EOF".to_owned()), + ParserError::ParserError("Expected: an expression:, found: EOF".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); let error_sql = "DECLARE profit DEFAULT"; assert_eq!( - ParserError::ParserError("Expected an expression:, found: EOF".to_owned()), + ParserError::ParserError("Expected: an expression:, found: EOF".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); } @@ -1327,7 +1327,7 @@ fn parse_snowflake_declare_multi_statements() { let error_sql = "DECLARE profit DEFAULT 42 c1 CURSOR FOR res;"; assert_eq!( - ParserError::ParserError("Expected end of statement, found: c1".to_owned()), + ParserError::ParserError("Expected: end of statement, found: c1".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); } @@ -1902,7 +1902,7 @@ fn test_snowflake_trim() { // missing comma separation let error_sql = "SELECT TRIM('xyz' 'a')"; assert_eq!( - ParserError::ParserError("Expected ), found: 'a'".to_owned()), + ParserError::ParserError("Expected: ), found: 'a'".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); } @@ -2064,7 +2064,7 @@ fn test_select_wildcard_with_ilike_double_quote() { let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE "%id" FROM tbl"#); assert_eq!( res.unwrap_err().to_string(), - "sql parser error: Expected ilike pattern, found: \"%id\"" + "sql parser error: Expected: ilike pattern, found: \"%id\"" ); } @@ -2073,7 +2073,7 @@ fn test_select_wildcard_with_ilike_number() { let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE 42 FROM tbl"#); assert_eq!( res.unwrap_err().to_string(), - "sql parser error: Expected ilike pattern, found: 42" + "sql parser error: Expected: ilike pattern, found: 42" ); } @@ -2082,7 +2082,7 @@ fn test_select_wildcard_with_ilike_replace() { let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE '%id%' EXCLUDE col FROM tbl"#); assert_eq!( res.unwrap_err().to_string(), - "sql parser error: Expected end of statement, found: EXCLUDE" + "sql parser error: Expected: end of statement, found: EXCLUDE" ); } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 1181c480b..e329abae7 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -428,7 +428,7 @@ fn invalid_empty_list() { let sql = "SELECT * FROM t1 WHERE a IN (,,)"; let sqlite = sqlite_with_options(ParserOptions::new().with_trailing_commas(true)); assert_eq!( - "sql parser error: Expected an expression:, found: ,", + "sql parser error: Expected: an expression:, found: ,", sqlite.parse_sql_statements(sql).unwrap_err().to_string() ); } @@ -452,17 +452,17 @@ fn parse_start_transaction_with_modifier() { }; let res = unsupported_dialects.parse_sql_statements("BEGIN DEFERRED"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: DEFERRED".to_string()), + ParserError::ParserError("Expected: end of statement, found: DEFERRED".to_string()), res.unwrap_err(), ); let res = unsupported_dialects.parse_sql_statements("BEGIN IMMEDIATE"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: IMMEDIATE".to_string()), + ParserError::ParserError("Expected: end of statement, found: IMMEDIATE".to_string()), res.unwrap_err(), ); let res = unsupported_dialects.parse_sql_statements("BEGIN EXCLUSIVE"); assert_eq!( - ParserError::ParserError("Expected end of statement, found: EXCLUSIVE".to_string()), + ParserError::ParserError("Expected: end of statement, found: EXCLUSIVE".to_string()), res.unwrap_err(), ); } From f3d2f78fb2b7ebdc539b0bec0be535b3d1b9d88f Mon Sep 17 00:00:00 2001 From: Bidaya0 Date: Sun, 23 Jun 2024 19:13:16 +0800 Subject: [PATCH 472/806] Support `TO` in `CREATE VIEW` clause for Clickhouse (#1313) Co-authored-by: Ifeanyi Ubah Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 12 ++++++++++-- src/parser/mod.rs | 9 +++++++++ tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_clickhouse.rs | 15 +++++++++++++++ tests/sqlparser_common.rs | 14 ++++++++++++++ tests/sqlparser_snowflake.rs | 1 + tests/sqlparser_sqlite.rs | 1 + 7 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 769bda598..70190b35b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2029,6 +2029,9 @@ pub enum Statement { if_not_exists: bool, /// if true, has SQLite `TEMP` or `TEMPORARY` clause temporary: bool, + /// if not None, has Clickhouse `TO` clause, specify the table into which to insert results + /// + to: Option, }, /// ```sql /// CREATE TABLE @@ -3329,15 +3332,20 @@ impl fmt::Display for Statement { with_no_schema_binding, if_not_exists, temporary, + to, } => { write!( f, - "CREATE {or_replace}{materialized}{temporary}VIEW {if_not_exists}{name}", + "CREATE {or_replace}{materialized}{temporary}VIEW {if_not_exists}{name}{to}", or_replace = if *or_replace { "OR REPLACE " } else { "" }, materialized = if *materialized { "MATERIALIZED " } else { "" }, name = name, temporary = if *temporary { "TEMPORARY " } else { "" }, - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + to = to + .as_ref() + .map(|to| format!(" TO {to}")) + .unwrap_or_default() )?; if let Some(comment) = comment { write!( diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 27520a6c4..c568640a9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4172,6 +4172,14 @@ impl<'a> Parser<'a> { }; } + let to = if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keyword(Keyword::TO) + { + Some(self.parse_object_name(false)?) + } else { + None + }; + let comment = if dialect_of!(self is SnowflakeDialect | GenericDialect) && self.parse_keyword(Keyword::COMMENT) { @@ -4209,6 +4217,7 @@ impl<'a> Parser<'a> { with_no_schema_binding, if_not_exists, temporary, + to, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index ec4ddca96..88e2ef912 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -315,6 +315,7 @@ fn parse_create_view_if_not_exists() { with_no_schema_binding: late_binding, if_not_exists, temporary, + .. } => { assert_eq!("mydataset.newview", name.to_string()); assert_eq!(Vec::::new(), columns); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index ed3b2de22..5cd483242 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -561,6 +561,21 @@ fn parse_select_star_except_no_parens() { ); } +#[test] +fn parse_create_materialized_view() { + // example sql + // https://clickhouse.com/docs/en/guides/developer/cascading-materialized-views + let sql = concat!( + "CREATE MATERIALIZED VIEW analytics.monthly_aggregated_data_mv ", + "TO analytics.monthly_aggregated_data ", + "AS SELECT toDate(toStartOfMonth(event_time)) ", + "AS month, domain_name, sumState(count_views) ", + "AS sumCountViews FROM analytics.hourly_data ", + "GROUP BY domain_name, month" + ); + clickhouse_and_generic().verified_stmt(sql); +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0149bad5d..f7162ddef 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6279,6 +6279,7 @@ fn parse_create_view() { with_no_schema_binding: late_binding, if_not_exists, temporary, + to, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -6291,6 +6292,7 @@ fn parse_create_view() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); + assert!(to.is_none()) } _ => unreachable!(), } @@ -6335,6 +6337,7 @@ fn parse_create_view_with_columns() { with_no_schema_binding: late_binding, if_not_exists, temporary, + to, } => { assert_eq!("v", name.to_string()); assert_eq!( @@ -6357,6 +6360,7 @@ fn parse_create_view_with_columns() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); + assert!(to.is_none()) } _ => unreachable!(), } @@ -6378,6 +6382,7 @@ fn parse_create_view_temporary() { with_no_schema_binding: late_binding, if_not_exists, temporary, + to, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -6390,6 +6395,7 @@ fn parse_create_view_temporary() { assert!(!late_binding); assert!(!if_not_exists); assert!(temporary); + assert!(to.is_none()) } _ => unreachable!(), } @@ -6411,6 +6417,7 @@ fn parse_create_or_replace_view() { with_no_schema_binding: late_binding, if_not_exists, temporary, + to, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -6423,6 +6430,7 @@ fn parse_create_or_replace_view() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); + assert!(to.is_none()) } _ => unreachable!(), } @@ -6448,6 +6456,7 @@ fn parse_create_or_replace_materialized_view() { with_no_schema_binding: late_binding, if_not_exists, temporary, + to, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -6460,6 +6469,7 @@ fn parse_create_or_replace_materialized_view() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); + assert!(to.is_none()) } _ => unreachable!(), } @@ -6481,6 +6491,7 @@ fn parse_create_materialized_view() { with_no_schema_binding: late_binding, if_not_exists, temporary, + to, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -6493,6 +6504,7 @@ fn parse_create_materialized_view() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); + assert!(to.is_none()) } _ => unreachable!(), } @@ -6514,6 +6526,7 @@ fn parse_create_materialized_view_with_cluster_by() { with_no_schema_binding: late_binding, if_not_exists, temporary, + to, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -6526,6 +6539,7 @@ fn parse_create_materialized_view_with_cluster_by() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); + assert!(to.is_none()) } _ => unreachable!(), } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 160bbcbd5..b6be2c3f5 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -552,6 +552,7 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { with_no_schema_binding: late_binding, if_not_exists, temporary, + .. } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index e329abae7..3670b1784 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -171,6 +171,7 @@ fn parse_create_view_temporary_if_not_exists() { with_no_schema_binding: late_binding, if_not_exists, temporary, + .. } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); From 7a9793b72e268f6e7e830ec6f4e857878e0b6bc7 Mon Sep 17 00:00:00 2001 From: Lorrens Pantelis <100197010+LorrensP-2158466@users.noreply.github.com> Date: Sun, 23 Jun 2024 13:14:57 +0200 Subject: [PATCH 473/806] Allow semi-colon at the end of UNCACHE statement (#1320) --- src/parser/mod.rs | 22 +++++++--------------- tests/sqlparser_common.rs | 6 +++--- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c568640a9..337c1dac5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3611,21 +3611,13 @@ impl<'a> Parser<'a> { /// Parse a UNCACHE TABLE statement pub fn parse_uncache_table(&mut self) -> Result { - let has_table = self.parse_keyword(Keyword::TABLE); - if has_table { - let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let table_name = self.parse_object_name(false)?; - if self.peek_token().token == Token::EOF { - Ok(Statement::UNCache { - table_name, - if_exists, - }) - } else { - self.expected("an `EOF`", self.peek_token()) - } - } else { - self.expected("a `TABLE` keyword", self.peek_token()) - } + self.expect_keyword(Keyword::TABLE)?; + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let table_name = self.parse_object_name(false)?; + Ok(Statement::UNCache { + table_name, + if_exists, + }) } /// SQLite-specific `CREATE VIRTUAL TABLE` diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f7162ddef..0f5afb341 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8465,19 +8465,19 @@ fn parse_uncache_table() { let res = parse_sql_statements("UNCACHE TABLE 'table_name' foo"); assert_eq!( - ParserError::ParserError("Expected: an `EOF`, found: foo".to_string()), + ParserError::ParserError("Expected: end of statement, found: foo".to_string()), res.unwrap_err() ); let res = parse_sql_statements("UNCACHE 'table_name' foo"); assert_eq!( - ParserError::ParserError("Expected: a `TABLE` keyword, found: 'table_name'".to_string()), + ParserError::ParserError("Expected: TABLE, found: 'table_name'".to_string()), res.unwrap_err() ); let res = parse_sql_statements("UNCACHE IF EXISTS 'table_name' foo"); assert_eq!( - ParserError::ParserError("Expected: a `TABLE` keyword, found: IF".to_string()), + ParserError::ParserError("Expected: TABLE, found: IF".to_string()), res.unwrap_err() ); } From a685e1199355b0150fd5a4f6c7b938ecc07a6818 Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 23 Jun 2024 19:36:05 +0800 Subject: [PATCH 474/806] Support parametric arguments to `FUNCTION` for ClickHouse dialect (#1315) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 12 ++++++++- src/ast/visitor.rs | 1 + src/parser/mod.rs | 33 ++++++++++++++++++++--- src/test_utils.rs | 1 + tests/sqlparser_clickhouse.rs | 50 +++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 14 ++++++++++ tests/sqlparser_duckdb.rs | 1 + tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 1 + tests/sqlparser_postgres.rs | 7 +++++ tests/sqlparser_redshift.rs | 1 + tests/sqlparser_snowflake.rs | 1 + tests/sqlparser_sqlite.rs | 1 + 13 files changed, 119 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 70190b35b..8182d1144 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4695,6 +4695,16 @@ impl fmt::Display for CloseCursor { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Function { pub name: ObjectName, + /// The parameters to the function, including any options specified within the + /// delimiting parentheses. + /// + /// Example: + /// ```plaintext + /// HISTOGRAM(0.5, 0.6)(x, y) + /// ``` + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/aggregate-functions/parametric-functions) + pub parameters: FunctionArguments, /// The arguments to the function, including any options specified within the /// delimiting parentheses. pub args: FunctionArguments, @@ -4723,7 +4733,7 @@ pub struct Function { impl fmt::Display for Function { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", self.name, self.args)?; + write!(f, "{}{}{}", self.name, self.parameters, self.args)?; if !self.within_group.is_empty() { write!( diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 57dcca2e5..1b8a43802 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -533,6 +533,7 @@ where /// null_treatment: None, /// filter: None, /// over: None, +/// parameters: FunctionArguments::None, /// within_group: vec![], /// }); /// } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 337c1dac5..537609973 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -27,6 +27,7 @@ use core::{ use log::debug; +use recursion::RecursionCounter; use IsLateral::*; use IsOptional::*; @@ -146,8 +147,6 @@ mod recursion { pub struct DepthGuard {} } -use recursion::RecursionCounter; - #[derive(PartialEq, Eq)] pub enum IsOptional { Optional, @@ -1002,6 +1001,7 @@ impl<'a> Parser<'a> { { Ok(Expr::Function(Function { name: ObjectName(vec![w.to_ident()]), + parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, filter: None, @@ -1058,6 +1058,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; Ok(Expr::Function(Function { name: ObjectName(vec![w.to_ident()]), + parameters: FunctionArguments::None, args: FunctionArguments::Subquery(query), filter: None, null_treatment: None, @@ -1293,6 +1294,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; return Ok(Expr::Function(Function { name, + parameters: FunctionArguments::None, args: FunctionArguments::Subquery(subquery), filter: None, null_treatment: None, @@ -1301,7 +1303,16 @@ impl<'a> Parser<'a> { })); } - let args = self.parse_function_argument_list()?; + let mut args = self.parse_function_argument_list()?; + let mut parameters = FunctionArguments::None; + // ClickHouse aggregations support parametric functions like `HISTOGRAM(0.5, 0.6)(x, y)` + // which (0.5, 0.6) is a parameter to the function. + if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.consume_token(&Token::LParen) + { + parameters = FunctionArguments::List(args); + args = self.parse_function_argument_list()?; + } let within_group = if self.parse_keywords(&[Keyword::WITHIN, Keyword::GROUP]) { self.expect_token(&Token::LParen)?; @@ -1350,6 +1361,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name, + parameters, args: FunctionArguments::List(args), null_treatment, filter, @@ -1382,6 +1394,7 @@ impl<'a> Parser<'a> { }; Ok(Expr::Function(Function { name, + parameters: FunctionArguments::None, args, filter: None, over: None, @@ -6470,6 +6483,7 @@ impl<'a> Parser<'a> { } else { Ok(Statement::Call(Function { name: object_name, + parameters: FunctionArguments::None, args: FunctionArguments::None, over: None, filter: None, @@ -8092,7 +8106,7 @@ impl<'a> Parser<'a> { pub fn parse_query_body(&mut self, precedence: u8) -> Result { // We parse the expression using a Pratt parser, as in `parse_expr()`. // Start by parsing a restricted SELECT or a `(subquery)`: - let mut expr = if self.parse_keyword(Keyword::SELECT) { + let expr = if self.parse_keyword(Keyword::SELECT) { SetExpr::Select(self.parse_select().map(Box::new)?) } else if self.consume_token(&Token::LParen) { // CTEs are not allowed here, but the parser currently accepts them @@ -8111,6 +8125,17 @@ impl<'a> Parser<'a> { ); }; + self.parse_remaining_set_exprs(expr, precedence) + } + + /// Parse any extra set expressions that may be present in a query body + /// + /// (this is its own function to reduce required stack size in debug builds) + fn parse_remaining_set_exprs( + &mut self, + mut expr: SetExpr, + precedence: u8, + ) -> Result { loop { // The query can be optionally followed by a set operator: let op = self.parse_set_operator(&self.peek_token().token); diff --git a/src/test_utils.rs b/src/test_utils.rs index 9af9c8098..1a31d4611 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -336,6 +336,7 @@ pub fn join(relation: TableFactor) -> Join { pub fn call(function: &str, args: impl IntoIterator) -> Expr { Expr::Function(Function { name: ObjectName(vec![Ident::new(function)]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: args diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 5cd483242..50d4faf5d 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -183,6 +183,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], @@ -553,6 +554,55 @@ fn parse_select_star_except() { clickhouse().verified_stmt("SELECT * EXCEPT (prev_status) FROM anomalies"); } +#[test] +fn parse_select_parametric_function() { + match clickhouse_and_generic().verified_stmt("SELECT HISTOGRAM(0.5, 0.6)(x, y) FROM t") { + Statement::Query(query) => { + let projection: &Vec = query.body.as_select().unwrap().projection.as_ref(); + assert_eq!(projection.len(), 1); + match &projection[0] { + UnnamedExpr(Expr::Function(f)) => { + let args = match &f.args { + FunctionArguments::List(ref args) => args, + _ => unreachable!(), + }; + assert_eq!(args.args.len(), 2); + assert_eq!( + args.args[0], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Identifier(Ident::from("x")))) + ); + assert_eq!( + args.args[1], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Identifier(Ident::from("y")))) + ); + + let parameters = match f.parameters { + FunctionArguments::List(ref args) => args, + _ => unreachable!(), + }; + assert_eq!(parameters.args.len(), 2); + assert_eq!( + parameters.args[0], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Number( + "0.5".parse().unwrap(), + false + )))) + ); + assert_eq!( + parameters.args[1], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Number( + "0.6".parse().unwrap(), + false + )))) + ); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } +} + #[test] fn parse_select_star_except_no_parens() { clickhouse().one_statement_parses_to( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0f5afb341..76e6a98bb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1045,6 +1045,7 @@ fn parse_select_count_wildcard() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], @@ -1066,6 +1067,7 @@ fn parse_select_count_distinct() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: Some(DuplicateTreatment::Distinct), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::UnaryOp { @@ -2151,6 +2153,7 @@ fn parse_select_having() { Some(Expr::BinaryOp { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], @@ -2180,6 +2183,7 @@ fn parse_select_qualify() { Some(Expr::BinaryOp { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("ROW_NUMBER")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], @@ -2523,6 +2527,7 @@ fn parse_listagg() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("LISTAGG")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: Some(DuplicateTreatment::Distinct), args: vec![ @@ -4227,6 +4232,7 @@ fn parse_named_argument_function() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("FUN")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![ @@ -4265,6 +4271,7 @@ fn parse_named_argument_function_with_eq_operator() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("FUN")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![ @@ -4337,6 +4344,7 @@ fn parse_window_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], @@ -4465,6 +4473,7 @@ fn test_parse_named_window() { value: "MIN".to_string(), quote_style: None, }]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( @@ -4494,6 +4503,7 @@ fn test_parse_named_window() { value: "MAX".to_string(), quote_style: None, }]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( @@ -8089,6 +8099,7 @@ fn parse_time_functions() { let select = verified_only_select(&sql); let select_localtime_func_call_ast = Function { name: ObjectName(vec![Ident::new(func_name)]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], @@ -9017,6 +9028,7 @@ fn parse_call() { assert_eq!( verified_stmt("CALL my_procedure('a')"), Statement::Call(Function { + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( @@ -9418,6 +9430,7 @@ fn test_selective_aggregation() { vec![ SelectItem::UnnamedExpr(Expr::Function(Function { name: ObjectName(vec![Ident::new("ARRAY_AGG")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( @@ -9435,6 +9448,7 @@ fn test_selective_aggregation() { SelectItem::ExprWithAlias { expr: Expr::Function(Function { name: ObjectName(vec![Ident::new("ARRAY_AGG")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 8d12945dd..eaa1faa90 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -488,6 +488,7 @@ fn test_duckdb_named_argument_function_with_assignment_operator() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("FUN")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![ diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index a5a6e2435..53280d7d8 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -381,6 +381,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index f570de11d..5f03bb093 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -354,6 +354,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 63c53227a..197597e9b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2369,6 +2369,7 @@ fn parse_array_subquery_expr() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("ARRAY")]), + parameters: FunctionArguments::None, args: FunctionArguments::Subquery(Box::new(Query { with: None, body: Box::new(SetExpr::SetOperation { @@ -2729,6 +2730,7 @@ fn test_composite_value() { Ident::new("information_schema"), Ident::new("_pg_expandarray") ]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Array( @@ -2955,6 +2957,7 @@ fn parse_current_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_CATALOG")]), + parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, filter: None, @@ -2966,6 +2969,7 @@ fn parse_current_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_USER")]), + parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, filter: None, @@ -2977,6 +2981,7 @@ fn parse_current_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("SESSION_USER")]), + parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, filter: None, @@ -2988,6 +2993,7 @@ fn parse_current_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("USER")]), + parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, filter: None, @@ -3438,6 +3444,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 0a5710ff4..938e6e887 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -136,6 +136,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b6be2c3f5..5e8fef0c5 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -892,6 +892,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![], diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 3670b1784..dd1e77d5d 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -335,6 +335,7 @@ fn parse_window_function_with_filter() { select.projection, vec![SelectItem::UnnamedExpr(Expr::Function(Function { name: ObjectName(vec![Ident::new(func_name)]), + parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( From f5ccef6ea9234dc2b9fcd15dfda2551aced19309 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Thu, 27 Jun 2024 15:56:21 +0400 Subject: [PATCH 475/806] Fix Snowflake `SELECT *` wildcard `REPLACE ... RENAME` order (#1321) --- src/ast/query.rs | 13 ++++++------ src/parser/mod.rs | 11 +++++----- tests/sqlparser_snowflake.rs | 39 ++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index fcd5b970d..0fde3e6b7 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -547,19 +547,20 @@ impl fmt::Display for IdentWithAlias { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WildcardAdditionalOptions { /// `[ILIKE...]`. - /// Snowflake syntax: + /// Snowflake syntax: pub opt_ilike: Option, /// `[EXCLUDE...]`. pub opt_exclude: Option, /// `[EXCEPT...]`. /// Clickhouse syntax: pub opt_except: Option, - /// `[RENAME ...]`. - pub opt_rename: Option, /// `[REPLACE]` /// BigQuery syntax: /// Clickhouse syntax: + /// Snowflake syntax: pub opt_replace: Option, + /// `[RENAME ...]`. + pub opt_rename: Option, } impl fmt::Display for WildcardAdditionalOptions { @@ -573,12 +574,12 @@ impl fmt::Display for WildcardAdditionalOptions { if let Some(except) = &self.opt_except { write!(f, " {except}")?; } - if let Some(rename) = &self.opt_rename { - write!(f, " {rename}")?; - } if let Some(replace) = &self.opt_replace { write!(f, " {replace}")?; } + if let Some(rename) = &self.opt_rename { + write!(f, " {rename}")?; + } Ok(()) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 537609973..33095c428 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10175,15 +10175,14 @@ impl<'a> Parser<'a> { } else { None }; - let opt_rename = if dialect_of!(self is GenericDialect | SnowflakeDialect) { - self.parse_optional_select_item_rename()? + let opt_replace = if dialect_of!(self is GenericDialect | BigQueryDialect | ClickHouseDialect | DuckDbDialect | SnowflakeDialect) + { + self.parse_optional_select_item_replace()? } else { None }; - - let opt_replace = if dialect_of!(self is GenericDialect | BigQueryDialect | ClickHouseDialect | DuckDbDialect | SnowflakeDialect) - { - self.parse_optional_select_item_replace()? + let opt_rename = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_rename()? } else { None }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5e8fef0c5..2f4ed1316 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1016,6 +1016,44 @@ fn test_select_wildcard_with_rename() { assert_eq!(expected, select.projection[0]); } +#[test] +fn test_select_wildcard_with_replace_and_rename() { + let select = snowflake_and_generic().verified_only_select( + "SELECT * REPLACE (col_z || col_z AS col_z) RENAME (col_z AS col_zz) FROM data", + ); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_replace: Some(ReplaceSelectItem { + items: vec![Box::new(ReplaceSelectElement { + expr: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col_z"))), + op: BinaryOperator::StringConcat, + right: Box::new(Expr::Identifier(Ident::new("col_z"))), + }, + column_name: Ident::new("col_z"), + as_keyword: true, + })], + }), + opt_rename: Some(RenameSelectItem::Multiple(vec![IdentWithAlias { + ident: Ident::new("col_z"), + alias: Ident::new("col_zz"), + }])), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); + + // rename cannot precede replace + // https://docs.snowflake.com/en/sql-reference/sql/select#parameters + assert_eq!( + snowflake_and_generic() + .parse_sql_statements( + "SELECT * RENAME (col_z AS col_zz) REPLACE (col_z || col_z AS col_z) FROM data" + ) + .unwrap_err() + .to_string(), + "sql parser error: Expected: end of statement, found: REPLACE" + ); +} + #[test] fn test_select_wildcard_with_exclude_and_rename() { let select = snowflake_and_generic() @@ -1031,6 +1069,7 @@ fn test_select_wildcard_with_exclude_and_rename() { assert_eq!(expected, select.projection[0]); // rename cannot precede exclude + // https://docs.snowflake.com/en/sql-reference/sql/select#parameters assert_eq!( snowflake_and_generic() .parse_sql_statements("SELECT * RENAME col_a AS col_b EXCLUDE col_z FROM data") From f9ab8dcc27fd2d55030b9c5fa71e41d5c08dd601 Mon Sep 17 00:00:00 2001 From: gstvg <28798827+gstvg@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:58:11 -0300 Subject: [PATCH 476/806] Support for DuckDB Union datatype (#1322) --- src/ast/data_type.rs | 9 +++- src/ast/mod.rs | 17 +++++++ src/parser/mod.rs | 31 +++++++++++++ tests/sqlparser_duckdb.rs | 95 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 6b1a542f4..e6477f56b 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::ast::{display_comma_separated, ObjectName, StructField}; +use crate::ast::{display_comma_separated, ObjectName, StructField, UnionField}; use super::{value::escape_single_quote_string, ColumnDef}; @@ -303,6 +303,10 @@ pub enum DataType { /// [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), + /// Union + /// + /// [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 @@ -516,6 +520,9 @@ impl fmt::Display for DataType { write!(f, "STRUCT") } } + DataType::Union(fields) => { + write!(f, "UNION({})", display_comma_separated(fields)) + } // ClickHouse DataType::Nullable(data_type) => { write!(f, "Nullable({})", data_type) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8182d1144..9ed837825 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -294,6 +294,23 @@ impl fmt::Display for StructField { } } +/// A field definition within a union +/// +/// [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))] +pub struct UnionField { + pub field_name: Ident, + pub field_type: DataType, +} + +impl fmt::Display for UnionField { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.field_name, self.field_type) + } +} + /// A dictionary field within a dictionary. /// /// [duckdb]: https://duckdb.org/docs/sql/data_types/struct#creating-structs diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 33095c428..f58304960 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2246,6 +2246,32 @@ impl<'a> Parser<'a> { )) } + /// DuckDB specific: Parse a Union type definition as a sequence of field-value pairs. + /// + /// Syntax: + /// + /// ```sql + /// UNION(field_name field_type[,...]) + /// ``` + /// + /// [1]: https://duckdb.org/docs/sql/data_types/union.html + fn parse_union_type_def(&mut self) -> Result, ParserError> { + self.expect_keyword(Keyword::UNION)?; + + self.expect_token(&Token::LParen)?; + + let fields = self.parse_comma_separated(|p| { + Ok(UnionField { + field_name: p.parse_identifier(false)?, + field_type: p.parse_data_type()?, + }) + })?; + + self.expect_token(&Token::RParen)?; + + Ok(fields) + } + /// DuckDB specific: Parse a duckdb dictionary [1] /// /// Syntax: @@ -7136,6 +7162,11 @@ impl<'a> Parser<'a> { trailing_bracket = _trailing_bracket; Ok(DataType::Struct(field_defs)) } + Keyword::UNION if dialect_of!(self is DuckDbDialect | GenericDialect) => { + self.prev_token(); + let fields = self.parse_union_type_def()?; + Ok(DataType::Union(fields)) + } Keyword::NULLABLE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { Ok(self.parse_sub_type(DataType::Nullable)?) } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index eaa1faa90..253318b32 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -545,3 +545,98 @@ fn test_array_index() { expr ); } + +#[test] +fn test_duckdb_union_datatype() { + let sql = "CREATE TABLE tbl1 (one UNION(a INT), two UNION(a INT, b INT), nested UNION(a UNION(b INT)))"; + let stmt = duckdb_and_generic().verified_stmt(sql); + assert_eq!( + Statement::CreateTable(CreateTable { + or_replace: Default::default(), + temporary: Default::default(), + external: Default::default(), + global: Default::default(), + if_not_exists: Default::default(), + transient: Default::default(), + volatile: Default::default(), + name: ObjectName(vec!["tbl1".into()]), + columns: vec![ + ColumnDef { + name: "one".into(), + data_type: DataType::Union(vec![UnionField { + field_name: "a".into(), + field_type: DataType::Int(None) + }]), + collation: Default::default(), + options: Default::default() + }, + ColumnDef { + name: "two".into(), + data_type: DataType::Union(vec![ + UnionField { + field_name: "a".into(), + field_type: DataType::Int(None) + }, + UnionField { + field_name: "b".into(), + field_type: DataType::Int(None) + } + ]), + collation: Default::default(), + options: Default::default() + }, + ColumnDef { + name: "nested".into(), + data_type: DataType::Union(vec![UnionField { + field_name: "a".into(), + field_type: DataType::Union(vec![UnionField { + field_name: "b".into(), + field_type: DataType::Int(None) + }]) + }]), + collation: Default::default(), + options: Default::default() + } + ], + constraints: Default::default(), + hive_distribution: HiveDistributionStyle::NONE, + hive_formats: Some(HiveFormat { + row_format: Default::default(), + serde_properties: Default::default(), + 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(), + order_by: Default::default(), + partition_by: Default::default(), + cluster_by: Default::default(), + options: Default::default(), + strict: Default::default(), + copy_grants: Default::default(), + enable_schema_evolution: Default::default(), + change_tracking: Default::default(), + data_retention_time_in_days: Default::default(), + max_data_extension_time_in_days: Default::default(), + default_ddl_collation: Default::default(), + with_aggregation_policy: Default::default(), + with_row_access_policy: Default::default(), + with_tags: Default::default() + }), + stmt + ); +} From 376889ae5de7b4e738dd097ce08b0867475aacbb Mon Sep 17 00:00:00 2001 From: Emil Sivervik Date: Sun, 30 Jun 2024 13:03:08 +0200 Subject: [PATCH 477/806] chore(docs): refine docs (#1326) --- src/parser/mod.rs | 105 +++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f58304960..869662976 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -71,11 +71,11 @@ mod recursion { use super::ParserError; /// Tracks remaining recursion depth. This value is decremented on - /// each call to `try_decrease()`, when it reaches 0 an error will + /// each call to [`RecursionCounter::try_decrease()`], when it reaches 0 an error will /// be returned. /// - /// Note: Uses an Rc and Cell in order to satisfy the Rust - /// borrow checker so the automatic DepthGuard decrement a + /// Note: Uses an [`std::rc::Rc`] and [`std::cell::Cell`] in order to satisfy the Rust + /// borrow checker so the automatic [`DepthGuard`] decrement a /// reference to the counter. pub(crate) struct RecursionCounter { remaining_depth: Rc>, @@ -92,7 +92,7 @@ mod recursion { /// Decreases the remaining depth by 1. /// - /// Returns `Err` if the remaining depth falls to 0. + /// Returns [`Err`] if the remaining depth falls to 0. /// /// Returns a [`DepthGuard`] which will adds 1 to the /// remaining depth upon drop; @@ -131,7 +131,7 @@ mod recursion { /// Implementation [`RecursionCounter`] if std is NOT available (and does not /// guard against stack overflow). /// - /// Has the same API as the std RecursionCounter implementation + /// Has the same API as the std [`RecursionCounter`] implementation /// but does not actually limit stack depth. pub(crate) struct RecursionCounter {} @@ -270,17 +270,17 @@ enum ParserState { pub struct Parser<'a> { tokens: Vec, - /// The index of the first unprocessed token in `self.tokens` + /// The index of the first unprocessed token in [`Parser::tokens`]. index: usize, /// The current state of the parser. state: ParserState, - /// The current dialect to use + /// The current dialect to use. dialect: &'a dyn Dialect, /// Additional options that allow you to mix & match behavior /// otherwise constrained to certain dialects (e.g. trailing - /// commas) and/or format of parse (e.g. unescaping) + /// commas) and/or format of parse (e.g. unescaping). options: ParserOptions, - /// ensure the stack does not overflow by limiting recursion depth + /// Ensure the stack does not overflow by limiting recursion depth. recursion_counter: RecursionCounter, } @@ -313,7 +313,6 @@ impl<'a> Parser<'a> { /// Specify the maximum recursion limit while parsing. /// - /// /// [`Parser`] prevents stack overflows by returning /// [`ParserError::RecursionLimitExceeded`] if the parser exceeds /// this depth while processing the query. @@ -338,7 +337,6 @@ impl<'a> Parser<'a> { /// Specify additional parser options /// - /// /// [`Parser`] supports additional options ([`ParserOptions`]) /// that allow you to mix & match behavior otherwise constrained /// to certain dialects (e.g. trailing commas). @@ -824,7 +822,7 @@ impl<'a> Parser<'a> { }) } - /// Parse a new expression including wildcard & qualified wildcard + /// Parse a new expression including wildcard & qualified wildcard. pub fn parse_wildcard_expr(&mut self) -> Result { let index = self.index; @@ -867,13 +865,13 @@ impl<'a> Parser<'a> { self.parse_expr() } - /// Parse a new expression + /// Parse a new expression. pub fn parse_expr(&mut self) -> Result { let _guard = self.recursion_counter.try_decrease()?; self.parse_subexpr(0) } - /// Parse tokens until the precedence changes + /// Parse tokens until the precedence changes. pub fn parse_subexpr(&mut self, precedence: u8) -> Result { debug!("parsing expr"); let mut expr = self.parse_prefix()?; @@ -908,8 +906,7 @@ impl<'a> Parser<'a> { Ok(expr) } - /// Get the precedence of the next token - /// With AND, OR, and XOR + /// Get the precedence of the next token, with AND, OR, and XOR. pub fn get_next_interval_precedence(&self) -> Result { let token = self.peek_token(); @@ -944,7 +941,7 @@ impl<'a> Parser<'a> { Ok(Statement::ReleaseSavepoint { name }) } - /// Parse an expression prefix + /// Parse an expression prefix. pub fn parse_prefix(&mut self) -> Result { // allow the dialect to override prefix parsing if let Some(prefix) = self.dialect.parse_prefix(self) { @@ -1456,8 +1453,7 @@ impl<'a> Parser<'a> { } } - /// parse a group by expr. a group by expr can be one of group sets, roll up, cube, or simple - /// expr. + /// Parse a group by expr. Group by expr can be one of group sets, roll up, cube, or simple expr. fn parse_group_by_expr(&mut self) -> Result { if self.dialect.supports_group_by_expr() { if self.parse_keywords(&[Keyword::GROUPING, Keyword::SETS]) { @@ -1484,7 +1480,7 @@ impl<'a> Parser<'a> { } } - /// parse a tuple with `(` and `)`. + /// Parse a tuple with `(` and `)`. /// If `lift_singleton` is true, then a singleton tuple is lifted to a tuple of length 1, otherwise it will fail. /// If `allow_empty` is true, then an empty tuple is allowed. fn parse_tuple( @@ -1953,13 +1949,11 @@ impl<'a> Parser<'a> { } } - /// Parses fulltext expressions [(1)] + /// Parses fulltext expressions [`sqlparser::ast::Expr::MatchAgainst`] /// /// # Errors /// 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. - /// - /// [(1)]: Expr::MatchAgainst pub fn parse_match_against(&mut self) -> Result { let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -2004,17 +1998,19 @@ impl<'a> Parser<'a> { }) } - /// Parse an INTERVAL expression. + /// Parse an `INTERVAL` expression. /// /// Some syntactically valid intervals: /// - /// 1. `INTERVAL '1' DAY` - /// 2. `INTERVAL '1-1' YEAR TO MONTH` - /// 3. `INTERVAL '1' SECOND` - /// 4. `INTERVAL '1:1:1.1' HOUR (5) TO SECOND (5)` - /// 5. `INTERVAL '1.1' SECOND (2, 2)` - /// 6. `INTERVAL '1:1' HOUR (5) TO MINUTE (5)` - /// 7. (MySql and BigQuey only):`INTERVAL 1 DAY` + /// ```sql + /// 1. INTERVAL '1' DAY + /// 2. INTERVAL '1-1' YEAR TO MONTH + /// 3. INTERVAL '1' SECOND + /// 4. INTERVAL '1:1:1.1' HOUR (5) TO SECOND (5) + /// 5. INTERVAL '1.1' SECOND (2, 2) + /// 6. INTERVAL '1:1' HOUR (5) TO MINUTE (5) + /// 7. (MySql & BigQuey only): INTERVAL 1 DAY + /// ``` /// /// Note that we do not currently attempt to parse the quoted value. pub fn parse_interval(&mut self) -> Result { @@ -2210,15 +2206,15 @@ impl<'a> Parser<'a> { )) } - /// Parse a field definition in a struct [1] or tuple [2]. + /// Parse a field definition in a [struct] or [tuple]. /// Syntax: /// /// ```sql /// [field_name] field_type /// ``` /// - /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#declaring_a_struct_type - /// [2]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple + /// [struct]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#declaring_a_struct_type + /// [tuple]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple fn parse_struct_field_def( &mut self, ) -> Result<(StructField, MatchedTrailingBracket), ParserError> { @@ -2272,7 +2268,7 @@ impl<'a> Parser<'a> { Ok(fields) } - /// DuckDB specific: Parse a duckdb dictionary [1] + /// DuckDB specific: Parse a duckdb [dictionary] /// /// Syntax: /// @@ -2280,7 +2276,7 @@ impl<'a> Parser<'a> { /// {'field_name': expr1[, ... ]} /// ``` /// - /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs fn parse_duckdb_struct_literal(&mut self) -> Result { self.expect_token(&Token::LBrace)?; @@ -2291,13 +2287,15 @@ impl<'a> Parser<'a> { Ok(Expr::Dictionary(fields)) } - /// Parse a field for a duckdb dictionary [1] + /// Parse a field for a duckdb [dictionary] + /// /// Syntax + /// /// ```sql /// 'name': expr /// ``` /// - /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs fn parse_duckdb_dictionary_field(&mut self) -> Result { let key = self.parse_identifier(false)?; @@ -2311,13 +2309,15 @@ impl<'a> Parser<'a> { }) } - /// Parse clickhouse map [1] + /// Parse clickhouse [map] + /// /// Syntax + /// /// ```sql /// Map(key_data_type, value_data_type) /// ``` /// - /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/map + /// [map]: https://clickhouse.com/docs/en/sql-reference/data-types/map fn parse_click_house_map_def(&mut self) -> Result<(DataType, DataType), ParserError> { self.expect_keyword(Keyword::MAP)?; self.expect_token(&Token::LParen)?; @@ -2329,13 +2329,15 @@ impl<'a> Parser<'a> { Ok((key_data_type, value_data_type)) } - /// Parse clickhouse tuple [1] + /// Parse clickhouse [tuple] + /// /// Syntax + /// /// ```sql /// Tuple([field_name] field_type, ...) /// ``` /// - /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple + /// [tuple]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple fn parse_click_house_tuple_def(&mut self) -> Result, ParserError> { self.expect_keyword(Keyword::TUPLE)?; self.expect_token(&Token::LParen)?; @@ -2649,7 +2651,7 @@ impl<'a> Parser<'a> { } } - /// parse the ESCAPE CHAR portion of LIKE, ILIKE, and SIMILAR TO + /// Parse the `ESCAPE CHAR` portion of `LIKE`, `ILIKE`, and `SIMILAR TO` pub fn parse_escape_char(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::ESCAPE) { Ok(Some(self.parse_literal_string()?)) @@ -2836,7 +2838,7 @@ impl<'a> Parser<'a> { }) } - /// Parses the parens following the `[ NOT ] IN` operator + /// Parses the parens following the `[ NOT ] IN` operator. pub fn parse_in(&mut self, expr: Expr, negated: bool) -> Result { // BigQuery allows `IN UNNEST(array_expression)` // https://cloud.google.com/bigquery/docs/reference/standard-sql/operators#in_operators @@ -2873,7 +2875,7 @@ impl<'a> Parser<'a> { Ok(in_op) } - /// Parses `BETWEEN AND `, assuming the `BETWEEN` keyword was already consumed + /// Parses `BETWEEN AND `, assuming the `BETWEEN` keyword was already consumed. pub fn parse_between(&mut self, expr: Expr, negated: bool) -> Result { // Stop parsing subexpressions for and on tokens with // precedence lower than that of `BETWEEN`, such as `AND`, `IS`, etc. @@ -2888,7 +2890,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, @@ -2898,7 +2900,7 @@ impl<'a> Parser<'a> { }) } - // use https://www.postgresql.org/docs/7.0/operators.htm#AEN2026 as a reference + // Use https://www.postgresql.org/docs/7.0/operators.htm#AEN2026 as a reference // higher number = higher precedence // // NOTE: The pg documentation is incomplete, e.g. the AT TIME ZONE operator @@ -3217,7 +3219,7 @@ impl<'a> Parser<'a> { /// 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`. + /// and returns [`None`]. #[must_use] pub fn parse_one_of_keywords(&mut self, keywords: &[Keyword]) -> Option { match self.peek_token().token { @@ -3393,8 +3395,7 @@ impl<'a> Parser<'a> { self.parse_comma_separated(f) } - /// Run a parser method `f`, reverting back to the current position - /// if unsuccessful. + /// Run a parser method `f`, reverting back to the current position if unsuccessful. #[must_use] fn maybe_parse(&mut self, mut f: F) -> Option where @@ -3409,8 +3410,8 @@ impl<'a> Parser<'a> { } } - /// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns `None` if `ALL` is parsed - /// and results in a `ParserError` if both `ALL` and `DISTINCT` are found. + /// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns [`None`] if `ALL` is parsed + /// and results in a [`ParserError`] if both `ALL` and `DISTINCT` are found. pub fn parse_all_or_distinct(&mut self) -> Result, ParserError> { let loc = self.peek_token().location; let all = self.parse_keyword(Keyword::ALL); From 0b1a413e64006286308500731323d50617dc6ed8 Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 30 Jun 2024 19:06:20 +0800 Subject: [PATCH 478/806] Fix a few typos in comment lines (#1316) Co-authored-by: Andrew Lamb --- src/parser/mod.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 869662976..563fd86bc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -108,7 +108,7 @@ mod recursion { } } - /// Guard that increass the remaining depth by 1 on drop + /// Guard that increases the remaining depth by 1 on drop pub struct DepthGuard { remaining_depth: Rc>, } @@ -194,7 +194,7 @@ const DEFAULT_REMAINING_DEPTH: usize = 50; /// nested such that the following declaration is possible: /// `ARRAY>` /// But the tokenizer recognizes the `>>` as a ShiftRight token. -/// We work-around that limitation when parsing a data type by accepting +/// We work around that limitation when parsing a data type by accepting /// either a `>` or `>>` token in such cases, remembering which variant we /// matched. /// In the latter case having matched a `>>`, the parent type will not look to @@ -1075,7 +1075,7 @@ impl<'a> Parser<'a> { let expr = self.parse_subexpr(Self::PLUS_MINUS_PREC)?; Ok(Expr::Prior(Box::new(expr))) } - // Here `w` is a word, check if it's a part of a multi-part + // Here `w` is a word, check if it's a part of a multipart // identifier, a function call, or a simple identifier: _ => match self.peek_token().token { Token::LParen | Token::Period => { @@ -2009,7 +2009,7 @@ impl<'a> Parser<'a> { /// 4. INTERVAL '1:1:1.1' HOUR (5) TO SECOND (5) /// 5. INTERVAL '1.1' SECOND (2, 2) /// 6. INTERVAL '1:1' HOUR (5) TO MINUTE (5) - /// 7. (MySql & BigQuey only): INTERVAL 1 DAY + /// 7. (MySql & BigQuery only): INTERVAL 1 DAY /// ``` /// /// Note that we do not currently attempt to parse the quoted value. @@ -2749,7 +2749,7 @@ impl<'a> Parser<'a> { match token.token { Token::Word(Word { value, - // path segments in SF dot notation can be unquoted or double quoted + // path segments in SF dot notation can be unquoted or double-quoted quote_style: quote_style @ (Some('"') | None), // some experimentation suggests that snowflake permits // any keyword here unquoted. @@ -2948,7 +2948,7 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::NOT => match self.peek_nth_token(1).token { // The precedence of NOT varies depending on keyword that // follows it. If it is followed by IN, BETWEEN, or LIKE, - // it takes on the precedence of those tokens. Otherwise it + // it takes on the precedence of those tokens. Otherwise, it // is not an infix operator, and therefore has zero // precedence. Token::Word(w) if w.keyword == Keyword::IN => Ok(Self::BETWEEN_PREC), @@ -3251,7 +3251,7 @@ impl<'a> Parser<'a> { } /// If the current token is the `expected` keyword, consume the token. - /// Otherwise return an error. + /// Otherwise, return an error. pub fn expect_keyword(&mut self, expected: Keyword) -> Result<(), ParserError> { if self.parse_keyword(expected) { Ok(()) @@ -4508,7 +4508,7 @@ impl<'a> Parser<'a> { self.peek_token(), ); }; - // Many dialects support the non standard `IF EXISTS` clause and allow + // Many dialects support the non-standard `IF EXISTS` clause and allow // specifying multiple objects to delete in a single statement let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let names = self.parse_comma_separated(|p| p.parse_object_name(false))?; @@ -4822,7 +4822,7 @@ impl<'a> Parser<'a> { continue; } _ => { - // Put back the semi-colon, this is the end of the DECLARE statement. + // Put back the semicolon, this is the end of the DECLARE statement. self.prev_token(); } } @@ -7278,7 +7278,7 @@ impl<'a> Parser<'a> { // ignore the and treat the multiple strings as // a single ." Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))), - // Support for MySql dialect double quoted string, `AS "HOUR"` for example + // Support for MySql dialect double-quoted string, `AS "HOUR"` for example Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))), _ => { if after_as { From 44d7a20f641c9cd8f0c3e08f7d77f02534452ce8 Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 30 Jun 2024 19:33:43 +0800 Subject: [PATCH 479/806] Support `GROUP BY WITH MODIFIER` for ClickHouse (#1323) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 4 +-- src/ast/query.rs | 56 +++++++++++++++++++++++++++------ src/keywords.rs | 1 + src/parser/mod.rs | 37 +++++++++++++++++++--- tests/sqlparser_clickhouse.rs | 57 ++++++++++++++++++++++++++++++++- tests/sqlparser_common.rs | 53 +++++++++++++++++-------------- tests/sqlparser_duckdb.rs | 4 +-- tests/sqlparser_mssql.rs | 4 +-- tests/sqlparser_mysql.rs | 16 +++++----- tests/sqlparser_postgres.rs | 59 ++++++++++++++++++++--------------- 10 files changed, 215 insertions(+), 76 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9ed837825..c7f461418 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -43,8 +43,8 @@ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml, - GroupByExpr, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, JoinOperator, - JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, + GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, + JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, diff --git a/src/ast/query.rs b/src/ast/query.rs index 0fde3e6b7..d00a0dfcc 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -299,10 +299,10 @@ impl fmt::Display for Select { write!(f, " WHERE {selection}")?; } match &self.group_by { - GroupByExpr::All => write!(f, " GROUP BY ALL")?, - GroupByExpr::Expressions(exprs) => { + GroupByExpr::All(_) => write!(f, " {}", self.group_by)?, + GroupByExpr::Expressions(exprs, _) => { if !exprs.is_empty() { - write!(f, " GROUP BY {}", display_comma_separated(exprs))?; + write!(f, " {}", self.group_by)? } } } @@ -1866,27 +1866,65 @@ impl fmt::Display for SelectInto { } } +/// ClickHouse supports GROUP BY WITH modifiers(includes ROLLUP|CUBE|TOTALS). +/// e.g. GROUP BY year WITH ROLLUP WITH TOTALS +/// +/// [ClickHouse]: +#[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 GroupByWithModifier { + Rollup, + Cube, + Totals, +} + +impl fmt::Display for GroupByWithModifier { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GroupByWithModifier::Rollup => write!(f, "WITH ROLLUP"), + GroupByWithModifier::Cube => write!(f, "WITH CUBE"), + GroupByWithModifier::Totals => write!(f, "WITH TOTALS"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum GroupByExpr { - /// ALL syntax of [Snowflake], and [DuckDB] + /// ALL syntax of [Snowflake], [DuckDB] and [ClickHouse]. /// /// [Snowflake]: /// [DuckDB]: - All, + /// [ClickHouse]: + /// + /// ClickHouse also supports WITH modifiers after GROUP BY ALL and expressions. + /// + /// [ClickHouse]: + All(Vec), /// Expressions - Expressions(Vec), + Expressions(Vec, Vec), } impl fmt::Display for GroupByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - GroupByExpr::All => write!(f, "GROUP BY ALL"), - GroupByExpr::Expressions(col_names) => { + GroupByExpr::All(modifiers) => { + write!(f, "GROUP BY ALL")?; + if !modifiers.is_empty() { + write!(f, " {}", display_separated(modifiers, " "))?; + } + Ok(()) + } + GroupByExpr::Expressions(col_names, modifiers) => { let col_names = display_comma_separated(col_names); - write!(f, "GROUP BY ({col_names})") + write!(f, "GROUP BY {col_names}")?; + if !modifiers.is_empty() { + write!(f, " {}", display_separated(modifiers, " "))?; + } + Ok(()) } } } diff --git a/src/keywords.rs b/src/keywords.rs index e75d45e44..5db55e9da 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -721,6 +721,7 @@ define_keywords!( TINYINT, TO, TOP, + TOTALS, TRAILING, TRANSACTION, TRANSIENT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 563fd86bc..4e9c3836b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8319,13 +8319,42 @@ impl<'a> Parser<'a> { }; let group_by = if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { - if self.parse_keyword(Keyword::ALL) { - GroupByExpr::All + let expressions = if self.parse_keyword(Keyword::ALL) { + None } else { - GroupByExpr::Expressions(self.parse_comma_separated(Parser::parse_group_by_expr)?) + Some(self.parse_comma_separated(Parser::parse_group_by_expr)?) + }; + + let mut modifiers = vec![]; + if dialect_of!(self is ClickHouseDialect | GenericDialect) { + loop { + if !self.parse_keyword(Keyword::WITH) { + break; + } + let keyword = self.expect_one_of_keywords(&[ + Keyword::ROLLUP, + Keyword::CUBE, + Keyword::TOTALS, + ])?; + modifiers.push(match keyword { + Keyword::ROLLUP => GroupByWithModifier::Rollup, + Keyword::CUBE => GroupByWithModifier::Cube, + Keyword::TOTALS => GroupByWithModifier::Totals, + _ => { + return parser_err!( + "BUG: expected to match GroupBy modifier keyword", + self.peek_token().location + ) + } + }); + } + } + match expressions { + None => GroupByExpr::All(modifiers), + Some(exprs) => GroupByExpr::Expressions(exprs, modifiers), } } else { - GroupByExpr::Expressions(vec![]) + GroupByExpr::Expressions(vec![], vec![]) }; let cluster_by = if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 50d4faf5d..0c188a24b 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -88,7 +88,7 @@ fn parse_map_access_expr() { right: Box::new(Expr::Value(Value::SingleQuotedString("foo".to_string()))), }), }), - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -626,6 +626,61 @@ fn parse_create_materialized_view() { clickhouse_and_generic().verified_stmt(sql); } +#[test] +fn parse_group_by_with_modifier() { + let clauses = ["x", "a, b", "ALL"]; + let modifiers = [ + "WITH ROLLUP", + "WITH CUBE", + "WITH TOTALS", + "WITH ROLLUP WITH CUBE", + ]; + let expected_modifiers = [ + vec![GroupByWithModifier::Rollup], + vec![GroupByWithModifier::Cube], + vec![GroupByWithModifier::Totals], + vec![GroupByWithModifier::Rollup, GroupByWithModifier::Cube], + ]; + for clause in &clauses { + for (modifier, expected_modifier) in modifiers.iter().zip(expected_modifiers.iter()) { + let sql = format!("SELECT * FROM t GROUP BY {clause} {modifier}"); + match clickhouse_and_generic().verified_stmt(&sql) { + Statement::Query(query) => { + let group_by = &query.body.as_select().unwrap().group_by; + if clause == &"ALL" { + assert_eq!(group_by, &GroupByExpr::All(expected_modifier.to_vec())); + } else { + assert_eq!( + group_by, + &GroupByExpr::Expressions( + clause + .split(", ") + .map(|c| Identifier(Ident::new(c))) + .collect(), + expected_modifier.to_vec() + ) + ); + } + } + _ => unreachable!(), + } + } + } + + // invalid cases + let invalid_cases = [ + "SELECT * FROM t GROUP BY x WITH", + "SELECT * FROM t GROUP BY x WITH ROLLUP CUBE", + "SELECT * FROM t GROUP BY x WITH WITH ROLLUP", + "SELECT * FROM t GROUP BY WITH ROLLUP", + ]; + for sql in invalid_cases { + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("Expected: one of ROLLUP or CUBE or TOTALS, found: WITH"); + } +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 76e6a98bb..ac2133946 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -392,9 +392,10 @@ fn parse_update_set_from() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![Expr::Identifier(Ident::new( - "id" - ))]), + group_by: GroupByExpr::Expressions( + vec![Expr::Identifier(Ident::new("id"))], + vec![] + ), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -2119,10 +2120,13 @@ fn parse_select_group_by() { let sql = "SELECT id, fname, lname FROM customer GROUP BY lname, fname"; let select = verified_only_select(sql); assert_eq!( - GroupByExpr::Expressions(vec![ - Expr::Identifier(Ident::new("lname")), - Expr::Identifier(Ident::new("fname")), - ]), + GroupByExpr::Expressions( + vec![ + Expr::Identifier(Ident::new("lname")), + Expr::Identifier(Ident::new("fname")), + ], + vec![] + ), select.group_by ); @@ -2137,7 +2141,7 @@ fn parse_select_group_by() { fn parse_select_group_by_all() { let sql = "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL"; let select = verified_only_select(sql); - assert_eq!(GroupByExpr::All, select.group_by); + assert_eq!(GroupByExpr::All(vec![]), select.group_by); one_statement_parses_to( "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL", @@ -4545,7 +4549,7 @@ fn test_parse_named_window() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -4974,7 +4978,7 @@ fn parse_interval_and_or_xor() { }), }), }), - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -6908,7 +6912,7 @@ fn lateral_function() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -7627,7 +7631,7 @@ fn parse_merge() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -9133,7 +9137,7 @@ fn parse_unload() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -9276,7 +9280,7 @@ fn parse_connect_by() { into: None, lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -9364,7 +9368,7 @@ fn parse_connect_by() { op: BinaryOperator::NotEq, right: Box::new(Expr::Value(number("42"))), }), - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -9484,15 +9488,18 @@ fn test_group_by_grouping_sets() { all_dialects_where(|d| d.supports_group_by_expr()) .verified_only_select(sql) .group_by, - GroupByExpr::Expressions(vec![Expr::GroupingSets(vec![ - vec![ - Expr::Identifier(Ident::new("city")), - Expr::Identifier(Ident::new("car_model")) - ], - vec![Expr::Identifier(Ident::new("city")),], - vec![Expr::Identifier(Ident::new("car_model"))], + GroupByExpr::Expressions( + vec![Expr::GroupingSets(vec![ + vec![ + Expr::Identifier(Ident::new("city")), + Expr::Identifier(Ident::new("car_model")) + ], + vec![Expr::Identifier(Ident::new("city")),], + vec![Expr::Identifier(Ident::new("car_model"))], + vec![] + ])], vec![] - ])]) + ) ); } diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 253318b32..948e150c9 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -171,7 +171,7 @@ fn test_select_union_by_name() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -209,7 +209,7 @@ fn test_select_union_by_name() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 5f03bb093..993850299 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -111,7 +111,7 @@ fn parse_create_procedure() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -528,7 +528,7 @@ fn parse_substring_in_select() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a25f4c208..4c18d4a75 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -907,7 +907,7 @@ fn parse_escaped_quote_identifiers_with_escape() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -954,7 +954,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -998,7 +998,7 @@ fn parse_escaped_backticks_with_escape() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1042,7 +1042,7 @@ fn parse_escaped_backticks_with_no_escape() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1703,7 +1703,7 @@ fn parse_select_with_numeric_prefix_column_name() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1756,7 +1756,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -2255,7 +2255,7 @@ fn parse_substring_in_select() { }], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -2559,7 +2559,7 @@ fn parse_hex_string_introducer() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 197597e9b..2606fb96e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1075,7 +1075,7 @@ fn parse_copy_to() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), having: None, named_window: vec![], window_before_qualify: false, @@ -2383,7 +2383,7 @@ fn parse_array_subquery_expr() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -2402,7 +2402,7 @@ fn parse_array_subquery_expr() { from: vec![], lateral_views: vec![], selection: None, - group_by: GroupByExpr::Expressions(vec![]), + group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -3711,14 +3711,17 @@ fn parse_select_group_by_grouping_sets() { "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, GROUPING SETS ((brand), (size), ())" ); assert_eq!( - GroupByExpr::Expressions(vec![ - Expr::Identifier(Ident::new("size")), - Expr::GroupingSets(vec![ - vec![Expr::Identifier(Ident::new("brand"))], - vec![Expr::Identifier(Ident::new("size"))], - vec![], - ]), - ]), + GroupByExpr::Expressions( + vec![ + Expr::Identifier(Ident::new("size")), + Expr::GroupingSets(vec![ + vec![Expr::Identifier(Ident::new("brand"))], + vec![Expr::Identifier(Ident::new("size"))], + vec![], + ]), + ], + vec![] + ), select.group_by ); } @@ -3729,13 +3732,16 @@ fn parse_select_group_by_rollup() { "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, ROLLUP (brand, size)", ); assert_eq!( - GroupByExpr::Expressions(vec![ - Expr::Identifier(Ident::new("size")), - Expr::Rollup(vec![ - vec![Expr::Identifier(Ident::new("brand"))], - vec![Expr::Identifier(Ident::new("size"))], - ]), - ]), + GroupByExpr::Expressions( + vec![ + Expr::Identifier(Ident::new("size")), + Expr::Rollup(vec![ + vec![Expr::Identifier(Ident::new("brand"))], + vec![Expr::Identifier(Ident::new("size"))], + ]), + ], + vec![] + ), select.group_by ); } @@ -3746,13 +3752,16 @@ fn parse_select_group_by_cube() { "SELECT brand, size, sum(sales) FROM items_sold GROUP BY size, CUBE (brand, size)", ); assert_eq!( - GroupByExpr::Expressions(vec![ - Expr::Identifier(Ident::new("size")), - Expr::Cube(vec![ - vec![Expr::Identifier(Ident::new("brand"))], - vec![Expr::Identifier(Ident::new("size"))], - ]), - ]), + GroupByExpr::Expressions( + vec![ + Expr::Identifier(Ident::new("size")), + Expr::Cube(vec![ + vec![Expr::Identifier(Ident::new("brand"))], + vec![Expr::Identifier(Ident::new("size"))], + ]), + ], + vec![] + ), select.group_by ); } From 700bd03d6f4aa97c5b0901fd399dd3c10114a760 Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 7 Jul 2024 19:17:43 +0800 Subject: [PATCH 480/806] Support `SETTINGS` pairs for ClickHouse dialect (#1327) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 2 +- src/ast/query.rs | 21 +++++++++++++++++++ src/keywords.rs | 3 +++ src/parser/mod.rs | 18 +++++++++++++++++ tests/sqlparser_clickhouse.rs | 38 ++++++++++++++++++++++++++++++++++- tests/sqlparser_common.rs | 6 ++++++ tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_mysql.rs | 15 ++++++++++++++ tests/sqlparser_postgres.rs | 11 +++++++--- 9 files changed, 111 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c7f461418..c904d4bc9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -48,7 +48,7 @@ pub use self::query::{ MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, SymbolDefinition, Table, + SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, }; diff --git a/src/ast/query.rs b/src/ast/query.rs index d00a0dfcc..241e45a9c 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -50,6 +50,10 @@ pub struct Query { /// `FOR JSON { AUTO | PATH } [ , INCLUDE_NULL_VALUES ]` /// (MSSQL-specific) pub for_clause: Option, + /// ClickHouse syntax: `SELECT * FROM t SETTINGS key1 = value1, key2 = value2` + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select#settings-in-select-query) + pub settings: Option>, } impl fmt::Display for Query { @@ -70,6 +74,9 @@ impl fmt::Display for Query { if !self.limit_by.is_empty() { write!(f, " BY {}", display_separated(&self.limit_by, ", "))?; } + if let Some(ref settings) = self.settings { + write!(f, " SETTINGS {}", display_comma_separated(settings))?; + } if let Some(ref fetch) = self.fetch { write!(f, " {fetch}")?; } @@ -828,6 +835,20 @@ impl fmt::Display for ConnectBy { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Setting { + pub key: Ident, + pub value: Value, +} + +impl fmt::Display for Setting { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} = {}", self.key, self.value) + } +} + /// An expression optionally followed by an alias. /// /// Example: diff --git a/src/keywords.rs b/src/keywords.rs index 5db55e9da..cbba92c5b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -650,6 +650,7 @@ define_keywords!( SESSION_USER, SET, SETS, + SETTINGS, SHARE, SHOW, SIMILAR, @@ -850,6 +851,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::FOR, // for MYSQL PARTITION SELECTION Keyword::PARTITION, + // for ClickHouse SELECT * FROM t SETTINGS ... + Keyword::SETTINGS, // for Snowflake START WITH .. CONNECT BY Keyword::START, Keyword::CONNECT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4e9c3836b..7614307bf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7871,6 +7871,7 @@ impl<'a> Parser<'a> { fetch: None, locks: vec![], for_clause: None, + settings: None, }) } else if self.parse_keyword(Keyword::UPDATE) { Ok(Query { @@ -7883,6 +7884,7 @@ impl<'a> Parser<'a> { fetch: None, locks: vec![], for_clause: None, + settings: None, }) } else { let body = self.parse_boxed_query_body(0)?; @@ -7928,6 +7930,20 @@ impl<'a> Parser<'a> { vec![] }; + let settings = if dialect_of!(self is ClickHouseDialect|GenericDialect) + && self.parse_keyword(Keyword::SETTINGS) + { + let key_values = self.parse_comma_separated(|p| { + let key = p.parse_identifier(false)?; + p.expect_token(&Token::Eq)?; + let value = p.parse_value()?; + Ok(Setting { key, value }) + })?; + Some(key_values) + } else { + None + }; + let fetch = if self.parse_keyword(Keyword::FETCH) { Some(self.parse_fetch()?) } else { @@ -7955,6 +7971,7 @@ impl<'a> Parser<'a> { fetch, locks, for_clause, + settings, }) } } @@ -9091,6 +9108,7 @@ impl<'a> Parser<'a> { fetch: None, locks: vec![], for_clause: None, + settings: None, }), alias, }) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 0c188a24b..b3e03c4ab 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -21,8 +21,8 @@ use test_utils::*; use sqlparser::ast::Expr::{BinaryOp, Identifier, MapAccess}; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Table; +use sqlparser::ast::Value::Number; use sqlparser::ast::*; - use sqlparser::dialect::ClickHouseDialect; use sqlparser::dialect::GenericDialect; @@ -549,6 +549,42 @@ 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) + }, + ]) + ); + } + _ => unreachable!(), + } + + 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", + ]; + for sql in invalid_cases { + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("Expected: SETTINGS key = value, found: "); + } +} #[test] fn parse_select_star_except() { clickhouse().verified_stmt("SELECT * EXCEPT (prev_status) FROM anomalies"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ac2133946..609d2600d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -413,6 +413,7 @@ fn parse_update_set_from() { fetch: None, locks: vec![], for_clause: None, + settings: None, }), alias: Some(TableAlias { name: Ident::new("t2"), @@ -3427,6 +3428,7 @@ fn parse_create_table_as_table() { fetch: None, locks: vec![], for_clause: None, + settings: None, }); match verified_stmt(sql1) { @@ -3452,6 +3454,7 @@ fn parse_create_table_as_table() { fetch: None, locks: vec![], for_clause: None, + settings: None, }); match verified_stmt(sql2) { @@ -4996,6 +4999,7 @@ fn parse_interval_and_or_xor() { fetch: None, locks: vec![], for_clause: None, + settings: None, }))]; assert_eq!(actual_ast, expected_ast); @@ -7649,6 +7653,7 @@ fn parse_merge() { fetch: None, locks: vec![], for_clause: None, + settings: None, }), alias: Some(TableAlias { name: Ident { @@ -9156,6 +9161,7 @@ fn parse_unload() { locks: vec![], for_clause: None, order_by: vec![], + settings: None, }), to: Ident { value: "s3://...".to_string(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 993850299..84ab474b0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -103,6 +103,7 @@ fn parse_create_procedure() { locks: vec![], for_clause: None, order_by: vec![], + settings: None, body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, @@ -546,6 +547,7 @@ fn parse_substring_in_select() { fetch: None, locks: vec![], for_clause: None, + settings: None, }), query ); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 4c18d4a75..cf9b717be 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -925,6 +925,7 @@ fn parse_escaped_quote_identifiers_with_escape() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ); } @@ -972,6 +973,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ); } @@ -1016,6 +1018,7 @@ fn parse_escaped_backticks_with_escape() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ); } @@ -1060,6 +1063,7 @@ fn parse_escaped_backticks_with_no_escape() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ); } @@ -1264,6 +1268,7 @@ fn parse_simple_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1306,6 +1311,7 @@ fn parse_ignore_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1348,6 +1354,7 @@ fn parse_priority_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1387,6 +1394,7 @@ fn parse_priority_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1434,6 +1442,7 @@ fn parse_insert_as() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1493,6 +1502,7 @@ fn parse_insert_as() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1536,6 +1546,7 @@ fn parse_replace_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1573,6 +1584,7 @@ fn parse_empty_row_insert() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -1633,6 +1645,7 @@ fn parse_insert_with_on_duplicate_update() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), source ); @@ -2273,6 +2286,7 @@ fn parse_substring_in_select() { fetch: None, locks: vec![], for_clause: None, + settings: None, }), query ); @@ -2578,6 +2592,7 @@ fn parse_hex_string_introducer() { fetch: None, locks: vec![], for_clause: None, + settings: None, })) ) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2606fb96e..243116a3f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1093,6 +1093,7 @@ fn parse_copy_to() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), to: true, target: CopyTarget::File { @@ -2421,6 +2422,7 @@ fn parse_array_subquery_expr() { fetch: None, locks: vec![], for_clause: None, + settings: None, })), filter: None, null_treatment: None, @@ -3941,7 +3943,8 @@ fn test_simple_postgres_insert_with_alias() { offset: None, fetch: None, locks: vec![], - for_clause: None + for_clause: None, + settings: None, })), partitioned: None, after_columns: vec![], @@ -4008,7 +4011,8 @@ fn test_simple_postgres_insert_with_alias() { offset: None, fetch: None, locks: vec![], - for_clause: None + for_clause: None, + settings: None, })), partitioned: None, after_columns: vec![], @@ -4071,7 +4075,8 @@ fn test_simple_insert_with_quoted_alias() { offset: None, fetch: None, locks: vec![], - for_clause: None + for_clause: None, + settings: None, })), partitioned: None, after_columns: vec![], From 0884dd920d2a2bbd5c8c67cbf9ed812ce8a1dd5d Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 7 Jul 2024 20:03:23 +0800 Subject: [PATCH 481/806] Support `PREWHERE` condition for ClickHouse dialect (#1328) --- src/ast/query.rs | 8 ++++++ src/keywords.rs | 3 +++ src/parser/mod.rs | 9 +++++++ tests/sqlparser_clickhouse.rs | 51 +++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 8 ++++++ tests/sqlparser_duckdb.rs | 2 ++ tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_mysql.rs | 8 ++++++ tests/sqlparser_postgres.rs | 3 +++ 9 files changed, 94 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index 241e45a9c..7d2626b2d 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -247,6 +247,11 @@ pub struct Select { pub from: Vec, /// LATERAL VIEWs pub lateral_views: Vec, + /// ClickHouse syntax: `PREWHERE a = 1 WHERE b = 2`, + /// and it can be used together with WHERE selection. + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/prewhere) + pub prewhere: Option, /// WHERE pub selection: Option, /// GROUP BY @@ -302,6 +307,9 @@ impl fmt::Display for Select { write!(f, "{lv}")?; } } + if let Some(ref prewhere) = self.prewhere { + write!(f, " PREWHERE {prewhere}")?; + } if let Some(ref selection) = self.selection { write!(f, " WHERE {selection}")?; } diff --git a/src/keywords.rs b/src/keywords.rs index cbba92c5b..eb69a209b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -558,6 +558,7 @@ define_keywords!( PRECISION, PREPARE, PRESERVE, + PREWHERE, PRIMARY, PRIOR, PRIVILEGES, @@ -851,6 +852,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::FOR, // for MYSQL PARTITION SELECTION Keyword::PARTITION, + // for Clickhouse PREWHERE + Keyword::PREWHERE, // for ClickHouse SELECT * FROM t SETTINGS ... Keyword::SETTINGS, // for Snowflake START WITH .. CONNECT BY diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7614307bf..a81d53e7c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8329,6 +8329,14 @@ impl<'a> Parser<'a> { } } + let prewhere = if dialect_of!(self is ClickHouseDialect|GenericDialect) + && self.parse_keyword(Keyword::PREWHERE) + { + Some(self.parse_expr()?) + } else { + None + }; + let selection = if self.parse_keyword(Keyword::WHERE) { Some(self.parse_expr()?) } else { @@ -8440,6 +8448,7 @@ impl<'a> Parser<'a> { into, from, lateral_views, + prewhere, selection, group_by, cluster_by, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index b3e03c4ab..29a5b15aa 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -63,6 +63,7 @@ fn parse_map_access_expr() { joins: vec![], }], lateral_views: vec![], + prewhere: None, selection: Some(BinaryOp { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("id"))), @@ -717,6 +718,56 @@ fn parse_group_by_with_modifier() { } } +#[test] +fn test_prewhere() { + match clickhouse_and_generic().verified_stmt("SELECT * FROM t PREWHERE x = 1 WHERE y = 2") { + Statement::Query(query) => { + let prewhere = query.body.as_select().unwrap().prewhere.as_ref(); + assert_eq!( + prewhere, + Some(&BinaryOp { + left: Box::new(Identifier(Ident::new("x"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + }) + ); + let selection = query.as_ref().body.as_select().unwrap().selection.as_ref(); + assert_eq!( + selection, + Some(&BinaryOp { + left: Box::new(Identifier(Ident::new("y"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("2".parse().unwrap(), false))), + }) + ); + } + _ => unreachable!(), + } + + match clickhouse_and_generic().verified_stmt("SELECT * FROM t PREWHERE x = 1 AND y = 2") { + Statement::Query(query) => { + let prewhere = query.body.as_select().unwrap().prewhere.as_ref(); + assert_eq!( + prewhere, + Some(&BinaryOp { + left: Box::new(BinaryOp { + left: Box::new(Identifier(Ident::new("x"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + }), + op: BinaryOperator::And, + right: Box::new(BinaryOp { + left: Box::new(Identifier(Ident::new("y"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("2".parse().unwrap(), false))), + }), + }) + ); + } + _ => unreachable!(), + } +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 609d2600d..256680b3e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -391,6 +391,7 @@ fn parse_update_set_from() { joins: vec![], }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions( vec![Expr::Identifier(Ident::new("id"))], @@ -4551,6 +4552,7 @@ fn test_parse_named_window() { joins: vec![], }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -4932,6 +4934,7 @@ fn parse_interval_and_or_xor() { joins: vec![], }], lateral_views: vec![], + prewhere: None, selection: Some(Expr::BinaryOp { left: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { @@ -6915,6 +6918,7 @@ fn lateral_function() { }], }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -7634,6 +7638,7 @@ fn parse_merge() { joins: vec![], }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -9141,6 +9146,7 @@ fn parse_unload() { joins: vec![], }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -9285,6 +9291,7 @@ fn parse_connect_by() { }], into: None, lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -9369,6 +9376,7 @@ fn parse_connect_by() { }], into: None, lateral_views: vec![], + prewhere: None, selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("employee_id"))), op: BinaryOperator::NotEq, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 948e150c9..400daa8a8 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -170,6 +170,7 @@ fn test_select_union_by_name() { joins: vec![], }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -208,6 +209,7 @@ fn test_select_union_by_name() { joins: vec![], }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 84ab474b0..e0e0f7c70 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -111,6 +111,7 @@ fn parse_create_procedure() { into: None, from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -528,6 +529,7 @@ fn parse_substring_in_select() { joins: vec![] }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index cf9b717be..a5fa75200 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -906,6 +906,7 @@ fn parse_escaped_quote_identifiers_with_escape() { into: None, from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -954,6 +955,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { into: None, from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -999,6 +1001,7 @@ fn parse_escaped_backticks_with_escape() { into: None, from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -1044,6 +1047,7 @@ fn parse_escaped_backticks_with_no_escape() { into: None, from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -1715,6 +1719,7 @@ fn parse_select_with_numeric_prefix_column_name() { joins: vec![] }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -1768,6 +1773,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { joins: vec![] }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -2267,6 +2273,7 @@ fn parse_substring_in_select() { joins: vec![] }], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -2572,6 +2579,7 @@ fn parse_hex_string_introducer() { })], from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 243116a3f..2d3097cf9 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1074,6 +1074,7 @@ fn parse_copy_to() { into: None, from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), having: None, @@ -2383,6 +2384,7 @@ fn parse_array_subquery_expr() { into: None, from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -2402,6 +2404,7 @@ fn parse_array_subquery_expr() { into: None, from: vec![], lateral_views: vec![], + prewhere: None, selection: None, group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], From f77192d4ec19c47c90654aa6514a7e63b0d67a0b Mon Sep 17 00:00:00 2001 From: Mohamed Abdeen <83442793+MohamedAbdeen21@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:31:33 +0300 Subject: [PATCH 482/806] Re-enable trailing commas in DCL (#1318) --- src/parser/mod.rs | 39 +++++++++++++++++++++++++++++---------- tests/sqlparser_common.rs | 12 ++++++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a81d53e7c..1dc6bff5e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -46,6 +46,9 @@ pub enum ParserError { RecursionLimitExceeded, } +// avoid clippy type_complexity warnings +type ParsedAction = (Keyword, Option>); + // Use `Parser::expected` instead, if possible macro_rules! parser_err { ($MSG:expr, $loc:expr) => { @@ -3334,6 +3337,29 @@ impl<'a> Parser<'a> { ret } + pub fn parse_actions_list(&mut self) -> Result, ParserError> { + let mut values = vec![]; + loop { + values.push(self.parse_grant_permission()?); + if !self.consume_token(&Token::Comma) { + break; + } else if self.options.trailing_commas { + match self.peek_token().token { + Token::Word(kw) if kw.keyword == Keyword::ON => { + break; + } + Token::RParen + | Token::SemiColon + | Token::EOF + | Token::RBracket + | Token::RBrace => break, + _ => continue, + } + } + } + Ok(values) + } + /// Parse a comma-separated list of 1+ items accepted by `F` pub fn parse_comma_separated(&mut self, mut f: F) -> Result, ParserError> where @@ -3347,9 +3373,7 @@ impl<'a> Parser<'a> { } else if self.options.trailing_commas { match self.peek_token().token { Token::Word(kw) - if keywords::RESERVED_FOR_COLUMN_ALIAS - .iter() - .any(|d| kw.keyword == *d) => + if keywords::RESERVED_FOR_COLUMN_ALIAS.contains(&kw.keyword) => { break; } @@ -9680,11 +9704,8 @@ impl<'a> Parser<'a> { with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), } } else { - let old_value = self.options.trailing_commas; - self.options.trailing_commas = false; - let (actions, err): (Vec<_>, Vec<_>) = self - .parse_comma_separated(Parser::parse_grant_permission)? + .parse_actions_list()? .into_iter() .map(|(kw, columns)| match kw { Keyword::DELETE => Ok(Action::Delete), @@ -9706,8 +9727,6 @@ impl<'a> Parser<'a> { }) .partition(Result::is_ok); - self.options.trailing_commas = old_value; - if !err.is_empty() { let errors: Vec = err.into_iter().filter_map(|x| x.err()).collect(); return Err(ParserError::ParserError(format!( @@ -9753,7 +9772,7 @@ impl<'a> Parser<'a> { Ok((privileges, objects)) } - pub fn parse_grant_permission(&mut self) -> Result<(Keyword, Option>), ParserError> { + pub fn parse_grant_permission(&mut self) -> Result { if let Some(kw) = self.parse_one_of_keywords(&[ Keyword::CONNECT, Keyword::CREATE, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 256680b3e..132874aa9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8942,6 +8942,11 @@ fn parse_trailing_comma() { "CREATE TABLE employees (name TEXT, age INT)", ); + trailing_commas.one_statement_parses_to( + "GRANT USAGE, SELECT, INSERT, ON p TO u", + "GRANT USAGE, SELECT, INSERT ON p TO u", + ); + trailing_commas.verified_stmt("SELECT album_id, name FROM track"); trailing_commas.verified_stmt("SELECT * FROM track ORDER BY milliseconds"); @@ -8961,6 +8966,13 @@ fn parse_trailing_comma() { ParserError::ParserError("Expected an expression, found: from".to_string()) ); + assert_eq!( + trailing_commas + .parse_sql_statements("REVOKE USAGE, SELECT, ON p TO u") + .unwrap_err(), + ParserError::ParserError("Expected a privilege keyword, found: ON".to_string()) + ); + assert_eq!( trailing_commas .parse_sql_statements("CREATE TABLE employees (name text, age int,)") From 66b4ec8486a18d2f542d6b83450d421ceca6572c Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Mon, 8 Jul 2024 11:32:45 +0100 Subject: [PATCH 483/806] Fix typo in `sqlparser-derive` README (#1310) --- derive/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/README.md b/derive/README.md index ad4978a89..ffb5d266e 100644 --- a/derive/README.md +++ b/derive/README.md @@ -97,7 +97,7 @@ impl Visit for TableFactor { match self { Self::Table { name, alias } => { visitor.pre_visit_relation(name)?; - alias.visit(name)?; + name.visit(visitor)?; visitor.post_visit_relation(name)?; alias.visit(visitor)?; } From 17e5c0c1b6c3c52e5ffd0d2caa4aad7bd7d35958 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 8 Jul 2024 07:37:00 -0400 Subject: [PATCH 484/806] Fix CI error message in CI (#1333) --- tests/sqlparser_common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 132874aa9..2b208016a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8970,7 +8970,7 @@ fn parse_trailing_comma() { trailing_commas .parse_sql_statements("REVOKE USAGE, SELECT, ON p TO u") .unwrap_err(), - ParserError::ParserError("Expected a privilege keyword, found: ON".to_string()) + ParserError::ParserError("Expected: a privilege keyword, found: ON".to_string()) ); assert_eq!( From bbee052890bb3eb64fe3e9fc20ad70ca06df3c5f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 8 Jul 2024 14:38:59 -0400 Subject: [PATCH 485/806] Add stale PR github workflow (#1331) --- .github/workflows/stale.yml | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..231252682 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,38 @@ +# 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: "Close stale PRs" +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-stale-prs: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + stale-pr-message: "Thank you for your contribution. Unfortunately, this pull request is stale because it has been open 60 days with no activity. Please remove the stale label or comment or this will be closed in 7 days." + days-before-pr-stale: 60 + days-before-pr-close: 7 + # do not close stale issues + days-before-issue-stale: -1 + days-before-issue-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} From 9f60eb1571c4513140cb9a95bd107e26fcf6c7be Mon Sep 17 00:00:00 2001 From: Lorrens Pantelis <100197010+LorrensP-2158466@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:46:49 +0200 Subject: [PATCH 486/806] Support `DROP PROCEDURE` statement (#1324) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 26 +++++++++ src/parser/mod.rs | 24 ++++++++- tests/sqlparser_postgres.rs | 102 ++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c904d4bc9..beee9f4bc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2215,6 +2215,16 @@ pub enum Statement { option: Option, }, /// ```sql + /// DROP PROCEDURE + /// ``` + DropProcedure { + if_exists: bool, + /// One or more function to drop + proc_desc: Vec, + /// `CASCADE` or `RESTRICT` + option: Option, + }, + /// ```sql /// DROP SECRET /// ``` DropSecret { @@ -3644,6 +3654,22 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DropProcedure { + if_exists, + proc_desc, + option, + } => { + write!( + f, + "DROP PROCEDURE{} {}", + if *if_exists { " IF EXISTS" } else { "" }, + display_comma_separated(proc_desc), + )?; + if let Some(op) = option { + write!(f, " {op}")?; + } + Ok(()) + } Statement::DropSecret { if_exists, temporary, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1dc6bff5e..a88cfcb9c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4524,11 +4524,13 @@ impl<'a> Parser<'a> { ObjectType::Stage } else if self.parse_keyword(Keyword::FUNCTION) { return self.parse_drop_function(); + } else if self.parse_keyword(Keyword::PROCEDURE) { + return self.parse_drop_procedure(); } else if self.parse_keyword(Keyword::SECRET) { return self.parse_drop_secret(temporary, persistent); } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION, STAGE or SEQUENCE after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION, PROCEDURE, STAGE or SEQUENCE after DROP", self.peek_token(), ); }; @@ -4580,6 +4582,26 @@ impl<'a> Parser<'a> { }) } + /// ```sql + /// DROP PROCEDURE [ IF EXISTS ] name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] [, ...] + /// [ CASCADE | RESTRICT ] + /// ``` + fn parse_drop_procedure(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let proc_desc = self.parse_comma_separated(Parser::parse_drop_function_desc)?; + let option = match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { + Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade), + Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict), + Some(_) => unreachable!(), // parse_one_of_keywords does not return other keywords + None => None, + }; + Ok(Statement::DropProcedure { + if_exists, + proc_desc, + option, + }) + } + fn parse_drop_function_desc(&mut self) -> Result { let name = self.parse_object_name(false)?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2d3097cf9..2da82c122 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3629,6 +3629,108 @@ fn parse_drop_function() { ); } +#[test] +fn parse_drop_procedure() { + let sql = "DROP PROCEDURE IF EXISTS test_proc"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropProcedure { + if_exists: true, + proc_desc: vec![DropFunctionDesc { + name: ObjectName(vec![Ident { + value: "test_proc".to_string(), + quote_style: None + }]), + args: None + }], + option: None + } + ); + + let sql = "DROP PROCEDURE IF EXISTS test_proc(a INTEGER, IN b INTEGER = 1)"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropProcedure { + if_exists: true, + proc_desc: vec![DropFunctionDesc { + name: ObjectName(vec![Ident { + value: "test_proc".to_string(), + quote_style: None + }]), + args: Some(vec![ + OperateFunctionArg::with_name("a", DataType::Integer(None)), + OperateFunctionArg { + mode: Some(ArgMode::In), + name: Some("b".into()), + data_type: DataType::Integer(None), + default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), + } + ]), + }], + option: None + } + ); + + let sql = "DROP PROCEDURE IF EXISTS test_proc1(a INTEGER, IN b INTEGER = 1), test_proc2(a VARCHAR, IN b INTEGER = 1)"; + assert_eq!( + pg().verified_stmt(sql), + Statement::DropProcedure { + if_exists: true, + proc_desc: vec![ + DropFunctionDesc { + name: ObjectName(vec![Ident { + value: "test_proc1".to_string(), + quote_style: None + }]), + args: Some(vec![ + OperateFunctionArg::with_name("a", DataType::Integer(None)), + OperateFunctionArg { + mode: Some(ArgMode::In), + name: Some("b".into()), + data_type: DataType::Integer(None), + default_expr: Some(Expr::Value(Value::Number( + "1".parse().unwrap(), + false + ))), + } + ]), + }, + DropFunctionDesc { + name: ObjectName(vec![Ident { + value: "test_proc2".to_string(), + quote_style: None + }]), + args: Some(vec![ + OperateFunctionArg::with_name("a", DataType::Varchar(None)), + OperateFunctionArg { + mode: Some(ArgMode::In), + name: Some("b".into()), + data_type: DataType::Integer(None), + default_expr: Some(Expr::Value(Value::Number( + "1".parse().unwrap(), + false + ))), + } + ]), + } + ], + option: None + } + ); + + let res = pg().parse_sql_statements("DROP PROCEDURE testproc DROP"); + assert_eq!( + ParserError::ParserError("Expected: end of statement, found: DROP".to_string()), + res.unwrap_err() + ); + + let res = pg().parse_sql_statements("DROP PROCEDURE testproc SET NULL"); + assert_eq!( + ParserError::ParserError("Expected: end of statement, found: SET".to_string()), + res.unwrap_err() + ); +} + #[test] fn parse_dollar_quoted_string() { let sql = "SELECT $$hello$$, $tag_name$world$tag_name$, $$Foo$Bar$$, $$Foo$Bar$$col_name, $$$$, $tag_name$$tag_name$"; From 07278952f9ba9c717652ae463febf14db13777ce Mon Sep 17 00:00:00 2001 From: hulk Date: Tue, 9 Jul 2024 19:49:04 +0800 Subject: [PATCH 487/806] Add support of FORMAT clause for ClickHouse parser (#1335) --- src/ast/mod.rs | 16 ++++++++-------- src/ast/query.rs | 28 ++++++++++++++++++++++++++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 16 ++++++++++++++++ tests/sqlparser_clickhouse.rs | 32 ++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 6 ++++++ tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_mysql.rs | 15 +++++++++++++++ tests/sqlparser_postgres.rs | 5 +++++ 9 files changed, 114 insertions(+), 8 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index beee9f4bc..58f094411 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -43,14 +43,14 @@ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml, - GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, - JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, - MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, - NonBlock, Offset, OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem, - RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, - Values, WildcardAdditionalOptions, With, + FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Join, + JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, + LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, + NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderByExpr, + PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, + ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, + SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion, + TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, diff --git a/src/ast/query.rs b/src/ast/query.rs index 7d2626b2d..70c781409 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -54,6 +54,11 @@ pub struct Query { /// /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select#settings-in-select-query) pub settings: Option>, + /// `SELECT * FROM t FORMAT JSONCompact` + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/format) + /// (ClickHouse-specific) + pub format_clause: Option, } impl fmt::Display for Query { @@ -86,6 +91,9 @@ impl fmt::Display for Query { if let Some(ref for_clause) = self.for_clause { write!(f, " {}", for_clause)?; } + if let Some(ref format) = self.format_clause { + write!(f, " {}", format)?; + } Ok(()) } } @@ -1959,6 +1967,26 @@ impl fmt::Display for GroupByExpr { } } +/// FORMAT identifier or FORMAT NULL clause, specific to ClickHouse. +/// +/// [ClickHouse]: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FormatClause { + Identifier(Ident), + Null, +} + +impl fmt::Display for FormatClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FormatClause::Identifier(ident) => write!(f, "FORMAT {}", ident), + FormatClause::Null => write!(f, "FORMAT NULL"), + } + } +} + /// FOR XML or FOR JSON clause, specific to MSSQL /// (formats the output of a query as XML or JSON) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/keywords.rs b/src/keywords.rs index eb69a209b..edd3271f3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -856,6 +856,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::PREWHERE, // for ClickHouse SELECT * FROM t SETTINGS ... Keyword::SETTINGS, + // for ClickHouse SELECT * FROM t FORMAT... + Keyword::FORMAT, // for Snowflake START WITH .. CONNECT BY Keyword::START, Keyword::CONNECT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a88cfcb9c..aada0bc56 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7918,6 +7918,7 @@ impl<'a> Parser<'a> { locks: vec![], for_clause: None, settings: None, + format_clause: None, }) } else if self.parse_keyword(Keyword::UPDATE) { Ok(Query { @@ -7931,6 +7932,7 @@ impl<'a> Parser<'a> { locks: vec![], for_clause: None, settings: None, + format_clause: None, }) } else { let body = self.parse_boxed_query_body(0)?; @@ -8006,6 +8008,18 @@ impl<'a> Parser<'a> { locks.push(self.parse_lock()?); } } + let format_clause = if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keyword(Keyword::FORMAT) + { + if self.parse_keyword(Keyword::NULL) { + Some(FormatClause::Null) + } else { + let ident = self.parse_identifier(false)?; + Some(FormatClause::Identifier(ident)) + } + } else { + None + }; Ok(Query { with, @@ -8018,6 +8032,7 @@ impl<'a> Parser<'a> { locks, for_clause, settings, + format_clause, }) } } @@ -9164,6 +9179,7 @@ impl<'a> Parser<'a> { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), alias, }) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 29a5b15aa..f6b787f5c 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -768,6 +768,38 @@ fn test_prewhere() { } } +#[test] +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); + match clickhouse_and_generic().verified_stmt(&sql) { + Statement::Query(query) => { + if *format == "NULL" { + assert_eq!(query.format_clause, Some(FormatClause::Null)); + } else { + assert_eq!( + query.format_clause, + Some(FormatClause::Identifier(Ident::new(*format))) + ); + } + } + _ => unreachable!(), + } + } + + let invalid_cases = [ + "SELECT * FROM t FORMAT", + "SELECT * FROM t FORMAT TabSeparated JSONCompact", + "SELECT * FROM t FORMAT TabSeparated TabSeparated", + ]; + for sql in &invalid_cases { + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("Expected: FORMAT {identifier}, found: "); + } +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2b208016a..86357234c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -415,6 +415,7 @@ fn parse_update_set_from() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), alias: Some(TableAlias { name: Ident::new("t2"), @@ -3430,6 +3431,7 @@ fn parse_create_table_as_table() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }); match verified_stmt(sql1) { @@ -3456,6 +3458,7 @@ fn parse_create_table_as_table() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }); match verified_stmt(sql2) { @@ -5003,6 +5006,7 @@ fn parse_interval_and_or_xor() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }))]; assert_eq!(actual_ast, expected_ast); @@ -7659,6 +7663,7 @@ fn parse_merge() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), alias: Some(TableAlias { name: Ident { @@ -9180,6 +9185,7 @@ fn parse_unload() { for_clause: None, order_by: vec![], settings: None, + format_clause: None, }), to: Ident { value: "s3://...".to_string(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index e0e0f7c70..6968347ec 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -104,6 +104,7 @@ fn parse_create_procedure() { for_clause: None, order_by: vec![], settings: None, + format_clause: None, body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, @@ -550,6 +551,7 @@ fn parse_substring_in_select() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), query ); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a5fa75200..74def31bf 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -927,6 +927,7 @@ fn parse_escaped_quote_identifiers_with_escape() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ); } @@ -976,6 +977,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ); } @@ -1022,6 +1024,7 @@ fn parse_escaped_backticks_with_escape() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ); } @@ -1068,6 +1071,7 @@ fn parse_escaped_backticks_with_no_escape() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ); } @@ -1273,6 +1277,7 @@ fn parse_simple_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1316,6 +1321,7 @@ fn parse_ignore_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1359,6 +1365,7 @@ fn parse_priority_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1399,6 +1406,7 @@ fn parse_priority_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1447,6 +1455,7 @@ fn parse_insert_as() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1507,6 +1516,7 @@ fn parse_insert_as() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1551,6 +1561,7 @@ fn parse_replace_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1589,6 +1600,7 @@ fn parse_empty_row_insert() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -1650,6 +1662,7 @@ fn parse_insert_with_on_duplicate_update() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), source ); @@ -2294,6 +2307,7 @@ fn parse_substring_in_select() { locks: vec![], for_clause: None, settings: None, + format_clause: None, }), query ); @@ -2601,6 +2615,7 @@ fn parse_hex_string_introducer() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })) ) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2da82c122..74f70a6e5 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1095,6 +1095,7 @@ fn parse_copy_to() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), to: true, target: CopyTarget::File { @@ -2426,6 +2427,7 @@ fn parse_array_subquery_expr() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), filter: None, null_treatment: None, @@ -4050,6 +4052,7 @@ fn test_simple_postgres_insert_with_alias() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), partitioned: None, after_columns: vec![], @@ -4118,6 +4121,7 @@ fn test_simple_postgres_insert_with_alias() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), partitioned: None, after_columns: vec![], @@ -4182,6 +4186,7 @@ fn test_simple_insert_with_quoted_alias() { locks: vec![], for_clause: None, settings: None, + format_clause: None, })), partitioned: None, after_columns: vec![], From 32b8276b328ad014cdfbeb85d1618bb0b25c7130 Mon Sep 17 00:00:00 2001 From: gai takano Date: Tue, 9 Jul 2024 20:49:48 +0900 Subject: [PATCH 488/806] Postgres: support for `OWNER TO` clause (#1314) Co-authored-by: Andrew Lamb --- src/ast/ddl.rs | 29 +++++++++++++++ src/ast/mod.rs | 4 +-- src/keywords.rs | 1 + src/parser/mod.rs | 19 ++++++++++ tests/sqlparser_postgres.rs | 72 +++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9c30999ab..1ed3857d7 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -157,6 +157,32 @@ pub enum AlterTableOperation { SwapWith { table_name: ObjectName }, /// 'SET TBLPROPERTIES ( { property_key [ = ] property_val } [, ...] )' SetTblProperties { table_properties: Vec }, + + /// `OWNER TO { | CURRENT_ROLE | CURRENT_USER | SESSION_USER }` + /// + /// Note: this is PostgreSQL-specific + OwnerTo { new_owner: Owner }, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Owner { + Ident(Ident), + CurrentRole, + CurrentUser, + SessionUser, +} + +impl fmt::Display for Owner { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Owner::Ident(ident) => write!(f, "{}", ident), + Owner::CurrentRole => write!(f, "CURRENT_ROLE"), + Owner::CurrentUser => write!(f, "CURRENT_USER"), + Owner::SessionUser => write!(f, "SESSION_USER"), + } + } } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -322,6 +348,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::SwapWith { table_name } => { write!(f, "SWAP WITH {table_name}") } + AlterTableOperation::OwnerTo { new_owner } => { + write!(f, "OWNER TO {new_owner}") + } AlterTableOperation::SetTblProperties { table_properties } => { write!( f, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 58f094411..b8d72e233 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -34,8 +34,8 @@ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue} pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ConstraintCharacteristics, DeferrableInitial, GeneratedAs, - GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, - ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, + GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Owner, Partition, + ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; diff --git a/src/keywords.rs b/src/keywords.rs index edd3271f3..7146c4efe 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -527,6 +527,7 @@ define_keywords!( OVERLAY, OVERWRITE, OWNED, + OWNER, PARALLEL, PARAMETER, PARQUET, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index aada0bc56..87166f503 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6447,6 +6447,25 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::WITH)?; let table_name = self.parse_object_name(false)?; AlterTableOperation::SwapWith { table_name } + } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) + && self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) + { + let new_owner = match self.parse_one_of_keywords( &[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) { + Some(Keyword::CURRENT_USER) => Owner::CurrentUser, + Some(Keyword::CURRENT_ROLE) => Owner::CurrentRole, + Some(Keyword::SESSION_USER) => Owner::SessionUser, + Some(_) => unreachable!(), + None => { + match self.parse_identifier(false) { + Ok(ident) => Owner::Ident(ident), + Err(e) => { + return Err(ParserError::ParserError(format!("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. {e}"))) + } + } + }, + }; + + AlterTableOperation::OwnerTo { new_owner } } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 74f70a6e5..9af4f4d6c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -713,6 +713,78 @@ fn parse_alter_table_add_columns() { } } +#[test] +fn parse_alter_table_owner_to() { + struct TestCase { + sql: &'static str, + expected_owner: Owner, + } + + let test_cases = vec![ + TestCase { + sql: "ALTER TABLE tab OWNER TO new_owner", + expected_owner: Owner::Ident(Ident::new("new_owner".to_string())), + }, + TestCase { + sql: "ALTER TABLE tab OWNER TO postgres", + expected_owner: Owner::Ident(Ident::new("postgres".to_string())), + }, + TestCase { + sql: "ALTER TABLE tab OWNER TO CREATE", // treats CREATE as an identifier + expected_owner: Owner::Ident(Ident::new("CREATE".to_string())), + }, + TestCase { + sql: "ALTER TABLE tab OWNER TO \"new_owner\"", + expected_owner: Owner::Ident(Ident::with_quote('\"', "new_owner".to_string())), + }, + TestCase { + sql: "ALTER TABLE tab OWNER TO CURRENT_USER", + expected_owner: Owner::CurrentUser, + }, + TestCase { + sql: "ALTER TABLE tab OWNER TO CURRENT_ROLE", + expected_owner: Owner::CurrentRole, + }, + TestCase { + sql: "ALTER TABLE tab OWNER TO SESSION_USER", + expected_owner: Owner::SessionUser, + }, + ]; + + for case in test_cases { + match pg_and_generic().verified_stmt(case.sql) { + Statement::AlterTable { + name, + if_exists: _, + only: _, + operations, + location: _, + } => { + assert_eq!(name.to_string(), "tab"); + assert_eq!( + operations, + vec![AlterTableOperation::OwnerTo { + new_owner: case.expected_owner.clone() + }] + ); + } + _ => unreachable!("Expected an AlterTable statement"), + } + } + + let res = pg().parse_sql_statements("ALTER TABLE tab OWNER TO CREATE FOO"); + assert_eq!( + ParserError::ParserError("Expected: end of statement, found: FOO".to_string()), + res.unwrap_err() + ); + + let res = pg().parse_sql_statements("ALTER TABLE tab OWNER TO 4"); + assert_eq!( + ParserError::ParserError("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: 4".to_string()), + res.unwrap_err() + ); +} + #[test] fn parse_create_table_if_not_exists() { let sql = "CREATE TABLE IF NOT EXISTS uk_cities ()"; From 4e956a172344952f1162405db74599391bc25860 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 9 Jul 2024 08:58:02 -0400 Subject: [PATCH 489/806] Add CHANGELOG for 0.48.0 (#1334) --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18df2e33a..ed5c9ecb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,43 @@ changes that break via addition as "Added". ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.48.0] 2024-07-09 + +Huge shout out to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs! + +### Fixed +* Fix CI error message in CI (#1333) - Thanks @alamb +* Fix typo in sqlparser-derive README (#1310) - Thanks @leoyvens +* Re-enable trailing commas in DCL (#1318) - Thanks @MohamedAbdeen21 +* Fix a few typos in comment lines (#1316) - Thanks @git-hulk +* Fix Snowflake `SELECT * wildcard REPLACE ... RENAME` order (#1321) - Thanks @alexander-beedie +* Allow semi-colon at the end of UNCACHE statement (#1320) - Thanks @LorrensP-2158466 +* Return errors, not panic, when integers fail to parse in `AUTO_INCREMENT` and `TOP` (#1305) - Thanks @eejbyfeldt + +### Added +* Support `OWNER TO` clause in Postgres (#1314) - Thanks @gainings +* Support `FORMAT` clause for ClickHouse (#1335) - Thanks @git-hulk +* Support `DROP PROCEDURE` statement (#1324) - Thanks @LorrensP-2158466 +* Support `PREWHERE` condition for ClickHouse dialect (#1328) - Thanks @git-hulk +* Support `SETTINGS` pairs for ClickHouse dialect (#1327) - Thanks @git-hulk +* Support `GROUP BY WITH MODIFIER` for ClickHouse dialect (#1323) - Thanks @git-hulk +* Support DuckDB Union datatype (#1322) - Thanks @gstvg +* Support parametric arguments to `FUNCTION` for ClickHouse dialect (#1315) - Thanks @git-hulk +* Support `TO` in `CREATE VIEW` clause for Clickhouse (#1313) - Thanks @Bidaya0 +* Support `UPDATE` statements that contain tuple assignments (#1317) - Thanks @lovasoa +* Support `BY NAME quantifier across all set ops (#1309) - Thanks @alexander-beedie +* Support SnowFlake exclusive `CREATE TABLE` options (#1233) - Thanks @balliegojr +* Support ClickHouse `CREATE TABLE` with primary key and parametrised table engine (#1289) - Thanks @7phs +* Support custom operators in Postgres (#1302) - Thanks @lovasoa +* Support ClickHouse data types (#1285) - Thanks @7phs + +### Changed +* Add stale PR github workflow (#1331) - Thanks @alamb +* Refine docs (#1326) - Thanks @emilsivervik +* Improve error messages with additional colons (#1319) - Thanks @LorrensP-2158466 +* Move Display fmt to struct for `CreateIndex` (#1307) - Thanks @philipcristiano +* Enhancing Trailing Comma Option (#1212) - Thanks @MohamedAbdeen21 +* Encapsulate `CreateTable`, `CreateIndex` into specific structs (#1291) - Thanks @philipcristiano ## [0.47.0] 2024-06-01 From 285f49258967df22a455febe22773d158dd2476f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 9 Jul 2024 08:58:59 -0400 Subject: [PATCH 490/806] chore: Release sqlparser version 0.48.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8d015968b..b0bee003e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.47.0" +version = "0.48.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 9108bffc9a021aa1f5137381c8f3aec47e71e319 Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 10 Jul 2024 05:43:22 +0800 Subject: [PATCH 491/806] Add support of table function WITH ORDINALITY modifier for Postgre Parser (#1337) --- src/ast/query.rs | 14 ++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 5 ++++ src/test_utils.rs | 2 ++ tests/sqlparser_bigquery.rs | 8 ++++++- tests/sqlparser_clickhouse.rs | 2 ++ tests/sqlparser_common.rs | 43 +++++++++++++++++++++++++++++++++++ tests/sqlparser_databricks.rs | 3 ++- tests/sqlparser_duckdb.rs | 2 ++ tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 3 +++ tests/sqlparser_mysql.rs | 5 ++++ tests/sqlparser_postgres.rs | 37 +++++++++++++++++++++++++++++- tests/sqlparser_redshift.rs | 3 +++ tests/sqlparser_snowflake.rs | 1 + tests/sqlparser_sqlite.rs | 3 ++- 16 files changed, 129 insertions(+), 4 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 70c781409..608ac2e96 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -913,6 +913,10 @@ pub enum TableFactor { /// Optional version qualifier to facilitate table time-travel, as /// supported by BigQuery and MSSQL. version: Option, + // Optional table function modifier to generate the ordinality for column. + /// For example, `SELECT * FROM generate_series(1, 10) WITH ORDINALITY AS t(a, b);` + /// [WITH ORDINALITY](https://www.postgresql.org/docs/current/functions-srf.html), supported by Postgres. + with_ordinality: bool, /// [Partition selection](https://dev.mysql.com/doc/refman/8.0/en/partitioning-selection.html), supported by MySQL. partitions: Vec, }, @@ -948,6 +952,7 @@ pub enum TableFactor { array_exprs: Vec, with_offset: bool, with_offset_alias: Option, + with_ordinality: bool, }, /// The `JSON_TABLE` table-valued function. /// Part of the SQL standard, but implemented only by MySQL, Oracle, and DB2. @@ -1293,6 +1298,7 @@ impl fmt::Display for TableFactor { with_hints, version, partitions, + with_ordinality, } => { write!(f, "{name}")?; if !partitions.is_empty() { @@ -1301,6 +1307,9 @@ impl fmt::Display for TableFactor { if let Some(args) = args { write!(f, "({})", display_comma_separated(args))?; } + if *with_ordinality { + write!(f, " WITH ORDINALITY")?; + } if let Some(alias) = alias { write!(f, " AS {alias}")?; } @@ -1354,9 +1363,14 @@ impl fmt::Display for TableFactor { array_exprs, with_offset, with_offset_alias, + with_ordinality, } => { write!(f, "UNNEST({})", display_comma_separated(array_exprs))?; + if *with_ordinality { + write!(f, " WITH ORDINALITY")?; + } + if let Some(alias) = alias { write!(f, " AS {alias}")?; } diff --git a/src/keywords.rs b/src/keywords.rs index 7146c4efe..a53eaccba 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -518,6 +518,7 @@ define_keywords!( OR, ORC, ORDER, + ORDINALITY, OUT, OUTER, OUTPUTFORMAT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 87166f503..e89eba9b1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9209,6 +9209,7 @@ impl<'a> Parser<'a> { let array_exprs = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; + let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); let alias = match self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS) { Ok(Some(alias)) => Some(alias), Ok(None) => None, @@ -9235,6 +9236,7 @@ impl<'a> Parser<'a> { array_exprs, with_offset, with_offset_alias, + with_ordinality, }) } else if self.parse_keyword_with_tokens(Keyword::JSON_TABLE, &[Token::LParen]) { let json_expr = self.parse_expr()?; @@ -9273,6 +9275,8 @@ impl<'a> Parser<'a> { None }; + let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; // MSSQL-specific table hints: @@ -9294,6 +9298,7 @@ impl<'a> Parser<'a> { with_hints, version, partitions, + with_ordinality, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { diff --git a/src/test_utils.rs b/src/test_utils.rs index 1a31d4611..1f5300be1 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -309,6 +309,7 @@ pub fn table(name: impl Into) -> TableFactor { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, } } @@ -323,6 +324,7 @@ pub fn table_with_alias(name: impl Into, alias: impl Into) -> Ta with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 88e2ef912..089a41889 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -224,6 +224,7 @@ fn parse_delete_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation ); @@ -1353,6 +1354,7 @@ fn parse_table_identifiers() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] },] @@ -1525,6 +1527,7 @@ fn parse_table_time_travel() { Value::SingleQuotedString(version) ))), partitions: vec![], + with_ordinality: false, }, joins: vec![] },] @@ -1551,7 +1554,8 @@ fn parse_join_constraint_unnest_alias() { Ident::new("a") ])], with_offset: false, - with_offset_alias: None + with_offset_alias: None, + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -1620,6 +1624,7 @@ fn parse_merge() { with_hints: Default::default(), version: Default::default(), partitions: Default::default(), + with_ordinality: false, }, table ); @@ -1634,6 +1639,7 @@ fn parse_merge() { with_hints: Default::default(), version: Default::default(), partitions: Default::default(), + with_ordinality: false, }, source ); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index f6b787f5c..99db3d10c 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -59,6 +59,7 @@ fn parse_map_access_expr() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -162,6 +163,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 86357234c..1adda149e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -359,6 +359,7 @@ fn parse_update_set_from() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }, @@ -387,6 +388,7 @@ fn parse_update_set_from() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -463,6 +465,7 @@ fn parse_update_with_table_alias() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }, @@ -530,6 +533,7 @@ fn parse_select_with_table_alias() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }] @@ -566,6 +570,7 @@ fn parse_delete_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation ); @@ -612,6 +617,7 @@ fn parse_delete_statement_for_multi_tables() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation ); @@ -623,6 +629,7 @@ fn parse_delete_statement_for_multi_tables() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].joins[0].relation ); @@ -648,6 +655,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation ); @@ -659,6 +667,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[1].relation ); @@ -670,6 +679,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, using[0].relation ); @@ -681,6 +691,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, using[0].joins[0].relation ); @@ -711,6 +722,7 @@ fn parse_where_delete_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation, ); @@ -755,6 +767,7 @@ fn parse_where_delete_with_alias_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation, ); @@ -770,6 +783,7 @@ fn parse_where_delete_with_alias_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }]), @@ -4551,6 +4565,7 @@ fn test_parse_named_window() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -4933,6 +4948,7 @@ fn parse_interval_and_or_xor() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -5286,6 +5302,7 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: true, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5303,6 +5320,7 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: false, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5320,6 +5338,7 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: true, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5340,6 +5359,7 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: false, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5364,6 +5384,7 @@ fn parse_unnest_in_from_clause() { )], with_offset: false, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5394,6 +5415,7 @@ fn parse_unnest_in_from_clause() { ], with_offset: false, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5503,6 +5525,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }, @@ -5514,6 +5537,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }, @@ -5533,6 +5557,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![Join { relation: TableFactor::Table { @@ -5542,6 +5567,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -5554,6 +5580,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![Join { relation: TableFactor::Table { @@ -5563,6 +5590,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -5585,6 +5613,7 @@ fn parse_cross_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::CrossJoin, }, @@ -5607,6 +5636,7 @@ fn parse_joins_on() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: f(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -5678,6 +5708,7 @@ fn parse_joins_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), } @@ -5741,6 +5772,7 @@ fn parse_natural_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: f(JoinConstraint::Natural), } @@ -6008,6 +6040,7 @@ fn parse_derived_tables() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -6905,6 +6938,7 @@ fn lateral_function() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![Join { relation: TableFactor::Function { @@ -7613,6 +7647,7 @@ fn parse_merge() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, } ); assert_eq!(table, table_no_into); @@ -7638,6 +7673,7 @@ fn parse_merge() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -8700,6 +8736,7 @@ fn parse_pivot_table() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }), aggregate_functions: vec![ expected_function("a", None), @@ -8769,6 +8806,7 @@ fn parse_unpivot_table() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }), value: Ident { value: "quantity".to_string(), @@ -8835,6 +8873,7 @@ fn parse_pivot_unpivot_table() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }), value: Ident { value: "population".to_string(), @@ -9159,6 +9198,7 @@ fn parse_unload() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -9304,6 +9344,7 @@ fn parse_connect_by() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -9389,6 +9430,7 @@ fn parse_connect_by() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -9548,6 +9590,7 @@ fn test_match_recognize() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }; fn check(options: &str, expect: TableFactor) { diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 90056f0f7..280b97b49 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -177,7 +177,8 @@ fn test_values_clause() { args: None, with_hints: vec![], version: None, - partitions: vec![] + partitions: vec![], + with_ordinality: false, }), query .body diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 400daa8a8..0e61b86c9 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -166,6 +166,7 @@ fn test_select_union_by_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -205,6 +206,7 @@ fn test_select_union_by_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 53280d7d8..5f0b9f575 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -359,6 +359,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 6968347ec..26bece81d 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -64,6 +64,7 @@ fn parse_table_time_travel() { Value::SingleQuotedString(version) ))), partitions: vec![], + with_ordinality: false, }, joins: vec![] },] @@ -335,6 +336,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); @@ -526,6 +528,7 @@ fn parse_substring_in_select() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] }], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 74def31bf..ec094bcd6 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1728,6 +1728,7 @@ fn parse_select_with_numeric_prefix_column_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] }], @@ -1782,6 +1783,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] }], @@ -1847,6 +1849,7 @@ fn parse_update_with_joins() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![Join { relation: TableFactor::Table { @@ -1859,6 +1862,7 @@ fn parse_update_with_joins() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ @@ -2282,6 +2286,7 @@ fn parse_substring_in_select() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] }], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9af4f4d6c..164bb72c7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3501,6 +3501,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); @@ -4054,7 +4055,8 @@ fn parse_join_constraint_unnest_alias() { Ident::new("a") ])], with_offset: false, - with_offset_alias: None + with_offset_alias: None, + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -4362,3 +4364,36 @@ fn parse_create_table_with_options() { _ => unreachable!(), } } + +#[test] +fn test_table_function_with_ordinality() { + let from = pg_and_generic() + .verified_only_select("SELECT * FROM generate_series(1, 10) WITH ORDINALITY AS t") + .from; + assert_eq!(1, from.len()); + match from[0].relation { + TableFactor::Table { + ref name, + with_ordinality: true, + .. + } => { + assert_eq!("generate_series", name.to_string().as_str()); + } + _ => panic!("Expecting TableFactor::Table with ordinality"), + } +} + +#[test] +fn test_table_unnest_with_ordinality() { + let from = pg_and_generic() + .verified_only_select("SELECT * FROM UNNEST([10, 20, 30]) WITH ORDINALITY AS t") + .from; + assert_eq!(1, from.len()); + match from[0].relation { + TableFactor::UNNEST { + with_ordinality: true, + .. + } => {} + _ => panic!("Expecting TableFactor::UNNEST with ordinality"), + } +} diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 938e6e887..440116e02 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -48,6 +48,7 @@ fn test_square_brackets_over_db_schema_table_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], } @@ -94,6 +95,7 @@ fn test_double_quotes_over_db_schema_table_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], } @@ -114,6 +116,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2f4ed1316..7a2288cbb 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -870,6 +870,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index dd1e77d5d..629ab5fc2 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -399,7 +399,8 @@ fn parse_update_tuple_row_values() { args: None, with_hints: vec![], version: None, - partitions: vec![] + partitions: vec![], + with_ordinality: false, }, joins: vec![], }, From 993216f3ac279e1e86a16de8696e60dc78d5a418 Mon Sep 17 00:00:00 2001 From: hulk Date: Sat, 13 Jul 2024 17:46:26 +0800 Subject: [PATCH 492/806] Enable PARTITION BY feature for PostgreSQL while parsing the create table statement (#1338) --- src/ast/helpers/stmt_create_table.rs | 4 +- src/parser/mod.rs | 59 +++++++++++++++------------- tests/sqlparser_postgres.rs | 44 +++++++++++++++++++++ 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index d862a36ae..92c75e6a4 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -496,9 +496,9 @@ impl TryFrom for CreateTableBuilder { } } -/// Helper return type when parsing configuration for a BigQuery `CREATE TABLE` statement. +/// Helper return type when parsing configuration for a `CREATE TABLE` statement. #[derive(Default)] -pub(crate) struct BigQueryTableConfiguration { +pub(crate) struct CreateTableConfiguration { pub partition_by: Option>, pub cluster_by: Option>>, pub options: Option>, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e89eba9b1..4d2319a08 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -31,7 +31,7 @@ use recursion::RecursionCounter; use IsLateral::*; use IsOptional::*; -use crate::ast::helpers::stmt_create_table::{BigQueryTableConfiguration, CreateTableBuilder}; +use crate::ast::helpers::stmt_create_table::{CreateTableBuilder, CreateTableConfiguration}; use crate::ast::*; use crate::dialect::*; use crate::keywords::{Keyword, ALL_KEYWORDS}; @@ -5416,11 +5416,7 @@ impl<'a> Parser<'a> { None }; - let big_query_config = if dialect_of!(self is BigQueryDialect | GenericDialect) { - self.parse_optional_big_query_create_table_config()? - } else { - Default::default() - }; + let create_table_config = self.parse_optional_create_table_config()?; // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { @@ -5505,39 +5501,46 @@ impl<'a> Parser<'a> { .collation(collation) .on_commit(on_commit) .on_cluster(on_cluster) - .partition_by(big_query_config.partition_by) - .cluster_by(big_query_config.cluster_by) - .options(big_query_config.options) + .partition_by(create_table_config.partition_by) + .cluster_by(create_table_config.cluster_by) + .options(create_table_config.options) .primary_key(primary_key) .strict(strict) .build()) } - /// Parse configuration like partitioning, clustering information during big-query table creation. - /// - fn parse_optional_big_query_create_table_config( + /// Parse configuration like 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) + fn parse_optional_create_table_config( &mut self, - ) -> Result { - let mut partition_by = None; - if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { - partition_by = Some(Box::new(self.parse_expr()?)); + ) -> Result { + let partition_by = if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | GenericDialect) + && self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) + { + Some(Box::new(self.parse_expr()?)) + } else { + None }; let mut cluster_by = None; - if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { - cluster_by = Some(WrappedCollection::NoWrapping( - self.parse_comma_separated(|p| p.parse_identifier(false))?, - )); - }; - let mut options = None; - if let Token::Word(word) = self.peek_token().token { - if word.keyword == Keyword::OPTIONS { - options = Some(self.parse_options(Keyword::OPTIONS)?); - } - }; + 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(false))?, + )); + }; + + if let Token::Word(word) = self.peek_token().token { + if word.keyword == Keyword::OPTIONS { + options = Some(self.parse_options(Keyword::OPTIONS)?); + } + }; + } - Ok(BigQueryTableConfiguration { + Ok(CreateTableConfiguration { partition_by, cluster_by, options, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 164bb72c7..ed17e9d8f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4039,6 +4039,50 @@ fn parse_create_table_with_alias() { } } +#[test] +fn parse_create_table_with_partition_by() { + let sql = "CREATE TABLE t1 (a INT, b TEXT) PARTITION BY RANGE(a)"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("t1", create_table.name.to_string()); + assert_eq!( + vec![ + ColumnDef { + name: "a".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![] + }, + ColumnDef { + name: "b".into(), + data_type: DataType::Text, + collation: None, + options: vec![] + } + ], + create_table.columns + ); + match *create_table.partition_by.unwrap() { + Expr::Function(f) => { + assert_eq!("RANGE", f.name.to_string()); + assert_eq!( + FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + clauses: vec![], + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("a")) + ))], + }), + f.args + ); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } +} + #[test] fn parse_join_constraint_unnest_alias() { assert_eq!( From 20f7ac59e38d52e293476b7ad844e7f744a16c43 Mon Sep 17 00:00:00 2001 From: hulk Date: Tue, 16 Jul 2024 01:54:44 +0800 Subject: [PATCH 493/806] Fix AS query clause should be after the create table options (#1339) --- src/ast/dml.rs | 6 +++--- src/parser/mod.rs | 14 +++++++------- tests/sqlparser_clickhouse.rs | 24 ++++++++++++++++++++++++ tests/sqlparser_mysql.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index b35b2b970..0ebbaa3e9 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -418,9 +418,6 @@ impl Display for CreateTable { write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; } - if let Some(query) = &self.query { - write!(f, " AS {query}")?; - } if let Some(default_charset) = &self.default_charset { write!(f, " DEFAULT CHARSET={default_charset}")?; } @@ -440,6 +437,9 @@ impl Display for CreateTable { if self.strict { write!(f, " STRICT")?; } + if let Some(query) = &self.query { + write!(f, " AS {query}")?; + } Ok(()) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4d2319a08..d00f28a55 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5418,13 +5418,6 @@ impl<'a> Parser<'a> { let create_table_config = self.parse_optional_create_table_config()?; - // Parse optional `AS ( query )` - let query = if self.parse_keyword(Keyword::AS) { - Some(self.parse_boxed_query()?) - } else { - None - }; - let default_charset = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARSET]) { self.expect_token(&Token::Eq)?; let next_token = self.next_token(); @@ -5477,6 +5470,13 @@ impl<'a> Parser<'a> { None }; + // Parse optional `AS ( query )` + let query = if self.parse_keyword(Keyword::AS) { + Some(self.parse_boxed_query()?) + } else { + None + }; + Ok(CreateTableBuilder::new(table_name) .temporary(temporary) .columns(columns) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 99db3d10c..752940551 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -802,6 +802,30 @@ fn test_query_with_format_clause() { } } +#[test] +fn parse_create_table_on_commit_and_as_query() { + let sql = r#"CREATE LOCAL TEMPORARY TABLE test ON COMMIT PRESERVE ROWS AS SELECT 1"#; + match clickhouse_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + on_commit, + query, + .. + }) => { + assert_eq!(name.to_string(), "test"); + assert_eq!(on_commit, Some(OnCommit::PreserveRows)); + assert_eq!( + query.unwrap().body.as_select().unwrap().projection, + vec![UnnamedExpr(Expr::Value(Value::Number( + "1".parse().unwrap(), + false + )))] + ); + } + _ => unreachable!(), + } +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ec094bcd6..c2ce407a7 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -812,6 +812,33 @@ 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"; + match mysql_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { + name, + collation, + query, + .. + }) => { + assert_eq!(name.to_string(), "foo"); + assert_eq!(collation, Some("utf8mb4_0900_ai_ci".to_string())); + assert_eq!( + query.unwrap().body.as_select().unwrap().projection, + vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))] + ); + } + _ => unreachable!(), + } + + 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(_)) + )); +} + #[test] fn parse_create_table_comment_character_set() { let sql = "CREATE TABLE foo (s TEXT CHARACTER SET utf8mb4 COMMENT 'comment')"; From 845a1aaddd371a586c41ab9b68ad21a4bbc3884f Mon Sep 17 00:00:00 2001 From: Nick Presta Date: Sat, 20 Jul 2024 06:51:12 -0400 Subject: [PATCH 494/806] [ClickHouse] Add support for WITH FILL to OrderByExpr (#1330) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 17 ++-- src/ast/query.rs | 91 +++++++++++++++++- src/keywords.rs | 3 + src/parser/mod.rs | 84 ++++++++++++++++- tests/sqlparser_clickhouse.rs | 169 ++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 35 +++++-- tests/sqlparser_mssql.rs | 4 +- tests/sqlparser_mysql.rs | 31 ++++--- tests/sqlparser_postgres.rs | 10 +- 9 files changed, 397 insertions(+), 47 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b8d72e233..2a519fc7c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -43,14 +43,15 @@ 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, Join, - JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, - LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, - NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderByExpr, - PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, - ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, - SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion, - TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, + FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Interpolate, + InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, + JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern, + MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, + OffsetRows, OrderBy, OrderByExpr, PivotValueSource, Query, RenameSelectItem, + RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, + SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, + TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, + Values, WildcardAdditionalOptions, With, WithFill, }; pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, diff --git a/src/ast/query.rs b/src/ast/query.rs index 608ac2e96..978604266 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -33,7 +33,7 @@ pub struct Query { /// SELECT or UNION / EXCEPT / INTERSECT pub body: Box, /// ORDER BY - pub order_by: Vec, + pub order_by: Option, /// `LIMIT { | ALL }` pub limit: Option, @@ -67,8 +67,17 @@ impl fmt::Display for Query { write!(f, "{with} ")?; } write!(f, "{}", self.body)?; - if !self.order_by.is_empty() { - write!(f, " ORDER BY {}", display_comma_separated(&self.order_by))?; + if let Some(ref order_by) = self.order_by { + write!(f, " ORDER BY")?; + if !order_by.exprs.is_empty() { + write!(f, " {}", display_comma_separated(&order_by.exprs))?; + } + if let Some(ref interpolate) = order_by.interpolate { + match &interpolate.exprs { + Some(exprs) => write!(f, " INTERPOLATE ({})", display_comma_separated(exprs))?, + None => write!(f, " INTERPOLATE")?, + } + } } if let Some(ref limit) = self.limit { write!(f, " LIMIT {limit}")?; @@ -1668,6 +1677,18 @@ pub enum JoinConstraint { None, } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct OrderBy { + pub exprs: Vec, + /// Optional: `INTERPOLATE` + /// Supported by [ClickHouse syntax] + /// + /// [ClickHouse syntax]: + pub interpolate: Option, +} + /// An `ORDER BY` expression #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1678,6 +1699,9 @@ pub struct OrderByExpr { pub asc: Option, /// Optional `NULLS FIRST` or `NULLS LAST` pub nulls_first: Option, + /// Optional: `WITH FILL` + /// Supported by [ClickHouse syntax]: + pub with_fill: Option, } impl fmt::Display for OrderByExpr { @@ -1693,6 +1717,67 @@ impl fmt::Display for OrderByExpr { Some(false) => write!(f, " NULLS LAST")?, None => (), } + if let Some(ref with_fill) = self.with_fill { + write!(f, " {}", with_fill)? + } + Ok(()) + } +} + +/// ClickHouse `WITH FILL` modifier for `ORDER BY` clause. +/// Supported by [ClickHouse syntax] +/// +/// [ClickHouse syntax]: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct WithFill { + pub from: Option, + pub to: Option, + pub step: Option, +} + +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)?; + } + if let Some(ref to) = self.to { + write!(f, " TO {}", to)?; + } + if let Some(ref step) = self.step { + write!(f, " STEP {}", step)?; + } + Ok(()) + } +} + +/// ClickHouse `INTERPOLATE` clause for use in `ORDER BY` clause when using `WITH FILL` modifier. +/// Supported by [ClickHouse syntax] +/// +/// [ClickHouse syntax]: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct InterpolateExpr { + pub column: Ident, + pub expr: Option, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Interpolate { + pub exprs: Option>, +} + +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)?; + } Ok(()) } } diff --git a/src/keywords.rs b/src/keywords.rs index a53eaccba..2b6900fba 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -297,6 +297,7 @@ define_keywords!( FILE, FILES, FILE_FORMAT, + FILL, FILTER, FIRST, FIRST_VALUE, @@ -382,6 +383,7 @@ define_keywords!( INT64, INT8, INTEGER, + INTERPOLATE, INTERSECT, INTERSECTION, INTERVAL, @@ -682,6 +684,7 @@ define_keywords!( STDDEV_SAMP, STDIN, STDOUT, + STEP, STORAGE_INTEGRATION, STORED, STRICT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d00f28a55..fb15275e9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7934,7 +7934,7 @@ impl<'a> Parser<'a> { body: self.parse_insert_setexpr_boxed()?, limit: None, limit_by: vec![], - order_by: vec![], + order_by: None, offset: None, fetch: None, locks: vec![], @@ -7948,7 +7948,7 @@ impl<'a> Parser<'a> { body: self.parse_update_setexpr_boxed()?, limit: None, limit_by: vec![], - order_by: vec![], + order_by: None, offset: None, fetch: None, locks: vec![], @@ -7960,9 +7960,19 @@ impl<'a> Parser<'a> { let body = self.parse_boxed_query_body(0)?; let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { - self.parse_comma_separated(Parser::parse_order_by_expr)? + let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; + let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) { + self.parse_interpolations()? + } else { + None + }; + + Some(OrderBy { + exprs: order_by_exprs, + interpolate, + }) } else { - vec![] + None }; let mut limit = None; @@ -9193,7 +9203,7 @@ impl<'a> Parser<'a> { subquery: Box::new(Query { with: None, body: Box::new(values), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -10519,13 +10529,77 @@ impl<'a> Parser<'a> { None }; + let with_fill = if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keywords(&[Keyword::WITH, Keyword::FILL]) + { + Some(self.parse_with_fill()?) + } else { + None + }; + Ok(OrderByExpr { expr, asc, nulls_first, + with_fill, }) } + // Parse a WITH FILL clause (ClickHouse dialect) + // that follow the WITH FILL keywords in a ORDER BY clause + pub fn parse_with_fill(&mut self) -> Result { + let from = if self.parse_keyword(Keyword::FROM) { + Some(self.parse_expr()?) + } else { + None + }; + + let to = if self.parse_keyword(Keyword::TO) { + Some(self.parse_expr()?) + } else { + None + }; + + let step = if self.parse_keyword(Keyword::STEP) { + Some(self.parse_expr()?) + } else { + None + }; + + Ok(WithFill { from, to, step }) + } + + // Parse a set of comma seperated INTERPOLATE expressions (ClickHouse dialect) + // that follow the INTERPOLATE keyword in an ORDER BY clause with the WITH FILL modifier + pub fn parse_interpolations(&mut self) -> Result, ParserError> { + if !self.parse_keyword(Keyword::INTERPOLATE) { + return Ok(None); + } + + if self.consume_token(&Token::LParen) { + let interpolations = self.parse_comma_separated0(|p| p.parse_interpolation())?; + self.expect_token(&Token::RParen)?; + // INTERPOLATE () and INTERPOLATE ( ... ) variants + return Ok(Some(Interpolate { + exprs: Some(interpolations), + })); + } + + // INTERPOLATE + Ok(Some(Interpolate { exprs: None })) + } + + // Parse a INTERPOLATE expression (ClickHouse dialect) + pub fn parse_interpolation(&mut self) -> Result { + let column = self.parse_identifier(false)?; + let expr = if self.parse_keyword(Keyword::AS) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(InterpolateExpr { column, expr }) + } + /// Parse a TOP clause, MSSQL equivalent of LIMIT, /// that follows after `SELECT [DISTINCT]`. pub fn parse_top(&mut self) -> Result { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 752940551..10d7d66ff 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -720,6 +720,175 @@ fn parse_group_by_with_modifier() { } } +#[test] +fn parse_select_order_by_with_fill_interpolate() { + let sql = "SELECT id, fname, lname FROM customer WHERE id < 5 \ + ORDER BY \ + fname ASC NULLS FIRST WITH FILL FROM 10 TO 20 STEP 2, \ + lname DESC NULLS LAST WITH FILL FROM 30 TO 40 STEP 3 \ + INTERPOLATE (col1 AS col1 + 1) \ + LIMIT 2"; + let select = clickhouse().verified_query(sql); + assert_eq!( + OrderBy { + exprs: vec![ + OrderByExpr { + expr: Expr::Identifier(Ident::new("fname")), + asc: Some(true), + nulls_first: Some(true), + with_fill: Some(WithFill { + from: Some(Expr::Value(number("10"))), + to: Some(Expr::Value(number("20"))), + step: Some(Expr::Value(number("2"))), + }), + }, + OrderByExpr { + expr: Expr::Identifier(Ident::new("lname")), + asc: Some(false), + nulls_first: Some(false), + with_fill: Some(WithFill { + from: Some(Expr::Value(number("30"))), + to: Some(Expr::Value(number("40"))), + step: Some(Expr::Value(number("3"))), + }), + }, + ], + interpolate: Some(Interpolate { + exprs: Some(vec![InterpolateExpr { + column: Ident::new("col1"), + expr: Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col1"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Value(number("1"))), + }), + }]) + }) + }, + select.order_by.expect("ORDER BY expected") + ); + assert_eq!(Some(Expr::Value(number("2"))), select.limit); +} + +#[test] +fn parse_select_order_by_with_fill_interpolate_multi_interpolates() { + let sql = "SELECT id, fname, lname FROM customer ORDER BY fname WITH FILL \ + INTERPOLATE (col1 AS col1 + 1) INTERPOLATE (col2 AS col2 + 2)"; + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("ORDER BY only accepts a single INTERPOLATE clause"); +} + +#[test] +fn parse_select_order_by_with_fill_interpolate_multi_with_fill_interpolates() { + let sql = "SELECT id, fname, lname FROM customer \ + ORDER BY \ + fname WITH FILL INTERPOLATE (col1 AS col1 + 1), \ + lname WITH FILL INTERPOLATE (col2 AS col2 + 2)"; + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("ORDER BY only accepts a single INTERPOLATE clause"); +} + +#[test] +fn parse_select_order_by_interpolate_not_last() { + let sql = "SELECT id, fname, lname FROM customer \ + ORDER BY \ + fname INTERPOLATE (col2 AS col2 + 2), + lname"; + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("ORDER BY INTERPOLATE must be in the last position"); +} + +#[test] +fn parse_with_fill() { + let sql = "SELECT fname FROM customer ORDER BY fname \ + WITH FILL FROM 10 TO 20 STEP 2"; + let select = clickhouse().verified_query(sql); + assert_eq!( + Some(WithFill { + from: Some(Expr::Value(number("10"))), + to: Some(Expr::Value(number("20"))), + step: Some(Expr::Value(number("2"))), + }), + select.order_by.expect("ORDER BY expected").exprs[0].with_fill + ); +} + +#[test] +fn parse_with_fill_missing_single_argument() { + let sql = "SELECT id, fname, lname FROM customer ORDER BY \ + fname WITH FILL FROM TO 20"; + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("WITH FILL requires expressions for all arguments"); +} + +#[test] +fn parse_with_fill_multiple_incomplete_arguments() { + let sql = "SELECT id, fname, lname FROM customer ORDER BY \ + fname WITH FILL FROM TO 20, lname WITH FILL FROM TO STEP 1"; + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("WITH FILL requires expressions for all arguments"); +} + +#[test] +fn parse_interpolate_body_with_columns() { + let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL \ + INTERPOLATE (col1 AS col1 + 1, col2 AS col3, col4 AS col4 + 4)"; + let select = clickhouse().verified_query(sql); + assert_eq!( + Some(Interpolate { + exprs: Some(vec![ + InterpolateExpr { + column: Ident::new("col1"), + expr: Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col1"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Value(number("1"))), + }), + }, + InterpolateExpr { + column: Ident::new("col2"), + expr: Some(Expr::Identifier(Ident::new("col3"))), + }, + InterpolateExpr { + column: Ident::new("col4"), + expr: Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col4"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Value(number("4"))), + }), + }, + ]) + }), + select.order_by.expect("ORDER BY expected").interpolate + ); +} + +#[test] +fn parse_interpolate_without_body() { + let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL INTERPOLATE"; + let select = clickhouse().verified_query(sql); + assert_eq!( + Some(Interpolate { exprs: None }), + select.order_by.expect("ORDER BY expected").interpolate + ); +} + +#[test] +fn parse_interpolate_with_empty_body() { + let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL INTERPOLATE ()"; + let select = clickhouse().verified_query(sql); + assert_eq!( + Some(Interpolate { + exprs: Some(vec![]) + }), + select.order_by.expect("ORDER BY expected").interpolate + ); +} + #[test] fn test_prewhere() { match clickhouse_and_generic().verified_stmt("SELECT * FROM t PREWHERE x = 1 WHERE y = 2") { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1adda149e..125e5f1f8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -409,7 +409,7 @@ fn parse_update_set_from() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -2065,19 +2065,22 @@ fn parse_select_order_by() { expr: Expr::Identifier(Ident::new("lname")), asc: Some(true), nulls_first: None, + with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), asc: Some(false), nulls_first: None, + with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("id")), asc: None, nulls_first: None, + with_fill: None, }, ], - select.order_by + select.order_by.expect("ORDER BY expected").exprs ); } chk("SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY lname ASC, fname DESC, id"); @@ -2097,14 +2100,16 @@ fn parse_select_order_by_limit() { expr: Expr::Identifier(Ident::new("lname")), asc: Some(true), nulls_first: None, + with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), asc: Some(false), nulls_first: None, + with_fill: None, }, ], - select.order_by + select.order_by.expect("ORDER BY expected").exprs ); assert_eq!(Some(Expr::Value(number("2"))), select.limit); } @@ -2120,14 +2125,16 @@ fn parse_select_order_by_nulls_order() { expr: Expr::Identifier(Ident::new("lname")), asc: Some(true), nulls_first: Some(true), + with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), asc: Some(false), nulls_first: Some(false), + with_fill: None, }, ], - select.order_by + select.order_by.expect("ORDER BY expeccted").exprs ); assert_eq!(Some(Expr::Value(number("2"))), select.limit); } @@ -2219,6 +2226,7 @@ fn parse_select_qualify() { expr: Expr::Identifier(Ident::new("o")), asc: None, nulls_first: None, + with_fill: None, }], window_frame: None, })), @@ -2579,6 +2587,7 @@ fn parse_listagg() { }), asc: None, nulls_first: None, + with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident { @@ -2587,6 +2596,7 @@ fn parse_listagg() { }), asc: None, nulls_first: None, + with_fill: None, }, ] }), @@ -3437,7 +3447,7 @@ fn parse_create_table_as_table() { table_name: Some("old_table".to_string()), schema_name: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -3464,7 +3474,7 @@ fn parse_create_table_as_table() { table_name: Some("old_table".to_string()), schema_name: Some("schema_name".to_string()), }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -4384,6 +4394,7 @@ fn parse_window_functions() { expr: Expr::Identifier(Ident::new("dt")), asc: Some(false), nulls_first: None, + with_fill: None, }], window_frame: None, })), @@ -4593,6 +4604,7 @@ fn test_parse_named_window() { }), asc: None, nulls_first: None, + with_fill: None, }], window_frame: None, }), @@ -5014,7 +5026,7 @@ fn parse_interval_and_or_xor() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -7300,11 +7312,13 @@ fn parse_create_index() { expr: Expr::Identifier(Ident::new("name")), asc: None, nulls_first: None, + with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("age")), asc: Some(false), nulls_first: None, + with_fill: None, }, ]; match verified_stmt(sql) { @@ -7334,11 +7348,13 @@ fn test_create_index_with_using_function() { expr: Expr::Identifier(Ident::new("name")), asc: None, nulls_first: None, + with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("age")), asc: Some(false), nulls_first: None, + with_fill: None, }, ]; match verified_stmt(sql) { @@ -7691,7 +7707,7 @@ fn parse_merge() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -9223,7 +9239,7 @@ fn parse_unload() { fetch: None, locks: vec![], for_clause: None, - order_by: vec![], + order_by: None, settings: None, format_clause: None, }), @@ -9622,6 +9638,7 @@ fn test_match_recognize() { expr: Expr::Identifier(Ident::new("price_date")), asc: None, nulls_first: None, + with_fill: None, }], measures: vec![ Measure { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 26bece81d..3e8b6afbf 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -103,7 +103,7 @@ fn parse_create_procedure() { fetch: None, locks: vec![], for_clause: None, - order_by: vec![], + order_by: None, settings: None, format_clause: None, body: Box::new(SetExpr::Select(Box::new(Select { @@ -546,7 +546,7 @@ fn parse_substring_in_select() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index c2ce407a7..b0b29f347 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -946,7 +946,7 @@ fn parse_escaped_quote_identifiers_with_escape() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -996,7 +996,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1043,7 +1043,7 @@ fn parse_escaped_backticks_with_escape() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1090,7 +1090,7 @@ fn parse_escaped_backticks_with_no_escape() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1296,7 +1296,7 @@ fn parse_simple_insert() { ] ] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1340,7 +1340,7 @@ fn parse_ignore_insert() { Expr::Value(number("1")) ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1384,7 +1384,7 @@ fn parse_priority_insert() { Expr::Value(number("1")) ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1425,7 +1425,7 @@ fn parse_priority_insert() { Expr::Value(number("1")) ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1474,7 +1474,7 @@ fn parse_insert_as() { "2024-01-01".to_string() ))]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1535,7 +1535,7 @@ fn parse_insert_as() { Expr::Value(Value::SingleQuotedString("2024-01-01".to_string())) ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1580,7 +1580,7 @@ fn parse_replace_insert() { Expr::Value(number("1")) ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1619,7 +1619,7 @@ fn parse_empty_row_insert() { explicit_row: false, rows: vec![vec![], vec![]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1681,7 +1681,7 @@ fn parse_insert_with_on_duplicate_update() { Expr::Value(Value::Boolean(true)), ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -1946,6 +1946,7 @@ fn parse_delete_with_order_by() { }), asc: Some(false), nulls_first: None, + with_fill: None, }], order_by ); @@ -2331,7 +2332,7 @@ fn parse_substring_in_select() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -2639,7 +2640,7 @@ fn parse_hex_string_introducer() { into: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ed17e9d8f..5ac421da0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1159,7 +1159,7 @@ fn parse_copy_to() { value_table_mode: None, connect_by: None, }))), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -2491,7 +2491,7 @@ fn parse_array_subquery_expr() { connect_by: None, }))), }), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -4162,7 +4162,7 @@ fn test_simple_postgres_insert_with_alias() { Expr::Value(Value::Number("123".to_string(), false)) ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -4231,7 +4231,7 @@ fn test_simple_postgres_insert_with_alias() { )) ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, @@ -4296,7 +4296,7 @@ fn test_simple_insert_with_quoted_alias() { Expr::Value(Value::SingleQuotedString("0123".to_string())) ]] })), - order_by: vec![], + order_by: None, limit: None, limit_by: vec![], offset: None, From 028ada8350d3b2ada4aa67f5e828b318565590f2 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Sat, 20 Jul 2024 12:55:24 +0200 Subject: [PATCH 495/806] Support subquery expression in SET expressions (#1343) --- src/parser/mod.rs | 42 +++++++++++++++++++++++++-------------- tests/sqlparser_common.rs | 30 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fb15275e9..132e4f04e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1208,20 +1208,18 @@ impl<'a> Parser<'a> { Ok(Expr::Value(self.parse_value()?)) } Token::LParen => { - let expr = - if self.parse_keyword(Keyword::SELECT) || self.parse_keyword(Keyword::WITH) { - self.prev_token(); - Expr::Subquery(self.parse_boxed_query()?) - } else if let Some(lambda) = self.try_parse_lambda() { - return Ok(lambda); - } else { - let exprs = self.parse_comma_separated(Parser::parse_expr)?; - match exprs.len() { - 0 => unreachable!(), // parse_comma_separated ensures 1 or more - 1 => Expr::Nested(Box::new(exprs.into_iter().next().unwrap())), - _ => Expr::Tuple(exprs), - } - }; + let expr = if let Some(expr) = self.try_parse_expr_sub_query()? { + expr + } else if let Some(lambda) = self.try_parse_lambda() { + return Ok(lambda); + } else { + let exprs = self.parse_comma_separated(Parser::parse_expr)?; + match exprs.len() { + 0 => unreachable!(), // parse_comma_separated ensures 1 or more + 1 => Expr::Nested(Box::new(exprs.into_iter().next().unwrap())), + _ => Expr::Tuple(exprs), + } + }; self.expect_token(&Token::RParen)?; if !self.consume_token(&Token::Period) { Ok(expr) @@ -1263,6 +1261,18 @@ impl<'a> Parser<'a> { } } + fn try_parse_expr_sub_query(&mut self) -> Result, ParserError> { + if self + .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) + .is_none() + { + return Ok(None); + } + self.prev_token(); + + Ok(Some(Expr::Subquery(self.parse_boxed_query()?))) + } + fn try_parse_lambda(&mut self) -> Option { if !self.dialect.supports_lambda_functions() { return None; @@ -8709,7 +8719,9 @@ impl<'a> Parser<'a> { let mut values = vec![]; loop { - let value = if let Ok(expr) = self.parse_expr() { + 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())? diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 125e5f1f8..b1afdf28b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7135,9 +7135,39 @@ fn parse_set_variable() { _ => unreachable!(), } + // Subquery expression + for (sql, canonical) in [ + ( + "SET (a) = (SELECT 22 FROM tbl1)", + "SET (a) = ((SELECT 22 FROM tbl1))", + ), + ( + "SET (a) = (SELECT 22 FROM tbl1, (SELECT 1 FROM tbl2))", + "SET (a) = ((SELECT 22 FROM tbl1, (SELECT 1 FROM tbl2)))", + ), + ( + "SET (a) = ((SELECT 22 FROM tbl1, (SELECT 1 FROM tbl2)))", + "SET (a) = ((SELECT 22 FROM tbl1, (SELECT 1 FROM tbl2)))", + ), + ( + "SET (a, b) = ((SELECT 22 FROM tbl1, (SELECT 1 FROM tbl2)), SELECT 33 FROM tbl3)", + "SET (a, b) = ((SELECT 22 FROM tbl1, (SELECT 1 FROM tbl2)), (SELECT 33 FROM tbl3))", + ), + ] { + multi_variable_dialects.one_statement_parses_to(sql, canonical); + } + let error_sqls = [ ("SET (a, b, c) = (1, 2, 3", "Expected: ), found: EOF"), ("SET (a, b, c) = 1, 2, 3", "Expected: (, found: 1"), + ( + "SET (a) = ((SELECT 22 FROM tbl1)", + "Expected: ), found: EOF", + ), + ( + "SET (a) = ((SELECT 22 FROM tbl1) (SELECT 22 FROM tbl1))", + "Expected: ), found: (", + ), ]; for (sql, error) in error_sqls { assert_eq!( From 71dc96658655e25288acdb9dc1d5c9d0f245016a Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Sun, 21 Jul 2024 14:02:12 +0400 Subject: [PATCH 496/806] Fix quoted identifier regression edge-case with "from" in SELECT (#1346) --- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 132e4f04e..175b02765 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10331,7 +10331,7 @@ impl<'a> Parser<'a> { Expr::Wildcard => Ok(SelectItem::Wildcard( self.parse_wildcard_additional_options()?, )), - Expr::Identifier(v) if v.value.to_lowercase() == "from" => { + Expr::Identifier(v) if v.value.to_lowercase() == "from" && v.quote_style.is_none() => { parser_err!( format!("Expected an expression, found: {}", v), self.peek_token().location diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b1afdf28b..dbadb4813 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9005,7 +9005,7 @@ fn parse_non_latin_identifiers() { #[test] fn parse_trailing_comma() { - // At the moment, Duck DB is the only dialect that allows + // At the moment, DuckDB is the only dialect that allows // trailing commas anywhere in the query let trailing_commas = TestedDialects { dialects: vec![Box::new(DuckDbDialect {})], @@ -9038,11 +9038,16 @@ fn parse_trailing_comma() { ); trailing_commas.verified_stmt("SELECT album_id, name FROM track"); - trailing_commas.verified_stmt("SELECT * FROM track ORDER BY milliseconds"); - trailing_commas.verified_stmt("SELECT DISTINCT ON (album_id) name FROM track"); + // check quoted "from" identifier edge-case + trailing_commas.one_statement_parses_to( + r#"SELECT "from", FROM "from""#, + r#"SELECT "from" FROM "from""#, + ); + trailing_commas.verified_stmt(r#"SELECT "from" FROM "from""#); + // doesn't allow any trailing commas let trailing_commas = TestedDialects { dialects: vec![Box::new(GenericDialect {})], From 48ea5640a221b91a93fad769f96cd2aa37932436 Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Sun, 21 Jul 2024 20:18:50 +0800 Subject: [PATCH 497/806] Support Map literal syntax for DuckDB and Generic (#1344) --- src/ast/mod.rs | 42 ++++++++++++++ src/dialect/duckdb.rs | 7 +++ src/dialect/generic.rs | 4 ++ src/dialect/mod.rs | 5 ++ src/parser/mod.rs | 44 ++++++++++++++ tests/sqlparser_common.rs | 95 +++++++++++++++++++++++++++++++ tests/sqlparser_custom_dialect.rs | 22 +++++++ 7 files changed, 219 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2a519fc7c..cdc2e2049 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -329,6 +329,37 @@ impl fmt::Display for DictionaryField { } } +/// Represents a Map expression. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Map { + pub entries: Vec, +} + +impl Display for Map { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "MAP {{{}}}", display_comma_separated(&self.entries)) + } +} + +/// A map field within a map. +/// +/// [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))] +pub struct MapEntry { + pub key: Box, + pub value: Box, +} + +impl fmt::Display for MapEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.key, self.value) + } +} + /// Options for `CAST` / `TRY_CAST` /// BigQuery: #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -764,6 +795,14 @@ pub enum Expr { /// ``` /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs Dictionary(Vec), + /// `DuckDB` specific `Map` literal expression [1] + /// + /// Syntax: + /// ```sql + /// syntax: Map {key1: value1[, ... ]} + /// ``` + /// [1]: https://duckdb.org/docs/sql/data_types/map#creating-maps + Map(Map), /// An access of nested data using subscript syntax, for example `array[2]`. Subscript { expr: Box, @@ -1331,6 +1370,9 @@ impl fmt::Display for Expr { Expr::Dictionary(fields) => { write!(f, "{{{}}}", display_comma_separated(fields)) } + Expr::Map(map) => { + write!(f, "{map}") + } Expr::Subscript { expr, subscript: key, diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index c6edeac14..1fc211685 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -48,4 +48,11 @@ impl Dialect for DuckDbDialect { fn supports_dictionary_syntax(&self) -> bool { true } + + // DuckDB uses this syntax for `MAP`s. + // + // https://duckdb.org/docs/sql/data_types/map.html#creating-maps + fn support_map_literal_syntax(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 33391d479..8d762d780 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -70,4 +70,8 @@ impl Dialect for GenericDialect { fn supports_select_wildcard_except(&self) -> bool { true } + + fn support_map_literal_syntax(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b223ead47..3ff7bb2a5 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -215,6 +215,11 @@ pub trait Dialect: Debug + Any { fn supports_dictionary_syntax(&self) -> bool { false } + /// Returns true if the dialect supports defining object using the + /// syntax like `Map {1: 10, 2: 20}`. + fn support_map_literal_syntax(&self) -> bool { + false + } /// Returns true if the dialect supports lambda functions, for example: /// /// ```sql diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 175b02765..878cabfcc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1078,6 +1078,9 @@ impl<'a> Parser<'a> { let expr = self.parse_subexpr(Self::PLUS_MINUS_PREC)?; Ok(Expr::Prior(Box::new(expr))) } + Keyword::MAP if self.peek_token() == Token::LBrace && self.dialect.support_map_literal_syntax() => { + self.parse_duckdb_map_literal() + } // Here `w` is a word, check if it's a part of a multipart // identifier, a function call, or a simple identifier: _ => match self.peek_token().token { @@ -2322,6 +2325,47 @@ impl<'a> Parser<'a> { }) } + /// DuckDB specific: Parse a duckdb [map] + /// + /// Syntax: + /// + /// ```sql + /// Map {key1: value1[, ... ]} + /// ``` + /// + /// [map]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps + fn parse_duckdb_map_literal(&mut self) -> Result { + self.expect_token(&Token::LBrace)?; + + let fields = self.parse_comma_separated(Self::parse_duckdb_map_field)?; + + self.expect_token(&Token::RBrace)?; + + Ok(Expr::Map(Map { entries: fields })) + } + + /// Parse a field for a duckdb [map] + /// + /// Syntax + /// + /// ```sql + /// key: value + /// ``` + /// + /// [map]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps + fn parse_duckdb_map_field(&mut self) -> Result { + let key = self.parse_expr()?; + + self.expect_token(&Token::Colon)?; + + let value = self.parse_expr()?; + + Ok(MapEntry { + key: Box::new(key), + value: Box::new(value), + }) + } + /// Parse clickhouse [map] /// /// Syntax diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index dbadb4813..ac5098f58 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10077,6 +10077,101 @@ fn test_dictionary_syntax() { ) } +#[test] +fn test_map_syntax() { + fn check(sql: &str, expect: Expr) { + assert_eq!( + all_dialects_where(|d| d.support_map_literal_syntax()).verified_expr(sql), + expect + ); + } + + check( + "MAP {'Alberta': 'Edmonton', 'Manitoba': 'Winnipeg'}", + Expr::Map(Map { + entries: vec![ + MapEntry { + key: Box::new(Expr::Value(Value::SingleQuotedString("Alberta".to_owned()))), + value: Box::new(Expr::Value(Value::SingleQuotedString( + "Edmonton".to_owned(), + ))), + }, + MapEntry { + key: Box::new(Expr::Value(Value::SingleQuotedString( + "Manitoba".to_owned(), + ))), + value: Box::new(Expr::Value(Value::SingleQuotedString( + "Winnipeg".to_owned(), + ))), + }, + ], + }), + ); + + fn number_expr(s: &str) -> Expr { + Expr::Value(number(s)) + } + + check( + "MAP {1: 10.0, 2: 20.0}", + Expr::Map(Map { + entries: vec![ + MapEntry { + key: Box::new(number_expr("1")), + value: Box::new(number_expr("10.0")), + }, + MapEntry { + key: Box::new(number_expr("2")), + value: Box::new(number_expr("20.0")), + }, + ], + }), + ); + + check( + "MAP {[1, 2, 3]: 10.0, [4, 5, 6]: 20.0}", + Expr::Map(Map { + entries: vec![ + MapEntry { + key: Box::new(Expr::Array(Array { + elem: vec![number_expr("1"), number_expr("2"), number_expr("3")], + named: false, + })), + value: Box::new(Expr::Value(number("10.0"))), + }, + MapEntry { + key: Box::new(Expr::Array(Array { + elem: vec![number_expr("4"), number_expr("5"), number_expr("6")], + named: false, + })), + value: Box::new(Expr::Value(number("20.0"))), + }, + ], + }), + ); + + check( + "MAP {'a': 10, 'b': 20}['a']", + Expr::Subscript { + expr: Box::new(Expr::Map(Map { + entries: vec![ + MapEntry { + key: Box::new(Expr::Value(Value::SingleQuotedString("a".to_owned()))), + value: Box::new(number_expr("10")), + }, + MapEntry { + key: Box::new(Expr::Value(Value::SingleQuotedString("b".to_owned()))), + value: Box::new(number_expr("20")), + }, + ], + })), + subscript: Box::new(Subscript::Index { + index: Expr::Value(Value::SingleQuotedString("a".to_owned())), + }), + }, + ); +} + #[test] fn parse_within_group() { verified_expr("PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sales_amount)"); diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index 516591382..5b29047a4 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -125,6 +125,28 @@ fn custom_statement_parser() -> Result<(), ParserError> { Ok(()) } +#[test] +fn test_map_syntax_not_support_default() -> Result<(), ParserError> { + #[derive(Debug)] + struct MyDialect {} + + impl Dialect for MyDialect { + fn is_identifier_start(&self, ch: char) -> bool { + is_identifier_start(ch) + } + + fn is_identifier_part(&self, ch: char) -> bool { + is_identifier_part(ch) + } + } + + let dialect = MyDialect {}; + let sql = "SELECT MAP {1: 2}"; + let ast = Parser::parse_sql(&dialect, sql); + assert!(ast.is_err()); + Ok(()) +} + fn is_identifier_start(ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } From b27abf00e2e67b28b25afc9da7c2ddd2a104c449 Mon Sep 17 00:00:00 2001 From: hulk Date: Tue, 23 Jul 2024 03:50:24 +0800 Subject: [PATCH 498/806] Allow to use `()` as the GROUP BY nothing (#1347) --- src/parser/mod.rs | 5 +++++ tests/sqlparser_common.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 878cabfcc..11fa9e4a9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1487,6 +1487,11 @@ impl<'a> Parser<'a> { let result = self.parse_comma_separated(|p| p.parse_tuple(true, true))?; self.expect_token(&Token::RParen)?; Ok(Expr::Rollup(result)) + } else if self.consume_tokens(&[Token::LParen, Token::RParen]) { + // PostgreSQL allow to use empty tuple as a group by expression, + // e.g. `GROUP BY (), name`. Please refer to GROUP BY Clause section in + // [PostgreSQL](https://www.postgresql.org/docs/16/sql-select.html) + Ok(Expr::Tuple(vec![])) } else { self.parse_expr() } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ac5098f58..dd3ed0515 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -42,6 +42,7 @@ mod test_utils; #[cfg(test)] use pretty_assertions::assert_eq; +use sqlparser::ast::Expr::Identifier; use sqlparser::test_utils::all_dialects_except; #[test] @@ -10278,3 +10279,30 @@ fn parse_auto_increment_too_large() { assert!(res.is_err(), "{res:?}"); } + +#[test] +fn test_group_by_nothing() { + let Select { group_by, .. } = all_dialects_where(|d| d.supports_group_by_expr()) + .verified_only_select("SELECT count(1) FROM t GROUP BY ()"); + { + std::assert_eq!( + GroupByExpr::Expressions(vec![Expr::Tuple(vec![])], vec![]), + group_by + ); + } + + let Select { group_by, .. } = all_dialects_where(|d| d.supports_group_by_expr()) + .verified_only_select("SELECT name, count(1) FROM t GROUP BY name, ()"); + { + std::assert_eq!( + GroupByExpr::Expressions( + vec![ + Identifier(Ident::new("name".to_string())), + Expr::Tuple(vec![]) + ], + vec![] + ), + group_by + ); + } +} From 390d4d3554580f618c6d8edd177b875b849f326f Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 24 Jul 2024 00:41:07 +0800 Subject: [PATCH 499/806] Add support of MATERIALIZED/ALIAS/EPHERMERAL default column options for ClickHouse (#1348) --- src/ast/ddl.rs | 21 ++++++++ src/keywords.rs | 2 + src/parser/mod.rs | 18 +++++++ tests/sqlparser_clickhouse.rs | 96 +++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1ed3857d7..5cc671cf5 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -923,6 +923,18 @@ pub enum ColumnOption { NotNull, /// `DEFAULT ` Default(Expr), + + /// ClickHouse supports `MATERIALIZE`, `EPHEMERAL` and `ALIAS` expr to generate default values. + /// Syntax: `b INT MATERIALIZE (a + 1)` + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values) + + /// `MATERIALIZE ` + Materialized(Expr), + /// `EPHEMERAL []` + Ephemeral(Option), + /// `ALIAS ` + Alias(Expr), + /// `{ PRIMARY KEY | UNIQUE } []` Unique { is_primary: bool, @@ -978,6 +990,15 @@ impl fmt::Display for ColumnOption { Null => write!(f, "NULL"), NotNull => write!(f, "NOT NULL"), Default(expr) => write!(f, "DEFAULT {expr}"), + Materialized(expr) => write!(f, "MATERIALIZED {expr}"), + Ephemeral(expr) => { + if let Some(e) = expr { + write!(f, "EPHEMERAL {e}") + } else { + write!(f, "EPHEMERAL") + } + } + Alias(expr) => write!(f, "ALIAS {expr}"), Unique { is_primary, characteristics, diff --git a/src/keywords.rs b/src/keywords.rs index 2b6900fba..e59e49339 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -77,6 +77,7 @@ define_keywords!( AFTER, AGAINST, AGGREGATION, + ALIAS, ALL, ALLOCATE, ALTER, @@ -267,6 +268,7 @@ define_keywords!( ENFORCED, ENGINE, ENUM, + EPHEMERAL, EPOCH, EQUALS, ERROR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 11fa9e4a9..f8267a7cb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5748,6 +5748,24 @@ impl<'a> Parser<'a> { Ok(Some(ColumnOption::Null)) } else if self.parse_keyword(Keyword::DEFAULT) { Ok(Some(ColumnOption::Default(self.parse_expr()?))) + } else if dialect_of!(self is ClickHouseDialect| GenericDialect) + && self.parse_keyword(Keyword::MATERIALIZED) + { + Ok(Some(ColumnOption::Materialized(self.parse_expr()?))) + } else if dialect_of!(self is ClickHouseDialect| GenericDialect) + && self.parse_keyword(Keyword::ALIAS) + { + Ok(Some(ColumnOption::Alias(self.parse_expr()?))) + } else if dialect_of!(self is ClickHouseDialect| GenericDialect) + && self.parse_keyword(Keyword::EPHEMERAL) + { + // The expression is optional for the EPHEMERAL syntax, so we need to check + // if the column definition has remaining tokens before parsing the expression. + if matches!(self.peek_token().token, Token::Comma | Token::RParen) { + Ok(Some(ColumnOption::Ephemeral(None))) + } else { + Ok(Some(ColumnOption::Ephemeral(Some(self.parse_expr()?)))) + } } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) { let characteristics = self.parse_constraint_characteristics()?; Ok(Some(ColumnOption::Unique { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 10d7d66ff..6fdadc366 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -493,6 +493,102 @@ fn parse_create_table_with_primary_key() { .expect_err("ORDER BY supports one expression with tuple"); } +#[test] +fn parse_create_table_with_variant_default_expressions() { + let sql = concat!( + "CREATE TABLE table (", + "a DATETIME MATERIALIZED now(),", + " b DATETIME EPHEMERAL now(),", + " c DATETIME EPHEMERAL,", + " d STRING ALIAS toString(c)", + ") ENGINE=MergeTree" + ); + match clickhouse_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ + ColumnDef { + name: Ident::new("a"), + data_type: DataType::Datetime(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Materialized(Expr::Function(Function { + name: ObjectName(vec![Ident::new("now")]), + args: FunctionArguments::List(FunctionArgumentList { + args: vec![], + duplicate_treatment: None, + clauses: vec![], + }), + parameters: FunctionArguments::None, + null_treatment: None, + filter: None, + over: None, + within_group: vec![], + })) + }], + }, + ColumnDef { + name: Ident::new("b"), + data_type: DataType::Datetime(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Ephemeral(Some(Expr::Function(Function { + name: ObjectName(vec![Ident::new("now")]), + args: FunctionArguments::List(FunctionArgumentList { + args: vec![], + duplicate_treatment: None, + clauses: vec![], + }), + parameters: FunctionArguments::None, + null_treatment: None, + filter: None, + over: None, + within_group: vec![], + }))) + }], + }, + ColumnDef { + name: Ident::new("c"), + data_type: DataType::Datetime(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Ephemeral(None) + }], + }, + ColumnDef { + name: Ident::new("d"), + data_type: DataType::String(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Alias(Expr::Function(Function { + name: ObjectName(vec![Ident::new("toString")]), + args: FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Identifier(Ident::new("c")) + ))], + duplicate_treatment: None, + clauses: vec![], + }), + parameters: FunctionArguments::None, + null_treatment: None, + filter: None, + over: None, + within_group: vec![], + })) + }], + } + ] + ) + } + _ => unreachable!(), + } +} + #[test] fn parse_create_view_with_fields_data_types() { match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) { From 1e82a145adcc090b2768814f19f23fd4d80267a5 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 23 Jul 2024 12:56:55 -0400 Subject: [PATCH 500/806] Add CHANGELOG for 0.49.0 (#1350) --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5c9ecb4..cf2d1321b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,27 @@ changes that break via addition as "Added". ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.49.0] 2024-07-23 +As always, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs! + +We are in the process of moving sqlparser to governed as part of the Apache +DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 + +### Fixed +* Fix quoted identifier regression edge-case with "from" in SELECT (#1346) - Thanks @alexander-beedie +* Fix `AS` query clause should be after the create table options (#1339) - Thanks @git-hulk + +### Added + +* Support `MATERIALIZED`/`ALIAS`/`EPHERMERAL` default column options for ClickHouse (#1348) - Thanks @git-hulk +* Support `()` as the `GROUP BY` nothing (#1347) - Thanks @git-hulk +* Support Map literal syntax for DuckDB and Generic (#1344) - Thanks @goldmedal +* Support subquery expression in `SET` expressions (#1343) - Thanks @iffyio +* Support `WITH FILL` for ClickHouse (#1330) - Thanks @nickpresta +* Support `PARTITION BY` for PostgreSQL in `CREATE TABLE` statement (#1338) - Thanks @git-hulk +* Support of table function `WITH ORDINALITY` modifier for Postgres (#1337) - Thanks @git-hulk + + ## [0.48.0] 2024-07-09 Huge shout out to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs! From 6c64d43e1bbf4ebc78754c63560894f0d867bdac Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 23 Jul 2024 13:11:16 -0400 Subject: [PATCH 501/806] chore: Release sqlparser version 0.49.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b0bee003e..4c510a8c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.48.0" +version = "0.49.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 547d82f07de4480d236a061a41bfadac21235434 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 29 Jul 2024 14:49:05 +0200 Subject: [PATCH 502/806] fix CI clippy `1.80` warnings (#1357) --- src/keywords.rs | 2 +- src/test_utils.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keywords.rs b/src/keywords.rs index e59e49339..4b599f12a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -20,7 +20,7 @@ //! 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/src/test_utils.rs b/src/test_utils.rs index 1f5300be1..5ed6339bd 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -111,10 +111,10 @@ impl TestedDialects { /// that: /// /// 1. parsing `sql` results in the same [`Statement`] as parsing - /// `canonical`. + /// `canonical`. /// /// 2. re-serializing the result of parsing `sql` produces the same - /// `canonical` sql string + /// `canonical` sql string 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); @@ -180,10 +180,10 @@ impl TestedDialects { /// Ensures that `sql` parses as a single [`Select`], and that additionally: /// /// 1. parsing `sql` results in the same [`Statement`] as parsing - /// `canonical`. + /// `canonical`. /// /// 2. re-serializing the result of parsing `sql` produces the same - /// `canonical` sql string + /// `canonical` sql string pub fn verified_only_select_with_canonical(&self, query: &str, canonical: &str) -> Select { let q = match self.one_statement_parses_to(query, canonical) { Statement::Query(query) => *query, From 7fdb2ec5d195bebca887a1532c49ec38741eca1b Mon Sep 17 00:00:00 2001 From: hulk Date: Tue, 30 Jul 2024 05:16:29 +0800 Subject: [PATCH 503/806] Allow to use the TABLE keyword in DESC|DESCRIBE|EXPLAIN TABLE statement (#1351) --- src/ast/mod.rs | 9 +++++++++ src/parser/mod.rs | 3 +++ tests/sqlparser_common.rs | 42 ++++++++++++++++++++++++++++----------- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cdc2e2049..d27baadc4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2699,6 +2699,11 @@ pub enum Statement { describe_alias: DescribeAlias, /// Hive style `FORMATTED | EXTENDED` hive_format: Option, + /// Snowflake and ClickHouse support `DESC|DESCRIBE TABLE ` syntax + /// + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/desc-table.html) + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/describe-table) + has_table_keyword: bool, /// Table name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] table_name: ObjectName, @@ -2872,6 +2877,7 @@ impl fmt::Display for Statement { Statement::ExplainTable { describe_alias, hive_format, + has_table_keyword, table_name, } => { write!(f, "{describe_alias} ")?; @@ -2879,6 +2885,9 @@ impl fmt::Display for Statement { if let Some(format) = hive_format { write!(f, "{} ", format)?; } + if *has_table_keyword { + write!(f, "TABLE ")?; + } write!(f, "{table_name}") } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f8267a7cb..931033f7b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7972,10 +7972,13 @@ impl<'a> Parser<'a> { _ => {} } + // only allow to use TABLE keyword for DESC|DESCRIBE statement + let has_table_keyword = self.parse_keyword(Keyword::TABLE); let table_name = self.parse_object_name(false)?; Ok(Statement::ExplainTable { describe_alias, hive_format, + has_table_keyword, table_name, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index dd3ed0515..e68f25eb2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4186,31 +4186,49 @@ fn run_explain_analyze( #[test] fn parse_explain_table() { let validate_explain = - |query: &str, expected_describe_alias: DescribeAlias| match verified_stmt(query) { - Statement::ExplainTable { - describe_alias, - hive_format, - table_name, - } => { - assert_eq!(describe_alias, expected_describe_alias); - assert_eq!(hive_format, None); - assert_eq!("test_identifier", table_name.to_string()); + |query: &str, expected_describe_alias: DescribeAlias, expected_table_keyword| { + match verified_stmt(query) { + Statement::ExplainTable { + describe_alias, + hive_format, + has_table_keyword, + table_name, + } => { + assert_eq!(describe_alias, expected_describe_alias); + assert_eq!(hive_format, None); + assert_eq!(has_table_keyword, expected_table_keyword); + assert_eq!("test_identifier", table_name.to_string()); + } + _ => panic!("Unexpected Statement, must be ExplainTable"), } - _ => panic!("Unexpected Statement, must be ExplainTable"), }; - validate_explain("EXPLAIN test_identifier", DescribeAlias::Explain); - validate_explain("DESCRIBE test_identifier", DescribeAlias::Describe); + validate_explain("EXPLAIN test_identifier", DescribeAlias::Explain, false); + validate_explain("DESCRIBE test_identifier", DescribeAlias::Describe, false); + validate_explain("DESC test_identifier", DescribeAlias::Desc, false); + validate_explain( + "EXPLAIN TABLE test_identifier", + DescribeAlias::Explain, + true, + ); + validate_explain( + "DESCRIBE TABLE test_identifier", + DescribeAlias::Describe, + true, + ); + validate_explain("DESC TABLE test_identifier", DescribeAlias::Desc, true); } #[test] fn explain_describe() { verified_stmt("DESCRIBE test.table"); + verified_stmt("DESCRIBE TABLE test.table"); } #[test] fn explain_desc() { verified_stmt("DESC test.table"); + verified_stmt("DESC TABLE test.table"); } #[test] From c3ba2f33c6f52ce4dfea87bae9e77460db8f917f Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Mon, 29 Jul 2024 14:17:11 -0700 Subject: [PATCH 504/806] Snowflake: support position with normal function call syntax (#1341) Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 30 ++++++++++++++++-------------- tests/sqlparser_common.rs | 30 +++++++++++++++++------------- tests/sqlparser_snowflake.rs | 6 ++++++ 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 931033f7b..b3120bb30 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1038,7 +1038,7 @@ impl<'a> Parser<'a> { Keyword::CEIL => self.parse_ceil_floor_expr(true), Keyword::FLOOR => self.parse_ceil_floor_expr(false), Keyword::POSITION if self.peek_token().token == Token::LParen => { - self.parse_position_expr() + self.parse_position_expr(w.to_ident()) } Keyword::SUBSTRING => self.parse_substring_expr(), Keyword::OVERLAY => self.parse_overlay_expr(), @@ -1707,24 +1707,26 @@ impl<'a> Parser<'a> { } } - pub fn parse_position_expr(&mut self) -> Result { - // PARSE SELECT POSITION('@' in field) - self.expect_token(&Token::LParen)?; + pub fn parse_position_expr(&mut self, ident: Ident) -> Result { + let position_expr = self.maybe_parse(|p| { + // PARSE SELECT POSITION('@' in field) + p.expect_token(&Token::LParen)?; - // Parse the subexpr till the IN keyword - let expr = self.parse_subexpr(Self::BETWEEN_PREC)?; - if self.parse_keyword(Keyword::IN) { - let from = self.parse_expr()?; - self.expect_token(&Token::RParen)?; + // Parse the subexpr till the IN keyword + let expr = p.parse_subexpr(Self::BETWEEN_PREC)?; + p.expect_keyword(Keyword::IN)?; + let from = p.parse_expr()?; + p.expect_token(&Token::RParen)?; Ok(Expr::Position { expr: Box::new(expr), r#in: Box::new(from), }) - } else { - parser_err!( - "Position function must include IN keyword".to_string(), - self.peek_token().location - ) + }); + match position_expr { + Some(expr) => Ok(expr), + // Snowflake supports `position` as an ordinary function call + // without the special `IN` syntax. + None => self.parse_function(ObjectName(vec![ident])), } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e68f25eb2..5de76f78f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4151,7 +4151,7 @@ fn parse_scalar_function_in_projection() { for function_name in names { // like SELECT sqrt(id) FROM foo - let sql = dbg!(format!("SELECT {function_name}(id) FROM foo")); + let sql = format!("SELECT {function_name}(id) FROM foo"); let select = verified_only_select(&sql); assert_eq!( &call(function_name, [Expr::Identifier(Ident::new("id"))]), @@ -8254,30 +8254,34 @@ fn parse_time_functions() { #[test] fn parse_position() { - let sql = "SELECT POSITION('@' IN field)"; - let select = verified_only_select(sql); assert_eq!( - &Expr::Position { + Expr::Position { expr: Box::new(Expr::Value(Value::SingleQuotedString("@".to_string()))), r#in: Box::new(Expr::Identifier(Ident::new("field"))), }, - expr_from_projection(only(&select.projection)) + verified_expr("POSITION('@' IN field)"), ); -} -#[test] -fn parse_position_negative() { - let sql = "SELECT POSITION(foo) from bar"; - let res = parse_sql_statements(sql); + // some dialects (e.g. snowflake) support position as a function call (i.e. without IN) assert_eq!( - ParserError::ParserError("Position function must include IN keyword".to_string()), - res.unwrap_err() + call( + "position", + [ + Expr::Value(Value::SingleQuotedString("an".to_owned())), + Expr::Value(Value::SingleQuotedString("banana".to_owned())), + Expr::Value(number("1")), + ] + ), + verified_expr("position('an', 'banana', 1)") ); +} +#[test] +fn parse_position_negative() { let sql = "SELECT POSITION(foo IN) from bar"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected: an expression:, found: )".to_string()), + ParserError::ParserError("Expected: (, found: )".to_string()), res.unwrap_err() ); } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7a2288cbb..7abb1a947 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2256,3 +2256,9 @@ fn asof_joins() { "ORDER BY s.observed", )); } + +#[test] +fn test_parse_position() { + snowflake().verified_query("SELECT position('an', 'banana', 1)"); + snowflake().verified_query("SELECT n, h, POSITION(n IN h) FROM pos"); +} From bc15f7b4ceab849a974e84fcd38bde353cb7c2d1 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 29 Jul 2024 23:18:16 +0200 Subject: [PATCH 505/806] Support for postgres String Constants with Unicode Escapes (#1355) --- src/ast/value.rs | 40 +++++++++++++++++++ src/dialect/generic.rs | 4 ++ src/dialect/mod.rs | 15 +++++++ src/dialect/postgresql.rs | 4 ++ src/parser/mod.rs | 7 ++++ src/tokenizer.rs | 78 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 32 +++++++++++++++ 7 files changed, 180 insertions(+) diff --git a/src/ast/value.rs b/src/ast/value.rs index 4c1a56a92..17cdb839d 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -52,6 +52,10 @@ pub enum Value { /// See [Postgres docs](https://www.postgresql.org/docs/8.3/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS) /// for more details. EscapedStringLiteral(String), + /// u&'string value' (postgres extension) + /// See [Postgres docs](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-UESCAPE) + /// for more details. + UnicodeStringLiteral(String), /// B'string value' SingleQuotedByteStringLiteral(String), /// B"string value" @@ -102,6 +106,7 @@ impl fmt::Display for Value { } Value::DollarQuotedString(v) => write!(f, "{v}"), Value::EscapedStringLiteral(v) => write!(f, "E'{}'", escape_escaped_string(v)), + Value::UnicodeStringLiteral(v) => write!(f, "U&'{}'", escape_unicode_string(v)), Value::NationalStringLiteral(v) => write!(f, "N'{v}'"), Value::HexStringLiteral(v) => write!(f, "X'{v}'"), Value::Boolean(v) => write!(f, "{v}"), @@ -347,6 +352,41 @@ pub fn escape_escaped_string(s: &str) -> EscapeEscapedStringLiteral<'_> { EscapeEscapedStringLiteral(s) } +pub struct EscapeUnicodeStringLiteral<'a>(&'a str); + +impl<'a> fmt::Display for EscapeUnicodeStringLiteral<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for c in self.0.chars() { + match c { + '\'' => { + write!(f, "''")?; + } + '\\' => { + write!(f, r#"\\"#)?; + } + x if x.is_ascii() => { + 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)?; + } else { + write!(f, "\\+{:06X}", codepoint)?; + } + } + } + } + Ok(()) + } +} + +pub fn escape_unicode_string(s: &str) -> EscapeUnicodeStringLiteral<'_> { + EscapeUnicodeStringLiteral(s) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8d762d780..2777dfb02 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -35,6 +35,10 @@ impl Dialect for GenericDialect { || ch == '_' } + fn supports_unicode_string_literal(&self) -> bool { + true + } + fn supports_group_by_expr(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3ff7bb2a5..22e0baeb2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -145,6 +145,21 @@ pub trait Dialect: Debug + Any { fn supports_string_literal_backslash_escape(&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: + /// ```sql + /// SELECT U&'\0061\0062\0063'; + /// ``` + /// This is equivalent to the string literal `'abc'`. + /// See + /// - [Postgres docs](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-UESCAPE) + /// - [H2 docs](http://www.h2database.com/html/grammar.html#string) + fn supports_unicode_string_literal(&self) -> bool { + false + } + /// Does the dialect support `FILTER (WHERE expr)` for aggregate queries? fn supports_filter_during_aggregation(&self) -> bool { false diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 0e04bfa27..8254e807b 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -40,6 +40,10 @@ impl Dialect for PostgreSqlDialect { ch.is_alphabetic() || ch.is_ascii_digit() || ch == '$' || ch == '_' } + fn supports_unicode_string_literal(&self) -> bool { + true + } + /// See fn is_custom_operator_part(&self, ch: char) -> bool { matches!( diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b3120bb30..2b1c1ab7f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1191,6 +1191,10 @@ impl<'a> Parser<'a> { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } + Token::UnicodeStringLiteral(_) => { + self.prev_token(); + Ok(Expr::Value(self.parse_value()?)) + } Token::Number(_, _) | Token::SingleQuotedString(_) | Token::DoubleQuotedString(_) @@ -1868,6 +1872,7 @@ impl<'a> Parser<'a> { } Token::SingleQuotedString(_) | Token::EscapedStringLiteral(_) + | Token::UnicodeStringLiteral(_) | Token::NationalStringLiteral(_) | Token::HexStringLiteral(_) => Some(Box::new(self.parse_expr()?)), _ => self.expected( @@ -6965,6 +6970,7 @@ impl<'a> Parser<'a> { } Token::NationalStringLiteral(ref s) => Ok(Value::NationalStringLiteral(s.to_string())), Token::EscapedStringLiteral(ref s) => Ok(Value::EscapedStringLiteral(s.to_string())), + Token::UnicodeStringLiteral(ref s) => Ok(Value::UnicodeStringLiteral(s.to_string())), Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), Token::Placeholder(ref s) => Ok(Value::Placeholder(s.to_string())), tok @ Token::Colon | tok @ Token::AtSign => { @@ -7056,6 +7062,7 @@ impl<'a> Parser<'a> { Token::EscapedStringLiteral(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { Ok(s) } + Token::UnicodeStringLiteral(s) => Ok(s), _ => self.expected("literal string", next_token), } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b8336cec8..be11a3140 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -94,6 +94,8 @@ pub enum Token { NationalStringLiteral(String), /// "escaped" string literal, which are an extension to the SQL standard: i.e: e'first \n second' or E 'first \n second' EscapedStringLiteral(String), + /// Unicode string literal: i.e: U&'first \000A second' + UnicodeStringLiteral(String), /// Hexadecimal string literal: i.e.: X'deadbeef' HexStringLiteral(String), /// Comma @@ -251,6 +253,7 @@ impl fmt::Display for Token { Token::DollarQuotedString(ref s) => write!(f, "{s}"), Token::NationalStringLiteral(ref s) => write!(f, "N'{s}'"), Token::EscapedStringLiteral(ref s) => write!(f, "E'{s}'"), + Token::UnicodeStringLiteral(ref s) => write!(f, "U&'{s}'"), Token::HexStringLiteral(ref s) => write!(f, "X'{s}'"), Token::SingleQuotedByteStringLiteral(ref s) => write!(f, "B'{s}'"), Token::TripleSingleQuotedByteStringLiteral(ref s) => write!(f, "B'''{s}'''"), @@ -794,6 +797,23 @@ impl<'a> Tokenizer<'a> { } } } + // Unicode string literals like U&'first \000A second' are supported in some dialects, including PostgreSQL + x @ 'u' | x @ 'U' if self.dialect.supports_unicode_string_literal() => { + chars.next(); // consume, to check the next char + if chars.peek() == Some(&'&') { + // we cannot advance the iterator here, as we need to consume the '&' later if the 'u' was an identifier + let mut chars_clone = chars.peekable.clone(); + chars_clone.next(); // consume the '&' in the clone + if chars_clone.peek() == Some(&'\'') { + chars.next(); // consume the '&' in the original iterator + let s = unescape_unicode_single_quoted_string(chars)?; + return Ok(Some(Token::UnicodeStringLiteral(s))); + } + } + // regular identifier starting with an "U" or "u" + let s = self.tokenize_word(x, chars); + Ok(Some(Token::make_word(&s, None))) + } // The spec only allows an uppercase 'X' to introduce a hex // string, but PostgreSQL, at least, allows a lowercase 'x' too. x @ 'x' | x @ 'X' => { @@ -1797,6 +1817,64 @@ impl<'a: 'b, 'b> Unescape<'a, 'b> { } } +fn unescape_unicode_single_quoted_string(chars: &mut State<'_>) -> Result { + let mut unescaped = String::new(); + chars.next(); // consume the opening quote + while let Some(c) = chars.next() { + match c { + '\'' => { + if chars.peek() == Some(&'\'') { + chars.next(); + unescaped.push('\''); + } else { + return Ok(unescaped); + } + } + '\\' => match chars.peek() { + Some('\\') => { + chars.next(); + unescaped.push('\\'); + } + Some('+') => { + chars.next(); + unescaped.push(take_char_from_hex_digits(chars, 6)?); + } + _ => unescaped.push(take_char_from_hex_digits(chars, 4)?), + }, + _ => { + unescaped.push(c); + } + } + } + Err(TokenizerError { + message: "Unterminated unicode encoded string literal".to_string(), + location: chars.location(), + }) +} + +fn take_char_from_hex_digits( + chars: &mut State<'_>, + max_digits: usize, +) -> Result { + let mut result = 0u32; + for _ in 0..max_digits { + let next_char = chars.next().ok_or_else(|| TokenizerError { + message: "Unexpected EOF while parsing hex digit in escaped unicode string." + .to_string(), + 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), + location: chars.location(), + })?; + result = result * 16 + digit; + } + char::from_u32(result).ok_or_else(|| TokenizerError { + message: format!("Invalid unicode character: {:x}", result), + location: chars.location(), + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5ac421da0..44231e7d3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4441,3 +4441,35 @@ fn test_table_unnest_with_ordinality() { _ => panic!("Expecting TableFactor::UNNEST with ordinality"), } } + +#[test] +fn test_escaped_string_literal() { + match pg().verified_expr(r#"E'\n'"#) { + Expr::Value(Value::EscapedStringLiteral(s)) => { + assert_eq!("\n", s); + } + _ => unreachable!(), + } +} + +#[test] +fn test_unicode_string_literal() { + let pairs = [ + // Example from the postgres docs + (r#"U&'\0441\043B\043E\043D'"#, "слон"), + // High unicode code point (> 0xFFFF) + (r#"U&'\+01F418'"#, "🐘"), + // Escaped backslash + (r#"U&'\\'"#, r#"\"#), + // Escaped single quote + (r#"U&''''"#, "'"), + ]; + for (input, expected) in pairs { + match pg_and_generic().verified_expr(input) { + Expr::Value(Value::UnicodeStringLiteral(s)) => { + assert_eq!(expected, s); + } + _ => unreachable!(), + } + } +} From f96658006f85b1e88cc112a36584391c01ee766d Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 31 Jul 2024 04:30:46 +0800 Subject: [PATCH 506/806] Allow to use the GLOBAL keyword before the join operator (#1353) --- src/ast/query.rs | 7 ++++ src/keywords.rs | 1 + src/parser/mod.rs | 5 +++ src/test_utils.rs | 1 + tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_common.rs | 71 ++++++++++++++++++++++++++++++++---- tests/sqlparser_mysql.rs | 1 + tests/sqlparser_postgres.rs | 1 + tests/sqlparser_snowflake.rs | 1 + 9 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 978604266..b318f686a 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1537,6 +1537,9 @@ impl Display for TableVersion { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Join { pub relation: TableFactor, + /// ClickHouse supports the optional `GLOBAL` keyword before the join operator. + /// See [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/join) + pub global: bool, pub join_operator: JoinOperator, } @@ -1563,6 +1566,10 @@ impl fmt::Display for Join { } Suffix(constraint) } + if self.global { + write!(f, " GLOBAL")?; + } + match &self.join_operator { JoinOperator::Inner(constraint) => write!( f, diff --git a/src/keywords.rs b/src/keywords.rs index 4b599f12a..ee2bd6173 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -850,6 +850,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::USING, Keyword::CLUSTER, Keyword::DISTRIBUTE, + Keyword::GLOBAL, // for MSSQL-specific OUTER APPLY (seems reserved in most dialects) Keyword::OUTER, Keyword::SET, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2b1c1ab7f..cd2cf2186 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9015,6 +9015,7 @@ impl<'a> Parser<'a> { // a table alias. let mut joins = vec![]; loop { + let global = self.parse_keyword(Keyword::GLOBAL); let join = if self.parse_keyword(Keyword::CROSS) { let join_operator = if self.parse_keyword(Keyword::JOIN) { JoinOperator::CrossJoin @@ -9026,6 +9027,7 @@ impl<'a> Parser<'a> { }; Join { relation: self.parse_table_factor()?, + global, join_operator, } } else if self.parse_keyword(Keyword::OUTER) { @@ -9033,6 +9035,7 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::APPLY)?; Join { relation: self.parse_table_factor()?, + global, join_operator: JoinOperator::OuterApply, } } else if self.parse_keyword(Keyword::ASOF) { @@ -9042,6 +9045,7 @@ impl<'a> Parser<'a> { let match_condition = self.parse_parenthesized(Self::parse_expr)?; Join { relation, + global, join_operator: JoinOperator::AsOf { match_condition, constraint: self.parse_join_constraint(false)?, @@ -9127,6 +9131,7 @@ impl<'a> Parser<'a> { let join_constraint = self.parse_join_constraint(natural)?; Join { relation, + global, join_operator: join_operator_type(join_constraint), } }; diff --git a/src/test_utils.rs b/src/test_utils.rs index 5ed6339bd..b8e9ecee4 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -331,6 +331,7 @@ pub fn table_with_alias(name: impl Into, alias: impl Into) -> Ta pub fn join(relation: TableFactor) -> Join { Join { relation, + global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 089a41889..a0dd5a662 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1557,6 +1557,7 @@ fn parse_join_constraint_unnest_alias() { with_offset_alias: None, with_ordinality: false, }, + global: false, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), op: BinaryOperator::Eq, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5de76f78f..a8f3919df 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5600,6 +5600,7 @@ fn parse_implicit_join() { partitions: vec![], with_ordinality: false, }, + global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], }, @@ -5623,6 +5624,7 @@ fn parse_implicit_join() { partitions: vec![], with_ordinality: false, }, + global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], }, @@ -5646,6 +5648,7 @@ fn parse_cross_join() { partitions: vec![], with_ordinality: false, }, + global: false, join_operator: JoinOperator::CrossJoin, }, only(only(select.from).joins), @@ -5657,6 +5660,7 @@ fn parse_joins_on() { fn join_with_constraint( relation: impl Into, alias: Option, + global: bool, f: impl Fn(JoinConstraint) -> JoinOperator, ) -> Join { Join { @@ -5669,6 +5673,7 @@ fn parse_joins_on() { partitions: vec![], with_ordinality: false, }, + global, join_operator: f(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), op: BinaryOperator::Eq, @@ -5682,6 +5687,7 @@ fn parse_joins_on() { vec![join_with_constraint( "t2", table_alias("foo"), + false, JoinOperator::Inner, )] ); @@ -5692,35 +5698,80 @@ fn parse_joins_on() { // Test parsing of different join operators assert_eq!( only(&verified_only_select("SELECT * FROM t1 JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, JoinOperator::Inner)] + vec![join_with_constraint("t2", None, false, JoinOperator::Inner)] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, JoinOperator::LeftOuter)] + vec![join_with_constraint( + "t2", + None, + false, + JoinOperator::LeftOuter + )] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 RIGHT JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, JoinOperator::RightOuter)] + vec![join_with_constraint( + "t2", + None, + false, + JoinOperator::RightOuter + )] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT SEMI JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, JoinOperator::LeftSemi)] + vec![join_with_constraint( + "t2", + None, + false, + JoinOperator::LeftSemi + )] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 RIGHT SEMI JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, JoinOperator::RightSemi)] + vec![join_with_constraint( + "t2", + None, + false, + JoinOperator::RightSemi + )] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT ANTI JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, JoinOperator::LeftAnti)] + vec![join_with_constraint( + "t2", + None, + false, + JoinOperator::LeftAnti + )] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 RIGHT ANTI JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, JoinOperator::RightAnti)] + vec![join_with_constraint( + "t2", + None, + false, + JoinOperator::RightAnti + )] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 FULL JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, JoinOperator::FullOuter)] + vec![join_with_constraint( + "t2", + None, + false, + JoinOperator::FullOuter + )] + ); + + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 GLOBAL FULL JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint( + "t2", + None, + true, + JoinOperator::FullOuter + )] ); } @@ -5741,6 +5792,7 @@ fn parse_joins_using() { partitions: vec![], with_ordinality: false, }, + global: false, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), } } @@ -5805,6 +5857,7 @@ fn parse_natural_join() { partitions: vec![], with_ordinality: false, }, + global: false, join_operator: f(JoinConstraint::Natural), } } @@ -6073,6 +6126,7 @@ fn parse_derived_tables() { partitions: vec![], with_ordinality: false, }, + global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], }), @@ -6983,6 +7037,7 @@ fn lateral_function() { ], alias: None, }, + global: false, join_operator: JoinOperator::LeftOuter(JoinConstraint::None), }], }], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b0b29f347..1c9c009d9 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1891,6 +1891,7 @@ fn parse_update_with_joins() { partitions: vec![], with_ordinality: false, }, + global: false, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("o"), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 44231e7d3..6410199ab 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4102,6 +4102,7 @@ fn parse_join_constraint_unnest_alias() { with_offset_alias: None, with_ordinality: false, }, + global: false, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), op: BinaryOperator::Eq, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7abb1a947..eaf8c1d14 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2206,6 +2206,7 @@ fn asof_joins() { relation: table_with_alias("trades_unixtime", "tu"), joins: vec![Join { relation: table_with_alias("quotes_unixtime", "qu"), + global: false, join_operator: JoinOperator::AsOf { match_condition: Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ From cc13841a370190df00cfc623593453054ac187e9 Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 31 Jul 2024 04:31:42 +0800 Subject: [PATCH 507/806] Add support of parsing ON CLUSTER in ALTER TABLE for ClickHouse (#1342) --- src/ast/dml.rs | 8 ++---- src/ast/helpers/stmt_create_table.rs | 4 +-- src/ast/mod.rs | 11 +++++++- src/parser/mod.rs | 21 +++++++-------- src/test_utils.rs | 1 + tests/sqlparser_common.rs | 38 ++++++++++++++++++++++++++-- tests/sqlparser_mysql.rs | 3 +++ tests/sqlparser_postgres.rs | 2 ++ 8 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 0ebbaa3e9..aad7d2e22 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -126,7 +126,7 @@ pub struct CreateTable { pub on_commit: Option, /// ClickHouse "ON CLUSTER" clause: /// - pub on_cluster: Option, + pub on_cluster: Option, /// ClickHouse "PRIMARY KEY " clause. /// pub primary_key: Option>, @@ -206,11 +206,7 @@ impl Display for CreateTable { name = self.name, )?; if let Some(on_cluster) = &self.on_cluster { - write!( - f, - " ON CLUSTER {}", - on_cluster.replace('{', "'{").replace('}', "}'") - )?; + write!(f, " ON CLUSTER {}", on_cluster)?; } if !self.columns.is_empty() || !self.constraints.is_empty() { write!(f, " ({}", display_comma_separated(&self.columns))?; diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 92c75e6a4..19efaeece 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -73,7 +73,7 @@ pub struct CreateTableBuilder { pub default_charset: Option, pub collation: Option, pub on_commit: Option, - pub on_cluster: Option, + pub on_cluster: Option, pub primary_key: Option>, pub order_by: Option>, pub partition_by: Option>, @@ -261,7 +261,7 @@ impl CreateTableBuilder { self } - pub fn on_cluster(mut self, on_cluster: Option) -> Self { + pub fn on_cluster(mut self, on_cluster: Option) -> Self { self.on_cluster = on_cluster; self } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d27baadc4..70f96c5c5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2162,6 +2162,10 @@ pub enum Statement { only: bool, operations: Vec, location: Option, + /// ClickHouse dialect supports `ON CLUSTER` clause for ALTER TABLE + /// 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, }, /// ```sql /// ALTER INDEX @@ -3632,6 +3636,7 @@ impl fmt::Display for Statement { only, operations, location, + on_cluster, } => { write!(f, "ALTER TABLE ")?; if *if_exists { @@ -3640,9 +3645,13 @@ impl fmt::Display for Statement { if *only { write!(f, "ONLY ")?; } + write!(f, "{name} ", name = name)?; + if let Some(cluster) = on_cluster { + write!(f, "ON CLUSTER {cluster} ")?; + } write!( f, - "{name} {operations}", + "{operations}", operations = display_comma_separated(operations) )?; if let Some(loc) = location { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cd2cf2186..725e24bfb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5379,6 +5379,14 @@ impl<'a> Parser<'a> { } } + fn parse_optional_on_cluster(&mut self) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::ON, Keyword::CLUSTER]) { + Ok(Some(self.parse_identifier(false)?)) + } else { + Ok(None) + } + } + pub fn parse_create_table( &mut self, or_replace: bool, @@ -5391,16 +5399,7 @@ impl<'a> Parser<'a> { let table_name = self.parse_object_name(allow_unquoted_hyphen)?; // Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs - let on_cluster = if self.parse_keywords(&[Keyword::ON, Keyword::CLUSTER]) { - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(s) => Some(s), - Token::Word(s) => Some(s.to_string()), - _ => self.expected("identifier or cluster literal", next_token)?, - } - } else { - None - }; + let on_cluster = self.parse_optional_on_cluster()?; let like = if self.parse_keyword(Keyword::LIKE) || self.parse_keyword(Keyword::ILIKE) { self.parse_object_name(allow_unquoted_hyphen).ok() @@ -6583,6 +6582,7 @@ impl<'a> Parser<'a> { 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; @@ -6604,6 +6604,7 @@ impl<'a> Parser<'a> { only, operations, location, + on_cluster, }) } Keyword::INDEX => { diff --git a/src/test_utils.rs b/src/test_utils.rs index b8e9ecee4..d9100d351 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -274,6 +274,7 @@ pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTa if_exists, only: is_only, operations, + on_cluster: _, location: _, } => { assert_eq!(name.to_string(), expected_name); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a8f3919df..44e245254 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3506,7 +3506,7 @@ fn parse_create_table_on_cluster() { let sql = "CREATE TABLE t ON CLUSTER '{cluster}' (a INT, b INT)"; match generic.verified_stmt(sql) { Statement::CreateTable(CreateTable { on_cluster, .. }) => { - assert_eq!(on_cluster.unwrap(), "{cluster}".to_string()); + assert_eq!(on_cluster.unwrap().to_string(), "'{cluster}'".to_string()); } _ => unreachable!(), } @@ -3515,7 +3515,7 @@ fn parse_create_table_on_cluster() { let sql = "CREATE TABLE t ON CLUSTER my_cluster (a INT, b INT)"; match generic.verified_stmt(sql) { Statement::CreateTable(CreateTable { on_cluster, .. }) => { - assert_eq!(on_cluster.unwrap(), "my_cluster".to_string()); + assert_eq!(on_cluster.unwrap().to_string(), "my_cluster".to_string()); } _ => unreachable!(), } @@ -3822,6 +3822,40 @@ fn parse_alter_table() { } } +#[test] +fn test_alter_table_with_on_cluster() { + match all_dialects() + .verified_stmt("ALTER TABLE t ON CLUSTER 'cluster' ADD CONSTRAINT bar PRIMARY KEY (baz)") + { + Statement::AlterTable { + name, on_cluster, .. + } => { + std::assert_eq!(name.to_string(), "t"); + std::assert_eq!(on_cluster, Some(Ident::with_quote('\'', "cluster"))); + } + _ => unreachable!(), + } + + match all_dialects() + .verified_stmt("ALTER TABLE t ON CLUSTER cluster_name ADD CONSTRAINT bar PRIMARY KEY (baz)") + { + Statement::AlterTable { + name, on_cluster, .. + } => { + std::assert_eq!(name.to_string(), "t"); + std::assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); + } + _ => unreachable!(), + } + + let res = all_dialects() + .parse_sql_statements("ALTER TABLE t ON CLUSTER 123 ADD CONSTRAINT bar PRIMARY KEY (baz)"); + std::assert_eq!( + res.unwrap_err(), + ParserError::ParserError("Expected: identifier, found: 123".to_string()) + ) +} + #[test] fn parse_alter_index() { let rename_index = "ALTER INDEX idx RENAME TO new_idx"; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 1c9c009d9..397a722b5 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1976,6 +1976,7 @@ fn parse_alter_table_add_column() { only, operations, location: _, + on_cluster: _, } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); @@ -2005,6 +2006,7 @@ fn parse_alter_table_add_column() { only, operations, location: _, + on_cluster: _, } => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); @@ -2042,6 +2044,7 @@ fn parse_alter_table_add_columns() { 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 6410199ab..7406bdd74 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -677,6 +677,7 @@ fn parse_alter_table_add_columns() { only, operations, location: _, + on_cluster: _, } => { assert_eq!(name.to_string(), "tab"); assert!(if_exists); @@ -759,6 +760,7 @@ fn parse_alter_table_owner_to() { only: _, operations, location: _, + on_cluster: _, } => { assert_eq!(name.to_string(), "tab"); assert_eq!( From a692ba5fd1902e0c40dc5714304594aee642a899 Mon Sep 17 00:00:00 2001 From: hulk Date: Fri, 2 Aug 2024 05:20:56 +0800 Subject: [PATCH 508/806] Add support of parsing OPTIMIZE TABLE statement for ClickHouse (#1359) --- src/ast/ddl.rs | 41 +++++++++++++++++++----- src/ast/mod.rs | 36 ++++++++++++++++++++- src/keywords.rs | 3 ++ src/parser/mod.rs | 45 +++++++++++++++++++++++++- tests/sqlparser_clickhouse.rs | 60 +++++++++++++++++++++++++++++++++++ 5 files changed, 175 insertions(+), 10 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 5cc671cf5..af679d469 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1296,20 +1296,45 @@ impl fmt::Display for UserDefinedTypeCompositeAttributeDef { } } -/// PARTITION statement used in ALTER TABLE et al. such as in Hive SQL +/// PARTITION statement used in ALTER TABLE et al. such as in Hive and ClickHouse SQL. +/// For example, ClickHouse's OPTIMIZE TABLE supports syntax like PARTITION ID 'partition_id' and PARTITION expr. +/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/optimize) #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct Partition { - pub partitions: Vec, +pub enum Partition { + Identifier(Ident), + Expr(Expr), + Partitions(Vec), } impl fmt::Display for Partition { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "PARTITION ({})", - display_comma_separated(&self.partitions) - ) + match self { + Partition::Identifier(id) => write!(f, "PARTITION ID {id}"), + Partition::Expr(expr) => write!(f, "PARTITION {expr}"), + Partition::Partitions(partitions) => { + write!(f, "PARTITION ({})", display_comma_separated(partitions)) + } + } + } +} + +/// DEDUPLICATE statement used in OPTIMIZE TABLE et al. such as in ClickHouse SQL +/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/optimize) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Deduplicate { + All, + ByExpression(Expr), +} + +impl fmt::Display for Deduplicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Deduplicate::All => write!(f, "DEDUPLICATE"), + Deduplicate::ByExpression(expr) => write!(f, "DEDUPLICATE BY {expr}"), + } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 70f96c5c5..6444556ef 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -33,7 +33,7 @@ pub use self::data_type::{ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue}; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, - ColumnOptionDef, ConstraintCharacteristics, DeferrableInitial, GeneratedAs, + ColumnOptionDef, ConstraintCharacteristics, Deduplicate, DeferrableInitial, GeneratedAs, GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Owner, Partition, ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, @@ -2831,6 +2831,18 @@ pub enum Statement { to: Ident, with: Vec, }, + /// ```sql + /// OPTIMIZE TABLE [db.]name [ON CLUSTER cluster] [PARTITION partition | PARTITION ID 'partition_id'] [FINAL] [DEDUPLICATE [BY expression]] + /// ``` + /// + /// See ClickHouse + OptimizeTable { + name: ObjectName, + on_cluster: Option, + partition: Option, + include_final: bool, + deduplicate: Option, + }, } impl fmt::Display for Statement { @@ -4283,6 +4295,28 @@ impl fmt::Display for Statement { Ok(()) } + Statement::OptimizeTable { + name, + on_cluster, + partition, + include_final, + deduplicate, + } => { + write!(f, "OPTIMIZE TABLE {name}")?; + if let Some(on_cluster) = on_cluster { + write!(f, " ON CLUSTER {on_cluster}", on_cluster = on_cluster)?; + } + if let Some(partition) = partition { + write!(f, " {partition}", partition = partition)?; + } + if *include_final { + write!(f, " FINAL")?; + } + if let Some(deduplicate) = deduplicate { + write!(f, " {deduplicate}")?; + } + Ok(()) + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index ee2bd6173..49bd969af 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -218,6 +218,7 @@ define_keywords!( DECADE, DECIMAL, DECLARE, + DEDUPLICATE, DEFAULT, DEFAULT_DDL_COLLATION, DEFERRABLE, @@ -301,6 +302,7 @@ define_keywords!( FILE_FORMAT, FILL, FILTER, + FINAL, FIRST, FIRST_VALUE, FIXEDSTRING, @@ -354,6 +356,7 @@ define_keywords!( HOSTS, HOUR, HOURS, + ID, IDENTITY, IF, IGNORE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 725e24bfb..67d58ea75 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -551,6 +551,10 @@ impl<'a> Parser<'a> { Keyword::LOAD if dialect_of!(self is DuckDbDialect | GenericDialect) => { Ok(self.parse_load()?) } + // `OPTIMIZE` is clickhouse specific https://clickhouse.tech/docs/en/sql-reference/statements/optimize/ + Keyword::OPTIMIZE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + Ok(self.parse_optimize_table()?) + } _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -6270,7 +6274,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let partitions = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; - Ok(Partition { partitions }) + Ok(Partition::Partitions(partitions)) } pub fn parse_alter_table_operation(&mut self) -> Result { @@ -11165,6 +11169,45 @@ impl<'a> Parser<'a> { Ok(Statement::Load { extension_name }) } + /// ```sql + /// OPTIMIZE TABLE [db.]name [ON CLUSTER cluster] [PARTITION partition | PARTITION ID 'partition_id'] [FINAL] [DEDUPLICATE [BY expression]] + /// ``` + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/optimize) + pub fn parse_optimize_table(&mut self) -> Result { + self.expect_keyword(Keyword::TABLE)?; + let name = self.parse_object_name(false)?; + let on_cluster = self.parse_optional_on_cluster()?; + + let partition = if self.parse_keyword(Keyword::PARTITION) { + if self.parse_keyword(Keyword::ID) { + Some(Partition::Identifier(self.parse_identifier(false)?)) + } else { + Some(Partition::Expr(self.parse_expr()?)) + } + } else { + None + }; + + let include_final = self.parse_keyword(Keyword::FINAL); + let deduplicate = if self.parse_keyword(Keyword::DEDUPLICATE) { + if self.parse_keyword(Keyword::BY) { + Some(Deduplicate::ByExpression(self.parse_expr()?)) + } else { + Some(Deduplicate::All) + } + } else { + None + }; + + Ok(Statement::OptimizeTable { + name, + on_cluster, + partition, + include_final, + deduplicate, + }) + } + /// ```sql /// CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] /// ``` diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 6fdadc366..5263be29e 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -25,6 +25,7 @@ use sqlparser::ast::Value::Number; use sqlparser::ast::*; use sqlparser::dialect::ClickHouseDialect; use sqlparser::dialect::GenericDialect; +use sqlparser::parser::ParserError::ParserError; #[test] fn parse_map_access_expr() { @@ -221,6 +222,65 @@ fn parse_create_table() { ); } +#[test] +fn parse_optimize_table() { + clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0"); + clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE db.t0"); + clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 ON CLUSTER 'cluster'"); + clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 ON CLUSTER 'cluster' FINAL"); + clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 FINAL DEDUPLICATE"); + clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 DEDUPLICATE"); + clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 DEDUPLICATE BY id"); + clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 FINAL DEDUPLICATE BY id"); + clickhouse_and_generic() + .verified_stmt("OPTIMIZE TABLE t0 PARTITION tuple('2023-04-22') DEDUPLICATE BY id"); + match clickhouse_and_generic().verified_stmt( + "OPTIMIZE TABLE t0 ON CLUSTER cluster PARTITION ID '2024-07' FINAL DEDUPLICATE BY id", + ) { + Statement::OptimizeTable { + name, + on_cluster, + partition, + include_final, + deduplicate, + .. + } => { + assert_eq!(name.to_string(), "t0"); + assert_eq!(on_cluster, Some(Ident::new("cluster"))); + assert_eq!( + partition, + Some(Partition::Identifier(Ident::with_quote('\'', "2024-07"))) + ); + assert!(include_final); + assert_eq!( + deduplicate, + Some(Deduplicate::ByExpression(Identifier(Ident::new("id")))) + ); + } + _ => unreachable!(), + } + + // negative cases + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements("OPTIMIZE TABLE t0 DEDUPLICATE BY") + .unwrap_err(), + ParserError("Expected: an expression:, found: EOF".to_string()) + ); + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements("OPTIMIZE TABLE t0 PARTITION") + .unwrap_err(), + ParserError("Expected: an expression:, found: EOF".to_string()) + ); + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements("OPTIMIZE TABLE t0 PARTITION ID") + .unwrap_err(), + ParserError("Expected: identifier, found: EOF".to_string()) + ); +} + fn column_def(name: Ident, data_type: DataType) -> ColumnDef { ColumnDef { name, From d49acc67b13e1d68f2e6a25546161a68e165da4f Mon Sep 17 00:00:00 2001 From: Jesse Date: Thu, 1 Aug 2024 23:28:15 +0200 Subject: [PATCH 509/806] Parse SETTINGS clause for ClickHouse table-valued functions (#1358) --- src/ast/mod.rs | 4 +- src/ast/query.rs | 25 ++++++++- src/parser/mod.rs | 95 ++++++++++++++++++++++++----------- tests/sqlparser_clickhouse.rs | 77 ++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 34 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6444556ef..e0c929a9d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -50,8 +50,8 @@ pub use self::query::{ OffsetRows, OrderBy, OrderByExpr, PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, - Values, WildcardAdditionalOptions, With, WithFill, + TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, + ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, }; pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, diff --git a/src/ast/query.rs b/src/ast/query.rs index b318f686a..cda7430be 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -899,6 +899,19 @@ impl fmt::Display for ExprWithAlias { } } +/// Arguments to a table-valued function +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TableFunctionArgs { + pub args: Vec, + /// ClickHouse-specific SETTINGS clause. + /// For example, + /// `SELECT * FROM executable('generate_random.py', TabSeparated, 'id UInt32, random String', SETTINGS send_chunk_header = false, pool_size = 16)` + /// [`executable` table function](https://clickhouse.com/docs/en/engines/table-functions/executable) + pub settings: Option>, +} + /// A table name or a parenthesized subquery with an optional alias #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -916,7 +929,7 @@ pub enum TableFactor { /// This field's value is `Some(v)`, where `v` is a (possibly empty) /// vector of arguments, in the case of a table-valued function call, /// whereas it's `None` in the case of a regular table name. - args: Option>, + args: Option, /// MSSQL-specific `WITH (...)` hints such as NOLOCK. with_hints: Vec, /// Optional version qualifier to facilitate table time-travel, as @@ -1314,7 +1327,15 @@ impl fmt::Display for TableFactor { write!(f, "PARTITION ({})", display_comma_separated(partitions))?; } if let Some(args) = args { - write!(f, "({})", display_comma_separated(args))?; + write!(f, "(")?; + write!(f, "{}", display_comma_separated(&args.args))?; + if let Some(ref settings) = args.settings { + if !args.args.is_empty() { + write!(f, ", ")?; + } + write!(f, "SETTINGS {}", display_comma_separated(settings))?; + } + write!(f, ")")?; } if *with_ordinality { write!(f, " WITH ORDINALITY")?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 67d58ea75..da9ca2672 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3430,6 +3430,29 @@ impl<'a> Parser<'a> { Ok(values) } + /// Parse the comma of a comma-separated syntax element. + /// Returns true if there is a next element + fn is_parse_comma_separated_end(&mut self) -> bool { + if !self.consume_token(&Token::Comma) { + true + } else if self.options.trailing_commas { + let token = self.peek_token().token; + match token { + Token::Word(ref kw) + if keywords::RESERVED_FOR_COLUMN_ALIAS.contains(&kw.keyword) => + { + true + } + Token::RParen | Token::SemiColon | Token::EOF | Token::RBracket | Token::RBrace => { + true + } + _ => false, + } + } else { + false + } + } + /// Parse a comma-separated list of 1+ items accepted by `F` pub fn parse_comma_separated(&mut self, mut f: F) -> Result, ParserError> where @@ -3438,22 +3461,8 @@ impl<'a> Parser<'a> { let mut values = vec![]; loop { values.push(f(self)?); - if !self.consume_token(&Token::Comma) { + if self.is_parse_comma_separated_end() { break; - } else if self.options.trailing_commas { - match self.peek_token().token { - Token::Word(kw) - if keywords::RESERVED_FOR_COLUMN_ALIAS.contains(&kw.keyword) => - { - break; - } - Token::RParen - | Token::SemiColon - | Token::EOF - | Token::RBracket - | Token::RBrace => break, - _ => continue, - } } } Ok(values) @@ -8104,19 +8113,7 @@ impl<'a> Parser<'a> { vec![] }; - let settings = if dialect_of!(self is ClickHouseDialect|GenericDialect) - && self.parse_keyword(Keyword::SETTINGS) - { - let key_values = self.parse_comma_separated(|p| { - let key = p.parse_identifier(false)?; - p.expect_token(&Token::Eq)?; - let value = p.parse_value()?; - Ok(Setting { key, value }) - })?; - Some(key_values) - } else { - None - }; + let settings = self.parse_settings()?; let fetch = if self.parse_keyword(Keyword::FETCH) { Some(self.parse_fetch()?) @@ -8163,6 +8160,23 @@ impl<'a> Parser<'a> { } } + fn parse_settings(&mut self) -> Result>, ParserError> { + let settings = if dialect_of!(self is ClickHouseDialect|GenericDialect) + && self.parse_keyword(Keyword::SETTINGS) + { + let key_values = self.parse_comma_separated(|p| { + let key = p.parse_identifier(false)?; + p.expect_token(&Token::Eq)?; + let value = p.parse_value()?; + Ok(Setting { key, value }) + })?; + Some(key_values) + } else { + None + }; + Ok(settings) + } + /// Parse a mssql `FOR [XML | JSON | BROWSE]` clause pub fn parse_for_clause(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::XML) { @@ -9382,9 +9396,9 @@ impl<'a> Parser<'a> { // Parse potential version qualifier let version = self.parse_table_version()?; - // Postgres, MSSQL: table-valued functions: + // Postgres, MSSQL, ClickHouse: table-valued functions: let args = if self.consume_token(&Token::LParen) { - Some(self.parse_optional_args()?) + Some(self.parse_table_function_args()?) } else { None }; @@ -10327,6 +10341,27 @@ impl<'a> Parser<'a> { } } + fn parse_table_function_args(&mut self) -> Result { + if self.consume_token(&Token::RParen) { + return Ok(TableFunctionArgs { + args: vec![], + settings: None, + }); + } + let mut args = vec![]; + let settings = loop { + if let Some(settings) = self.parse_settings()? { + break Some(settings); + } + args.push(self.parse_function_args()?); + if self.is_parse_comma_separated_end() { + break None; + } + }; + self.expect_token(&Token::RParen)?; + Ok(TableFunctionArgs { args, settings }) + } + /// Parses a potentially empty list of arguments to a window function /// (including the closing parenthesis). /// diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 5263be29e..4108958fb 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1151,6 +1151,83 @@ fn parse_create_table_on_commit_and_as_query() { } } +#[test] +fn parse_select_table_function_settings() { + fn check_settings(sql: &str, expected: &TableFunctionArgs) { + match clickhouse_and_generic().verified_stmt(sql) { + Statement::Query(q) => { + let from = &q.body.as_select().unwrap().from; + assert_eq!(from.len(), 1); + assert_eq!(from[0].joins, vec![]); + match &from[0].relation { + Table { args, .. } => { + let args = args.as_ref().unwrap(); + assert_eq!(args, expected); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + } + check_settings( + "SELECT * FROM table_function(arg, SETTINGS s0 = 3, s1 = 's')", + &TableFunctionArgs { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier("arg".into()), + ))], + + settings: Some(vec![ + Setting { + key: "s0".into(), + value: Value::Number("3".parse().unwrap(), false), + }, + Setting { + key: "s1".into(), + value: Value::SingleQuotedString("s".into()), + }, + ]), + }, + ); + check_settings( + r#"SELECT * FROM table_function(arg)"#, + &TableFunctionArgs { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier("arg".into()), + ))], + settings: None, + }, + ); + check_settings( + "SELECT * FROM table_function(SETTINGS s0 = 3, s1 = 's')", + &TableFunctionArgs { + args: vec![], + settings: Some(vec![ + Setting { + key: "s0".into(), + value: Value::Number("3".parse().unwrap(), false), + }, + Setting { + key: "s1".into(), + value: Value::SingleQuotedString("s".into()), + }, + ]), + }, + ); + 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)", + ]; + for sql in invalid_cases { + clickhouse_and_generic() + .parse_sql_statements(sql) + .expect_err("Expected: SETTINGS key = value, found: "); + } +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], From 8f8c96f87ffe58945a0875c9c897f36c989b0095 Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Sun, 4 Aug 2024 19:47:32 +0800 Subject: [PATCH 510/806] Support parsing empty map literal syntax for DuckDB and Genric (#1361) --- src/parser/mod.rs | 42 ++++++++++++++++----------------------- tests/sqlparser_common.rs | 2 ++ 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index da9ca2672..fe8acb4f2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1849,17 +1849,9 @@ impl<'a> Parser<'a> { /// Parses an array expression `[ex1, ex2, ..]` /// if `named` is `true`, came from an expression like `ARRAY[ex1, ex2]` pub fn parse_array_expr(&mut self, named: bool) -> Result { - if self.peek_token().token == Token::RBracket { - let _ = self.next_token(); // consume ] - Ok(Expr::Array(Array { - elem: vec![], - named, - })) - } else { - let exprs = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RBracket)?; - Ok(Expr::Array(Array { elem: exprs, named })) - } + let exprs = self.parse_comma_separated0(Parser::parse_expr, Token::RBracket)?; + self.expect_token(&Token::RBracket)?; + Ok(Expr::Array(Array { elem: exprs, named })) } pub fn parse_listagg_on_overflow(&mut self) -> Result, ParserError> { @@ -2352,11 +2344,8 @@ impl<'a> Parser<'a> { /// [map]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps fn parse_duckdb_map_literal(&mut self) -> Result { self.expect_token(&Token::LBrace)?; - - let fields = self.parse_comma_separated(Self::parse_duckdb_map_field)?; - + let fields = self.parse_comma_separated0(Self::parse_duckdb_map_field, Token::RBrace)?; self.expect_token(&Token::RBrace)?; - Ok(Expr::Map(Map { entries: fields })) } @@ -2937,7 +2926,7 @@ impl<'a> Parser<'a> { Expr::InList { expr: Box::new(expr), list: if self.dialect.supports_in_empty_list() { - self.parse_comma_separated0(Parser::parse_expr)? + self.parse_comma_separated0(Parser::parse_expr, Token::RParen)? } else { self.parse_comma_separated(Parser::parse_expr)? }, @@ -3479,18 +3468,20 @@ impl<'a> Parser<'a> { } /// Parse a comma-separated list of 0+ items accepted by `F` - pub fn parse_comma_separated0(&mut self, f: F) -> Result, ParserError> + /// * `end_token` - expected end token for the closure (e.g. [Token::RParen], [Token::RBrace] ...) + pub fn parse_comma_separated0( + &mut self, + f: F, + end_token: Token, + ) -> Result, ParserError> where F: FnMut(&mut Parser<'a>) -> Result, { - // () - if matches!(self.peek_token().token, Token::RParen) { + if self.peek_token().token == end_token { return Ok(vec![]); } - // (,) - if self.options.trailing_commas - && matches!(self.peek_tokens(), [Token::Comma, Token::RParen]) - { + + if self.options.trailing_commas && self.peek_tokens() == [Token::Comma, end_token] { let _ = self.consume_token(&Token::Comma); return Ok(vec![]); } @@ -4059,7 +4050,7 @@ impl<'a> Parser<'a> { }) }; self.expect_token(&Token::LParen)?; - let args = self.parse_comma_separated0(parse_function_param)?; + let args = self.parse_comma_separated0(parse_function_param, Token::RParen)?; self.expect_token(&Token::RParen)?; let return_type = if self.parse_keyword(Keyword::RETURNS) { @@ -10713,7 +10704,8 @@ impl<'a> Parser<'a> { } if self.consume_token(&Token::LParen) { - let interpolations = self.parse_comma_separated0(|p| p.parse_interpolation())?; + let interpolations = + self.parse_comma_separated0(|p| p.parse_interpolation(), Token::RParen)?; self.expect_token(&Token::RParen)?; // INTERPOLATE () and INTERPOLATE ( ... ) variants return Ok(Some(Interpolate { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 44e245254..7ec017269 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10282,6 +10282,8 @@ fn test_map_syntax() { }), }, ); + + check("MAP {}", Expr::Map(Map { entries: vec![] })); } #[test] From a5480ae4982d84d37c6294b3e70ca24fb72d6a4d Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 6 Aug 2024 12:49:37 +0100 Subject: [PATCH 511/806] Support `Dialect` level precedence, update Postgres `Dialect` to match Postgres (#1360) --- src/ast/operator.rs | 2 +- src/dialect/mod.rs | 165 +++++++++++++++++++++++++++++++++++- src/dialect/postgresql.rs | 134 +++++++++++++++++++++++++++++ src/dialect/snowflake.rs | 9 ++ src/parser/mod.rs | 148 +++++--------------------------- tests/sqlparser_postgres.rs | 112 ++++++++++++++++++++++++ 6 files changed, 440 insertions(+), 130 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index e70df344a..db6ed0564 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -151,7 +151,7 @@ pub enum BinaryOperator { Arrow, /// The `->>` operator. /// - /// On PostgreSQL, this operator that extracts a JSON object field or JSON + /// On PostgreSQL, this operator extracts a JSON object field or JSON /// array element and converts it to text, for example `'{"a":"b"}'::json /// ->> 'a'` or `[1, 2, 3]'::json ->> 2`. /// diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 22e0baeb2..fc45545d4 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -24,12 +24,13 @@ mod redshift; mod snowflake; mod sqlite; -use crate::ast::{Expr, Statement}; use core::any::{Any, TypeId}; use core::fmt::Debug; use core::iter::Peekable; use core::str::Chars; +use log::debug; + pub use self::ansi::AnsiDialect; pub use self::bigquery::BigQueryDialect; pub use self::clickhouse::ClickHouseDialect; @@ -43,8 +44,11 @@ pub use self::postgresql::PostgreSqlDialect; pub use self::redshift::RedshiftSqlDialect; pub use self::snowflake::SnowflakeDialect; pub use self::sqlite::SQLiteDialect; +use crate::ast::{Expr, Statement}; pub use crate::keywords; +use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; +use crate::tokenizer::Token; #[cfg(not(feature = "std"))] use alloc::boxed::Box; @@ -300,13 +304,172 @@ pub trait Dialect: Debug + Any { // return None to fall back to the default behavior None } + + /// Get the precedence of the next token. This "full" method means all precedence logic and remain + /// in the dialect. while still allowing overriding the `get_next_precedence` method with the option to + /// fallback to the default behavior. + /// + /// Higher number => higher precedence + fn get_next_precedence_full(&self, parser: &Parser) -> Result { + if let Some(precedence) = self.get_next_precedence(parser) { + return precedence; + } + + let token = parser.peek_token(); + debug!("get_next_precedence() {:?}", token); + match token.token { + Token::Word(w) if w.keyword == Keyword::OR => Ok(OR_PREC), + Token::Word(w) if w.keyword == Keyword::AND => Ok(AND_PREC), + Token::Word(w) if w.keyword == Keyword::XOR => Ok(XOR_PREC), + + Token::Word(w) if w.keyword == Keyword::AT => { + match ( + parser.peek_nth_token(1).token, + parser.peek_nth_token(2).token, + ) { + (Token::Word(w), Token::Word(w2)) + if w.keyword == Keyword::TIME && w2.keyword == Keyword::ZONE => + { + Ok(AT_TZ_PREC) + } + _ => Ok(UNKNOWN_PREC), + } + } + + Token::Word(w) if w.keyword == Keyword::NOT => match parser.peek_nth_token(1).token { + // The precedence of NOT varies depending on keyword that + // follows it. If it is followed by IN, BETWEEN, or LIKE, + // it takes on the precedence of those tokens. Otherwise, it + // is not an infix operator, and therefore has zero + // precedence. + Token::Word(w) if w.keyword == Keyword::IN => Ok(BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::LIKE => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(LIKE_PREC), + _ => Ok(UNKNOWN_PREC), + }, + Token::Word(w) if w.keyword == Keyword::IS => Ok(IS_PREC), + Token::Word(w) if w.keyword == Keyword::IN => Ok(BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::LIKE => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::DIV => Ok(MUL_DIV_MOD_OP_PREC), + Token::Eq + | Token::Lt + | Token::LtEq + | Token::Neq + | Token::Gt + | Token::GtEq + | Token::DoubleEq + | Token::Tilde + | Token::TildeAsterisk + | Token::ExclamationMarkTilde + | Token::ExclamationMarkTildeAsterisk + | Token::DoubleTilde + | Token::DoubleTildeAsterisk + | Token::ExclamationMarkDoubleTilde + | Token::ExclamationMarkDoubleTildeAsterisk + | Token::Spaceship => Ok(EQ_PREC), + Token::Pipe => Ok(PIPE_PREC), + Token::Caret | Token::Sharp | Token::ShiftRight | Token::ShiftLeft => Ok(CARET_PREC), + Token::Ampersand => Ok(AMPERSAND_PREC), + Token::Plus | Token::Minus => Ok(PLUS_MINUS_PREC), + Token::Mul | Token::Div | Token::DuckIntDiv | Token::Mod | Token::StringConcat => { + Ok(MUL_DIV_MOD_OP_PREC) + } + Token::DoubleColon + | Token::ExclamationMark + | Token::LBracket + | Token::Overlap + | Token::CaretAt => Ok(DOUBLE_COLON_PREC), + // Token::Colon if (self as dyn Dialect).is::() => Ok(DOUBLE_COLON_PREC), + Token::Arrow + | Token::LongArrow + | Token::HashArrow + | Token::HashLongArrow + | Token::AtArrow + | Token::ArrowAt + | Token::HashMinus + | Token::AtQuestion + | Token::AtAt + | Token::Question + | Token::QuestionAnd + | Token::QuestionPipe + | Token::CustomBinaryOperator(_) => Ok(PG_OTHER_PREC), + _ => Ok(UNKNOWN_PREC), + } + } + /// Dialect-specific statement parser override fn parse_statement(&self, _parser: &mut Parser) -> Option> { // return None to fall back to the default behavior None } + + /// The following precedence values are used directly by `Parse` or in dialects, + /// so have to be made public by the dialect. + fn prec_double_colon(&self) -> u8 { + DOUBLE_COLON_PREC + } + + fn prec_mul_div_mod_op(&self) -> u8 { + MUL_DIV_MOD_OP_PREC + } + + fn prec_plus_minus(&self) -> u8 { + PLUS_MINUS_PREC + } + + fn prec_between(&self) -> u8 { + BETWEEN_PREC + } + + fn prec_like(&self) -> u8 { + LIKE_PREC + } + + fn prec_unary_not(&self) -> u8 { + UNARY_NOT_PREC + } + + fn prec_unknown(&self) -> u8 { + UNKNOWN_PREC + } } +// Define the lexical Precedence of operators. +// +// Uses (APPROXIMATELY) as a reference +// higher number = higher precedence +// +// NOTE: The pg documentation is incomplete, e.g. the AT TIME ZONE operator +// actually has higher precedence than addition. +// See . +const DOUBLE_COLON_PREC: u8 = 50; +const AT_TZ_PREC: u8 = 41; +const MUL_DIV_MOD_OP_PREC: u8 = 40; +const PLUS_MINUS_PREC: u8 = 30; +const XOR_PREC: u8 = 24; +const AMPERSAND_PREC: u8 = 23; +const CARET_PREC: u8 = 22; +const PIPE_PREC: u8 = 21; +const BETWEEN_PREC: u8 = 20; +const EQ_PREC: u8 = 20; +const LIKE_PREC: u8 = 19; +const IS_PREC: u8 = 17; +const PG_OTHER_PREC: u8 = 16; +const UNARY_NOT_PREC: u8 = 15; +const AND_PREC: u8 = 10; +const OR_PREC: u8 = 5; +const UNKNOWN_PREC: u8 = 0; + impl dyn Dialect { #[inline] pub fn is(&self) -> bool { diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 8254e807b..293fb9e7d 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -9,6 +9,7 @@ // 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 log::debug; use crate::ast::{CommentObject, Statement}; use crate::dialect::Dialect; @@ -20,6 +21,23 @@ use crate::tokenizer::Token; #[derive(Debug)] pub struct PostgreSqlDialect {} +const DOUBLE_COLON_PREC: u8 = 140; +const BRACKET_PREC: u8 = 130; +const COLLATE_PREC: u8 = 120; +const AT_TZ_PREC: u8 = 110; +const CARET_PREC: u8 = 100; +const MUL_DIV_MOD_OP_PREC: u8 = 90; +const PLUS_MINUS_PREC: u8 = 80; +// there's no XOR operator in PostgreSQL, but support it here to avoid breaking tests +const XOR_PREC: u8 = 75; +const PG_OTHER_PREC: u8 = 70; +const BETWEEN_LIKE_PREC: u8 = 60; +const EQ_PREC: u8 = 50; +const IS_PREC: u8 = 40; +const NOT_PREC: u8 = 30; +const AND_PREC: u8 = 20; +const OR_PREC: u8 = 10; + impl Dialect for PostgreSqlDialect { fn identifier_quote_style(&self, _identifier: &str) -> Option { Some('"') @@ -67,6 +85,102 @@ impl Dialect for PostgreSqlDialect { ) } + fn get_next_precedence(&self, parser: &Parser) -> Option> { + let token = parser.peek_token(); + debug!("get_next_precedence() {:?}", token); + + let precedence = match token.token { + Token::Word(w) if w.keyword == Keyword::OR => OR_PREC, + Token::Word(w) if w.keyword == Keyword::XOR => XOR_PREC, + Token::Word(w) if w.keyword == Keyword::AND => AND_PREC, + Token::Word(w) if w.keyword == Keyword::AT => { + match ( + parser.peek_nth_token(1).token, + parser.peek_nth_token(2).token, + ) { + (Token::Word(w), Token::Word(w2)) + if w.keyword == Keyword::TIME && w2.keyword == Keyword::ZONE => + { + AT_TZ_PREC + } + _ => self.prec_unknown(), + } + } + + Token::Word(w) if w.keyword == Keyword::NOT => match parser.peek_nth_token(1).token { + // The precedence of NOT varies depending on keyword that + // follows it. If it is followed by IN, BETWEEN, or LIKE, + // it takes on the precedence of those tokens. Otherwise, it + // is not an infix operator, and therefore has zero + // precedence. + Token::Word(w) if w.keyword == Keyword::IN => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::BETWEEN => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::LIKE => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::ILIKE => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::RLIKE => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::REGEXP => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::SIMILAR => BETWEEN_LIKE_PREC, + _ => self.prec_unknown(), + }, + Token::Word(w) if w.keyword == Keyword::IS => IS_PREC, + Token::Word(w) if w.keyword == Keyword::IN => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::BETWEEN => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::LIKE => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::ILIKE => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::RLIKE => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::REGEXP => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::SIMILAR => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::OPERATOR => BETWEEN_LIKE_PREC, + Token::Word(w) if w.keyword == Keyword::DIV => MUL_DIV_MOD_OP_PREC, + Token::Word(w) if w.keyword == Keyword::COLLATE => COLLATE_PREC, + Token::Eq + | Token::Lt + | Token::LtEq + | Token::Neq + | Token::Gt + | Token::GtEq + | Token::DoubleEq + | Token::Tilde + | Token::TildeAsterisk + | Token::ExclamationMarkTilde + | Token::ExclamationMarkTildeAsterisk + | Token::DoubleTilde + | Token::DoubleTildeAsterisk + | Token::ExclamationMarkDoubleTilde + | Token::ExclamationMarkDoubleTildeAsterisk + | Token::Spaceship => EQ_PREC, + Token::Caret => CARET_PREC, + Token::Plus | Token::Minus => PLUS_MINUS_PREC, + Token::Mul | Token::Div | Token::Mod => MUL_DIV_MOD_OP_PREC, + Token::DoubleColon => DOUBLE_COLON_PREC, + Token::LBracket => BRACKET_PREC, + Token::Arrow + | Token::LongArrow + | Token::HashArrow + | Token::HashLongArrow + | Token::AtArrow + | Token::ArrowAt + | Token::HashMinus + | Token::AtQuestion + | Token::AtAt + | Token::Question + | Token::QuestionAnd + | Token::QuestionPipe + | Token::ExclamationMark + | Token::Overlap + | Token::CaretAt + | Token::StringConcat + | Token::Sharp + | Token::ShiftRight + | Token::ShiftLeft + | Token::Pipe + | Token::Ampersand + | Token::CustomBinaryOperator(_) => PG_OTHER_PREC, + _ => self.prec_unknown(), + }; + Some(Ok(precedence)) + } + fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.parse_keyword(Keyword::COMMENT) { Some(parse_comment(parser)) @@ -82,6 +196,26 @@ impl Dialect for PostgreSqlDialect { fn supports_group_by_expr(&self) -> bool { true } + + fn prec_mul_div_mod_op(&self) -> u8 { + MUL_DIV_MOD_OP_PREC + } + + fn prec_plus_minus(&self) -> u8 { + PLUS_MINUS_PREC + } + + fn prec_between(&self) -> u8 { + BETWEEN_LIKE_PREC + } + + fn prec_like(&self) -> u8 { + BETWEEN_LIKE_PREC + } + + fn prec_unary_not(&self) -> u8 { + NOT_PREC + } } pub fn parse_comment(parser: &mut Parser) -> Result { diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 9f1d7f27b..fe35d8da3 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -145,6 +145,15 @@ impl Dialect for SnowflakeDialect { None } + + fn get_next_precedence(&self, parser: &Parser) -> Option> { + let token = parser.peek_token(); + // Snowflake supports the `:` cast operator unlike other dialects + match token.token { + Token::Colon => Some(Ok(self.prec_double_colon())), + _ => None, + } + } } /// Parse snowflake create table statement. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fe8acb4f2..1fdba5ecf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -875,7 +875,7 @@ impl<'a> Parser<'a> { /// Parse a new expression. pub fn parse_expr(&mut self) -> Result { let _guard = self.recursion_counter.try_decrease()?; - self.parse_subexpr(0) + self.parse_subexpr(self.dialect.prec_unknown()) } /// Parse tokens until the precedence changes. @@ -897,7 +897,7 @@ impl<'a> Parser<'a> { } pub fn parse_interval_expr(&mut self) -> Result { - let precedence = 0; + let precedence = self.dialect.prec_unknown(); let mut expr = self.parse_prefix()?; loop { @@ -918,9 +918,9 @@ impl<'a> Parser<'a> { let token = self.peek_token(); match token.token { - Token::Word(w) if w.keyword == Keyword::AND => Ok(0), - Token::Word(w) if w.keyword == Keyword::OR => Ok(0), - Token::Word(w) if w.keyword == Keyword::XOR => Ok(0), + Token::Word(w) if w.keyword == Keyword::AND => Ok(self.dialect.prec_unknown()), + Token::Word(w) if w.keyword == Keyword::OR => Ok(self.dialect.prec_unknown()), + Token::Word(w) if w.keyword == Keyword::XOR => Ok(self.dialect.prec_unknown()), _ => self.get_next_precedence(), } } @@ -1079,7 +1079,7 @@ impl<'a> Parser<'a> { self.parse_bigquery_struct_literal() } Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => { - let expr = self.parse_subexpr(Self::PLUS_MINUS_PREC)?; + let expr = self.parse_subexpr(self.dialect.prec_plus_minus())?; Ok(Expr::Prior(Box::new(expr))) } Keyword::MAP if self.peek_token() == Token::LBrace && self.dialect.support_map_literal_syntax() => { @@ -1167,7 +1167,7 @@ impl<'a> Parser<'a> { }; Ok(Expr::UnaryOp { op, - expr: Box::new(self.parse_subexpr(Self::MUL_DIV_MOD_OP_PREC)?), + expr: Box::new(self.parse_subexpr(self.dialect.prec_mul_div_mod_op())?), }) } tok @ Token::DoubleExclamationMark @@ -1187,7 +1187,7 @@ impl<'a> Parser<'a> { }; Ok(Expr::UnaryOp { op, - expr: Box::new(self.parse_subexpr(Self::PLUS_MINUS_PREC)?), + expr: Box::new(self.parse_subexpr(self.dialect.prec_plus_minus())?), }) } Token::EscapedStringLiteral(_) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => @@ -1716,12 +1716,13 @@ impl<'a> Parser<'a> { } pub fn parse_position_expr(&mut self, ident: Ident) -> Result { + let between_prec = self.dialect.prec_between(); let position_expr = self.maybe_parse(|p| { // PARSE SELECT POSITION('@' in field) p.expect_token(&Token::LParen)?; // Parse the subexpr till the IN keyword - let expr = p.parse_subexpr(Self::BETWEEN_PREC)?; + let expr = p.parse_subexpr(between_prec)?; p.expect_keyword(Keyword::IN)?; let from = p.parse_expr()?; p.expect_token(&Token::RParen)?; @@ -1963,12 +1964,12 @@ impl<'a> Parser<'a> { } _ => Ok(Expr::UnaryOp { op: UnaryOperator::Not, - expr: Box::new(self.parse_subexpr(Self::UNARY_NOT_PREC)?), + expr: Box::new(self.parse_subexpr(self.dialect.prec_unary_not())?), }), }, _ => Ok(Expr::UnaryOp { op: UnaryOperator::Not, - expr: Box::new(self.parse_subexpr(Self::UNARY_NOT_PREC)?), + expr: Box::new(self.parse_subexpr(self.dialect.prec_unary_not())?), }), } } @@ -2641,7 +2642,7 @@ impl<'a> Parser<'a> { Ok(Expr::RLike { negated, expr: Box::new(expr), - pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), + pattern: Box::new(self.parse_subexpr(self.dialect.prec_like())?), regexp, }) } else if self.parse_keyword(Keyword::IN) { @@ -2652,21 +2653,21 @@ impl<'a> Parser<'a> { Ok(Expr::Like { negated, expr: Box::new(expr), - pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), + pattern: Box::new(self.parse_subexpr(self.dialect.prec_like())?), escape_char: self.parse_escape_char()?, }) } else if self.parse_keyword(Keyword::ILIKE) { Ok(Expr::ILike { negated, expr: Box::new(expr), - pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), + pattern: Box::new(self.parse_subexpr(self.dialect.prec_like())?), escape_char: self.parse_escape_char()?, }) } else if self.parse_keywords(&[Keyword::SIMILAR, Keyword::TO]) { Ok(Expr::SimilarTo { negated, expr: Box::new(expr), - pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), + pattern: Box::new(self.parse_subexpr(self.dialect.prec_like())?), escape_char: self.parse_escape_char()?, }) } else { @@ -2941,9 +2942,9 @@ impl<'a> Parser<'a> { pub fn parse_between(&mut self, expr: Expr, negated: bool) -> Result { // Stop parsing subexpressions for and on tokens with // precedence lower than that of `BETWEEN`, such as `AND`, `IS`, etc. - let low = self.parse_subexpr(Self::BETWEEN_PREC)?; + let low = self.parse_subexpr(self.dialect.prec_between())?; self.expect_keyword(Keyword::AND)?; - let high = self.parse_subexpr(Self::BETWEEN_PREC)?; + let high = self.parse_subexpr(self.dialect.prec_between())?; Ok(Expr::Between { expr: Box::new(expr), negated, @@ -2962,118 +2963,9 @@ impl<'a> Parser<'a> { }) } - // Use https://www.postgresql.org/docs/7.0/operators.htm#AEN2026 as a reference - // higher number = higher precedence - // - // NOTE: The pg documentation is incomplete, e.g. the AT TIME ZONE operator - // actually has higher precedence than addition. - // See https://postgrespro.com/list/thread-id/2673331. - const AT_TZ_PREC: u8 = 41; - const MUL_DIV_MOD_OP_PREC: u8 = 40; - const PLUS_MINUS_PREC: u8 = 30; - const XOR_PREC: u8 = 24; - const BETWEEN_PREC: u8 = 20; - const LIKE_PREC: u8 = 19; - const IS_PREC: u8 = 17; - const PG_OTHER_PREC: u8 = 16; - const UNARY_NOT_PREC: u8 = 15; - const AND_PREC: u8 = 10; - const OR_PREC: u8 = 5; - /// Get the precedence of the next token pub fn get_next_precedence(&self) -> Result { - // allow the dialect to override precedence logic - if let Some(precedence) = self.dialect.get_next_precedence(self) { - return precedence; - } - - let token = self.peek_token(); - debug!("get_next_precedence() {:?}", token); - let [token_0, token_1, token_2] = self.peek_tokens_with_location(); - debug!("0: {token_0} 1: {token_1} 2: {token_2}"); - match token.token { - Token::Word(w) if w.keyword == Keyword::OR => Ok(Self::OR_PREC), - Token::Word(w) if w.keyword == Keyword::AND => Ok(Self::AND_PREC), - Token::Word(w) if w.keyword == Keyword::XOR => Ok(Self::XOR_PREC), - - Token::Word(w) if w.keyword == Keyword::AT => { - match (self.peek_nth_token(1).token, self.peek_nth_token(2).token) { - (Token::Word(w), Token::Word(w2)) - if w.keyword == Keyword::TIME && w2.keyword == Keyword::ZONE => - { - Ok(Self::AT_TZ_PREC) - } - _ => Ok(0), - } - } - - Token::Word(w) if w.keyword == Keyword::NOT => match self.peek_nth_token(1).token { - // The precedence of NOT varies depending on keyword that - // follows it. If it is followed by IN, BETWEEN, or LIKE, - // it takes on the precedence of those tokens. Otherwise, it - // is not an infix operator, and therefore has zero - // precedence. - Token::Word(w) if w.keyword == Keyword::IN => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), - _ => Ok(0), - }, - Token::Word(w) if w.keyword == Keyword::IS => Ok(Self::IS_PREC), - Token::Word(w) if w.keyword == Keyword::IN => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), - Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(Self::BETWEEN_PREC), - Token::Word(w) if w.keyword == Keyword::DIV => Ok(Self::MUL_DIV_MOD_OP_PREC), - Token::Eq - | Token::Lt - | Token::LtEq - | Token::Neq - | Token::Gt - | Token::GtEq - | Token::DoubleEq - | Token::Tilde - | Token::TildeAsterisk - | Token::ExclamationMarkTilde - | Token::ExclamationMarkTildeAsterisk - | Token::DoubleTilde - | Token::DoubleTildeAsterisk - | Token::ExclamationMarkDoubleTilde - | Token::ExclamationMarkDoubleTildeAsterisk - | Token::Spaceship => Ok(20), - Token::Pipe => Ok(21), - Token::Caret | Token::Sharp | Token::ShiftRight | Token::ShiftLeft => Ok(22), - Token::Ampersand => Ok(23), - Token::Plus | Token::Minus => Ok(Self::PLUS_MINUS_PREC), - Token::Mul | Token::Div | Token::DuckIntDiv | Token::Mod | Token::StringConcat => { - Ok(Self::MUL_DIV_MOD_OP_PREC) - } - Token::DoubleColon => Ok(50), - Token::Colon if dialect_of!(self is SnowflakeDialect) => Ok(50), - Token::ExclamationMark => Ok(50), - Token::LBracket | Token::Overlap | Token::CaretAt => Ok(50), - Token::Arrow - | Token::LongArrow - | Token::HashArrow - | Token::HashLongArrow - | Token::AtArrow - | Token::ArrowAt - | Token::HashMinus - | Token::AtQuestion - | Token::AtAt - | Token::Question - | Token::QuestionAnd - | Token::QuestionPipe - | Token::CustomBinaryOperator(_) => Ok(Self::PG_OTHER_PREC), - _ => Ok(0), - } + self.dialect.get_next_precedence_full(self) } /// Return the first non-whitespace token that has not yet been processed @@ -8051,7 +7943,7 @@ impl<'a> Parser<'a> { format_clause: None, }) } else { - let body = self.parse_boxed_query_body(0)?; + let body = self.parse_boxed_query_body(self.dialect.prec_unknown())?; let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7406bdd74..150f06913 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4476,3 +4476,115 @@ fn test_unicode_string_literal() { } } } + +fn check_arrow_precedence(sql: &str, arrow_operator: BinaryOperator) { + assert_eq!( + pg().verified_stmt(sql), + Statement::Query(Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "foo".to_string(), + quote_style: None, + })), + op: arrow_operator, + right: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), + }), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::SingleQuotedString("spam".to_string()))), + })], + 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![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: None, + }))), + order_by: None, + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + })) + ) +} + +#[test] +fn arrow_precedence() { + check_arrow_precedence("SELECT foo -> 'bar' = 'spam'", BinaryOperator::Arrow); +} + +#[test] +fn long_arrow_precedence() { + check_arrow_precedence("SELECT foo ->> 'bar' = 'spam'", BinaryOperator::LongArrow); +} + +#[test] +fn arrow_cast_precedence() { + // check this matches postgres where you would need `(foo -> 'bar')::TEXT` + let stmt = pg().verified_stmt("SELECT foo -> 'bar'::TEXT"); + assert_eq!( + stmt, + Statement::Query(Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + projection: vec![SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "foo".to_string(), + quote_style: None, + })), + op: BinaryOperator::Arrow, + right: Box::new(Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), + data_type: DataType::Text, + format: None, + }), + })], + 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![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: None, + }))), + order_by: None, + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + })) + ) +} From da484c57c4a5682da24c070d76c872148e54bbfe Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 6 Aug 2024 08:23:07 -0400 Subject: [PATCH 512/806] Improve comments on `Dialect` (#1366) --- src/dialect/mod.rs | 90 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index fc45545d4..9033ecc78 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -66,7 +66,8 @@ macro_rules! dialect_of { /// Encapsulates the differences between SQL implementations. /// /// # SQL Dialects -/// SQL implementations deviatiate from one another, either due to +/// +/// SQL implementations deviate from one another, either due to /// custom extensions or various historical reasons. This trait /// encapsulates the parsing differences between dialects. /// @@ -114,16 +115,20 @@ pub trait Dialect: Debug + Any { fn is_delimited_identifier_start(&self, ch: char) -> bool { ch == '"' || ch == '`' } + /// Return the character used to quote identifiers. fn identifier_quote_style(&self, _identifier: &str) -> Option { None } + /// Determine if quoted characters are proper for identifier fn is_proper_identifier_inside_quotes(&self, mut _chars: Peekable>) -> bool { true } + /// Determine if a character is a valid start character for an unquoted identifier fn is_identifier_start(&self, ch: char) -> bool; + /// Determine if a character is a valid unquoted identifier character fn is_identifier_part(&self, ch: char) -> bool; @@ -168,6 +173,7 @@ pub trait Dialect: Debug + Any { fn supports_filter_during_aggregation(&self) -> bool { false } + /// Returns true if the dialect supports referencing another named window /// within a window clause declaration. /// @@ -179,6 +185,7 @@ pub trait Dialect: Debug + Any { fn supports_window_clause_named_window_reference(&self) -> bool { false } + /// Returns true if the dialect supports `ARRAY_AGG() [WITHIN GROUP (ORDER BY)]` expressions. /// Otherwise, the dialect should expect an `ORDER BY` without the `WITHIN GROUP` clause, e.g. [`ANSI`] /// @@ -186,38 +193,47 @@ pub trait Dialect: Debug + Any { fn supports_within_after_array_aggregation(&self) -> bool { false } + /// Returns true if the dialects supports `group sets, roll up, or cube` expressions. fn supports_group_by_expr(&self) -> bool { false } + /// Returns true if the dialect supports CONNECT BY. fn supports_connect_by(&self) -> bool { false } + /// Returns true if the dialect supports the MATCH_RECOGNIZE operation. fn supports_match_recognize(&self) -> bool { false } + /// Returns true if the dialect supports `(NOT) IN ()` expressions fn supports_in_empty_list(&self) -> bool { false } + /// Returns true if the dialect supports `BEGIN {DEFERRED | IMMEDIATE | EXCLUSIVE} [TRANSACTION]` statements fn supports_start_transaction_modifier(&self) -> bool { false } + /// Returns true if the dialect supports named arguments of the form FUN(a = '1', b = '2'). fn supports_named_fn_args_with_eq_operator(&self) -> bool { false } + /// Returns true if the dialect supports identifiers starting with a numeric - /// prefix such as tables named: `59901_user_login` + /// prefix such as tables named `59901_user_login` fn supports_numeric_prefix(&self) -> bool { false } + /// Returns true if the dialects supports specifying null treatment - /// as part of a window function's parameter list. As opposed + /// as part of a window function's parameter list as opposed /// to after the parameter list. + /// /// i.e The following syntax returns true /// ```sql /// FIRST_VALUE(a IGNORE NULLS) OVER () @@ -229,16 +245,19 @@ pub trait Dialect: Debug + Any { fn supports_window_function_null_treatment_arg(&self) -> bool { false } + /// Returns true if the dialect supports defining structs or objects using a /// syntax like `{'x': 1, 'y': 2, 'z': 3}`. fn supports_dictionary_syntax(&self) -> bool { false } + /// Returns true if the dialect supports defining object using the /// syntax like `Map {1: 10, 2: 20}`. fn support_map_literal_syntax(&self) -> bool { false } + /// Returns true if the dialect supports lambda functions, for example: /// /// ```sql @@ -247,6 +266,7 @@ pub trait Dialect: Debug + Any { fn supports_lambda_functions(&self) -> bool { false } + /// Returns true if the dialect supports multiple variable assignment /// using parentheses in a `SET` variable declaration. /// @@ -256,6 +276,7 @@ pub trait Dialect: Debug + Any { fn supports_parenthesized_set_variables(&self) -> bool { false } + /// Returns true if the dialect supports an `EXCEPT` clause following a /// wildcard in a select list. /// @@ -266,30 +287,40 @@ pub trait Dialect: Debug + Any { fn supports_select_wildcard_except(&self) -> bool { false } + /// Returns true if the dialect has a CONVERT function which accepts a type first /// and an expression second, e.g. `CONVERT(varchar, 1)` fn convert_type_before_value(&self) -> bool { false } + /// Returns true if the dialect supports triple quoted string /// e.g. `"""abc"""` fn supports_triple_quoted_string(&self) -> bool { false } + /// Dialect-specific prefix parser override fn parse_prefix(&self, _parser: &mut Parser) -> Option> { // return None to fall back to the default behavior None } + /// Does the dialect support trailing commas around the query? fn supports_trailing_commas(&self) -> bool { false } + /// Does the dialect support trailing commas in the projection list? fn supports_projection_trailing_commas(&self) -> bool { self.supports_trailing_commas() } + /// Dialect-specific infix parser override + /// + /// This method is called to parse the next infix expression. + /// + /// If `None` is returned, falls back to the default behavior. fn parse_infix( &self, _parser: &mut Parser, @@ -299,24 +330,33 @@ pub trait Dialect: Debug + Any { // return None to fall back to the default behavior None } + /// Dialect-specific precedence override + /// + /// This method is called to get the precedence of the next token. + /// + /// If `None` is returned, falls back to the default behavior. fn get_next_precedence(&self, _parser: &Parser) -> Option> { // return None to fall back to the default behavior None } - /// Get the precedence of the next token. This "full" method means all precedence logic and remain - /// in the dialect. while still allowing overriding the `get_next_precedence` method with the option to - /// fallback to the default behavior. + /// Get the precedence of the next token, looking at the full token stream. /// - /// Higher number => higher precedence + /// A higher number => higher precedence + /// + /// See [`Self::get_next_precedence`] to override the behavior for just the + /// next token. + /// + /// The default implementation is used for many dialects, but can be + /// overridden to provide dialect-specific behavior. fn get_next_precedence_full(&self, parser: &Parser) -> Result { if let Some(precedence) = self.get_next_precedence(parser) { return precedence; } let token = parser.peek_token(); - debug!("get_next_precedence() {:?}", token); + debug!("get_next_precedence_full() {:?}", token); match token.token { Token::Word(w) if w.keyword == Keyword::OR => Ok(OR_PREC), Token::Word(w) if w.keyword == Keyword::AND => Ok(AND_PREC), @@ -408,37 +448,67 @@ pub trait Dialect: Debug + Any { } /// Dialect-specific statement parser override + /// + /// This method is called to parse the next statement. + /// + /// If `None` is returned, falls back to the default behavior. fn parse_statement(&self, _parser: &mut Parser) -> Option> { // return None to fall back to the default behavior None } - /// The following precedence values are used directly by `Parse` or in dialects, - /// so have to be made public by the dialect. + // The following precedence values are used directly by `Parse` or in dialects, + // so have to be made public by the dialect. + + /// Return the precedence of the `::` operator. + /// + /// Default is 50. fn prec_double_colon(&self) -> u8 { DOUBLE_COLON_PREC } + /// Return the precedence of `*`, `/`, and `%` operators. + /// + /// Default is 40. fn prec_mul_div_mod_op(&self) -> u8 { MUL_DIV_MOD_OP_PREC } + /// Return the precedence of the `+` and `-` operators. + /// + /// Default is 30. fn prec_plus_minus(&self) -> u8 { PLUS_MINUS_PREC } + /// Return the precedence of the `BETWEEN` operator. + /// + /// For example `BETWEEN AND ` + /// + /// Default is 22. fn prec_between(&self) -> u8 { BETWEEN_PREC } + /// Return the precedence of the `LIKE` operator. + /// + /// Default is 19. fn prec_like(&self) -> u8 { LIKE_PREC } + /// Return the precedence of the unary `NOT` operator. + /// + /// For example `NOT (a OR b)` + /// + /// Default is 15. fn prec_unary_not(&self) -> u8 { UNARY_NOT_PREC } + /// Return the default (unknown) precedence. + /// + /// Default is 0. fn prec_unknown(&self) -> u8 { UNKNOWN_PREC } From dfb8b81630ec7285c7ffc9e9113105ef1af56023 Mon Sep 17 00:00:00 2001 From: hulk Date: Thu, 8 Aug 2024 02:02:11 +0800 Subject: [PATCH 513/806] Add support of ATTACH/DETACH PARTITION for ClickHouse (#1362) --- src/ast/ddl.rs | 25 ++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 24 ++++++++++++- tests/sqlparser_clickhouse.rs | 65 +++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index af679d469..d207f5766 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -72,6 +72,21 @@ pub enum AlterTableOperation { if_exists: bool, cascade: bool, }, + /// `ATTACH PART|PARTITION ` + /// Note: this is a ClickHouse-specific operation, please refer to + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/pakrtition#attach-partitionpart) + AttachPartition { + // PART is not a short form of PARTITION, it's a separate keyword + // which represents a physical file on disk and partition is a logical entity. + partition: Partition, + }, + /// `DETACH PART|PARTITION ` + /// Note: this is a ClickHouse-specific operation, please refer to + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/partition#detach-partitionpart) + DetachPartition { + // See `AttachPartition` for more details + partition: Partition, + }, /// `DROP PRIMARY KEY` /// /// Note: this is a MySQL-specific operation. @@ -272,6 +287,12 @@ impl fmt::Display for AlterTableOperation { column_name, if *cascade { " CASCADE" } else { "" } ), + AlterTableOperation::AttachPartition { partition } => { + write!(f, "ATTACH {partition}") + } + AlterTableOperation::DetachPartition { partition } => { + write!(f, "DETACH {partition}") + } AlterTableOperation::EnableAlwaysRule { name } => { write!(f, "ENABLE ALWAYS RULE {name}") } @@ -1305,6 +1326,9 @@ impl fmt::Display for UserDefinedTypeCompositeAttributeDef { pub enum Partition { Identifier(Ident), Expr(Expr), + /// ClickHouse supports PART expr which represents physical partition in disk. + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/partition#attach-partitionpart) + Part(Expr), Partitions(Vec), } @@ -1313,6 +1337,7 @@ impl fmt::Display for Partition { match self { Partition::Identifier(id) => write!(f, "PARTITION ID {id}"), Partition::Expr(expr) => write!(f, "PARTITION {expr}"), + Partition::Part(expr) => write!(f, "PART {expr}"), Partition::Partitions(partitions) => { write!(f, "PARTITION ({})", display_comma_separated(partitions)) } diff --git a/src/keywords.rs b/src/keywords.rs index 49bd969af..c175da874 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -539,6 +539,7 @@ define_keywords!( PARALLEL, PARAMETER, PARQUET, + PART, PARTITION, PARTITIONED, PARTITIONS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1fdba5ecf..b6d4c307f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6432,7 +6432,7 @@ impl<'a> Parser<'a> { } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) && self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { - let new_owner = match self.parse_one_of_keywords( &[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) { + let new_owner = match self.parse_one_of_keywords(&[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) { Some(Keyword::CURRENT_USER) => Owner::CurrentUser, Some(Keyword::CURRENT_ROLE) => Owner::CurrentRole, Some(Keyword::SESSION_USER) => Owner::SessionUser, @@ -6448,6 +6448,18 @@ impl<'a> Parser<'a> { }; AlterTableOperation::OwnerTo { new_owner } + } else if dialect_of!(self is ClickHouseDialect|GenericDialect) + && self.parse_keyword(Keyword::ATTACH) + { + AlterTableOperation::AttachPartition { + partition: self.parse_part_or_partition()?, + } + } else if dialect_of!(self is ClickHouseDialect|GenericDialect) + && self.parse_keyword(Keyword::DETACH) + { + AlterTableOperation::DetachPartition { + partition: self.parse_part_or_partition()?, + } } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; @@ -6465,6 +6477,16 @@ impl<'a> Parser<'a> { Ok(operation) } + fn parse_part_or_partition(&mut self) -> Result { + let keyword = self.expect_one_of_keywords(&[Keyword::PART, Keyword::PARTITION])?; + match keyword { + Keyword::PART => Ok(Partition::Part(self.parse_expr()?)), + Keyword::PARTITION => Ok(Partition::Expr(self.parse_expr()?)), + // unreachable because expect_one_of_keywords used above + _ => unreachable!(), + } + } + pub fn parse_alter(&mut self) -> Result { let object_type = self.expect_one_of_keywords(&[ Keyword::VIEW, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 4108958fb..4676e6e50 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -222,6 +222,71 @@ fn parse_create_table() { ); } +#[test] +fn parse_alter_table_attach_and_detach_partition() { + for operation in &["ATTACH", "DETACH"] { + match clickhouse_and_generic() + .verified_stmt(format!("ALTER TABLE t0 {operation} PARTITION part").as_str()) + { + Statement::AlterTable { + name, operations, .. + } => { + pretty_assertions::assert_eq!("t0", name.to_string()); + pretty_assertions::assert_eq!( + operations[0], + if operation == &"ATTACH" { + AlterTableOperation::AttachPartition { + partition: Partition::Expr(Identifier(Ident::new("part"))), + } + } else { + AlterTableOperation::DetachPartition { + partition: Partition::Expr(Identifier(Ident::new("part"))), + } + } + ); + } + _ => unreachable!(), + } + + match clickhouse_and_generic() + .verified_stmt(format!("ALTER TABLE t1 {operation} PART part").as_str()) + { + Statement::AlterTable { + name, operations, .. + } => { + pretty_assertions::assert_eq!("t1", name.to_string()); + pretty_assertions::assert_eq!( + operations[0], + if operation == &"ATTACH" { + AlterTableOperation::AttachPartition { + partition: Partition::Part(Identifier(Ident::new("part"))), + } + } else { + AlterTableOperation::DetachPartition { + partition: Partition::Part(Identifier(Ident::new("part"))), + } + } + ); + } + _ => unreachable!(), + } + + // negative cases + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements(format!("ALTER TABLE t0 {operation} PARTITION").as_str()) + .unwrap_err(), + ParserError("Expected: an expression:, found: EOF".to_string()) + ); + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements(format!("ALTER TABLE t0 {operation} PART").as_str()) + .unwrap_err(), + ParserError("Expected: an expression:, found: EOF".to_string()) + ); + } +} + #[test] fn parse_optimize_table() { clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0"); From 2d4b9b3e5683c8f415ecd35a9b78a3a0f8b2fcb8 Mon Sep 17 00:00:00 2001 From: Jesse Date: Wed, 7 Aug 2024 20:30:01 +0200 Subject: [PATCH 514/806] Make `Parser::maybe_parse` pub (#1364) --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b6d4c307f..9b252ce29 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3383,7 +3383,7 @@ impl<'a> Parser<'a> { /// Run a parser method `f`, reverting back to the current position if unsuccessful. #[must_use] - fn maybe_parse(&mut self, mut f: F) -> Option + pub fn maybe_parse(&mut self, mut f: F) -> Option where F: FnMut(&mut Parser) -> Result, { From 68a04cd40218bf5a3244c6574f091bde344f6d12 Mon Sep 17 00:00:00 2001 From: hulk Date: Fri, 9 Aug 2024 04:57:21 +0800 Subject: [PATCH 515/806] Update version of GitHub Actions (#1363) --- .github/workflows/rust.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 64c4d114a..1d2c34276 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,31 +8,31 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 with: components: rustfmt # Note that `nightly` is required for `license_template_path`, as # it's an unstable feature. rust-version: nightly - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: cargo +nightly fmt -- --check --config-path <(echo 'license_template_path = "HEADER"') lint: runs-on: ubuntu-latest steps: - name: Set up Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 with: components: clippy - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: cargo clippy --all-targets --all-features -- -D warnings compile: runs-on: ubuntu-latest steps: - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - - uses: actions/checkout@master + uses: hecrj/setup-rust-action@v2 + - uses: actions/checkout@v4 - run: cargo check --all-targets --all-features docs: @@ -41,18 +41,18 @@ jobs: RUSTDOCFLAGS: "-Dwarnings" steps: - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - - uses: actions/checkout@master + uses: hecrj/setup-rust-action@v2 + - uses: actions/checkout@v4 - run: cargo doc --document-private-items --no-deps --workspace --all-features compile-no-std: runs-on: ubuntu-latest steps: - name: Set up Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 with: targets: 'thumbv6m-none-eabi' - - uses: actions/checkout@master + - uses: actions/checkout@v4 - run: cargo check --no-default-features --target thumbv6m-none-eabi test: @@ -62,7 +62,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.rust }} - name: Install Tarpaulin @@ -72,7 +72,7 @@ jobs: version: 0.14.2 use-tool-cache: true - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Test run: cargo test --all-features @@ -80,7 +80,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Rust - uses: hecrj/setup-rust-action@v1 + uses: hecrj/setup-rust-action@v2 with: rust-version: stable - name: Install Tarpaulin @@ -90,7 +90,7 @@ jobs: version: 0.14.2 use-tool-cache: true - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Coverage run: cargo tarpaulin -o Lcov --output-dir ./coverage - name: Coveralls @@ -104,8 +104,8 @@ jobs: needs: [test] steps: - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - - uses: actions/checkout@v2 + uses: hecrj/setup-rust-action@v2 + - uses: actions/checkout@v4 - name: Publish shell: bash run: | From 1e209d87415a5adfedccac8cee3e2860122e4acb Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 8 Aug 2024 16:58:31 -0400 Subject: [PATCH 516/806] Simplify arrow_cast tests (#1367) --- tests/sqlparser_postgres.rs | 120 +++++++++--------------------------- 1 file changed, 29 insertions(+), 91 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 150f06913..f370748d2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4479,112 +4479,50 @@ fn test_unicode_string_literal() { fn check_arrow_precedence(sql: &str, arrow_operator: BinaryOperator) { assert_eq!( - pg().verified_stmt(sql), - Statement::Query(Box::new(Query { - with: None, - body: Box::new(SetExpr::Select(Box::new(Select { - distinct: None, - top: None, - projection: vec![SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident { - value: "foo".to_string(), - quote_style: None, - })), - op: arrow_operator, - right: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), - }), - op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString("spam".to_string()))), - })], - 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![], - qualify: None, - window_before_qualify: false, - value_table_mode: None, - connect_by: None, - }))), - order_by: None, - limit: None, - limit_by: vec![], - offset: None, - fetch: None, - locks: vec![], - for_clause: None, - settings: None, - format_clause: None, - })) + pg().verified_expr(sql), + Expr::BinaryOp { + left: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "foo".to_string(), + quote_style: None, + })), + op: arrow_operator, + right: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), + }), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::SingleQuotedString("spam".to_string()))), + } ) } #[test] fn arrow_precedence() { - check_arrow_precedence("SELECT foo -> 'bar' = 'spam'", BinaryOperator::Arrow); + check_arrow_precedence("foo -> 'bar' = 'spam'", BinaryOperator::Arrow); } #[test] fn long_arrow_precedence() { - check_arrow_precedence("SELECT foo ->> 'bar' = 'spam'", BinaryOperator::LongArrow); + check_arrow_precedence("foo ->> 'bar' = 'spam'", BinaryOperator::LongArrow); } #[test] fn arrow_cast_precedence() { // check this matches postgres where you would need `(foo -> 'bar')::TEXT` - let stmt = pg().verified_stmt("SELECT foo -> 'bar'::TEXT"); + let stmt = pg().verified_expr("foo -> 'bar'::TEXT"); assert_eq!( stmt, - Statement::Query(Box::new(Query { - with: None, - body: Box::new(SetExpr::Select(Box::new(Select { - distinct: None, - top: None, - projection: vec![SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident { - value: "foo".to_string(), - quote_style: None, - })), - op: BinaryOperator::Arrow, - right: Box::new(Expr::Cast { - kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), - data_type: DataType::Text, - format: None, - }), - })], - 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![], - qualify: None, - window_before_qualify: false, - value_table_mode: None, - connect_by: None, - }))), - order_by: None, - limit: None, - limit_by: vec![], - offset: None, - fetch: None, - locks: vec![], - for_clause: None, - settings: None, - format_clause: None, - })) + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident { + value: "foo".to_string(), + quote_style: None, + })), + op: BinaryOperator::Arrow, + right: Box::new(Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), + data_type: DataType::Text, + format: None, + }), + } ) } From ca5262c13f5b7587c1700f26c97e147676981f6e Mon Sep 17 00:00:00 2001 From: hulk Date: Tue, 13 Aug 2024 18:59:19 +0800 Subject: [PATCH 517/806] Use the local GitHub Action to replace setup-rust-action (#1371) --- .github/actions/setup-builder/action.yaml | 42 ++++++++++++++++++++ .github/workflows/rust.yml | 47 +++++++++++------------ 2 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 .github/actions/setup-builder/action.yaml diff --git a/.github/actions/setup-builder/action.yaml b/.github/actions/setup-builder/action.yaml new file mode 100644 index 000000000..61faa055b --- /dev/null +++ b/.github/actions/setup-builder/action.yaml @@ -0,0 +1,42 @@ +# 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: Prepare Rust Builder +description: 'Prepare Rust Build Environment' +inputs: + rust-version: + description: 'version of rust to install (e.g. stable)' + required: true + default: 'stable' + targets: + description: 'The toolchain targets to add, comma-separated' + default: '' + +runs: + using: "composite" + steps: + - name: Setup Rust Toolchain + shell: bash + run: | + echo "Installing ${{ inputs.rust-version }}" + if [ -n "${{ inputs.targets}}" ]; then + rustup toolchain install ${{ inputs.rust-version }} -t ${{ inputs.targets }} + else + rustup toolchain install ${{ inputs.rust-version }} + fi + rustup default ${{ inputs.rust-version }} + rustup component add rustfmt clippy diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1d2c34276..146ea3120 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -7,32 +7,29 @@ jobs: codestyle: runs-on: ubuntu-latest steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v2 + - uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder with: - components: rustfmt # Note that `nightly` is required for `license_template_path`, as # it's an unstable feature. rust-version: nightly - - uses: actions/checkout@v4 - run: cargo +nightly fmt -- --check --config-path <(echo 'license_template_path = "HEADER"') lint: runs-on: ubuntu-latest steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v2 - with: - components: clippy - uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder - run: cargo clippy --all-targets --all-features -- -D warnings compile: runs-on: ubuntu-latest steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v2 - uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder - run: cargo check --all-targets --all-features docs: @@ -40,19 +37,19 @@ jobs: env: RUSTDOCFLAGS: "-Dwarnings" steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v2 - uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder - run: cargo doc --document-private-items --no-deps --workspace --all-features compile-no-std: runs-on: ubuntu-latest steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v2 + - uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder with: targets: 'thumbv6m-none-eabi' - - uses: actions/checkout@v4 - run: cargo check --no-default-features --target thumbv6m-none-eabi test: @@ -61,8 +58,10 @@ jobs: rust: [stable, beta, nightly] runs-on: ubuntu-latest steps: - - name: Setup Rust - uses: hecrj/setup-rust-action@v2 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder with: rust-version: ${{ matrix.rust }} - name: Install Tarpaulin @@ -71,16 +70,16 @@ jobs: crate: cargo-tarpaulin version: 0.14.2 use-tool-cache: true - - name: Checkout - uses: actions/checkout@v4 - name: Test run: cargo test --all-features test-coverage: runs-on: ubuntu-latest steps: - - name: Setup Rust - uses: hecrj/setup-rust-action@v2 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder with: rust-version: stable - name: Install Tarpaulin @@ -89,8 +88,6 @@ jobs: crate: cargo-tarpaulin version: 0.14.2 use-tool-cache: true - - name: Checkout - uses: actions/checkout@v4 - name: Coverage run: cargo tarpaulin -o Lcov --output-dir ./coverage - name: Coveralls @@ -103,9 +100,9 @@ jobs: runs-on: ubuntu-latest needs: [test] steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v2 - uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder - name: Publish shell: bash run: | From f5b818e74b8364fe2ffac70e3b3d13167b808215 Mon Sep 17 00:00:00 2001 From: Seve Martinez <20816697+seve-martinez@users.noreply.github.com> Date: Tue, 13 Aug 2024 05:56:18 -0700 Subject: [PATCH 518/806] supporting snowflake extract syntax (#1374) Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 29 +++++++++++++++++++++++++++-- src/parser/mod.rs | 21 ++++++++++++++++++++- tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_common.rs | 1 + tests/sqlparser_snowflake.rs | 29 +++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e0c929a9d..86e2592a3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -477,6 +477,22 @@ pub enum CastKind { DoubleColon, } +/// `EXTRACT` syntax variants. +/// +/// In Snowflake dialect, the `EXTRACT` expression can support either the `from` syntax +/// or the comma syntax. +/// +/// 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 ExtractSyntax { + /// `EXTRACT( FROM )` + From, + /// `EXTRACT( , )` + Comma, +} + /// An SQL expression of any type. /// /// The parser does not distinguish between expressions of different types @@ -637,13 +653,15 @@ pub enum Expr { time_zone: Box, }, /// Extract a field from a timestamp e.g. `EXTRACT(MONTH FROM foo)` + /// Or `EXTRACT(MONTH, foo)` /// /// Syntax: /// ```sql - /// EXTRACT(DateTimeField FROM ) + /// EXTRACT(DateTimeField FROM ) | EXTRACT(DateTimeField, ) /// ``` Extract { field: DateTimeField, + syntax: ExtractSyntax, expr: Box, }, /// ```sql @@ -1197,7 +1215,14 @@ impl fmt::Display for Expr { write!(f, "{expr}::{data_type}") } }, - Expr::Extract { field, expr } => write!(f, "EXTRACT({field} FROM {expr})"), + Expr::Extract { + field, + syntax, + expr, + } => match syntax { + ExtractSyntax::From => write!(f, "EXTRACT({field} FROM {expr})"), + ExtractSyntax::Comma => write!(f, "EXTRACT({field}, {expr})"), + }, Expr::Ceil { expr, field } => { if field == &DateTimeField::NoDateTime { write!(f, "CEIL({expr})") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9b252ce29..60a7b4d0b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1682,12 +1682,25 @@ impl<'a> Parser<'a> { pub fn parse_extract_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; let field = self.parse_date_time_field()?; - self.expect_keyword(Keyword::FROM)?; + + let syntax = if self.parse_keyword(Keyword::FROM) { + ExtractSyntax::From + } else if self.consume_token(&Token::Comma) + && dialect_of!(self is SnowflakeDialect | GenericDialect) + { + ExtractSyntax::Comma + } else { + return Err(ParserError::ParserError( + "Expected 'FROM' or ','".to_string(), + )); + }; + let expr = self.parse_expr()?; self.expect_token(&Token::RParen)?; Ok(Expr::Extract { field, expr: Box::new(expr), + syntax, }) } @@ -1950,6 +1963,12 @@ impl<'a> Parser<'a> { } _ => self.expected("date/time field", next_token), }, + Token::SingleQuotedString(_) if dialect_of!(self is SnowflakeDialect | GenericDialect) => + { + self.prev_token(); + let custom = self.parse_identifier(false)?; + Ok(DateTimeField::Custom(custom)) + } _ => self.expected("date/time field", next_token), } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index a0dd5a662..134c8ddad 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2136,6 +2136,7 @@ fn parse_extract_weekday() { assert_eq!( &Expr::Extract { field: DateTimeField::Week(Some(Ident::new("MONDAY"))), + syntax: ExtractSyntax::From, expr: Box::new(Expr::Identifier(Ident::new("d"))), }, expr_from_projection(only(&select.projection)), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7ec017269..293269cdd 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2430,6 +2430,7 @@ fn parse_extract() { assert_eq!( &Expr::Extract { field: DateTimeField::Year, + syntax: ExtractSyntax::From, expr: Box::new(Expr::Identifier(Ident::new("d"))), }, expr_from_projection(only(&select.projection)), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index eaf8c1d14..a331c7df9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2019,6 +2019,35 @@ fn parse_extract_custom_part() { assert_eq!( &Expr::Extract { field: DateTimeField::Custom(Ident::new("eod")), + syntax: ExtractSyntax::From, + expr: Box::new(Expr::Identifier(Ident::new("d"))), + }, + expr_from_projection(only(&select.projection)), + ); +} + +#[test] +fn parse_extract_comma() { + let sql = "SELECT EXTRACT(HOUR, d)"; + let select = snowflake_and_generic().verified_only_select(sql); + assert_eq!( + &Expr::Extract { + field: DateTimeField::Hour, + syntax: ExtractSyntax::Comma, + expr: Box::new(Expr::Identifier(Ident::new("d"))), + }, + expr_from_projection(only(&select.projection)), + ); +} + +#[test] +fn parse_extract_comma_quoted() { + let sql = "SELECT EXTRACT('hour', d)"; + let select = snowflake_and_generic().verified_only_select(sql); + assert_eq!( + &Expr::Extract { + field: DateTimeField::Custom(Ident::with_quote('\'', "hour")), + syntax: ExtractSyntax::Comma, expr: Box::new(Expr::Identifier(Ident::new("d"))), }, expr_from_projection(only(&select.projection)), From b072ce2589a16a850b456223979f75b799aaf7aa Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Wed, 14 Aug 2024 15:11:16 +0200 Subject: [PATCH 519/806] Adding support for parsing CREATE TRIGGER and DROP TRIGGER statements (#1352) Co-authored-by: hulk Co-authored-by: Ifeanyi Ubah Co-authored-by: Andrew Lamb --- src/ast/data_type.rs | 5 + src/ast/ddl.rs | 2 +- src/ast/mod.rs | 172 ++++++++++++- src/ast/trigger.rs | 158 ++++++++++++ src/keywords.rs | 5 +- src/parser/mod.rs | 216 +++++++++++++++- src/test_utils.rs | 1 + tests/sqlparser_postgres.rs | 488 +++++++++++++++++++++++++++++++++++- 8 files changed, 1022 insertions(+), 25 deletions(-) create mode 100644 src/ast/trigger.rs diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index e6477f56b..ff2a3ad04 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -319,6 +319,10 @@ pub enum DataType { /// [`SQLiteDialect`](crate::dialect::SQLiteDialect), from statements such /// as `CREATE TABLE t1 (a)`. Unspecified, + /// Trigger data type, returned by functions associated with triggers + /// + /// [postgresql]: https://www.postgresql.org/docs/current/plpgsql-trigger.html + Trigger, } impl fmt::Display for DataType { @@ -543,6 +547,7 @@ impl fmt::Display for DataType { write!(f, "Nested({})", display_comma_separated(fields)) } DataType::Unspecified => Ok(()), + DataType::Trigger => write!(f, "TRIGGER"), } } } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d207f5766..bebd98604 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1175,7 +1175,7 @@ fn display_option_spaced(option: &Option) -> impl fmt::Displ /// ` = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]` /// /// Used in UNIQUE and foreign key constraints. The individual settings may occur in any order. -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Default, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct ConstraintCharacteristics { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 86e2592a3..ae0522ccc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -53,6 +53,12 @@ pub use self::query::{ TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, }; + +pub use self::trigger::{ + TriggerEvent, TriggerExecBody, TriggerExecBodyType, TriggerObject, TriggerPeriod, + TriggerReferencing, TriggerReferencingType, +}; + pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, @@ -71,6 +77,7 @@ mod dml; pub mod helpers; mod operator; mod query; +mod trigger; mod value; #[cfg(feature = "visitor")] @@ -2282,7 +2289,7 @@ pub enum Statement { DropFunction { if_exists: bool, /// One or more function to drop - func_desc: Vec, + func_desc: Vec, /// `CASCADE` or `RESTRICT` option: Option, }, @@ -2292,7 +2299,7 @@ pub enum Statement { DropProcedure { if_exists: bool, /// One or more function to drop - proc_desc: Vec, + proc_desc: Vec, /// `CASCADE` or `RESTRICT` option: Option, }, @@ -2618,6 +2625,96 @@ pub enum Statement { /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_remote_function) remote_connection: Option, }, + /// CREATE TRIGGER + /// + /// Examples: + /// + /// ```sql + /// CREATE TRIGGER trigger_name + /// BEFORE INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + /// + /// Postgres: + CreateTrigger { + /// The `OR REPLACE` clause is used to re-create the trigger if it already exists. + /// + /// Example: + /// ```sql + /// CREATE OR REPLACE TRIGGER trigger_name + /// AFTER INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + or_replace: bool, + /// The `CONSTRAINT` keyword is used to create a trigger as a constraint. + is_constraint: bool, + /// The name of the trigger to be created. + name: ObjectName, + /// Determines whether the function is called before, after, or instead of the event. + /// + /// Example of BEFORE: + /// + /// ```sql + /// CREATE TRIGGER trigger_name + /// BEFORE INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + /// + /// Example of AFTER: + /// + /// ```sql + /// CREATE TRIGGER trigger_name + /// AFTER INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + /// + /// Example of INSTEAD OF: + /// + /// ```sql + /// CREATE TRIGGER trigger_name + /// INSTEAD OF INSERT ON table_name + /// FOR EACH ROW + /// EXECUTE FUNCTION trigger_function(); + /// ``` + period: TriggerPeriod, + /// Multiple events can be specified using OR, such as `INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`. + events: Vec, + /// The table on which the trigger is to be created. + table_name: ObjectName, + /// The optional referenced table name that can be referenced via + /// the `FROM` keyword. + referenced_table_name: Option, + /// This keyword immediately precedes the declaration of one or two relation names that provide access to the transition relations of the triggering statement. + referencing: Vec, + /// This specifies whether the trigger function should be fired once for + /// every row affected by the trigger event, or just once per SQL statement. + trigger_object: TriggerObject, + /// Whether to include the `EACH` term of the `FOR EACH`, as it is optional syntax. + include_each: bool, + /// Triggering conditions + condition: Option, + /// Execute logic block + exec_body: TriggerExecBody, + /// The characteristic of the trigger, which include whether the trigger is `DEFERRABLE`, `INITIALLY DEFERRED`, or `INITIALLY IMMEDIATE`, + characteristics: Option, + }, + /// DROP TRIGGER + /// + /// ```sql + /// DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] + /// ``` + /// + DropTrigger { + if_exists: bool, + trigger_name: ObjectName, + table_name: ObjectName, + /// `CASCADE` or `RESTRICT` + option: Option, + }, /// ```sql /// CREATE PROCEDURE /// ``` @@ -3394,6 +3491,71 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateTrigger { + or_replace, + is_constraint, + name, + period, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + condition, + include_each, + exec_body, + characteristics, + } => { + write!( + f, + "CREATE {or_replace}{is_constraint}TRIGGER {name} {period}", + 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 "))?; + } + write!(f, " ON {table_name}")?; + + if let Some(referenced_table_name) = referenced_table_name { + write!(f, " FROM {referenced_table_name}")?; + } + + if let Some(characteristics) = characteristics { + write!(f, " {characteristics}")?; + } + + if !referencing.is_empty() { + write!(f, " REFERENCING {}", display_separated(referencing, " "))?; + } + + if *include_each { + write!(f, " FOR EACH {trigger_object}")?; + } else { + write!(f, " FOR {trigger_object}")?; + } + if let Some(condition) = condition { + write!(f, " WHEN {condition}")?; + } + write!(f, " EXECUTE {exec_body}") + } + Statement::DropTrigger { + if_exists, + trigger_name, + table_name, + option, + } => { + write!(f, "DROP TRIGGER")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {trigger_name} ON {table_name}")?; + if let Some(option) = option { + write!(f, " {option}")?; + } + Ok(()) + } Statement::CreateProcedure { name, or_alter, @@ -6026,16 +6188,16 @@ impl fmt::Display for DropFunctionOption { } } -/// Function describe in DROP FUNCTION. +/// Generic function description for DROP FUNCTION and CREATE TRIGGER. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct DropFunctionDesc { +pub struct FunctionDesc { pub name: ObjectName, pub args: Option>, } -impl fmt::Display for DropFunctionDesc { +impl fmt::Display for FunctionDesc { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; if let Some(args) = &self.args { diff --git a/src/ast/trigger.rs b/src/ast/trigger.rs new file mode 100644 index 000000000..a0913db94 --- /dev/null +++ b/src/ast/trigger.rs @@ -0,0 +1,158 @@ +// Licensed 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. + +//! SQL Abstract Syntax Tree (AST) for triggers. +use super::*; + +/// This specifies whether the trigger function should be fired once for every row affected by the trigger event, or just once per SQL statement. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TriggerObject { + Row, + Statement, +} + +impl fmt::Display for TriggerObject { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TriggerObject::Row => write!(f, "ROW"), + TriggerObject::Statement => write!(f, "STATEMENT"), + } + } +} + +/// This clause indicates whether the following relation name is for the before-image transition relation or the after-image transition relation +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TriggerReferencingType { + OldTable, + NewTable, +} + +impl fmt::Display for TriggerReferencingType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TriggerReferencingType::OldTable => write!(f, "OLD TABLE"), + TriggerReferencingType::NewTable => write!(f, "NEW TABLE"), + } + } +} + +/// This keyword immediately precedes the declaration of one or two relation names that provide access to the transition relations of the triggering 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 TriggerReferencing { + pub refer_type: TriggerReferencingType, + pub is_as: bool, + pub transition_relation_name: ObjectName, +} + +impl fmt::Display for TriggerReferencing { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{refer_type}{is_as} {relation_name}", + refer_type = self.refer_type, + is_as = if self.is_as { " AS" } else { "" }, + relation_name = self.transition_relation_name + ) + } +} + +/// Used to describe trigger events +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TriggerEvent { + Insert, + Update(Vec), + Delete, + Truncate, +} + +impl fmt::Display for TriggerEvent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TriggerEvent::Insert => write!(f, "INSERT"), + TriggerEvent::Update(columns) => { + write!(f, "UPDATE")?; + if !columns.is_empty() { + write!(f, " OF")?; + write!(f, " {}", display_comma_separated(columns))?; + } + Ok(()) + } + TriggerEvent::Delete => write!(f, "DELETE"), + TriggerEvent::Truncate => write!(f, "TRUNCATE"), + } + } +} + +/// Trigger period +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TriggerPeriod { + After, + Before, + InsteadOf, +} + +impl fmt::Display for TriggerPeriod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TriggerPeriod::After => write!(f, "AFTER"), + TriggerPeriod::Before => write!(f, "BEFORE"), + TriggerPeriod::InsteadOf => write!(f, "INSTEAD OF"), + } + } +} + +/// Types of trigger body execution body. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TriggerExecBodyType { + Function, + Procedure, +} + +impl fmt::Display for TriggerExecBodyType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TriggerExecBodyType::Function => write!(f, "FUNCTION"), + TriggerExecBodyType::Procedure => write!(f, "PROCEDURE"), + } + } +} +/// This keyword immediately precedes the declaration of one or two relation names that provide access to the transition relations of the triggering 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 TriggerExecBody { + pub exec_type: TriggerExecBodyType, + pub func_desc: FunctionDesc, +} + +impl fmt::Display for TriggerExecBody { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{exec_type} {func_desc}", + exec_type = self.exec_type, + func_desc = self.func_desc + ) + } +} diff --git a/src/keywords.rs b/src/keywords.rs index c175da874..0c9d3dd6c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -20,7 +20,7 @@ //! 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}; @@ -108,6 +108,7 @@ define_keywords!( AVRO, BACKWARD, BASE64, + BEFORE, BEGIN, BEGIN_FRAME, BEGIN_PARTITION, @@ -378,6 +379,7 @@ define_keywords!( INSENSITIVE, INSERT, INSTALL, + INSTEAD, INT, INT128, INT16, @@ -683,6 +685,7 @@ define_keywords!( STABLE, STAGE, START, + STATEMENT, STATIC, STATISTICS, STATUS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 60a7b4d0b..5706df56c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3368,6 +3368,25 @@ impl<'a> Parser<'a> { Ok(values) } + /// Parse a keyword-separated list of 1+ items accepted by `F` + pub fn parse_keyword_separated( + &mut self, + keyword: Keyword, + mut f: F, + ) -> Result, ParserError> + where + F: FnMut(&mut Parser<'a>) -> Result, + { + let mut values = vec![]; + loop { + values.push(f(self)?); + if !self.parse_keyword(keyword) { + break; + } + } + Ok(values) + } + pub fn parse_parenthesized(&mut self, mut f: F) -> Result where F: FnMut(&mut Parser<'a>) -> Result, @@ -3471,6 +3490,10 @@ impl<'a> Parser<'a> { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { self.parse_create_function(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]) { + self.parse_create_trigger(or_replace, true) } else if self.parse_keyword(Keyword::MACRO) { self.parse_create_macro(or_replace, temporary) } else if self.parse_keyword(Keyword::SECRET) { @@ -4061,6 +4084,180 @@ impl<'a> Parser<'a> { }) } + /// Parse statements of the DropTrigger type such as: + /// + /// ```sql + /// 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) { + self.prev_token(); + return self.expected("an object type after DROP", self.peek_token()); + } + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let trigger_name = self.parse_object_name(false)?; + self.expect_keyword(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + let option = self + .parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) + .map(|keyword| match keyword { + Keyword::CASCADE => ReferentialAction::Cascade, + Keyword::RESTRICT => ReferentialAction::Restrict, + _ => unreachable!(), + }); + Ok(Statement::DropTrigger { + if_exists, + trigger_name, + table_name, + option, + }) + } + + pub fn parse_create_trigger( + &mut self, + or_replace: bool, + is_constraint: bool, + ) -> Result { + if !dialect_of!(self is PostgreSqlDialect | GenericDialect) { + self.prev_token(); + return self.expected("an object type after CREATE", self.peek_token()); + } + + let name = self.parse_object_name(false)?; + let period = self.parse_trigger_period()?; + + let events = self.parse_keyword_separated(Keyword::OR, Parser::parse_trigger_event)?; + self.expect_keyword(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + + let referenced_table_name = if self.parse_keyword(Keyword::FROM) { + self.parse_object_name(true).ok() + } else { + None + }; + + let characteristics = self.parse_constraint_characteristics()?; + + let mut referencing = vec![]; + if self.parse_keyword(Keyword::REFERENCING) { + while let Some(refer) = self.parse_trigger_referencing()? { + referencing.push(refer); + } + } + + self.expect_keyword(Keyword::FOR)?; + let include_each = self.parse_keyword(Keyword::EACH); + let trigger_object = + match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { + Keyword::ROW => TriggerObject::Row, + Keyword::STATEMENT => TriggerObject::Statement, + _ => unreachable!(), + }; + + let condition = self + .parse_keyword(Keyword::WHEN) + .then(|| self.parse_expr()) + .transpose()?; + + self.expect_keyword(Keyword::EXECUTE)?; + + let exec_body = self.parse_trigger_exec_body()?; + + Ok(Statement::CreateTrigger { + or_replace, + is_constraint, + name, + period, + events, + table_name, + referenced_table_name, + referencing, + trigger_object, + include_each, + condition, + exec_body, + characteristics, + }) + } + + pub fn parse_trigger_period(&mut self) -> Result { + Ok( + match self.expect_one_of_keywords(&[ + Keyword::BEFORE, + Keyword::AFTER, + Keyword::INSTEAD, + ])? { + Keyword::BEFORE => TriggerPeriod::Before, + Keyword::AFTER => TriggerPeriod::After, + Keyword::INSTEAD => self + .expect_keyword(Keyword::OF) + .map(|_| TriggerPeriod::InsteadOf)?, + _ => unreachable!(), + }, + ) + } + + pub fn parse_trigger_event(&mut self) -> Result { + Ok( + match self.expect_one_of_keywords(&[ + Keyword::INSERT, + Keyword::UPDATE, + Keyword::DELETE, + Keyword::TRUNCATE, + ])? { + Keyword::INSERT => TriggerEvent::Insert, + Keyword::UPDATE => { + if self.parse_keyword(Keyword::OF) { + let cols = self.parse_comma_separated(|ident| { + Parser::parse_identifier(ident, false) + })?; + TriggerEvent::Update(cols) + } else { + TriggerEvent::Update(vec![]) + } + } + Keyword::DELETE => TriggerEvent::Delete, + Keyword::TRUNCATE => TriggerEvent::Truncate, + _ => unreachable!(), + }, + ) + } + + pub fn parse_trigger_referencing(&mut self) -> Result, ParserError> { + let refer_type = match self.parse_one_of_keywords(&[Keyword::OLD, Keyword::NEW]) { + Some(Keyword::OLD) if self.parse_keyword(Keyword::TABLE) => { + TriggerReferencingType::OldTable + } + Some(Keyword::NEW) if self.parse_keyword(Keyword::TABLE) => { + TriggerReferencingType::NewTable + } + _ => { + return Ok(None); + } + }; + + let is_as = self.parse_keyword(Keyword::AS); + let transition_relation_name = self.parse_object_name(false)?; + Ok(Some(TriggerReferencing { + refer_type, + is_as, + transition_relation_name, + })) + } + + pub fn parse_trigger_exec_body(&mut self) -> Result { + Ok(TriggerExecBody { + exec_type: match self + .expect_one_of_keywords(&[Keyword::FUNCTION, Keyword::PROCEDURE])? + { + Keyword::FUNCTION => TriggerExecBodyType::Function, + Keyword::PROCEDURE => TriggerExecBodyType::Procedure, + _ => unreachable!(), + }, + func_desc: self.parse_function_desc()?, + }) + } + pub fn parse_create_macro( &mut self, or_replace: bool, @@ -4509,9 +4706,11 @@ impl<'a> Parser<'a> { return self.parse_drop_procedure(); } else if self.parse_keyword(Keyword::SECRET) { return self.parse_drop_secret(temporary, persistent); + } else if self.parse_keyword(Keyword::TRIGGER) { + return self.parse_drop_trigger(); } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION, PROCEDURE, STAGE or SEQUENCE after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION, PROCEDURE, STAGE, TRIGGER, SECRET or SEQUENCE after DROP", self.peek_token(), ); }; @@ -4550,7 +4749,7 @@ impl<'a> Parser<'a> { /// ``` fn parse_drop_function(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let func_desc = self.parse_comma_separated(Parser::parse_drop_function_desc)?; + let func_desc = self.parse_comma_separated(Parser::parse_function_desc)?; let option = match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade), Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict), @@ -4569,7 +4768,7 @@ impl<'a> Parser<'a> { /// ``` fn parse_drop_procedure(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let proc_desc = self.parse_comma_separated(Parser::parse_drop_function_desc)?; + let proc_desc = self.parse_comma_separated(Parser::parse_function_desc)?; let option = match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade), Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict), @@ -4583,7 +4782,7 @@ impl<'a> Parser<'a> { }) } - fn parse_drop_function_desc(&mut self) -> Result { + fn parse_function_desc(&mut self) -> Result { let name = self.parse_object_name(false)?; let args = if self.consume_token(&Token::LParen) { @@ -4598,7 +4797,7 @@ impl<'a> Parser<'a> { None }; - Ok(DropFunctionDesc { name, args }) + Ok(FunctionDesc { name, args }) } /// See [DuckDB Docs](https://duckdb.org/docs/sql/statements/create_secret.html) for more details. @@ -5882,11 +6081,7 @@ impl<'a> Parser<'a> { pub fn parse_constraint_characteristics( &mut self, ) -> Result, ParserError> { - let mut cc = ConstraintCharacteristics { - deferrable: None, - initially: None, - enforced: None, - }; + let mut cc = ConstraintCharacteristics::default(); loop { if cc.deferrable.is_none() && self.parse_keywords(&[Keyword::NOT, Keyword::DEFERRABLE]) @@ -7285,6 +7480,7 @@ impl<'a> Parser<'a> { let field_defs = self.parse_click_house_tuple_def()?; Ok(DataType::Tuple(field_defs)) } + Keyword::TRIGGER => Ok(DataType::Trigger), _ => { self.prev_token(); let type_name = self.parse_object_name(false)?; diff --git a/src/test_utils.rs b/src/test_utils.rs index d9100d351..5c05ec996 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -124,6 +124,7 @@ impl TestedDialects { } let only_statement = statements.pop().unwrap(); + if !canonical.is_empty() { assert_eq!(canonical, only_statement.to_string()) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index f370748d2..2f9fe86c9 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3623,7 +3623,7 @@ fn parse_drop_function() { pg().verified_stmt(sql), Statement::DropFunction { if_exists: true, - func_desc: vec![DropFunctionDesc { + func_desc: vec![FunctionDesc { name: ObjectName(vec![Ident { value: "test_func".to_string(), quote_style: None @@ -3639,7 +3639,7 @@ fn parse_drop_function() { pg().verified_stmt(sql), Statement::DropFunction { if_exists: true, - func_desc: vec![DropFunctionDesc { + func_desc: vec![FunctionDesc { name: ObjectName(vec![Ident { value: "test_func".to_string(), quote_style: None @@ -3664,7 +3664,7 @@ fn parse_drop_function() { Statement::DropFunction { if_exists: true, func_desc: vec![ - DropFunctionDesc { + FunctionDesc { name: ObjectName(vec![Ident { value: "test_func1".to_string(), quote_style: None @@ -3682,7 +3682,7 @@ fn parse_drop_function() { } ]), }, - DropFunctionDesc { + FunctionDesc { name: ObjectName(vec![Ident { value: "test_func2".to_string(), quote_style: None @@ -3713,7 +3713,7 @@ fn parse_drop_procedure() { pg().verified_stmt(sql), Statement::DropProcedure { if_exists: true, - proc_desc: vec![DropFunctionDesc { + proc_desc: vec![FunctionDesc { name: ObjectName(vec![Ident { value: "test_proc".to_string(), quote_style: None @@ -3729,7 +3729,7 @@ fn parse_drop_procedure() { pg().verified_stmt(sql), Statement::DropProcedure { if_exists: true, - proc_desc: vec![DropFunctionDesc { + proc_desc: vec![FunctionDesc { name: ObjectName(vec![Ident { value: "test_proc".to_string(), quote_style: None @@ -3754,7 +3754,7 @@ fn parse_drop_procedure() { Statement::DropProcedure { if_exists: true, proc_desc: vec![ - DropFunctionDesc { + FunctionDesc { name: ObjectName(vec![Ident { value: "test_proc1".to_string(), quote_style: None @@ -3772,7 +3772,7 @@ fn parse_drop_procedure() { } ]), }, - DropFunctionDesc { + FunctionDesc { name: ObjectName(vec![Ident { value: "test_proc2".to_string(), quote_style: None @@ -4455,6 +4455,478 @@ fn test_escaped_string_literal() { } } +#[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"; + let expected = Statement::CreateTrigger { + or_replace: false, + is_constraint: false, + name: ObjectName(vec![Ident::new("check_insert")]), + period: TriggerPeriod::Before, + events: vec![TriggerEvent::Insert], + table_name: ObjectName(vec![Ident::new("accounts")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Row, + include_each: true, + condition: None, + exec_body: TriggerExecBody { + exec_type: TriggerExecBodyType::Function, + func_desc: FunctionDesc { + name: ObjectName(vec![Ident::new("check_account_insert")]), + args: None, + }, + }, + characteristics: None, + }; + + assert_eq!(pg().verified_stmt(sql), expected); +} + +#[test] +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_replace: false, + is_constraint: false, + name: ObjectName(vec![Ident::new("check_update")]), + period: TriggerPeriod::After, + events: vec![TriggerEvent::Update(vec![])], + table_name: ObjectName(vec![Ident::new("accounts")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Row, + include_each: true, + condition: Some(Expr::Nested(Box::new(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("NEW"), + Ident::new("balance"), + ])), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(number("10000"))), + }))), + exec_body: TriggerExecBody { + exec_type: TriggerExecBodyType::Function, + func_desc: FunctionDesc { + name: ObjectName(vec![Ident::new("check_account_update")]), + args: None, + }, + }, + characteristics: None, + }; + + assert_eq!(pg().verified_stmt(sql), expected); +} + +#[test] +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_replace: false, + is_constraint: false, + name: ObjectName(vec![Ident::new("check_delete")]), + period: TriggerPeriod::InsteadOf, + events: vec![TriggerEvent::Delete], + table_name: ObjectName(vec![Ident::new("accounts")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Row, + include_each: true, + condition: None, + exec_body: TriggerExecBody { + exec_type: TriggerExecBodyType::Function, + func_desc: FunctionDesc { + name: ObjectName(vec![Ident::new("check_account_deletes")]), + args: None, + }, + }, + characteristics: None, + }; + + assert_eq!(pg().verified_stmt(sql), expected); +} + +#[test] +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_replace: false, + is_constraint: true, + name: ObjectName(vec![Ident::new("check_multiple_events")]), + period: TriggerPeriod::Before, + events: vec![ + TriggerEvent::Insert, + TriggerEvent::Update(vec![]), + TriggerEvent::Delete, + ], + table_name: ObjectName(vec![Ident::new("accounts")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Row, + include_each: true, + condition: None, + exec_body: TriggerExecBody { + exec_type: TriggerExecBodyType::Function, + func_desc: FunctionDesc { + name: ObjectName(vec![Ident::new("check_account_changes")]), + args: None, + }, + }, + characteristics: Some(ConstraintCharacteristics { + deferrable: Some(true), + initially: Some(DeferrableInitial::Deferred), + enforced: None, + }), + }; + + assert_eq!(pg().verified_stmt(sql), expected); +} + +#[test] +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_replace: false, + is_constraint: false, + name: ObjectName(vec![Ident::new("check_referencing")]), + period: TriggerPeriod::Before, + events: vec![TriggerEvent::Insert], + table_name: ObjectName(vec![Ident::new("accounts")]), + referenced_table_name: None, + referencing: vec![ + TriggerReferencing { + refer_type: TriggerReferencingType::NewTable, + is_as: true, + transition_relation_name: ObjectName(vec![Ident::new("new_accounts")]), + }, + TriggerReferencing { + refer_type: TriggerReferencingType::OldTable, + is_as: true, + transition_relation_name: ObjectName(vec![Ident::new("old_accounts")]), + }, + ], + trigger_object: TriggerObject::Row, + include_each: true, + condition: None, + exec_body: TriggerExecBody { + exec_type: TriggerExecBodyType::Function, + func_desc: FunctionDesc { + name: ObjectName(vec![Ident::new("check_account_referencing")]), + args: None, + }, + }, + characteristics: None, + }; + + assert_eq!(pg().verified_stmt(sql), expected); +} + +#[test] +/// While in the parse_create_trigger test we test the full syntax of the CREATE TRIGGER statement, +/// here we test the invalid cases of the CREATE TRIGGER statement which should cause an appropriate +/// error to be returned. +fn parse_create_trigger_invalid_cases() { + // Test invalid cases for the CREATE TRIGGER statement + let invalid_cases = vec![ + ( + "CREATE TRIGGER check_update BEFORE UPDATE ON accounts FUNCTION check_account_update", + "Expected: FOR, found: FUNCTION" + ), + ( + "CREATE TRIGGER check_update TOMORROW UPDATE ON accounts EXECUTE FUNCTION check_account_update", + "Expected: one of BEFORE or AFTER or INSTEAD, found: TOMORROW" + ), + ( + "CREATE TRIGGER check_update BEFORE SAVE ON accounts EXECUTE FUNCTION check_account_update", + "Expected: one of INSERT or UPDATE or DELETE or TRUNCATE, found: SAVE" + ) + ]; + + for (sql, expected_error) in invalid_cases { + let res = pg().parse_sql_statements(sql); + assert_eq!( + format!("sql parser error: {expected_error}"), + res.unwrap_err().to_string() + ); + } +} + +#[test] +fn parse_drop_trigger() { + for if_exists in [true, false] { + for option in [ + None, + Some(ReferentialAction::Cascade), + Some(ReferentialAction::Restrict), + ] { + let sql = &format!( + "DROP TRIGGER{} check_update ON table_name{}", + if if_exists { " IF EXISTS" } else { "" }, + option + .map(|o| format!(" {}", o)) + .unwrap_or_else(|| "".to_string()) + ); + assert_eq!( + pg().verified_stmt(sql), + Statement::DropTrigger { + if_exists, + trigger_name: ObjectName(vec![Ident::new("check_update")]), + table_name: ObjectName(vec![Ident::new("table_name")]), + option + } + ); + } + } +} + +#[test] +fn parse_drop_trigger_invalid_cases() { + // Test invalid cases for the DROP TRIGGER statement + let invalid_cases = vec![ + ( + "DROP TRIGGER check_update ON table_name CASCADE RESTRICT", + "Expected: end of statement, found: RESTRICT", + ), + ( + "DROP TRIGGER check_update ON table_name CASCADE CASCADE", + "Expected: end of statement, found: CASCADE", + ), + ( + "DROP TRIGGER check_update ON table_name CASCADE CASCADE CASCADE", + "Expected: end of statement, found: CASCADE", + ), + ]; + + for (sql, expected_error) in invalid_cases { + let res = pg().parse_sql_statements(sql); + assert_eq!( + format!("sql parser error: {expected_error}"), + res.unwrap_err().to_string() + ); + } +} + +#[test] +fn parse_trigger_related_functions() { + // First we define all parts of the trigger definition, + // including the table creation, the function creation, the trigger creation and the trigger drop. + // The following example is taken from the PostgreSQL documentation + + let sql_table_creation = r#" + CREATE TABLE emp ( + empname text, + salary integer, + last_date timestamp, + last_user text + ); + "#; + + let sql_create_function = r#" + CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$ + BEGIN + -- Check that empname and salary are given + IF NEW.empname IS NULL THEN + RAISE EXCEPTION 'empname cannot be null'; + END IF; + IF NEW.salary IS NULL THEN + RAISE EXCEPTION '% cannot have null salary', NEW.empname; + END IF; + + -- Who works for us when they must pay for it? + IF NEW.salary < 0 THEN + RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; + END IF; + + -- Remember who changed the payroll when + NEW.last_date := current_timestamp; + NEW.last_user := current_user; + RETURN NEW; + END; + $emp_stamp$ LANGUAGE plpgsql; + "#; + + let sql_create_trigger = r#" + CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp + FOR EACH ROW EXECUTE FUNCTION emp_stamp(); + "#; + + let sql_drop_trigger = r#" + DROP TRIGGER emp_stamp ON emp; + "#; + + // 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 + )) + .unwrap(); + + assert_eq!(statements.len(), 4); + let drop_trigger = statements.pop().unwrap(); + let create_trigger = statements.pop().unwrap(); + let create_function = statements.pop().unwrap(); + let create_table = statements.pop().unwrap(); + + // Check the first statement + let create_table = match create_table { + Statement::CreateTable(create_table) => create_table, + _ => panic!("Expected CreateTable statement"), + }; + + assert_eq!( + create_table, + CreateTable { + or_replace: false, + temporary: false, + external: false, + global: None, + if_not_exists: false, + transient: false, + volatile: false, + name: ObjectName(vec![Ident::new("emp")]), + columns: vec![ + ColumnDef { + name: "empname".into(), + data_type: DataType::Text, + collation: None, + options: vec![], + }, + ColumnDef { + name: "salary".into(), + data_type: DataType::Integer(None), + collation: None, + options: vec![], + }, + ColumnDef { + name: "last_date".into(), + data_type: DataType::Timestamp(None, TimezoneInfo::None), + collation: None, + options: vec![], + }, + ColumnDef { + name: "last_user".into(), + data_type: DataType::Text, + collation: None, + options: vec![], + }, + ], + constraints: vec![], + hive_distribution: HiveDistributionStyle::NONE, + hive_formats: Some(HiveFormat { + row_format: None, + serde_properties: None, + 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, + order_by: None, + partition_by: None, + cluster_by: None, + options: None, + strict: false, + copy_grants: false, + enable_schema_evolution: None, + change_tracking: None, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + default_ddl_collation: None, + with_aggregation_policy: None, + with_row_access_policy: None, + with_tags: None, + } + ); + + // Check the second statement + + assert_eq!( + create_function, + Statement::CreateFunction { + or_replace: false, + temporary: false, + if_not_exists: false, + name: ObjectName(vec![Ident::new("emp_stamp")]), + args: None, + return_type: Some(DataType::Trigger), + function_body: Some( + CreateFunctionBody::AsBeforeOptions( + Expr::Value( + Value::DollarQuotedString( + DollarQuotedString { + value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n \n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n \n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(), + tag: Some( + "emp_stamp".to_owned(), + ), + }, + ), + ), + ), + ), + behavior: None, + called_on_null: None, + parallel: None, + using: None, + language: Some(Ident::new("plpgsql")), + determinism_specifier: None, + options: None, + remote_connection: None + } + ); + + // Check the third statement + + assert_eq!( + create_trigger, + Statement::CreateTrigger { + or_replace: false, + is_constraint: false, + name: ObjectName(vec![Ident::new("emp_stamp")]), + period: TriggerPeriod::Before, + events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![])], + table_name: ObjectName(vec![Ident::new("emp")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Row, + include_each: true, + condition: None, + exec_body: TriggerExecBody { + exec_type: TriggerExecBodyType::Function, + func_desc: FunctionDesc { + name: ObjectName(vec![Ident::new("emp_stamp")]), + args: None, + } + }, + characteristics: None + } + ); + + // Check the fourth statement + assert_eq!( + drop_trigger, + Statement::DropTrigger { + if_exists: false, + trigger_name: ObjectName(vec![Ident::new("emp_stamp")]), + table_name: ObjectName(vec![Ident::new("emp")]), + option: None + } + ); +} + #[test] fn test_unicode_string_literal() { let pairs = [ From c2f46ae07b8cdcfd72f5edef0274f029b1500de6 Mon Sep 17 00:00:00 2001 From: Seve Martinez <20816697+seve-martinez@users.noreply.github.com> Date: Wed, 14 Aug 2024 06:11:40 -0700 Subject: [PATCH 520/806] adding support for scale in CEIL and FLOOR functions (#1377) --- src/ast/mod.rs | 48 +++++++++++++++++++++-------- src/parser/mod.rs | 20 +++++++++--- tests/sqlparser_common.rs | 64 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 113 insertions(+), 19 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ae0522ccc..e3e9a5371 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -500,6 +500,24 @@ pub enum ExtractSyntax { Comma, } +/// The syntax used in a CEIL or FLOOR expression. +/// +/// The `CEIL/FLOOR( TO ` + AngleBrackets, +} + /// Timestamp and Time data types information about TimeZone formatting. /// /// This is more related to a display information than real differences between each variant. To diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e3e9a5371..c4533ef57 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -28,7 +28,8 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; pub use self::data_type::{ - ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, + ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, + StructBracketKind, TimezoneInfo, }; pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue}; pub use self::ddl::{ diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b6ec150f5..900ad9081 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2266,6 +2266,23 @@ impl<'a> Parser<'a> { )) } + /// Duckdb Struct Data Type + fn parse_duckdb_struct_type_def(&mut self) -> Result, ParserError> { + self.expect_keyword(Keyword::STRUCT)?; + self.expect_token(&Token::LParen)?; + let struct_body = self.parse_comma_separated(|parser| { + let field_name = parser.parse_identifier(false)?; + let field_type = parser.parse_data_type()?; + + Ok(StructField { + field_name: Some(field_name), + field_type, + }) + }); + self.expect_token(&Token::RParen)?; + struct_body + } + /// Parse a field definition in a [struct] or [tuple]. /// Syntax: /// @@ -7495,12 +7512,20 @@ impl<'a> Parser<'a> { )))) } } + Keyword::STRUCT if dialect_of!(self is DuckDbDialect) => { + self.prev_token(); + let field_defs = self.parse_duckdb_struct_type_def()?; + Ok(DataType::Struct(field_defs, StructBracketKind::Parentheses)) + } Keyword::STRUCT if dialect_of!(self is BigQueryDialect | GenericDialect) => { self.prev_token(); let (field_defs, _trailing_bracket) = self.parse_struct_type_def(Self::parse_struct_field_def)?; trailing_bracket = _trailing_bracket; - Ok(DataType::Struct(field_defs)) + Ok(DataType::Struct( + field_defs, + StructBracketKind::AngleBrackets, + )) } Keyword::UNION if dialect_of!(self is DuckDbDialect | GenericDialect) => { self.prev_token(); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 134c8ddad..57cf9d7fd 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -489,28 +489,34 @@ fn parse_nested_data_types() { vec![ ColumnDef { name: Ident::new("x"), - data_type: DataType::Struct(vec![ - StructField { - field_name: Some("a".into()), - field_type: DataType::Array(ArrayElemTypeDef::AngleBracket( - Box::new(DataType::Int64,) - )) - }, - StructField { - field_name: Some("b".into()), - field_type: DataType::Bytes(Some(42)) - }, - ]), + data_type: DataType::Struct( + vec![ + StructField { + field_name: Some("a".into()), + field_type: DataType::Array(ArrayElemTypeDef::AngleBracket( + Box::new(DataType::Int64,) + )) + }, + StructField { + field_name: Some("b".into()), + field_type: DataType::Bytes(Some(42)) + }, + ], + StructBracketKind::AngleBrackets + ), collation: None, options: vec![], }, ColumnDef { name: Ident::new("y"), data_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( - DataType::Struct(vec![StructField { - field_name: None, - field_type: DataType::Int64, - }]), + DataType::Struct( + vec![StructField { + field_name: None, + field_type: DataType::Int64, + }], + StructBracketKind::AngleBrackets + ), ))), collation: None, options: vec![], @@ -708,10 +714,13 @@ fn parse_typed_struct_syntax_bigquery() { }, StructField { field_name: Some("str".into()), - field_type: DataType::Struct(vec![StructField { - field_name: None, - field_type: DataType::Bool - }]) + field_type: DataType::Struct( + vec![StructField { + field_name: None, + field_type: DataType::Bool + }], + StructBracketKind::AngleBrackets + ) }, ] }, @@ -730,12 +739,15 @@ fn parse_typed_struct_syntax_bigquery() { fields: vec![ StructField { field_name: Some("x".into()), - field_type: DataType::Struct(Default::default()) + field_type: DataType::Struct( + Default::default(), + StructBracketKind::AngleBrackets + ) }, StructField { field_name: Some("y".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( - DataType::Struct(Default::default()) + DataType::Struct(Default::default(), StructBracketKind::AngleBrackets) ))) }, ] @@ -1013,10 +1025,13 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }, StructField { field_name: Some("str".into()), - field_type: DataType::Struct(vec![StructField { - field_name: None, - field_type: DataType::Bool - }]) + field_type: DataType::Struct( + vec![StructField { + field_name: None, + field_type: DataType::Bool + }], + StructBracketKind::AngleBrackets + ) }, ] }, @@ -1035,12 +1050,15 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { fields: vec![ StructField { field_name: Some("x".into()), - field_type: DataType::Struct(Default::default()) + field_type: DataType::Struct( + Default::default(), + StructBracketKind::AngleBrackets + ) }, StructField { field_name: Some("y".into()), field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( - DataType::Struct(Default::default()) + DataType::Struct(Default::default(), StructBracketKind::AngleBrackets) ))) }, ] diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 0e61b86c9..6e6c4e230 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -32,6 +32,118 @@ fn duckdb_and_generic() -> TestedDialects { } } +#[test] +fn test_struct() { + // s STRUCT(v VARCHAR, i INTEGER) + let struct_type1 = DataType::Struct( + vec![ + StructField { + field_name: Some(Ident::new("v")), + field_type: DataType::Varchar(None), + }, + StructField { + field_name: Some(Ident::new("i")), + field_type: DataType::Integer(None), + }, + ], + StructBracketKind::Parentheses, + ); + + // basic struct + let statement = duckdb().verified_stmt(r#"CREATE TABLE t1 (s STRUCT(v VARCHAR, i INTEGER))"#); + assert_eq!( + column_defs(statement), + vec![ColumnDef { + name: "s".into(), + data_type: struct_type1.clone(), + collation: None, + options: vec![], + }] + ); + + // struct array + let statement = duckdb().verified_stmt(r#"CREATE TABLE t1 (s STRUCT(v VARCHAR, i INTEGER)[])"#); + assert_eq!( + column_defs(statement), + vec![ColumnDef { + name: "s".into(), + data_type: DataType::Array(ArrayElemTypeDef::SquareBracket( + Box::new(struct_type1), + None + )), + collation: None, + options: vec![], + }] + ); + + // s STRUCT(v VARCHAR, s STRUCT(a1 INTEGER, a2 VARCHAR)) + let struct_type2 = DataType::Struct( + vec![ + StructField { + field_name: Some(Ident::new("v")), + field_type: DataType::Varchar(None), + }, + StructField { + field_name: Some(Ident::new("s")), + field_type: DataType::Struct( + vec![ + StructField { + field_name: Some(Ident::new("a1")), + field_type: DataType::Integer(None), + }, + StructField { + field_name: Some(Ident::new("a2")), + field_type: DataType::Varchar(None), + }, + ], + StructBracketKind::Parentheses, + ), + }, + ], + StructBracketKind::Parentheses, + ); + + // nested struct + let statement = duckdb().verified_stmt( + r#"CREATE TABLE t1 (s STRUCT(v VARCHAR, s STRUCT(a1 INTEGER, a2 VARCHAR))[])"#, + ); + + assert_eq!( + column_defs(statement), + vec![ColumnDef { + name: "s".into(), + data_type: DataType::Array(ArrayElemTypeDef::SquareBracket( + Box::new(struct_type2), + None + )), + collation: None, + options: vec![], + }] + ); + + // failing test (duckdb does not support bracket syntax) + let sql_list = vec![ + r#"CREATE TABLE t1 (s STRUCT(v VARCHAR, i INTEGER)))"#, + r#"CREATE TABLE t1 (s STRUCT(v VARCHAR, i INTEGER>)"#, + r#"CREATE TABLE t1 (s STRUCT)"#, + r#"CREATE TABLE t1 (s STRUCT v VARCHAR, i INTEGER )"#, + r#"CREATE TABLE t1 (s STRUCT VARCHAR, i INTEGER )"#, + r#"CREATE TABLE t1 (s STRUCT (VARCHAR, INTEGER))"#, + ]; + + for sql in sql_list { + duckdb().parse_sql_statements(sql).unwrap_err(); + } +} + +/// Returns the ColumnDefinitions from a CreateTable statement +fn column_defs(statement: Statement) -> Vec { + match statement { + Statement::CreateTable(CreateTable { columns, .. }) => columns, + _ => panic!("Expected CreateTable"), + } +} + #[test] fn test_select_wildcard_with_exclude() { let select = duckdb().verified_only_select("SELECT * EXCLUDE (col_a) FROM data"); From dd78084ca000cc96630560a88a94630ee32a559f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 16 Aug 2024 05:54:58 -0400 Subject: [PATCH 525/806] fix: only require `DESCRIBE TABLE` for Snowflake and ClickHouse dialect (#1386) --- src/dialect/clickhouse.rs | 4 ++++ src/dialect/mod.rs | 11 +++++++++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 21 +++++++++++++-------- tests/sqlparser_clickhouse.rs | 30 ++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 13 ------------- tests/sqlparser_hive.rs | 22 +++++++++++----------- tests/sqlparser_snowflake.rs | 30 ++++++++++++++++++++++++++++++ 8 files changed, 103 insertions(+), 32 deletions(-) diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 155b827e1..34940467a 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -33,4 +33,8 @@ impl Dialect for ClickHouseDialect { fn supports_select_wildcard_except(&self) -> bool { true } + + fn describe_requires_table_keyword(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 4c7572af0..df3022adc 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -485,9 +485,20 @@ pub trait Dialect: Debug + Any { } } + /// Returns the precedence when the precedence is otherwise unknown fn prec_unknown(&self) -> u8 { 0 } + + /// Returns true if this dialect requires the `TABLE` keyword after `DESCRIBE` + /// + /// Defaults to false. + /// + /// If true, the following statement is valid: `DESCRIBE TABLE my_table` + /// If false, the following statements are valid: `DESCRIBE my_table` and `DESCRIBE table` + fn describe_requires_table_keyword(&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 b22eb236f..89508c3bb 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -154,6 +154,10 @@ impl Dialect for SnowflakeDialect { _ => None, } } + + fn describe_requires_table_keyword(&self) -> bool { + true + } } /// Parse snowflake create table statement. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 900ad9081..2632d807a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8185,15 +8185,20 @@ impl<'a> Parser<'a> { format, }), _ => { - let mut hive_format = None; - match self.parse_one_of_keywords(&[Keyword::EXTENDED, Keyword::FORMATTED]) { - Some(Keyword::EXTENDED) => hive_format = Some(HiveDescribeFormat::Extended), - Some(Keyword::FORMATTED) => hive_format = Some(HiveDescribeFormat::Formatted), - _ => {} - } + let hive_format = + match self.parse_one_of_keywords(&[Keyword::EXTENDED, Keyword::FORMATTED]) { + Some(Keyword::EXTENDED) => Some(HiveDescribeFormat::Extended), + Some(Keyword::FORMATTED) => Some(HiveDescribeFormat::Formatted), + _ => None, + }; + + let has_table_keyword = if self.dialect.describe_requires_table_keyword() { + // only allow to use TABLE keyword for DESC|DESCRIBE statement + self.parse_keyword(Keyword::TABLE) + } else { + false + }; - // only allow to use TABLE keyword for DESC|DESCRIBE statement - let has_table_keyword = self.parse_keyword(Keyword::TABLE); let table_name = self.parse_object_name(false)?; Ok(Statement::ExplainTable { describe_alias, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 4c96febab..931899ff9 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1376,6 +1376,36 @@ fn parse_select_table_function_settings() { } } +#[test] +fn explain_describe() { + clickhouse().verified_stmt("DESCRIBE test.table"); + clickhouse().verified_stmt("DESCRIBE TABLE test.table"); +} + +#[test] +fn explain_desc() { + clickhouse().verified_stmt("DESC test.table"); + clickhouse().verified_stmt("DESC TABLE test.table"); +} + +#[test] +fn parse_explain_table() { + match clickhouse().verified_stmt("EXPLAIN TABLE test_identifier") { + Statement::ExplainTable { + describe_alias, + hive_format, + has_table_keyword, + table_name, + } => { + pretty_assertions::assert_eq!(describe_alias, DescribeAlias::Explain); + pretty_assertions::assert_eq!(hive_format, None); + pretty_assertions::assert_eq!(has_table_keyword, true); + pretty_assertions::assert_eq!("test_identifier", table_name.to_string()); + } + _ => panic!("Unexpected Statement, must be ExplainTable"), + } +} + fn clickhouse() -> TestedDialects { TestedDialects { dialects: vec![Box::new(ClickHouseDialect {})], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 896a043c4..517e978b4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4301,29 +4301,16 @@ fn parse_explain_table() { validate_explain("EXPLAIN test_identifier", DescribeAlias::Explain, false); validate_explain("DESCRIBE test_identifier", DescribeAlias::Describe, false); validate_explain("DESC test_identifier", DescribeAlias::Desc, false); - validate_explain( - "EXPLAIN TABLE test_identifier", - DescribeAlias::Explain, - true, - ); - validate_explain( - "DESCRIBE TABLE test_identifier", - DescribeAlias::Describe, - true, - ); - validate_explain("DESC TABLE test_identifier", DescribeAlias::Desc, true); } #[test] fn explain_describe() { verified_stmt("DESCRIBE test.table"); - verified_stmt("DESCRIBE TABLE test.table"); } #[test] fn explain_desc() { verified_stmt("DESC test.table"); - verified_stmt("DESC TABLE test.table"); } #[test] diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 5f0b9f575..157dad060 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -21,7 +21,7 @@ use sqlparser::ast::{ UnaryOperator, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; -use sqlparser::parser::{ParserError, ParserOptions}; +use sqlparser::parser::ParserError; use sqlparser::test_utils::*; #[test] @@ -35,18 +35,11 @@ fn parse_table_create() { hive().verified_stmt(serdeproperties); } -fn generic(options: Option) -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(GenericDialect {})], - options, - } -} - #[test] fn parse_describe() { - let describe = r#"DESCRIBE namespace.`table`"#; - hive().verified_stmt(describe); - generic(None).verified_stmt(describe); + hive_and_generic().verified_stmt(r#"DESCRIBE namespace.`table`"#); + hive_and_generic().verified_stmt(r#"DESCRIBE namespace.table"#); + hive_and_generic().verified_stmt(r#"DESCRIBE table"#); } #[test] @@ -414,3 +407,10 @@ fn hive() -> TestedDialects { options: None, } } + +fn hive_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(HiveDialect {}), Box::new(GenericDialect {})], + options: None, + } +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a331c7df9..a4f29c04f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2292,3 +2292,33 @@ fn test_parse_position() { snowflake().verified_query("SELECT position('an', 'banana', 1)"); snowflake().verified_query("SELECT n, h, POSITION(n IN h) FROM pos"); } + +#[test] +fn explain_describe() { + snowflake().verified_stmt("DESCRIBE test.table"); + snowflake().verified_stmt("DESCRIBE TABLE test.table"); +} + +#[test] +fn explain_desc() { + snowflake().verified_stmt("DESC test.table"); + snowflake().verified_stmt("DESC TABLE test.table"); +} + +#[test] +fn parse_explain_table() { + match snowflake().verified_stmt("EXPLAIN TABLE test_identifier") { + Statement::ExplainTable { + describe_alias, + hive_format, + has_table_keyword, + table_name, + } => { + assert_eq!(describe_alias, DescribeAlias::Explain); + assert_eq!(hive_format, None); + assert_eq!(has_table_keyword, true); + assert_eq!("test_identifier", table_name.to_string()); + } + _ => panic!("Unexpected Statement, must be ExplainTable"), + } +} From ee2e2634ed418fcf8a68e757d9e9c36ca839404a Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 16 Aug 2024 06:05:04 -0400 Subject: [PATCH 526/806] CHANGELOG for 0.50.0 (#1383) --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2d1321b..526374714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,44 @@ changes that break via addition as "Added". ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +## [0.50.0] 2024-08-15 +Again, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs 🙏. +Without them this project would not be possible. + +Reminder: are in the process of moving sqlparser to governed as part of the Apache +DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 + +# Fixed +* Clippy 1.80 warnings (#1357) - Thanks @lovasoa + +# Added +* Support `STRUCT` and list of structs for DuckDB dialect (#1372) - Thanks @jayzhan211 +* Support custom lexical precedence in PostgreSQL dialect (#1379) - Thanks @samuelcolvin +* Support `FREEZE|UNFREEZE PARTITION` syntax for ClickHouse (#1380) - Thanks @git-hulk +* Support scale in `CEIL` and `FLOOR` functions (#1377) - Thanks @seve-martinez +* Support `CREATE TRIGGER` and `DROP TRIGGER` statements (#1352) - Thanks @LucaCappelletti94 +* Support `EXTRACT` syntax for snowflake (#1374) - Thanks @seve-martinez +* Support `ATTACH` / `DETACH PARTITION` for ClickHouse (#1362) - Thanks @git-hulk +* Support Dialect level precedence, update Postgres Dialect to match Postgres (#1360) - Thanks @samuelcolvin +* Support parsing empty map literal syntax for DuckDB and Generic dialects (#1361) - Thanks @goldmedal +* Support `SETTINGS` clause for ClickHouse table-valued functions (#1358) - Thanks @Jesse-Bakker +* Support `OPTIMIZE TABLE` statement for ClickHouse (#1359) - Thanks @git-hulk +* Support `ON CLUSTER` in `ALTER TABLE` for ClickHouse (#1342) - Thanks @git-hulk +* Support `GLOBAL` keyword before the join operator (#1353) - Thanks @git-hulk +* Support postgres String Constants with Unicode Escapes (#1355) - Thanks @lovasoa +* Support position with normal function call syntax for Snowflake (#1341) - Thanks @jmhain +* Support `TABLE` keyword in `DESC|DESCRIBE|EXPLAIN TABLE` statement (#1351) - Thanks @git-hulk + +# Changed +* Only require `DESCRIBE TABLE` for Snowflake and ClickHouse dialect (#1386) - Thanks @ alamb +* Rename (unreleased) `get_next_precedence_full` to `get_next_precedence_default` (#1378) - Thanks @samuelcolvin +* Use local GitHub Action to replace setup-rust-action (#1371) - Thanks @git-hulk +* Simplify arrow_cast tests (#1367) - Thanks @alamb +* Update version of GitHub Actions (#1363) - Thanks @git-hulk +* Make `Parser::maybe_parse` pub (#1364) - Thanks @Jesse-Bakker +* Improve comments on 1Dialect` (#1366) - Thanks @alamb + + ## [0.49.0] 2024-07-23 As always, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs! From 11a6e6ff7bf64513bc81d849a0b86ea296b0dd62 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 16 Aug 2024 06:05:54 -0400 Subject: [PATCH 527/806] chore: Release sqlparser version 0.50.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4c510a8c6..30c40dd60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.49.0" +version = "0.50.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 19e694aa91d2211557cfc0fa76a408ec1c664896 Mon Sep 17 00:00:00 2001 From: hulk Date: Fri, 23 Aug 2024 00:33:44 +0800 Subject: [PATCH 528/806] Support `ADD PROJECTION` syntax for ClickHouse (#1390) --- src/ast/ddl.rs | 22 +++++- src/ast/mod.rs | 2 +- src/ast/query.rs | 54 ++++++++++--- src/keywords.rs | 1 + src/parser/mod.rs | 145 +++++++++++++++++++++------------- tests/sqlparser_clickhouse.rs | 72 +++++++++++++++++ 6 files changed, 231 insertions(+), 65 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 265ae7727..31e216a9f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -26,7 +26,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, - ObjectName, SequenceOptions, SqlOption, + ObjectName, ProjectionSelect, SequenceOptions, SqlOption, }; use crate::tokenizer::Token; @@ -48,6 +48,15 @@ pub enum AlterTableOperation { /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] column_position: Option, }, + /// `ADD PROJECTION [IF NOT EXISTS] name ( SELECT [GROUP BY] [ORDER BY])` + /// + /// Note: this is a ClickHouse-specific operation. + /// Please refer to [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#add-projection) + AddProjection { + if_not_exists: bool, + name: Ident, + select: ProjectionSelect, + }, /// `DISABLE ROW LEVEL SECURITY` /// /// Note: this is a PostgreSQL-specific operation. @@ -255,6 +264,17 @@ impl fmt::Display for AlterTableOperation { Ok(()) } + AlterTableOperation::AddProjection { + if_not_exists, + name, + select: query, + } => { + write!(f, "ADD PROJECTION")?; + if *if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " {} ({})", name, query) + } AlterTableOperation::AlterColumn { column_name, op } => { write!(f, "ALTER COLUMN {column_name} {op}") } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c4533ef57..4f9aac885 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -48,7 +48,7 @@ pub use self::query::{ InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, - OffsetRows, OrderBy, OrderByExpr, PivotValueSource, Query, RenameSelectItem, + OffsetRows, OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, diff --git a/src/ast/query.rs b/src/ast/query.rs index cda7430be..c52d01105 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -68,16 +68,7 @@ impl fmt::Display for Query { } write!(f, "{}", self.body)?; if let Some(ref order_by) = self.order_by { - write!(f, " ORDER BY")?; - if !order_by.exprs.is_empty() { - write!(f, " {}", display_comma_separated(&order_by.exprs))?; - } - if let Some(ref interpolate) = order_by.interpolate { - match &interpolate.exprs { - Some(exprs) => write!(f, " INTERPOLATE ({})", display_comma_separated(exprs))?, - None => write!(f, " INTERPOLATE")?, - } - } + write!(f, " {order_by}")?; } if let Some(ref limit) = self.limit { write!(f, " LIMIT {limit}")?; @@ -107,6 +98,33 @@ impl fmt::Display for Query { } } +/// Query syntax for ClickHouse ADD PROJECTION statement. +/// Its syntax is similar to SELECT statement, but it is used to add a new projection to a table. +/// Syntax is `SELECT [GROUP BY] [ORDER BY]` +/// +/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#add-projection) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ProjectionSelect { + pub projection: Vec, + pub order_by: Option, + pub group_by: Option, +} + +impl fmt::Display for ProjectionSelect { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SELECT {}", display_comma_separated(&self.projection))?; + if let Some(ref group_by) = self.group_by { + write!(f, " {group_by}")?; + } + if let Some(ref order_by) = self.order_by { + write!(f, " {order_by}")?; + } + Ok(()) + } +} + /// A node in a tree, representing a "query body" expression, roughly: /// `SELECT ... [ {UNION|EXCEPT|INTERSECT} SELECT ...]` #[allow(clippy::large_enum_variant)] @@ -1717,6 +1735,22 @@ pub struct OrderBy { pub interpolate: Option, } +impl fmt::Display for OrderBy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ORDER BY")?; + if !self.exprs.is_empty() { + write!(f, " {}", display_comma_separated(&self.exprs))?; + } + if let Some(ref interpolate) = self.interpolate { + match &interpolate.exprs { + Some(exprs) => write!(f, " INTERPOLATE ({})", display_comma_separated(exprs))?, + None => write!(f, " INTERPOLATE")?, + } + } + Ok(()) + } +} + /// An `ORDER BY` expression #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/keywords.rs b/src/keywords.rs index 538c2d380..acb913d57 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -576,6 +576,7 @@ define_keywords!( PRIVILEGES, PROCEDURE, PROGRAM, + PROJECTION, PURGE, QUALIFY, QUARTER, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2632d807a..1eff8f7d5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6424,10 +6424,38 @@ impl<'a> Parser<'a> { Ok(Partition::Partitions(partitions)) } + pub fn parse_projection_select(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + self.expect_keyword(Keyword::SELECT)?; + let projection = self.parse_projection()?; + let group_by = self.parse_optional_group_by()?; + let order_by = self.parse_optional_order_by()?; + self.expect_token(&Token::RParen)?; + Ok(ProjectionSelect { + projection, + group_by, + order_by, + }) + } + pub fn parse_alter_table_add_projection(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_identifier(false)?; + let query = self.parse_projection_select()?; + Ok(AlterTableOperation::AddProjection { + if_not_exists, + name, + select: query, + }) + } + 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) + } else if dialect_of!(self is ClickHouseDialect|GenericDialect) + && self.parse_keyword(Keyword::PROJECTION) + { + return self.parse_alter_table_add_projection(); } else { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); @@ -7672,6 +7700,66 @@ impl<'a> Parser<'a> { } } + pub fn parse_optional_group_by(&mut self) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { + let expressions = if self.parse_keyword(Keyword::ALL) { + None + } else { + Some(self.parse_comma_separated(Parser::parse_group_by_expr)?) + }; + + let mut modifiers = vec![]; + if dialect_of!(self is ClickHouseDialect | GenericDialect) { + loop { + if !self.parse_keyword(Keyword::WITH) { + break; + } + let keyword = self.expect_one_of_keywords(&[ + Keyword::ROLLUP, + Keyword::CUBE, + Keyword::TOTALS, + ])?; + modifiers.push(match keyword { + Keyword::ROLLUP => GroupByWithModifier::Rollup, + Keyword::CUBE => GroupByWithModifier::Cube, + Keyword::TOTALS => GroupByWithModifier::Totals, + _ => { + return parser_err!( + "BUG: expected to match GroupBy modifier keyword", + self.peek_token().location + ) + } + }); + } + } + let group_by = match expressions { + None => GroupByExpr::All(modifiers), + Some(exprs) => GroupByExpr::Expressions(exprs, modifiers), + }; + Ok(Some(group_by)) + } else { + Ok(None) + } + } + + pub fn parse_optional_order_by(&mut self) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; + let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) { + self.parse_interpolations()? + } else { + None + }; + + Ok(Some(OrderBy { + exprs: order_by_exprs, + interpolate, + })) + } else { + Ok(None) + } + } + /// Parse a possibly qualified, possibly quoted identifier, e.g. /// `foo` or `myschema."table" /// @@ -8264,21 +8352,7 @@ impl<'a> Parser<'a> { } else { let body = self.parse_boxed_query_body(self.dialect.prec_unknown())?; - let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { - let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; - let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) { - self.parse_interpolations()? - } else { - None - }; - - Some(OrderBy { - exprs: order_by_exprs, - interpolate, - }) - } else { - None - }; + let order_by = self.parse_optional_order_by()?; let mut limit = None; let mut offset = None; @@ -8746,44 +8820,9 @@ impl<'a> Parser<'a> { None }; - let group_by = if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { - let expressions = if self.parse_keyword(Keyword::ALL) { - None - } else { - Some(self.parse_comma_separated(Parser::parse_group_by_expr)?) - }; - - let mut modifiers = vec![]; - if dialect_of!(self is ClickHouseDialect | GenericDialect) { - loop { - if !self.parse_keyword(Keyword::WITH) { - break; - } - let keyword = self.expect_one_of_keywords(&[ - Keyword::ROLLUP, - Keyword::CUBE, - Keyword::TOTALS, - ])?; - modifiers.push(match keyword { - Keyword::ROLLUP => GroupByWithModifier::Rollup, - Keyword::CUBE => GroupByWithModifier::Cube, - Keyword::TOTALS => GroupByWithModifier::Totals, - _ => { - return parser_err!( - "BUG: expected to match GroupBy modifier keyword", - self.peek_token().location - ) - } - }); - } - } - match expressions { - None => GroupByExpr::All(modifiers), - Some(exprs) => GroupByExpr::Expressions(exprs, modifiers), - } - } else { - GroupByExpr::Expressions(vec![], vec![]) - }; + let group_by = self + .parse_optional_group_by()? + .unwrap_or_else(|| GroupByExpr::Expressions(vec![], vec![])); let cluster_by = if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { self.parse_comma_separated(Parser::parse_expr)? diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 931899ff9..fe255cda5 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -287,6 +287,78 @@ fn parse_alter_table_attach_and_detach_partition() { } } +#[test] +fn parse_alter_table_add_projection() { + match clickhouse_and_generic().verified_stmt(concat!( + "ALTER TABLE t0 ADD PROJECTION IF NOT EXISTS my_name", + " (SELECT a, b GROUP BY a ORDER BY b)", + )) { + Statement::AlterTable { + name, operations, .. + } => { + assert_eq!(name, ObjectName(vec!["t0".into()])); + assert_eq!(1, operations.len()); + assert_eq!( + operations[0], + AlterTableOperation::AddProjection { + if_not_exists: true, + name: "my_name".into(), + select: ProjectionSelect { + projection: vec![ + UnnamedExpr(Identifier(Ident::new("a"))), + UnnamedExpr(Identifier(Ident::new("b"))), + ], + group_by: Some(GroupByExpr::Expressions( + vec![Identifier(Ident::new("a"))], + vec![] + )), + order_by: Some(OrderBy { + exprs: vec![OrderByExpr { + expr: Identifier(Ident::new("b")), + asc: None, + nulls_first: None, + with_fill: None, + }], + interpolate: None, + }), + } + } + ) + } + _ => unreachable!(), + } + + // leave out IF NOT EXISTS is allowed + clickhouse_and_generic() + .verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b GROUP BY a ORDER BY b)"); + // leave out GROUP BY is allowed + clickhouse_and_generic() + .verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b ORDER BY b)"); + // leave out ORDER BY is allowed + clickhouse_and_generic() + .verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b GROUP BY a)"); + + // missing select query is not allowed + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name") + .unwrap_err(), + ParserError("Expected: (, found: EOF".to_string()) + ); + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name ()") + .unwrap_err(), + ParserError("Expected: SELECT, found: )".to_string()) + ); + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name (SELECT)") + .unwrap_err(), + ParserError("Expected: an expression:, found: )".to_string()) + ); +} + #[test] fn parse_optimize_table() { clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0"); From 7282ce22f9f031a2c9fd5831427f01f2f0dbb978 Mon Sep 17 00:00:00 2001 From: Kacper Muda Date: Fri, 23 Aug 2024 17:42:51 +0200 Subject: [PATCH 529/806] feat: support different USE statement syntaxes (#1387) --- src/ast/dcl.rs | 27 ++++++++++++ src/ast/mod.rs | 13 ++---- src/keywords.rs | 2 + src/parser/mod.rs | 27 +++++++++++- tests/sqlparser_clickhouse.rs | 33 +++++++++++++++ tests/sqlparser_databricks.rs | 74 +++++++++++++++++++++++++++++++++ tests/sqlparser_duckdb.rs | 52 +++++++++++++++++++++++ tests/sqlparser_hive.rs | 32 +++++++++++++- tests/sqlparser_mssql.rs | 32 ++++++++++++++ tests/sqlparser_mysql.rs | 33 ++++++++++++--- tests/sqlparser_snowflake.rs | 78 +++++++++++++++++++++++++++++++++++ 11 files changed, 386 insertions(+), 17 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index f90de34d4..1b0a77095 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -193,3 +193,30 @@ impl fmt::Display for AlterRoleOperation { } } } + +/// A `USE` (`Statement::Use`) operation +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Use { + Catalog(ObjectName), // e.g. `USE CATALOG foo.bar` + Schema(ObjectName), // e.g. `USE SCHEMA foo.bar` + Database(ObjectName), // e.g. `USE DATABASE foo.bar` + Warehouse(ObjectName), // e.g. `USE WAREHOUSE foo.bar` + Object(ObjectName), // e.g. `USE foo.bar` + Default, // e.g. `USE DEFAULT` +} + +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::Object(name) => write!(f, "{}", name), + Use::Default => write!(f, "DEFAULT"), + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4f9aac885..8a56f3158 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -31,7 +31,7 @@ pub use self::data_type::{ ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, StructBracketKind, TimezoneInfo, }; -pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue}; +pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue, Use}; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ConstraintCharacteristics, Deduplicate, DeferrableInitial, GeneratedAs, @@ -2515,11 +2515,9 @@ pub enum Statement { /// Note: this is a MySQL-specific statement. ShowCollation { filter: Option }, /// ```sql - /// USE + /// `USE ...` /// ``` - /// - /// Note: This is a MySQL-specific statement. - Use { db_name: Ident }, + Use(Use), /// ```sql /// START [ TRANSACTION | WORK ] | START TRANSACTION } ... /// ``` @@ -4125,10 +4123,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Use { db_name } => { - write!(f, "USE {db_name}")?; - Ok(()) - } + Statement::Use(use_expr) => use_expr.fmt(f), Statement::ShowCollation { filter } => { write!(f, "SHOW COLLATION")?; if let Some(filter) = filter { diff --git a/src/keywords.rs b/src/keywords.rs index acb913d57..d2dcc57d1 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -137,6 +137,7 @@ define_keywords!( CASCADED, CASE, CAST, + CATALOG, CEIL, CEILING, CENTURY, @@ -804,6 +805,7 @@ define_keywords!( VIEW, VIRTUAL, VOLATILE, + WAREHOUSE, WEEK, WHEN, WHENEVER, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1eff8f7d5..8f8c3f050 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9264,8 +9264,31 @@ impl<'a> Parser<'a> { } pub fn parse_use(&mut self) -> Result { - let db_name = self.parse_identifier(false)?; - Ok(Statement::Use { db_name }) + // Determine which keywords are recognized by the current dialect + let parsed_keyword = if dialect_of!(self is HiveDialect) { + // HiveDialect accepts USE DEFAULT; statement without any db specified + if self.parse_keyword(Keyword::DEFAULT) { + return Ok(Statement::Use(Use::Default)); + } + None // HiveDialect doesn't expect any other specific keyword after `USE` + } else if dialect_of!(self is DatabricksDialect) { + self.parse_one_of_keywords(&[Keyword::CATALOG, Keyword::DATABASE, Keyword::SCHEMA]) + } else if dialect_of!(self is SnowflakeDialect) { + self.parse_one_of_keywords(&[Keyword::DATABASE, Keyword::SCHEMA, Keyword::WAREHOUSE]) + } else { + None // No specific keywords for other dialects, including GenericDialect + }; + + let obj_name = self.parse_object_name(false)?; + let result = match parsed_keyword { + Some(Keyword::CATALOG) => Use::Catalog(obj_name), + Some(Keyword::DATABASE) => Use::Database(obj_name), + Some(Keyword::SCHEMA) => Use::Schema(obj_name), + Some(Keyword::WAREHOUSE) => Use::Warehouse(obj_name), + _ => Use::Object(obj_name), + }; + + Ok(Statement::Use(result)) } pub fn parse_table_and_joins(&mut self) -> Result { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index fe255cda5..c8157bced 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1232,6 +1232,39 @@ fn test_prewhere() { } } +#[test] +fn parse_use() { + let valid_object_names = [ + "mydb", + "SCHEMA", + "DATABASE", + "CATALOG", + "WAREHOUSE", + "DEFAULT", + ]; + let quote_styles = ['"', '`']; + + for object_name in &valid_object_names { + // Test single identifier without quotes + assert_eq!( + clickhouse().verified_stmt(&format!("USE {}", object_name)), + Statement::Use(Use::Object(ObjectName(vec![Ident::new( + object_name.to_string() + )]))) + ); + 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)), + Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + quote, + object_name.to_string(), + )]))) + ); + } + } +} + #[test] fn test_query_with_format_clause() { let format_options = vec!["TabSeparated", "JSONCompact", "NULL"]; diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 280b97b49..ee0cf2d7d 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -189,3 +189,77 @@ fn test_values_clause() { // TODO: support this example from https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-qry-select-values.html#examples // databricks().verified_query("VALUES 1, 2, 3"); } + +#[test] +fn parse_use() { + let valid_object_names = ["mydb", "WAREHOUSE", "DEFAULT"]; + let quote_styles = ['"', '`']; + + for object_name in &valid_object_names { + // Test single identifier without quotes + assert_eq!( + databricks().verified_stmt(&format!("USE {}", object_name)), + Statement::Use(Use::Object(ObjectName(vec![Ident::new( + object_name.to_string() + )]))) + ); + 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)), + Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + quote, + object_name.to_string(), + )]))) + ); + } + } + + 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)), + Statement::Use(Use::Catalog(ObjectName(vec![Ident::with_quote( + quote, + "my_catalog".to_string(), + )]))) + ); + assert_eq!( + databricks().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), + Statement::Use(Use::Database(ObjectName(vec![Ident::with_quote( + quote, + "my_database".to_string(), + )]))) + ); + assert_eq!( + databricks().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), + Statement::Use(Use::Schema(ObjectName(vec![Ident::with_quote( + quote, + "my_schema".to_string(), + )]))) + ); + } + + // Test single identifier with keyword and no quotes + assert_eq!( + databricks().verified_stmt("USE CATALOG my_catalog"), + Statement::Use(Use::Catalog(ObjectName(vec![Ident::new("my_catalog")]))) + ); + assert_eq!( + databricks().verified_stmt("USE DATABASE my_schema"), + Statement::Use(Use::Database(ObjectName(vec![Ident::new("my_schema")]))) + ); + assert_eq!( + databricks().verified_stmt("USE SCHEMA my_schema"), + Statement::Use(Use::Schema(ObjectName(vec![Ident::new("my_schema")]))) + ); + + // Test invalid syntax - missing identifier + let invalid_cases = ["USE SCHEMA", "USE DATABASE", "USE CATALOG"]; + for sql in &invalid_cases { + assert_eq!( + databricks().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected: identifier, found: EOF".to_string()), + ); + } +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 6e6c4e230..488fddfd3 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -756,3 +756,55 @@ fn test_duckdb_union_datatype() { stmt ); } + +#[test] +fn parse_use() { + let valid_object_names = [ + "mydb", + "SCHEMA", + "DATABASE", + "CATALOG", + "WAREHOUSE", + "DEFAULT", + ]; + let quote_styles = ['"', '\'']; + + for object_name in &valid_object_names { + // Test single identifier without quotes + assert_eq!( + duckdb().verified_stmt(&format!("USE {}", object_name)), + Statement::Use(Use::Object(ObjectName(vec![Ident::new( + object_name.to_string() + )]))) + ); + 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)), + Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + quote, + object_name.to_string(), + )]))) + ); + } + } + + 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)), + Statement::Use(Use::Object(ObjectName(vec![ + Ident::with_quote(quote, "CATALOG"), + Ident::with_quote(quote, "my_schema") + ]))) + ); + } + // Test double identifier without quotes + assert_eq!( + duckdb().verified_stmt("USE mydb.my_schema"), + Statement::Use(Use::Object(ObjectName(vec![ + Ident::new("mydb"), + Ident::new("my_schema") + ]))) + ); +} diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 157dad060..bd242035e 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -18,7 +18,7 @@ use sqlparser::ast::{ CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OneOrManyWithParens, SelectItem, Statement, TableFactor, - UnaryOperator, Value, + UnaryOperator, Use, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::ParserError; @@ -401,6 +401,36 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } +#[test] +fn parse_use() { + let valid_object_names = ["mydb", "SCHEMA", "DATABASE", "CATALOG", "WAREHOUSE"]; + let quote_styles = ['\'', '"', '`']; + for object_name in &valid_object_names { + // Test single identifier without quotes + assert_eq!( + hive().verified_stmt(&format!("USE {}", object_name)), + Statement::Use(Use::Object(ObjectName(vec![Ident::new( + object_name.to_string() + )]))) + ); + for "e in "e_styles { + // Test single identifier with different type of quotes + assert_eq!( + hive().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + quote, + object_name.to_string(), + )]))) + ); + } + } + // Test DEFAULT keyword that is special case in Hive + assert_eq!( + hive().verified_stmt("USE DEFAULT"), + Statement::Use(Use::Default) + ); +} + fn hive() -> TestedDialects { TestedDialects { dialects: vec![Box::new(HiveDialect {})], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 3e8b6afbf..5c2ec8763 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -621,6 +621,38 @@ fn parse_mssql_declare() { ); } +#[test] +fn parse_use() { + let valid_object_names = [ + "mydb", + "SCHEMA", + "DATABASE", + "CATALOG", + "WAREHOUSE", + "DEFAULT", + ]; + let quote_styles = ['\'', '"']; + for object_name in &valid_object_names { + // Test single identifier without quotes + assert_eq!( + ms().verified_stmt(&format!("USE {}", object_name)), + Statement::Use(Use::Object(ObjectName(vec![Ident::new( + object_name.to_string() + )]))) + ); + for "e in "e_styles { + // Test single identifier with different type of quotes + assert_eq!( + ms().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + quote, + object_name.to_string(), + )]))) + ); + } + } +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 397a722b5..33587c35a 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -444,12 +444,35 @@ fn parse_show_collation() { #[test] fn parse_use() { - assert_eq!( - mysql_and_generic().verified_stmt("USE mydb"), - Statement::Use { - db_name: Ident::new("mydb") + let valid_object_names = [ + "mydb", + "SCHEMA", + "DATABASE", + "CATALOG", + "WAREHOUSE", + "DEFAULT", + ]; + let quote_styles = ['\'', '"', '`']; + for object_name in &valid_object_names { + // Test single identifier without quotes + assert_eq!( + mysql_and_generic().verified_stmt(&format!("USE {}", object_name)), + Statement::Use(Use::Object(ObjectName(vec![Ident::new( + object_name.to_string() + )]))) + ); + 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)), + Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + quote, + object_name.to_string(), + )]))) + ); } - ); + } } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a4f29c04f..d0876fc50 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2322,3 +2322,81 @@ fn parse_explain_table() { _ => panic!("Unexpected Statement, must be ExplainTable"), } } + +#[test] +fn parse_use() { + let valid_object_names = ["mydb", "CATALOG", "DEFAULT"]; + let quote_styles = ['\'', '"', '`']; + for object_name in &valid_object_names { + // Test single identifier without quotes + std::assert_eq!( + snowflake().verified_stmt(&format!("USE {}", object_name)), + Statement::Use(Use::Object(ObjectName(vec![Ident::new( + object_name.to_string() + )]))) + ); + for "e in "e_styles { + // Test single identifier with different type of quotes + std::assert_eq!( + snowflake().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), + Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + quote, + object_name.to_string(), + )]))) + ); + } + } + + for "e in "e_styles { + // Test double identifier with different type of quotes + std::assert_eq!( + snowflake().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), + Statement::Use(Use::Object(ObjectName(vec![ + Ident::with_quote(quote, "CATALOG"), + Ident::with_quote(quote, "my_schema") + ]))) + ); + } + // Test double identifier without quotes + std::assert_eq!( + snowflake().verified_stmt("USE mydb.my_schema"), + Statement::Use(Use::Object(ObjectName(vec![ + Ident::new("mydb"), + Ident::new("my_schema") + ]))) + ); + + for "e in "e_styles { + // Test single and double identifier with keyword and different type of quotes + std::assert_eq!( + snowflake().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), + Statement::Use(Use::Database(ObjectName(vec![Ident::with_quote( + quote, + "my_database".to_string(), + )]))) + ); + std::assert_eq!( + snowflake().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), + Statement::Use(Use::Schema(ObjectName(vec![Ident::with_quote( + quote, + "my_schema".to_string(), + )]))) + ); + std::assert_eq!( + snowflake().verified_stmt(&format!("USE SCHEMA {0}CATALOG{0}.{0}my_schema{0}", quote)), + Statement::Use(Use::Schema(ObjectName(vec![ + Ident::with_quote(quote, "CATALOG"), + Ident::with_quote(quote, "my_schema") + ]))) + ); + } + + // Test invalid syntax - missing identifier + let invalid_cases = ["USE SCHEMA", "USE DATABASE", "USE WAREHOUSE"]; + for sql in &invalid_cases { + std::assert_eq!( + snowflake().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected: identifier, found: EOF".to_string()), + ); + } +} From 222b7d127acb265472cc9bf2e81ef2453a53a9ce Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 26 Aug 2024 20:56:57 +0100 Subject: [PATCH 530/806] allow `DateTimeField::Custom` with `EXTRACT` in Postgres (#1394) --- src/dialect/generic.rs | 8 ++++ src/dialect/mod.rs | 10 +++++ src/dialect/postgresql.rs | 8 ++++ src/dialect/snowflake.rs | 8 ++++ src/parser/mod.rs | 5 +-- tests/sqlparser_common.rs | 78 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 111 insertions(+), 6 deletions(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 2777dfb02..211abd976 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -78,4 +78,12 @@ impl Dialect for GenericDialect { fn support_map_literal_syntax(&self) -> bool { true } + + fn allow_extract_custom(&self) -> bool { + true + } + + fn allow_extract_single_quotes(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index df3022adc..143c8e1c9 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -499,6 +499,16 @@ pub trait Dialect: Debug + Any { fn describe_requires_table_keyword(&self) -> bool { false } + + /// Returns true if this dialect allows the `EXTRACT` function to words other than [`Keyword`]. + fn allow_extract_custom(&self) -> bool { + false + } + + /// Returns true if this dialect allows the `EXTRACT` function to use single quotes in the part being extracted. + fn allow_extract_single_quotes(&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 c25a80f67..8abaa4a5f 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -154,6 +154,14 @@ impl Dialect for PostgreSqlDialect { Precedence::Or => OR_PREC, } } + + fn allow_extract_custom(&self) -> bool { + true + } + + fn allow_extract_single_quotes(&self) -> bool { + true + } } pub fn parse_comment(parser: &mut Parser) -> Result { diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 89508c3bb..4f37004b1 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -158,6 +158,14 @@ impl Dialect for SnowflakeDialect { fn describe_requires_table_keyword(&self) -> bool { true } + + fn allow_extract_custom(&self) -> bool { + true + } + + fn allow_extract_single_quotes(&self) -> bool { + true + } } /// Parse snowflake create table statement. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8f8c3f050..302e5e660 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1970,15 +1970,14 @@ impl<'a> Parser<'a> { Keyword::TIMEZONE_HOUR => Ok(DateTimeField::TimezoneHour), Keyword::TIMEZONE_MINUTE => Ok(DateTimeField::TimezoneMinute), Keyword::TIMEZONE_REGION => Ok(DateTimeField::TimezoneRegion), - _ if dialect_of!(self is SnowflakeDialect | GenericDialect) => { + _ if self.dialect.allow_extract_custom() => { self.prev_token(); let custom = self.parse_identifier(false)?; Ok(DateTimeField::Custom(custom)) } _ => self.expected("date/time field", next_token), }, - Token::SingleQuotedString(_) if dialect_of!(self is SnowflakeDialect | GenericDialect) => - { + Token::SingleQuotedString(_) if self.dialect.allow_extract_single_quotes() => { self.prev_token(); let custom = self.parse_identifier(false)?; Ok(DateTimeField::Custom(custom)) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 517e978b4..dd4aad146 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2474,7 +2474,7 @@ fn parse_extract() { verified_stmt("SELECT EXTRACT(TIMEZONE_REGION FROM d)"); verified_stmt("SELECT EXTRACT(TIME FROM d)"); - let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let dialects = all_dialects_except(|d| d.allow_extract_custom()); let res = dialects.parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)"); assert_eq!( ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()), @@ -2573,7 +2573,7 @@ fn parse_ceil_datetime() { verified_stmt("SELECT CEIL(d TO SECOND) FROM df"); verified_stmt("SELECT CEIL(d TO MILLISECOND) FROM df"); - let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let dialects = all_dialects_except(|d| d.allow_extract_custom()); let res = dialects.parse_sql_statements("SELECT CEIL(d TO JIFFY) FROM df"); assert_eq!( ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()), @@ -2600,7 +2600,7 @@ fn parse_floor_datetime() { verified_stmt("SELECT FLOOR(d TO SECOND) FROM df"); verified_stmt("SELECT FLOOR(d TO MILLISECOND) FROM df"); - let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let dialects = all_dialects_except(|d| d.allow_extract_custom()); let res = dialects.parse_sql_statements("SELECT FLOOR(d TO JIFFY) FROM df"); assert_eq!( ParserError::ParserError("Expected: date/time field, found: JIFFY".to_string()), @@ -10467,3 +10467,75 @@ fn test_group_by_nothing() { ); } } + +#[test] +fn test_extract_seconds_ok() { + let dialects = all_dialects_where(|d| d.allow_extract_custom()); + let stmt = dialects.verified_expr("EXTRACT(seconds FROM '2 seconds'::INTERVAL)"); + + assert_eq!( + stmt, + Expr::Extract { + field: DateTimeField::Custom(Ident { + value: "seconds".to_string(), + quote_style: None, + }), + syntax: ExtractSyntax::From, + expr: Box::new(Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Value(Value::SingleQuotedString( + "2 seconds".to_string() + ))), + data_type: DataType::Interval, + format: None, + }), + } + ) +} + +#[test] +fn test_extract_seconds_single_quote_ok() { + let dialects = all_dialects_where(|d| d.allow_extract_custom()); + let stmt = dialects.verified_expr(r#"EXTRACT('seconds' FROM '2 seconds'::INTERVAL)"#); + + assert_eq!( + stmt, + Expr::Extract { + field: DateTimeField::Custom(Ident { + value: "seconds".to_string(), + quote_style: Some('\''), + }), + syntax: ExtractSyntax::From, + expr: Box::new(Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Value(Value::SingleQuotedString( + "2 seconds".to_string() + ))), + data_type: DataType::Interval, + format: None, + }), + } + ) +} + +#[test] +fn test_extract_seconds_err() { + let sql = "SELECT EXTRACT(seconds FROM '2 seconds'::INTERVAL)"; + let dialects = all_dialects_except(|d| d.allow_extract_custom()); + let err = dialects.parse_sql_statements(sql).unwrap_err(); + assert_eq!( + err.to_string(), + "sql parser error: Expected: date/time field, found: seconds" + ); +} + +#[test] +fn test_extract_seconds_single_quote_err() { + let sql = r#"SELECT EXTRACT('seconds' FROM '2 seconds'::INTERVAL)"#; + let dialects = all_dialects_except(|d| d.allow_extract_single_quotes()); + let err = dialects.parse_sql_statements(sql).unwrap_err(); + assert_eq!( + err.to_string(), + "sql parser error: Expected: date/time field, found: 'seconds'" + ); +} From 7b4ac7ca9f4cc8e129035a0e55d0cf8a67215d4a Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 1 Sep 2024 19:21:26 +0800 Subject: [PATCH 531/806] Add support of parsing CLUSTERED BY clause for Hive (#1397) --- src/ast/ddl.rs | 29 ++++++++++- src/ast/dml.rs | 30 +++++------ src/ast/helpers/stmt_create_table.rs | 16 ++++-- src/ast/mod.rs | 15 ++---- src/keywords.rs | 3 ++ src/parser/mod.rs | 33 ++++++++++++- tests/sqlparser_duckdb.rs | 1 + tests/sqlparser_hive.rs | 74 ++++++++++++++++++++++++++-- tests/sqlparser_postgres.rs | 1 + 9 files changed, 166 insertions(+), 36 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 31e216a9f..2d1778c7b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -26,7 +26,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, - ObjectName, ProjectionSelect, SequenceOptions, SqlOption, + ObjectName, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Value, }; use crate::tokenizer::Token; @@ -1417,3 +1417,30 @@ impl fmt::Display for Deduplicate { } } } + +/// Hive supports `CLUSTERED BY` statement in `CREATE TABLE`. +/// Syntax: `CLUSTERED BY (col_name, ...) [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS` +/// +/// [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ClusteredBy { + pub columns: Vec, + pub sorted_by: Option>, + pub num_buckets: Value, +} + +impl fmt::Display for ClusteredBy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CLUSTERED BY ({})", + display_comma_separated(&self.columns) + )?; + if let Some(ref sorted_by) = self.sorted_by { + write!(f, " SORTED BY ({})", display_comma_separated(sorted_by))?; + } + write!(f, " INTO {} BUCKETS", self.num_buckets) + } +} diff --git a/src/ast/dml.rs b/src/ast/dml.rs index aad7d2e22..95ed9f00e 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -22,11 +22,11 @@ use sqlparser_derive::{Visit, VisitMut}; pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ - display_comma_separated, display_separated, CommentDef, Expr, FileFormat, FromTable, - HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, - MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, - RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, TableWithJoins, Tag, - WrappedCollection, + display_comma_separated, display_separated, ClusteredBy, CommentDef, Expr, FileFormat, + FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, + InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, + OrderByExpr, Query, RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, + TableWithJoins, Tag, WrappedCollection, }; /// CREATE INDEX statement. @@ -140,6 +140,9 @@ pub struct CreateTable { /// BigQuery: Table clustering column list. /// pub cluster_by: Option>>, + /// Hive: Table clustering column list. + /// + pub clustered_by: Option, /// BigQuery: Table options list. /// pub options: Option>, @@ -236,19 +239,6 @@ impl Display for CreateTable { HiveDistributionStyle::PARTITIONED { columns } => { write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?; } - HiveDistributionStyle::CLUSTERED { - columns, - sorted_by, - num_buckets, - } => { - write!(f, " CLUSTERED BY ({})", display_comma_separated(columns))?; - if !sorted_by.is_empty() { - write!(f, " SORTED BY ({})", display_comma_separated(sorted_by))?; - } - if *num_buckets > 0 { - write!(f, " INTO {num_buckets} BUCKETS")?; - } - } HiveDistributionStyle::SKEWED { columns, on, @@ -267,6 +257,10 @@ impl Display for CreateTable { _ => (), } + if let Some(clustered_by) = &self.clustered_by { + write!(f, " {clustered_by}")?; + } + if let Some(HiveFormat { row_format, serde_properties, diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 19efaeece..82532b291 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -9,9 +9,9 @@ use sqlparser_derive::{Visit, VisitMut}; use super::super::dml::CreateTable; use crate::ast::{ - ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, - OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, TableConstraint, - TableEngine, Tag, WrappedCollection, + ClusteredBy, ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, + ObjectName, OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, + TableConstraint, TableEngine, Tag, WrappedCollection, }; use crate::parser::ParserError; @@ -78,6 +78,7 @@ pub struct CreateTableBuilder { pub order_by: Option>, pub partition_by: Option>, pub cluster_by: Option>>, + pub clustered_by: Option, pub options: Option>, pub strict: bool, pub copy_grants: bool, @@ -125,6 +126,7 @@ impl CreateTableBuilder { order_by: None, partition_by: None, cluster_by: None, + clustered_by: None, options: None, strict: false, copy_grants: false, @@ -286,6 +288,11 @@ impl CreateTableBuilder { self } + pub fn clustered_by(mut self, clustered_by: Option) -> Self { + self.clustered_by = clustered_by; + self + } + pub fn options(mut self, options: Option>) -> Self { self.options = options; self @@ -380,6 +387,7 @@ impl CreateTableBuilder { order_by: self.order_by, partition_by: self.partition_by, cluster_by: self.cluster_by, + clustered_by: self.clustered_by, options: self.options, strict: self.strict, copy_grants: self.copy_grants, @@ -434,6 +442,7 @@ impl TryFrom for CreateTableBuilder { order_by, partition_by, cluster_by, + clustered_by, options, strict, copy_grants, @@ -476,6 +485,7 @@ impl TryFrom for CreateTableBuilder { order_by, partition_by, cluster_by, + clustered_by, options, strict, copy_grants, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8a56f3158..0e6357cbc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -33,11 +33,11 @@ pub use self::data_type::{ }; pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue, Use}; pub use self::ddl::{ - AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, - ColumnOptionDef, ConstraintCharacteristics, Deduplicate, DeferrableInitial, GeneratedAs, - GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Owner, Partition, - ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, ViewColumnDef, + AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ClusteredBy, ColumnDef, + ColumnOption, ColumnOptionDef, ConstraintCharacteristics, Deduplicate, DeferrableInitial, + GeneratedAs, GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Owner, + Partition, ProcedureParam, ReferentialAction, TableConstraint, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -5398,11 +5398,6 @@ pub enum HiveDistributionStyle { PARTITIONED { columns: Vec, }, - CLUSTERED { - columns: Vec, - sorted_by: Vec, - num_buckets: i32, - }, SKEWED { columns: Vec, on: Vec, diff --git a/src/keywords.rs b/src/keywords.rs index d2dcc57d1..ce4972f98 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -125,6 +125,7 @@ define_keywords!( BOTH, BROWSE, BTREE, + BUCKETS, BY, BYPASSRLS, BYTEA, @@ -156,6 +157,7 @@ define_keywords!( CLONE, CLOSE, CLUSTER, + CLUSTERED, COALESCE, COLLATE, COLLATION, @@ -675,6 +677,7 @@ define_keywords!( SNAPSHOT, SOME, SORT, + SORTED, SOURCE, SPATIAL, SPECIFIC, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 302e5e660..4fb794b91 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5377,7 +5377,7 @@ impl<'a> Parser<'a> { }) } - //TODO: Implement parsing for Skewed and Clustered + //TODO: Implement parsing for Skewed pub fn parse_hive_distribution(&mut self) -> Result { if self.parse_keywords(&[Keyword::PARTITIONED, Keyword::BY]) { self.expect_token(&Token::LParen)?; @@ -5574,6 +5574,7 @@ impl<'a> Parser<'a> { let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]); 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)?; @@ -5720,6 +5721,7 @@ impl<'a> Parser<'a> { .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) @@ -6099,6 +6101,35 @@ impl<'a> Parser<'a> { })) } + pub fn parse_optional_clustered_by(&mut self) -> Result, ParserError> { + let clustered_by = if dialect_of!(self is HiveDialect|GenericDialect) + && self.parse_keywords(&[Keyword::CLUSTERED, Keyword::BY]) + { + let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + + let sorted_by = if self.parse_keywords(&[Keyword::SORTED, Keyword::BY]) { + self.expect_token(&Token::LParen)?; + let sorted_by_columns = self.parse_comma_separated(|p| p.parse_order_by_expr())?; + self.expect_token(&Token::RParen)?; + Some(sorted_by_columns) + } else { + None + }; + + self.expect_keyword(Keyword::INTO)?; + let num_buckets = self.parse_number_value()?; + self.expect_keyword(Keyword::BUCKETS)?; + Some(ClusteredBy { + columns, + sorted_by, + num_buckets, + }) + } else { + None + }; + Ok(clustered_by) + } + pub fn parse_referential_action(&mut self) -> Result { if self.parse_keyword(Keyword::RESTRICT) { Ok(ReferentialAction::Restrict) diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 488fddfd3..12368a88c 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -741,6 +741,7 @@ fn test_duckdb_union_datatype() { order_by: Default::default(), partition_by: Default::default(), cluster_by: Default::default(), + clustered_by: Default::default(), options: Default::default(), strict: Default::default(), copy_grants: Default::default(), diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index bd242035e..1bb4229e1 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -16,9 +16,9 @@ //! is also tested (on the inputs it can handle). use sqlparser::ast::{ - CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionArgumentList, - FunctionArguments, Ident, ObjectName, OneOrManyWithParens, SelectItem, Statement, TableFactor, - UnaryOperator, Use, Value, + ClusteredBy, CreateFunctionBody, CreateFunctionUsing, CreateTable, Expr, Function, + FunctionArgumentList, FunctionArguments, Ident, ObjectName, OneOrManyWithParens, OrderByExpr, + SelectItem, Statement, TableFactor, UnaryOperator, Use, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::ParserError; @@ -115,6 +115,74 @@ fn create_table_like() { hive().verified_stmt(like); } +#[test] +fn create_table_with_clustered_by() { + let sql = concat!( + "CREATE TABLE db.table_name (a INT, b STRING)", + " PARTITIONED BY (a INT, b STRING)", + " CLUSTERED BY (a, b) SORTED BY (a ASC, b DESC)", + " INTO 4 BUCKETS" + ); + match hive_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { clustered_by, .. }) => { + assert_eq!( + clustered_by.unwrap(), + ClusteredBy { + columns: vec![Ident::new("a"), Ident::new("b")], + sorted_by: Some(vec![ + OrderByExpr { + expr: Expr::Identifier(Ident::new("a")), + asc: Some(true), + nulls_first: None, + with_fill: None, + }, + OrderByExpr { + expr: Expr::Identifier(Ident::new("b")), + asc: Some(false), + nulls_first: None, + with_fill: None, + }, + ]), + num_buckets: Value::Number("4".parse().unwrap(), false), + } + ) + } + _ => unreachable!(), + } + + // SORTED BY is optional + hive_and_generic().verified_stmt("CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY (a, b) INTO 4 BUCKETS"); + + // missing INTO BUCKETS + assert_eq!( + hive_and_generic().parse_sql_statements( + "CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY (a, b)" + ).unwrap_err(), + ParserError::ParserError("Expected: INTO, found: EOF".to_string()) + ); + // missing CLUSTER BY columns + assert_eq!( + hive_and_generic().parse_sql_statements( + "CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY () INTO 4 BUCKETS" + ).unwrap_err(), + ParserError::ParserError("Expected: identifier, found: )".to_string()) + ); + // missing SORT BY columns + assert_eq!( + hive_and_generic().parse_sql_statements( + "CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY (a, b) SORTED BY INTO 4 BUCKETS" + ).unwrap_err(), + ParserError::ParserError("Expected: (, found: INTO".to_string()) + ); + // missing number BUCKETS + assert_eq!( + hive_and_generic().parse_sql_statements( + "CREATE TABLE db.table_name (a INT, b STRING) PARTITIONED BY (a INT, b STRING) CLUSTERED BY (a, b) SORTED BY (a ASC, b DESC) INTO" + ).unwrap_err(), + ParserError::ParserError("Expected: a value, found: EOF".to_string()) + ); +} + // Turning off this test until we can parse identifiers starting with numbers :( #[test] fn test_identifier() { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2f9fe86c9..59afd7402 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4838,6 +4838,7 @@ fn parse_trigger_related_functions() { order_by: None, partition_by: None, cluster_by: None, + clustered_by: None, options: None, strict: false, copy_grants: false, From df86f259ce2924c9b92512526341798aeafddaa5 Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 1 Sep 2024 19:21:59 +0800 Subject: [PATCH 532/806] Fix identifier starts with `$` should be regarded as a placeholder in SQLite (#1402) --- src/dialect/sqlite.rs | 1 - tests/sqlparser_sqlite.rs | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index daad6a159..cc08d7961 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -41,7 +41,6 @@ impl Dialect for SQLiteDialect { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' - || ch == '$' || ('\u{007f}'..='\u{ffff}').contains(&ch) } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 629ab5fc2..ce11b015a 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -20,6 +20,7 @@ mod test_utils; use test_utils::*; use sqlparser::ast::SelectItem::UnnamedExpr; +use sqlparser::ast::Value::Placeholder; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SQLiteDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -470,6 +471,22 @@ fn parse_start_transaction_with_modifier() { ); } +#[test] +fn test_dollar_identifier_as_placeholder() { + // This relates to the discussion in issue #291. The `$id` should be treated as a placeholder, + // not as an identifier in SQLite dialect. + // + // Reference: https://www.sqlite.org/lang_expr.html#varparam + match sqlite().verified_expr("id = $id") { + Expr::BinaryOp { op, left, right } => { + assert_eq!(op, BinaryOperator::Eq); + assert_eq!(left, Box::new(Expr::Identifier(Ident::new("id")))); + assert_eq!(right, Box::new(Expr::Value(Placeholder("$id".to_string())))); + } + _ => unreachable!(), + } +} + fn sqlite() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SQLiteDialect {})], From d64beea4d4993dae9917dee1fdd79f6132e7bf93 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sun, 1 Sep 2024 12:23:29 +0100 Subject: [PATCH 533/806] cleanup `parse_statement` (#1407) --- src/parser/mod.rs | 102 +++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4fb794b91..b57ea614e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -478,88 +478,88 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match &next_token.token { Token::Word(w) => match w.keyword { - Keyword::KILL => Ok(self.parse_kill()?), - Keyword::FLUSH => Ok(self.parse_flush()?), - Keyword::DESC => Ok(self.parse_explain(DescribeAlias::Desc)?), - Keyword::DESCRIBE => Ok(self.parse_explain(DescribeAlias::Describe)?), - Keyword::EXPLAIN => Ok(self.parse_explain(DescribeAlias::Explain)?), - Keyword::ANALYZE => Ok(self.parse_analyze()?), + Keyword::KILL => self.parse_kill(), + Keyword::FLUSH => self.parse_flush(), + Keyword::DESC => self.parse_explain(DescribeAlias::Desc), + Keyword::DESCRIBE => self.parse_explain(DescribeAlias::Describe), + Keyword::EXPLAIN => self.parse_explain(DescribeAlias::Explain), + Keyword::ANALYZE => self.parse_analyze(), Keyword::SELECT | Keyword::WITH | Keyword::VALUES => { self.prev_token(); - Ok(Statement::Query(self.parse_boxed_query()?)) + self.parse_boxed_query().map(Statement::Query) } - Keyword::TRUNCATE => Ok(self.parse_truncate()?), + Keyword::TRUNCATE => self.parse_truncate(), Keyword::ATTACH => { if dialect_of!(self is DuckDbDialect) { - Ok(self.parse_attach_duckdb_database()?) + self.parse_attach_duckdb_database() } else { - Ok(self.parse_attach_database()?) + self.parse_attach_database() } } Keyword::DETACH if dialect_of!(self is DuckDbDialect | GenericDialect) => { - Ok(self.parse_detach_duckdb_database()?) - } - Keyword::MSCK => Ok(self.parse_msck()?), - Keyword::CREATE => Ok(self.parse_create()?), - Keyword::CACHE => Ok(self.parse_cache_table()?), - Keyword::DROP => Ok(self.parse_drop()?), - Keyword::DISCARD => Ok(self.parse_discard()?), - Keyword::DECLARE => Ok(self.parse_declare()?), - Keyword::FETCH => Ok(self.parse_fetch_statement()?), - Keyword::DELETE => Ok(self.parse_delete()?), - Keyword::INSERT => Ok(self.parse_insert()?), - Keyword::REPLACE => Ok(self.parse_replace()?), - Keyword::UNCACHE => Ok(self.parse_uncache_table()?), - Keyword::UPDATE => Ok(self.parse_update()?), - Keyword::ALTER => Ok(self.parse_alter()?), - Keyword::CALL => Ok(self.parse_call()?), - Keyword::COPY => Ok(self.parse_copy()?), - Keyword::CLOSE => Ok(self.parse_close()?), - Keyword::SET => Ok(self.parse_set()?), - Keyword::SHOW => Ok(self.parse_show()?), - Keyword::USE => Ok(self.parse_use()?), - Keyword::GRANT => Ok(self.parse_grant()?), - Keyword::REVOKE => Ok(self.parse_revoke()?), - Keyword::START => Ok(self.parse_start_transaction()?), + self.parse_detach_duckdb_database() + } + Keyword::MSCK => self.parse_msck(), + Keyword::CREATE => self.parse_create(), + Keyword::CACHE => self.parse_cache_table(), + Keyword::DROP => self.parse_drop(), + Keyword::DISCARD => self.parse_discard(), + Keyword::DECLARE => self.parse_declare(), + Keyword::FETCH => self.parse_fetch_statement(), + Keyword::DELETE => self.parse_delete(), + Keyword::INSERT => self.parse_insert(), + Keyword::REPLACE => self.parse_replace(), + Keyword::UNCACHE => self.parse_uncache_table(), + Keyword::UPDATE => self.parse_update(), + Keyword::ALTER => self.parse_alter(), + Keyword::CALL => self.parse_call(), + Keyword::COPY => self.parse_copy(), + Keyword::CLOSE => self.parse_close(), + Keyword::SET => self.parse_set(), + Keyword::SHOW => self.parse_show(), + Keyword::USE => self.parse_use(), + 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 => Ok(self.parse_begin()?), + 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 => Ok(self.parse_end()?), - Keyword::SAVEPOINT => Ok(self.parse_savepoint()?), - Keyword::RELEASE => Ok(self.parse_release()?), - Keyword::COMMIT => Ok(self.parse_commit()?), - Keyword::ROLLBACK => Ok(self.parse_rollback()?), - Keyword::ASSERT => Ok(self.parse_assert()?), + Keyword::END => self.parse_end(), + Keyword::SAVEPOINT => self.parse_savepoint(), + Keyword::RELEASE => self.parse_release(), + Keyword::COMMIT => self.parse_commit(), + Keyword::ROLLBACK => self.parse_rollback(), + Keyword::ASSERT => self.parse_assert(), // `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific // syntaxes. They are used for Postgres prepared statement. - Keyword::DEALLOCATE => Ok(self.parse_deallocate()?), - Keyword::EXECUTE => Ok(self.parse_execute()?), - Keyword::PREPARE => Ok(self.parse_prepare()?), - Keyword::MERGE => Ok(self.parse_merge()?), + Keyword::DEALLOCATE => self.parse_deallocate(), + Keyword::EXECUTE => self.parse_execute(), + Keyword::PREPARE => self.parse_prepare(), + Keyword::MERGE => self.parse_merge(), // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html - Keyword::PRAGMA => Ok(self.parse_pragma()?), - Keyword::UNLOAD => Ok(self.parse_unload()?), + Keyword::PRAGMA => self.parse_pragma(), + Keyword::UNLOAD => self.parse_unload(), // `INSTALL` is duckdb specific https://duckdb.org/docs/extensions/overview Keyword::INSTALL if dialect_of!(self is DuckDbDialect | GenericDialect) => { - Ok(self.parse_install()?) + self.parse_install() } // `LOAD` is duckdb specific https://duckdb.org/docs/extensions/overview Keyword::LOAD if dialect_of!(self is DuckDbDialect | GenericDialect) => { - Ok(self.parse_load()?) + self.parse_load() } // `OPTIMIZE` is clickhouse specific https://clickhouse.tech/docs/en/sql-reference/statements/optimize/ Keyword::OPTIMIZE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { - Ok(self.parse_optimize_table()?) + self.parse_optimize_table() } _ => self.expected("an SQL statement", next_token), }, Token::LParen => { self.prev_token(); - Ok(Statement::Query(self.parse_boxed_query()?)) + self.parse_boxed_query().map(Statement::Query) } _ => self.expected("an SQL statement", next_token), } From 4d52ee728017248e2ec4cdc7a1a32eeb96619bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=9E=97=E4=BC=9F?= Date: Sun, 1 Sep 2024 19:38:20 +0800 Subject: [PATCH 534/806] Support create index with clause (#1389) Co-authored-by: Ifeanyi Ubah Co-authored-by: Andrew Lamb --- src/ast/dml.rs | 5 ++++ src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 6 +++++ src/dialect/postgresql.rs | 4 ++++ src/parser/mod.rs | 12 ++++++++++ tests/sqlparser_common.rs | 47 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 14 +++++++++++ 7 files changed, 92 insertions(+) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 95ed9f00e..884b378db 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -45,6 +45,8 @@ pub struct CreateIndex { pub if_not_exists: bool, pub include: Vec, pub nulls_distinct: Option, + /// WITH clause: + pub with: Vec, pub predicate: Option, } @@ -83,6 +85,9 @@ impl Display for CreateIndex { write!(f, " NULLS NOT DISTINCT")?; } } + if !self.with.is_empty() { + write!(f, " WITH ({})", display_comma_separated(&self.with))?; + } if let Some(predicate) = &self.predicate { write!(f, " WHERE {predicate}")?; } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 211abd976..c8f1c00d9 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -86,4 +86,8 @@ impl Dialect for GenericDialect { fn allow_extract_single_quotes(&self) -> bool { true } + + fn supports_create_index_with_clause(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 143c8e1c9..2a74d9925 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -509,6 +509,12 @@ pub trait Dialect: Debug + Any { fn allow_extract_single_quotes(&self) -> bool { false } + + /// Does the dialect support with clause in create index statement? + /// e.g. `CREATE INDEX idx ON t WITH (key = value, key2)` + fn supports_create_index_with_clause(&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 8abaa4a5f..eba3a6989 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -162,6 +162,10 @@ impl Dialect for PostgreSqlDialect { fn allow_extract_single_quotes(&self) -> bool { true } + + fn supports_create_index_with_clause(&self) -> bool { + true + } } pub fn parse_comment(parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b57ea614e..977372656 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5324,6 +5324,17 @@ impl<'a> Parser<'a> { None }; + let with = if self.dialect.supports_create_index_with_clause() + && self.parse_keyword(Keyword::WITH) + { + self.expect_token(&Token::LParen)?; + let with_params = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + with_params + } else { + Vec::new() + }; + let predicate = if self.parse_keyword(Keyword::WHERE) { Some(self.parse_expr()?) } else { @@ -5340,6 +5351,7 @@ impl<'a> Parser<'a> { if_not_exists, include, nulls_distinct, + with, predicate, })) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index dd4aad146..fbe97171b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7554,6 +7554,7 @@ fn test_create_index_with_using_function() { if_not_exists, include, nulls_distinct: None, + with, predicate: None, }) => { assert_eq!("idx_name", name.to_string()); @@ -7564,6 +7565,52 @@ fn test_create_index_with_using_function() { assert!(!concurrently); assert!(if_not_exists); assert!(include.is_empty()); + assert!(with.is_empty()); + } + _ => unreachable!(), + } +} + +#[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")), + asc: None, + nulls_first: None, + with_fill: None, + }]; + let with_parameters = vec![ + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("fillfactor"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(number("70"))), + }, + Expr::Identifier(Ident::new("single_param")), + ]; + let dialects = all_dialects_where(|d| d.supports_create_index_with_clause()); + match dialects.verified_stmt(sql) { + Statement::CreateIndex(CreateIndex { + name: Some(name), + table_name, + using: None, + columns, + unique, + concurrently, + if_not_exists, + include, + nulls_distinct: None, + with, + predicate: None, + }) => { + pretty_assertions::assert_eq!("title_idx", name.to_string()); + pretty_assertions::assert_eq!("films", table_name.to_string()); + pretty_assertions::assert_eq!(indexed_columns, columns); + assert!(unique); + assert!(!concurrently); + assert!(!if_not_exists); + assert!(include.is_empty()); + pretty_assertions::assert_eq!(with_parameters, with); } _ => unreachable!(), } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 59afd7402..d96211823 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2251,6 +2251,7 @@ fn parse_create_index() { if_not_exists, nulls_distinct: None, include, + with, predicate: None, }) => { assert_eq_vec(&["my_index"], &name); @@ -2261,6 +2262,7 @@ fn parse_create_index() { assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); + assert!(with.is_empty()); } _ => unreachable!(), } @@ -2280,6 +2282,7 @@ fn parse_create_anonymous_index() { if_not_exists, include, nulls_distinct: None, + with, predicate: None, }) => { assert_eq!(None, name); @@ -2290,6 +2293,7 @@ fn parse_create_anonymous_index() { assert!(!if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); + assert!(with.is_empty()); } _ => unreachable!(), } @@ -2309,6 +2313,7 @@ fn parse_create_index_concurrently() { if_not_exists, include, nulls_distinct: None, + with, predicate: None, }) => { assert_eq_vec(&["my_index"], &name); @@ -2319,6 +2324,7 @@ fn parse_create_index_concurrently() { assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); + assert!(with.is_empty()); } _ => unreachable!(), } @@ -2338,6 +2344,7 @@ fn parse_create_index_with_predicate() { if_not_exists, include, nulls_distinct: None, + with, predicate: Some(_), }) => { assert_eq_vec(&["my_index"], &name); @@ -2348,6 +2355,7 @@ fn parse_create_index_with_predicate() { assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); + assert!(with.is_empty()); } _ => unreachable!(), } @@ -2367,6 +2375,7 @@ fn parse_create_index_with_include() { if_not_exists, include, nulls_distinct: None, + with, predicate: None, }) => { assert_eq_vec(&["my_index"], &name); @@ -2377,6 +2386,7 @@ fn parse_create_index_with_include() { assert!(if_not_exists); assert_eq_vec(&["col1", "col2"], &columns); assert_eq_vec(&["col3"], &include); + assert!(with.is_empty()); } _ => unreachable!(), } @@ -2396,6 +2406,7 @@ fn parse_create_index_with_nulls_distinct() { if_not_exists, include, nulls_distinct: Some(nulls_distinct), + with, predicate: None, }) => { assert_eq_vec(&["my_index"], &name); @@ -2407,6 +2418,7 @@ fn parse_create_index_with_nulls_distinct() { assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); assert!(!nulls_distinct); + assert!(with.is_empty()); } _ => unreachable!(), } @@ -2423,6 +2435,7 @@ fn parse_create_index_with_nulls_distinct() { if_not_exists, include, nulls_distinct: Some(nulls_distinct), + with, predicate: None, }) => { assert_eq_vec(&["my_index"], &name); @@ -2434,6 +2447,7 @@ fn parse_create_index_with_nulls_distinct() { assert_eq_vec(&["col1", "col2"], &columns); assert!(include.is_empty()); assert!(nulls_distinct); + assert!(with.is_empty()); } _ => unreachable!(), } From 1bed87a8ea2c5d92f8a45ece093be8c0726d6963 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Fri, 6 Sep 2024 01:13:35 +1000 Subject: [PATCH 535/806] Suppor postgres `TRUNCATE` syntax (#1406) --- src/ast/mod.rs | 77 ++++++++++++++++++++++++++++++++-- src/keywords.rs | 1 + src/parser/mod.rs | 36 +++++++++++++++- tests/sqlparser_postgres.rs | 82 ++++++++++++++++++++++++++++++++++--- 4 files changed, 185 insertions(+), 11 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0e6357cbc..2bb7a161a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2011,11 +2011,19 @@ pub enum Statement { /// ``` /// Truncate (Hive) Truncate { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, + table_names: Vec, partitions: Option>, /// 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 + /// [ CASCADE | RESTRICT ] + cascade: Option, }, /// ```sql /// MSCK @@ -3131,12 +3139,35 @@ impl fmt::Display for Statement { Ok(()) } Statement::Truncate { - table_name, + table_names, partitions, table, + only, + identity, + cascade, } => { let table = if *table { "TABLE " } else { "" }; - write!(f, "TRUNCATE {table}{table_name}")?; + let only = if *only { "ONLY " } else { "" }; + + write!( + f, + "TRUNCATE {table}{only}{table_names}", + table_names = display_comma_separated(table_names) + )?; + + if let Some(identity) = identity { + match identity { + TruncateIdentityOption::Restart => write!(f, " RESTART IDENTITY")?, + TruncateIdentityOption::Continue => write!(f, " CONTINUE IDENTITY")?, + } + } + if let Some(cascade) = cascade { + match cascade { + TruncateCascadeOption::Cascade => write!(f, " CASCADE")?, + TruncateCascadeOption::Restrict => write!(f, " RESTRICT")?, + } + } + if let Some(ref parts) = partitions { if !parts.is_empty() { write!(f, " PARTITION ({})", display_comma_separated(parts))?; @@ -4587,6 +4618,44 @@ impl fmt::Display for SequenceOptions { } } +/// Target of a `TRUNCATE TABLE` command +/// +/// Note this is its own struct because `visit_relation` requires an `ObjectName` (not a `Vec`) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct TruncateTableTarget { + /// name of the table being truncated + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, +} + +impl fmt::Display for TruncateTableTarget { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +/// PostgreSQL identity option for TRUNCATE table +/// [ RESTART IDENTITY | CONTINUE IDENTITY ] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TruncateIdentityOption { + Restart, + Continue, +} + +/// PostgreSQL cascade option for TRUNCATE table +/// [ CASCADE | RESTRICT ] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TruncateCascadeOption { + Cascade, + Restrict, +} + /// Can use to describe options in create sequence or table column type identity /// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/keywords.rs b/src/keywords.rs index ce4972f98..ae0f14f18 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -177,6 +177,7 @@ define_keywords!( CONNECTION, CONSTRAINT, CONTAINS, + CONTINUE, CONVERT, COPY, COPY_OPTIONS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 977372656..30e776787 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -681,17 +681,49 @@ impl<'a> Parser<'a> { pub fn parse_truncate(&mut self) -> Result { let table = self.parse_keyword(Keyword::TABLE); - let table_name = self.parse_object_name(false)?; + let only = self.parse_keyword(Keyword::ONLY); + + let table_names = self + .parse_comma_separated(|p| p.parse_object_name(false))? + .into_iter() + .map(|n| TruncateTableTarget { name: n }) + .collect(); + let mut partitions = None; if self.parse_keyword(Keyword::PARTITION) { self.expect_token(&Token::LParen)?; partitions = Some(self.parse_comma_separated(Parser::parse_expr)?); self.expect_token(&Token::RParen)?; } + + let mut identity = None; + let mut cascade = None; + + if dialect_of!(self is PostgreSqlDialect | GenericDialect) { + identity = if self.parse_keywords(&[Keyword::RESTART, Keyword::IDENTITY]) { + Some(TruncateIdentityOption::Restart) + } else if self.parse_keywords(&[Keyword::CONTINUE, Keyword::IDENTITY]) { + Some(TruncateIdentityOption::Continue) + } else { + None + }; + + cascade = if self.parse_keyword(Keyword::CASCADE) { + Some(TruncateCascadeOption::Cascade) + } else if self.parse_keyword(Keyword::RESTRICT) { + Some(TruncateCascadeOption::Restrict) + } else { + None + }; + }; + Ok(Statement::Truncate { - table_name, + table_names, partitions, table, + only, + identity, + cascade, }) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d96211823..1ebb5d54c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -571,6 +571,10 @@ fn parse_alter_table_constraints_rename() { fn parse_alter_table_disable() { pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE ROW LEVEL SECURITY"); pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE RULE rule_name"); +} + +#[test] +fn parse_alter_table_disable_trigger() { pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE TRIGGER ALL"); pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE TRIGGER USER"); pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE TRIGGER trigger_name"); @@ -589,6 +593,13 @@ fn parse_alter_table_enable() { pg_and_generic().verified_stmt("ALTER TABLE tab ENABLE TRIGGER trigger_name"); } +#[test] +fn parse_truncate_table() { + pg_and_generic() + .verified_stmt("TRUNCATE TABLE \"users\", \"orders\" RESTART IDENTITY RESTRICT"); + pg_and_generic().verified_stmt("TRUNCATE users, orders RESTART IDENTITY"); +} + #[test] fn parse_create_extension() { pg_and_generic().verified_stmt("CREATE EXTENSION extension_name"); @@ -3967,11 +3978,72 @@ fn parse_select_group_by_cube() { #[test] fn parse_truncate() { let truncate = pg_and_generic().verified_stmt("TRUNCATE db.table_name"); + let table_name = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); + let table_names = vec![TruncateTableTarget { + name: table_name.clone(), + }]; + assert_eq!( + Statement::Truncate { + table_names, + partitions: None, + table: false, + only: false, + identity: None, + cascade: None, + }, + truncate + ); +} + +#[test] +fn parse_truncate_with_options() { + let truncate = pg_and_generic() + .verified_stmt("TRUNCATE TABLE ONLY db.table_name RESTART IDENTITY CASCADE"); + + let table_name = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); + let table_names = vec![TruncateTableTarget { + name: table_name.clone(), + }]; + assert_eq!( Statement::Truncate { - table_name: ObjectName(vec![Ident::new("db"), Ident::new("table_name")]), + table_names, partitions: None, - table: false + table: true, + only: true, + identity: Some(TruncateIdentityOption::Restart), + cascade: Some(TruncateCascadeOption::Cascade) + }, + truncate + ); +} + +#[test] +fn parse_truncate_with_table_list() { + let truncate = pg().verified_stmt( + "TRUNCATE TABLE db.table_name, db.other_table_name RESTART IDENTITY CASCADE", + ); + + let table_name_a = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); + let table_name_b = ObjectName(vec![Ident::new("db"), Ident::new("other_table_name")]); + + let table_names = vec![ + TruncateTableTarget { + name: table_name_a.clone(), + }, + TruncateTableTarget { + name: table_name_b.clone(), + }, + ]; + + assert_eq!( + Statement::Truncate { + table_names, + partitions: None, + table: true, + only: false, + identity: Some(TruncateIdentityOption::Restart), + cascade: Some(TruncateCascadeOption::Cascade) }, truncate ); @@ -4745,12 +4817,12 @@ fn parse_trigger_related_functions() { IF NEW.salary IS NULL THEN RAISE EXCEPTION '% cannot have null salary', NEW.empname; END IF; - + -- Who works for us when they must pay for it? IF NEW.salary < 0 THEN RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; END IF; - + -- Remember who changed the payroll when NEW.last_date := current_timestamp; NEW.last_user := current_user; @@ -4883,7 +4955,7 @@ fn parse_trigger_related_functions() { Expr::Value( Value::DollarQuotedString( DollarQuotedString { - value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n \n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n \n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(), + value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n\n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n\n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(), tag: Some( "emp_stamp".to_owned(), ), From aa714e3447ebb7e470d746607a9c6441717f1b46 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Fri, 6 Sep 2024 15:16:09 +0100 Subject: [PATCH 536/806] Fix `INTERVAL` parsing to support expressions and units via dialect (#1398) --- src/dialect/ansi.rs | 4 + src/dialect/bigquery.rs | 4 + src/dialect/clickhouse.rs | 4 + src/dialect/databricks.rs | 4 + src/dialect/hive.rs | 4 + src/dialect/mod.rs | 21 +++ src/dialect/mysql.rs | 4 + src/parser/mod.rs | 145 +++++++++----------- tests/sqlparser_bigquery.rs | 21 ++- tests/sqlparser_common.rs | 254 ++++++++++++++++++++++++++++++------ 10 files changed, 331 insertions(+), 134 deletions(-) diff --git a/src/dialect/ansi.rs b/src/dialect/ansi.rs index d07bc07eb..61ae5829e 100644 --- a/src/dialect/ansi.rs +++ b/src/dialect/ansi.rs @@ -24,4 +24,8 @@ impl Dialect for AnsiDialect { fn is_identifier_part(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_' } + + fn require_interval_qualifier(&self) -> bool { + true + } } diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index d3673337f..3bce6702b 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -63,4 +63,8 @@ impl Dialect for BigQueryDialect { fn supports_select_wildcard_except(&self) -> bool { true } + + fn require_interval_qualifier(&self) -> bool { + true + } } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 34940467a..09735cbee 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -37,4 +37,8 @@ impl Dialect for ClickHouseDialect { fn describe_requires_table_keyword(&self) -> bool { true } + + fn require_interval_qualifier(&self) -> bool { + true + } } diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index 42d432d3d..d3661444b 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -38,4 +38,8 @@ impl Dialect for DatabricksDialect { fn supports_select_wildcard_except(&self) -> bool { true } + + fn require_interval_qualifier(&self) -> bool { + true + } } diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index 2340df611..b32d44cb9 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -42,4 +42,8 @@ impl Dialect for HiveDialect { fn supports_numeric_prefix(&self) -> bool { true } + + fn require_interval_qualifier(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 2a74d9925..0be8c17c7 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -515,6 +515,27 @@ pub trait Dialect: Debug + Any { fn supports_create_index_with_clause(&self) -> bool { false } + + /// Whether `INTERVAL` expressions require units (called "qualifiers" in the ANSI SQL spec) to be specified, + /// e.g. `INTERVAL 1 DAY` vs `INTERVAL 1`. + /// + /// Expressions within intervals (e.g. `INTERVAL '1' + '1' DAY`) are only allowed when units are required. + /// + /// See for more information. + /// + /// When `true`: + /// * `INTERVAL '1' DAY` is VALID + /// * `INTERVAL 1 + 1 DAY` is VALID + /// * `INTERVAL '1' + '1' DAY` is VALID + /// * `INTERVAL '1'` is INVALID + /// + /// When `false`: + /// * `INTERVAL '1'` is VALID + /// * `INTERVAL '1' DAY` is VALID — unit is not required, but still allowed + /// * `INTERVAL 1 + 1 DAY` is INVALID + fn require_interval_qualifier(&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 32525658b..b8c4631fd 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -84,6 +84,10 @@ impl Dialect for MySqlDialect { None } } + + fn require_interval_qualifier(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 30e776787..26e9e05fc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -56,15 +56,6 @@ macro_rules! parser_err { }; } -// Returns a successful result if the optional expression is some -macro_rules! return_ok_if_some { - ($e:expr) => {{ - if let Some(v) = $e { - return Ok(v); - } - }}; -} - #[cfg(feature = "std")] /// Implementation [`RecursionCounter`] if std is available mod recursion { @@ -928,35 +919,6 @@ impl<'a> Parser<'a> { Ok(expr) } - pub fn parse_interval_expr(&mut self) -> Result { - let precedence = self.dialect.prec_unknown(); - let mut expr = self.parse_prefix()?; - - loop { - let next_precedence = self.get_next_interval_precedence()?; - - if precedence >= next_precedence { - break; - } - - expr = self.parse_infix(expr, next_precedence)?; - } - - Ok(expr) - } - - /// Get the precedence of the next token, with AND, OR, and XOR. - pub fn get_next_interval_precedence(&self) -> Result { - let token = self.peek_token(); - - match token.token { - Token::Word(w) if w.keyword == Keyword::AND => Ok(self.dialect.prec_unknown()), - Token::Word(w) if w.keyword == Keyword::OR => Ok(self.dialect.prec_unknown()), - Token::Word(w) if w.keyword == Keyword::XOR => Ok(self.dialect.prec_unknown()), - _ => self.get_next_precedence(), - } - } - pub fn parse_assert(&mut self) -> Result { let condition = self.parse_expr()?; let message = if self.parse_keyword(Keyword::AS) { @@ -1004,7 +966,7 @@ impl<'a> Parser<'a> { // name is not followed by a string literal, but in fact in PostgreSQL it is a valid // expression that should parse as the column name "date". let loc = self.peek_token().location; - return_ok_if_some!(self.maybe_parse(|parser| { + let opt_expr = self.maybe_parse(|parser| { match parser.parse_data_type()? { DataType::Interval => parser.parse_interval(), // PostgreSQL allows almost any identifier to be used as custom data type name, @@ -1020,7 +982,11 @@ impl<'a> Parser<'a> { value: parser.parse_literal_string()?, }), } - })); + }); + + if let Some(expr) = opt_expr { + return Ok(expr); + } let next_token = self.next_token(); let expr = match next_token.token { @@ -2110,52 +2076,32 @@ impl<'a> Parser<'a> { // don't currently try to parse it. (The sign can instead be included // inside the value string.) - // The first token in an interval is a string literal which specifies - // the duration of the interval. - let value = self.parse_interval_expr()?; + // to match the different flavours of INTERVAL syntax, we only allow expressions + // if the dialect requires an interval qualifier, + // see https://github.com/sqlparser-rs/sqlparser-rs/pull/1398 for more details + let value = if self.dialect.require_interval_qualifier() { + // parse a whole expression so `INTERVAL 1 + 1 DAY` is valid + self.parse_expr()? + } else { + // parse a prefix expression so `INTERVAL 1 DAY` is valid, but `INTERVAL 1 + 1 DAY` is not + // this also means that `INTERVAL '5 days' > INTERVAL '1 day'` treated properly + self.parse_prefix()? + }; // Following the string literal is a qualifier which indicates the units // of the duration specified in the string literal. // // Note that PostgreSQL allows omitting the qualifier, so we provide // this more general implementation. - let leading_field = match self.peek_token().token { - Token::Word(kw) - if [ - Keyword::YEAR, - Keyword::MONTH, - Keyword::WEEK, - Keyword::DAY, - Keyword::HOUR, - Keyword::MINUTE, - Keyword::SECOND, - Keyword::CENTURY, - Keyword::DECADE, - Keyword::DOW, - Keyword::DOY, - Keyword::EPOCH, - Keyword::ISODOW, - Keyword::ISOYEAR, - Keyword::JULIAN, - Keyword::MICROSECOND, - Keyword::MICROSECONDS, - Keyword::MILLENIUM, - Keyword::MILLENNIUM, - Keyword::MILLISECOND, - Keyword::MILLISECONDS, - Keyword::NANOSECOND, - Keyword::NANOSECONDS, - Keyword::QUARTER, - Keyword::TIMEZONE, - Keyword::TIMEZONE_HOUR, - Keyword::TIMEZONE_MINUTE, - ] - .iter() - .any(|d| kw.keyword == *d) => - { - Some(self.parse_date_time_field()?) - } - _ => None, + let leading_field = if self.next_token_is_temporal_unit() { + Some(self.parse_date_time_field()?) + } else if self.dialect.require_interval_qualifier() { + return parser_err!( + "INTERVAL requires a unit after the literal value", + self.peek_token().location + ); + } else { + None }; let (leading_precision, last_field, fsec_precision) = @@ -2192,6 +2138,45 @@ impl<'a> Parser<'a> { })) } + /// Peek at the next token and determine if it is a temporal unit + /// like `second`. + pub fn next_token_is_temporal_unit(&mut self) -> bool { + if let Token::Word(word) = self.peek_token().token { + matches!( + word.keyword, + Keyword::YEAR + | Keyword::MONTH + | Keyword::WEEK + | Keyword::DAY + | Keyword::HOUR + | Keyword::MINUTE + | Keyword::SECOND + | Keyword::CENTURY + | Keyword::DECADE + | Keyword::DOW + | Keyword::DOY + | Keyword::EPOCH + | Keyword::ISODOW + | Keyword::ISOYEAR + | Keyword::JULIAN + | Keyword::MICROSECOND + | Keyword::MICROSECONDS + | Keyword::MILLENIUM + | Keyword::MILLENNIUM + | Keyword::MILLISECOND + | Keyword::MILLISECONDS + | Keyword::NANOSECOND + | Keyword::NANOSECONDS + | Keyword::QUARTER + | Keyword::TIMEZONE + | Keyword::TIMEZONE_HOUR + | Keyword::TIMEZONE_MINUTE + ) + } else { + false + } + } + /// Bigquery specific: Parse a struct literal /// Syntax /// ```sql diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 57cf9d7fd..4f84b376d 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -13,7 +13,6 @@ #[macro_use] mod test_utils; -use sqlparser::ast; use std::ops::Deref; use sqlparser::ast::*; @@ -830,16 +829,14 @@ fn parse_typed_struct_syntax_bigquery() { expr_from_projection(&select.projection[3]) ); - let sql = r#"SELECT STRUCT(INTERVAL '1-2 3 4:5:6.789999'), STRUCT(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#; + let sql = r#"SELECT STRUCT(INTERVAL '2' HOUR), STRUCT(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#; let select = bigquery().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Interval(ast::Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "1-2 3 4:5:6.789999".to_string() - ))), - leading_field: None, + values: vec![Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString("2".to_string()))), + leading_field: Some(DateTimeField::Hour), leading_precision: None, last_field: None, fractional_seconds_precision: None @@ -1141,16 +1138,14 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { expr_from_projection(&select.projection[3]) ); - let sql = r#"SELECT STRUCT(INTERVAL '1-2 3 4:5:6.789999'), STRUCT(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#; + let sql = r#"SELECT STRUCT(INTERVAL '1' MONTH), STRUCT(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#; let select = bigquery_and_generic().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Interval(ast::Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "1-2 3 4:5:6.789999".to_string() - ))), - leading_field: None, + values: vec![Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + leading_field: Some(DateTimeField::Month), leading_precision: None, last_field: None, fractional_seconds_precision: None diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fbe97171b..0ed677db9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4472,7 +4472,8 @@ fn parse_window_functions() { sum(qux) OVER (ORDER BY a \ GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) \ FROM foo"; - let select = verified_only_select(sql); + let dialects = all_dialects_except(|d| d.require_interval_qualifier()); + let select = dialects.verified_only_select(sql); const EXPECTED_PROJ_QTY: usize = 7; assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); @@ -4886,7 +4887,9 @@ fn parse_literal_timestamp_with_time_zone() { } #[test] -fn parse_interval() { +fn parse_interval_all() { + // these intervals expressions all work with both variants of INTERVAL + let sql = "SELECT INTERVAL '1-1' YEAR TO MONTH"; let select = verified_only_select(sql); assert_eq!( @@ -4954,23 +4957,6 @@ fn parse_interval() { expr_from_projection(only(&select.projection)), ); - let sql = "SELECT INTERVAL 1 + 1 DAY"; - let select = verified_only_select(sql); - assert_eq!( - &Expr::Interval(Interval { - value: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), - op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), - }), - leading_field: Some(DateTimeField::Day), - leading_precision: None, - last_field: None, - fractional_seconds_precision: None, - }), - expr_from_projection(only(&select.projection)), - ); - let sql = "SELECT INTERVAL '10' HOUR (1)"; let select = verified_only_select(sql); assert_eq!( @@ -4984,21 +4970,6 @@ fn parse_interval() { expr_from_projection(only(&select.projection)), ); - let sql = "SELECT INTERVAL '1 DAY'"; - let select = verified_only_select(sql); - assert_eq!( - &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 DAY" - )))), - leading_field: None, - leading_precision: None, - last_field: None, - fractional_seconds_precision: None, - }), - expr_from_projection(only(&select.projection)), - ); - let result = parse_sql_statements("SELECT INTERVAL '1' SECOND TO SECOND"); assert_eq!( ParserError::ParserError("Expected: end of statement, found: SECOND".to_string()), @@ -5024,12 +4995,212 @@ fn parse_interval() { verified_only_select("SELECT INTERVAL '1' HOUR TO MINUTE"); verified_only_select("SELECT INTERVAL '1' HOUR TO SECOND"); verified_only_select("SELECT INTERVAL '1' MINUTE TO SECOND"); - verified_only_select("SELECT INTERVAL '1 YEAR'"); - verified_only_select("SELECT INTERVAL '1 YEAR' AS one_year"); - one_statement_parses_to( + verified_only_select("SELECT INTERVAL 1 YEAR"); + verified_only_select("SELECT INTERVAL 1 MONTH"); + verified_only_select("SELECT INTERVAL 1 DAY"); + verified_only_select("SELECT INTERVAL 1 HOUR"); + verified_only_select("SELECT INTERVAL 1 MINUTE"); + verified_only_select("SELECT INTERVAL 1 SECOND"); +} + +#[test] +fn parse_interval_dont_require_unit() { + let dialects = all_dialects_except(|d| d.require_interval_qualifier()); + + let sql = "SELECT INTERVAL '1 DAY'"; + let select = dialects.verified_only_select(sql); + assert_eq!( + &Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( + "1 DAY" + )))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + }), + expr_from_projection(only(&select.projection)), + ); + dialects.verified_only_select("SELECT INTERVAL '1 YEAR'"); + dialects.verified_only_select("SELECT INTERVAL '1 MONTH'"); + dialects.verified_only_select("SELECT INTERVAL '1 DAY'"); + dialects.verified_only_select("SELECT INTERVAL '1 HOUR'"); + dialects.verified_only_select("SELECT INTERVAL '1 MINUTE'"); + dialects.verified_only_select("SELECT INTERVAL '1 SECOND'"); +} + +#[test] +fn parse_interval_require_unit() { + let dialects = all_dialects_where(|d| d.require_interval_qualifier()); + + let sql = "SELECT INTERVAL '1 DAY'"; + let err = dialects.parse_sql_statements(sql).unwrap_err(); + assert_eq!( + err.to_string(), + "sql parser error: INTERVAL requires a unit after the literal value" + ) +} + +#[test] +fn parse_interval_require_qualifier() { + let dialects = all_dialects_where(|d| d.require_interval_qualifier()); + + let sql = "SELECT INTERVAL 1 + 1 DAY"; + let select = dialects.verified_only_select(sql); + assert_eq!( + expr_from_projection(only(&select.projection)), + &Expr::Interval(Interval { + value: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Value(number("1"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Value(number("1"))), + }), + leading_field: Some(DateTimeField::Day), + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + }), + ); + + let sql = "SELECT INTERVAL '1' + '1' DAY"; + let select = dialects.verified_only_select(sql); + assert_eq!( + expr_from_projection(only(&select.projection)), + &Expr::Interval(Interval { + value: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + }), + leading_field: Some(DateTimeField::Day), + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + }), + ); + + let sql = "SELECT INTERVAL '1' + '2' - '3' DAY"; + let select = dialects.verified_only_select(sql); + assert_eq!( + expr_from_projection(only(&select.projection)), + &Expr::Interval(Interval { + value: Box::new(Expr::BinaryOp { + left: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Value(Value::SingleQuotedString("2".to_string()))), + }), + op: BinaryOperator::Minus, + right: Box::new(Expr::Value(Value::SingleQuotedString("3".to_string()))), + }), + leading_field: Some(DateTimeField::Day), + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + }), + ); +} + +#[test] +fn parse_interval_disallow_interval_expr() { + let dialects = all_dialects_except(|d| d.require_interval_qualifier()); + + let sql = "SELECT INTERVAL '1 DAY'"; + let select = dialects.verified_only_select(sql); + assert_eq!( + expr_from_projection(only(&select.projection)), + &Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( + "1 DAY" + )))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + }), + ); + + dialects.verified_only_select("SELECT INTERVAL '1 YEAR'"); + dialects.verified_only_select("SELECT INTERVAL '1 YEAR' AS one_year"); + dialects.one_statement_parses_to( "SELECT INTERVAL '1 YEAR' one_year", "SELECT INTERVAL '1 YEAR' AS one_year", ); + + let sql = "SELECT INTERVAL '1 DAY' > INTERVAL '1 SECOND'"; + let select = dialects.verified_only_select(sql); + assert_eq!( + expr_from_projection(only(&select.projection)), + &Expr::BinaryOp { + left: Box::new(Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( + "1 DAY" + )))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + })), + op: BinaryOperator::Gt, + right: Box::new(Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( + "1 SECOND" + )))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + })) + } + ); +} + +#[test] +fn interval_disallow_interval_expr_gt() { + let dialects = all_dialects_except(|d| d.require_interval_qualifier()); + let expr = dialects.verified_expr("INTERVAL '1 second' > x"); + assert_eq!( + expr, + Expr::BinaryOp { + left: Box::new(Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString( + "1 second".to_string() + ))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + },)), + op: BinaryOperator::Gt, + right: Box::new(Expr::Identifier(Ident { + value: "x".to_string(), + quote_style: None, + })), + } + ) +} + +#[test] +fn interval_disallow_interval_expr_double_colon() { + let dialects = all_dialects_except(|d| d.require_interval_qualifier()); + let expr = dialects.verified_expr("INTERVAL '1 second'::TEXT"); + assert_eq!( + expr, + Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Interval(Interval { + value: Box::new(Expr::Value(Value::SingleQuotedString( + "1 second".to_string() + ))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + })), + data_type: DataType::Text, + format: None, + } + ) } #[test] @@ -5038,7 +5209,8 @@ fn parse_interval_and_or_xor() { WHERE d3_date > d1_date + INTERVAL '5 days' \ AND d2_date > d1_date + INTERVAL '3 days'"; - let actual_ast = Parser::parse_sql(&GenericDialect {}, sql).unwrap(); + let dialects = all_dialects_except(|d| d.require_interval_qualifier()); + let actual_ast = dialects.parse_sql_statements(sql).unwrap(); let expected_ast = vec![Statement::Query(Box::new(Query { with: None, @@ -5140,19 +5312,19 @@ fn parse_interval_and_or_xor() { assert_eq!(actual_ast, expected_ast); - verified_stmt( + dialects.verified_stmt( "SELECT col FROM test \ WHERE d3_date > d1_date + INTERVAL '5 days' \ AND d2_date > d1_date + INTERVAL '3 days'", ); - verified_stmt( + dialects.verified_stmt( "SELECT col FROM test \ WHERE d3_date > d1_date + INTERVAL '5 days' \ OR d2_date > d1_date + INTERVAL '3 days'", ); - verified_stmt( + dialects.verified_stmt( "SELECT col FROM test \ WHERE d3_date > d1_date + INTERVAL '5 days' \ XOR d2_date > d1_date + INTERVAL '3 days'", From 4875dadbf5e691dd1e8ef179145829d8ca3ef4d8 Mon Sep 17 00:00:00 2001 From: Emil Ejbyfeldt Date: Fri, 6 Sep 2024 16:17:20 +0200 Subject: [PATCH 537/806] fix: Fix stack overflow in parse_subexpr (#1410) --- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 26e9e05fc..413f5a690 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -897,12 +897,12 @@ impl<'a> Parser<'a> { /// Parse a new expression. pub fn parse_expr(&mut self) -> Result { - let _guard = self.recursion_counter.try_decrease()?; self.parse_subexpr(self.dialect.prec_unknown()) } /// Parse tokens until the precedence changes. pub fn parse_subexpr(&mut self, precedence: u8) -> Result { + let _guard = self.recursion_counter.try_decrease()?; debug!("parsing expr"); let mut expr = self.parse_prefix()?; debug!("prefix: {:?}", expr); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0ed677db9..0f7a88d74 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8980,6 +8980,13 @@ fn parse_deeply_nested_parens_hits_recursion_limits() { assert_eq!(ParserError::RecursionLimitExceeded, res.unwrap_err()); } +#[test] +fn parse_deeply_nested_unary_op_hits_recursion_limits() { + let sql = format!("SELECT {}", "+".repeat(1000)); + let res = parse_sql_statements(&sql); + assert_eq!(ParserError::RecursionLimitExceeded, res.unwrap_err()); +} + #[test] fn parse_deeply_nested_expr_hits_recursion_limits() { let dialect = GenericDialect {}; From a7b49b5072c0fd00d1166cf11dd4091c2455caa1 Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 11 Sep 2024 03:26:07 +0800 Subject: [PATCH 538/806] Add support of `DROP|CLEAR|MATERIALIZE PROJECTION` syntax for ClickHouse (#1417) --- src/ast/ddl.rs | 64 +++++++++++++++++++++++ src/keywords.rs | 2 + src/parser/mod.rs | 36 +++++++++++++ tests/sqlparser_clickhouse.rs | 96 +++++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 2d1778c7b..b5444b8da 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -57,6 +57,33 @@ pub enum AlterTableOperation { name: Ident, select: ProjectionSelect, }, + + /// `DROP PROJECTION [IF EXISTS] name` + /// + /// Note: this is a ClickHouse-specific operation. + /// Please refer to [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#drop-projection) + DropProjection { if_exists: bool, name: Ident }, + + /// `MATERIALIZE PROJECTION [IF EXISTS] name [IN PARTITION partition_name]` + /// + /// Note: this is a ClickHouse-specific operation. + /// Please refer to [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#materialize-projection) + MaterializeProjection { + if_exists: bool, + name: Ident, + partition: Option, + }, + + /// `CLEAR PROJECTION [IF EXISTS] name [IN PARTITION partition_name]` + /// + /// Note: this is a ClickHouse-specific operation. + /// Please refer to [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#clear-projection) + ClearProjection { + if_exists: bool, + name: Ident, + partition: Option, + }, + /// `DISABLE ROW LEVEL SECURITY` /// /// Note: this is a PostgreSQL-specific operation. @@ -275,6 +302,43 @@ impl fmt::Display for AlterTableOperation { } write!(f, " {} ({})", name, query) } + AlterTableOperation::DropProjection { if_exists, name } => { + write!(f, "DROP PROJECTION")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", name) + } + AlterTableOperation::MaterializeProjection { + if_exists, + name, + partition, + } => { + write!(f, "MATERIALIZE PROJECTION")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", name)?; + if let Some(partition) = partition { + write!(f, " IN PARTITION {}", partition)?; + } + Ok(()) + } + AlterTableOperation::ClearProjection { + if_exists, + name, + partition, + } => { + write!(f, "CLEAR PROJECTION")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", name)?; + if let Some(partition) = partition { + write!(f, " IN PARTITION {}", partition)?; + } + Ok(()) + } AlterTableOperation::AlterColumn { column_name, op } => { write!(f, "ALTER COLUMN {column_name} {op}") } diff --git a/src/keywords.rs b/src/keywords.rs index ae0f14f18..3ee447cfe 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -153,6 +153,7 @@ define_keywords!( CHARSET, CHAR_LENGTH, CHECK, + CLEAR, CLOB, CLONE, CLOSE, @@ -450,6 +451,7 @@ define_keywords!( MATCHES, MATCH_CONDITION, MATCH_RECOGNIZE, + MATERIALIZE, MATERIALIZED, MAX, MAXVALUE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 413f5a690..70454e119 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6615,6 +6615,36 @@ impl<'a> Parser<'a> { self.peek_token(), ); } + } else if self.parse_keywords(&[Keyword::CLEAR, Keyword::PROJECTION]) + && dialect_of!(self is ClickHouseDialect|GenericDialect) + { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier(false)?; + let partition = if self.parse_keywords(&[Keyword::IN, Keyword::PARTITION]) { + Some(self.parse_identifier(false)?) + } else { + None + }; + AlterTableOperation::ClearProjection { + if_exists, + name, + partition, + } + } else if self.parse_keywords(&[Keyword::MATERIALIZE, Keyword::PROJECTION]) + && dialect_of!(self is ClickHouseDialect|GenericDialect) + { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier(false)?; + let partition = if self.parse_keywords(&[Keyword::IN, Keyword::PARTITION]) { + Some(self.parse_identifier(false)?) + } else { + None + }; + AlterTableOperation::MaterializeProjection { + if_exists, + name, + partition, + } } else if self.parse_keyword(Keyword::DROP) { if self.parse_keywords(&[Keyword::IF, Keyword::EXISTS, Keyword::PARTITION]) { self.expect_token(&Token::LParen)?; @@ -6645,6 +6675,12 @@ impl<'a> Parser<'a> { && dialect_of!(self is MySqlDialect | GenericDialect) { AlterTableOperation::DropPrimaryKey + } else if self.parse_keyword(Keyword::PROJECTION) + && dialect_of!(self is ClickHouseDialect|GenericDialect) + { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier(false)?; + AlterTableOperation::DropProjection { if_exists, name } } else { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index c8157bced..f89da7ee9 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -359,6 +359,102 @@ fn parse_alter_table_add_projection() { ); } +#[test] +fn parse_alter_table_drop_projection() { + match clickhouse_and_generic().verified_stmt("ALTER TABLE t0 DROP PROJECTION IF EXISTS my_name") + { + Statement::AlterTable { + name, operations, .. + } => { + assert_eq!(name, ObjectName(vec!["t0".into()])); + assert_eq!(1, operations.len()); + assert_eq!( + operations[0], + AlterTableOperation::DropProjection { + if_exists: true, + name: "my_name".into(), + } + ) + } + _ => unreachable!(), + } + // allow to skip `IF EXISTS` + clickhouse_and_generic().verified_stmt("ALTER TABLE t0 DROP PROJECTION my_name"); + + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements("ALTER TABLE t0 DROP PROJECTION") + .unwrap_err(), + ParserError("Expected: identifier, found: EOF".to_string()) + ); +} + +#[test] +fn parse_alter_table_clear_and_materialize_projection() { + for keyword in ["CLEAR", "MATERIALIZE"] { + match clickhouse_and_generic().verified_stmt( + format!("ALTER TABLE t0 {keyword} PROJECTION IF EXISTS my_name IN PARTITION p0",) + .as_str(), + ) { + Statement::AlterTable { + name, operations, .. + } => { + assert_eq!(name, ObjectName(vec!["t0".into()])); + assert_eq!(1, operations.len()); + assert_eq!( + operations[0], + if keyword == "CLEAR" { + AlterTableOperation::ClearProjection { + if_exists: true, + name: "my_name".into(), + partition: Some(Ident::new("p0")), + } + } else { + AlterTableOperation::MaterializeProjection { + if_exists: true, + name: "my_name".into(), + partition: Some(Ident::new("p0")), + } + } + ) + } + _ => unreachable!(), + } + // allow to skip `IF EXISTS` + clickhouse_and_generic().verified_stmt( + format!("ALTER TABLE t0 {keyword} PROJECTION my_name IN PARTITION p0",).as_str(), + ); + // allow to skip `IN PARTITION partition_name` + clickhouse_and_generic() + .verified_stmt(format!("ALTER TABLE t0 {keyword} PROJECTION my_name",).as_str()); + + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements(format!("ALTER TABLE t0 {keyword} PROJECTION",).as_str()) + .unwrap_err(), + ParserError("Expected: identifier, found: EOF".to_string()) + ); + + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements( + format!("ALTER TABLE t0 {keyword} PROJECTION my_name IN PARTITION",).as_str() + ) + .unwrap_err(), + ParserError("Expected: identifier, found: EOF".to_string()) + ); + + assert_eq!( + clickhouse_and_generic() + .parse_sql_statements( + format!("ALTER TABLE t0 {keyword} PROJECTION my_name IN",).as_str() + ) + .unwrap_err(), + ParserError("Expected: end of statement, found: IN".to_string()) + ); + } +} + #[test] fn parse_optimize_table() { clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0"); From 6ba60689448ccec2cdc80efe7a792f9216835fbf Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 11 Sep 2024 03:43:35 +0800 Subject: [PATCH 539/806] Fix Hive table comment should be after table column definitions (#1413) --- src/ast/dml.rs | 9 +++++++++ src/ast/mod.rs | 8 +++++++- src/parser/mod.rs | 18 ++++++++++++++---- tests/sqlparser_hive.rs | 35 ++++++++++++++++++++++++++++++++++- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 884b378db..c0e58e21a 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -226,6 +226,13 @@ impl Display for CreateTable { // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens write!(f, " ()")?; } + + // 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 { + write!(f, " COMMENT '{comment}'")?; + } + // Only for SQLite if self.without_rowid { write!(f, " WITHOUT ROWID")?; @@ -336,6 +343,8 @@ impl Display for CreateTable { CommentDef::WithoutEq(comment) => { write!(f, " COMMENT '{comment}'")?; } + // For CommentDef::AfterColumnDefsWithoutEq will be displayed after column definition + CommentDef::AfterColumnDefsWithoutEq(_) => (), } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2bb7a161a..ff949cf53 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6807,12 +6807,18 @@ 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) => write!(f, "{comment}"), + CommentDef::WithEq(comment) + | CommentDef::WithoutEq(comment) + | CommentDef::AfterColumnDefsWithoutEq(comment) => write!(f, "{comment}"), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 70454e119..b39685cbf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5598,6 +5598,17 @@ 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 + }; // SQLite supports `WITHOUT ROWID` at the end of `CREATE TABLE` let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]); @@ -5708,15 +5719,14 @@ impl<'a> Parser<'a> { let strict = self.parse_keyword(Keyword::STRICT); - let comment = if self.parse_keyword(Keyword::COMMENT) { + // Excludes Hive dialect here since it has been handled after table column definitions. + if !dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { let _ = self.consume_token(&Token::Eq); let next_token = self.next_token(); - match next_token.token { + comment = match next_token.token { Token::SingleQuotedString(str) => Some(CommentDef::WithoutEq(str)), _ => self.expected("comment", next_token)?, } - } else { - None }; // Parse optional `AS ( query )` diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 1bb4229e1..5426dfbc4 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -16,7 +16,7 @@ //! is also tested (on the inputs it can handle). use sqlparser::ast::{ - ClusteredBy, CreateFunctionBody, CreateFunctionUsing, CreateTable, Expr, Function, + ClusteredBy, CommentDef, CreateFunctionBody, CreateFunctionUsing, CreateTable, Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OneOrManyWithParens, OrderByExpr, SelectItem, Statement, TableFactor, UnaryOperator, Use, Value, }; @@ -115,6 +115,39 @@ fn create_table_like() { hive().verified_stmt(like); } +#[test] +fn create_table_with_comment() { + let sql = concat!( + "CREATE TABLE db.table_name (a INT, b STRING)", + " COMMENT 'table comment'", + " PARTITIONED BY (a INT, b STRING)", + " CLUSTERED BY (a, b) SORTED BY (a ASC, b DESC)", + " INTO 4 BUCKETS" + ); + match hive().verified_stmt(sql) { + Statement::CreateTable(CreateTable { comment, .. }) => { + assert_eq!( + comment, + Some(CommentDef::AfterColumnDefsWithoutEq( + "table comment".to_string() + )) + ) + } + _ => unreachable!(), + } + + // negative test case + let invalid_sql = concat!( + "CREATE TABLE db.table_name (a INT, b STRING)", + " PARTITIONED BY (a INT, b STRING)", + " COMMENT 'table comment'", + ); + assert_eq!( + hive().parse_sql_statements(invalid_sql).unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: COMMENT".to_string()) + ); +} + #[test] fn create_table_with_clustered_by() { let sql = concat!( From 8305c5d416b6689448f317155d996b812aa7089a Mon Sep 17 00:00:00 2001 From: gstvg <28798827+gstvg@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:48:42 -0300 Subject: [PATCH 540/806] minor: Implement common traits for OneOrManyWithParens (#1368) --- src/ast/mod.rs | 315 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 314 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ff949cf53..480442b1e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -20,6 +20,7 @@ use alloc::{ }; use core::fmt::{self, Display}; +use core::ops::Deref; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -993,7 +994,26 @@ impl fmt::Display for LambdaFunction { /// Encapsulates the common pattern in SQL where either one unparenthesized item /// such as an identifier or expression is permitted, or multiple of the same -/// item in a parenthesized list. +/// item in a parenthesized list. For accessing items regardless of the form, +/// `OneOrManyWithParens` implements `Deref` and `IntoIterator`, +/// so you can call slice methods on it and iterate over items +/// # Examples +/// Acessing as a slice: +/// ``` +/// # use sqlparser::ast::OneOrManyWithParens; +/// let one = OneOrManyWithParens::One("a"); +/// +/// assert_eq!(one[0], "a"); +/// assert_eq!(one.len(), 1); +/// ``` +/// Iterating: +/// ``` +/// # use sqlparser::ast::OneOrManyWithParens; +/// let one = OneOrManyWithParens::One("a"); +/// let many = OneOrManyWithParens::Many(vec!["a", "b"]); +/// +/// assert_eq!(one.into_iter().chain(many).collect::>(), vec!["a", "a", "b"] ); +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -1004,6 +1024,125 @@ pub enum OneOrManyWithParens { Many(Vec), } +impl Deref for OneOrManyWithParens { + type Target = [T]; + + fn deref(&self) -> &[T] { + match self { + OneOrManyWithParens::One(one) => core::slice::from_ref(one), + OneOrManyWithParens::Many(many) => many, + } + } +} + +impl AsRef<[T]> for OneOrManyWithParens { + fn as_ref(&self) -> &[T] { + self + } +} + +impl<'a, T> IntoIterator for &'a OneOrManyWithParens { + type Item = &'a T; + type IntoIter = core::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// Owned iterator implementation of `OneOrManyWithParens` +#[derive(Debug, Clone)] +pub struct OneOrManyWithParensIntoIter { + inner: OneOrManyWithParensIntoIterInner, +} + +#[derive(Debug, Clone)] +enum OneOrManyWithParensIntoIterInner { + One(core::iter::Once), + Many( as IntoIterator>::IntoIter), +} + +impl core::iter::FusedIterator for OneOrManyWithParensIntoIter +where + core::iter::Once: core::iter::FusedIterator, + as IntoIterator>::IntoIter: core::iter::FusedIterator, +{ +} + +impl core::iter::ExactSizeIterator for OneOrManyWithParensIntoIter +where + core::iter::Once: core::iter::ExactSizeIterator, + as IntoIterator>::IntoIter: core::iter::ExactSizeIterator, +{ +} + +impl core::iter::Iterator for OneOrManyWithParensIntoIter { + type Item = T; + + fn next(&mut self) -> Option { + match &mut self.inner { + OneOrManyWithParensIntoIterInner::One(one) => one.next(), + OneOrManyWithParensIntoIterInner::Many(many) => many.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.inner { + OneOrManyWithParensIntoIterInner::One(one) => one.size_hint(), + OneOrManyWithParensIntoIterInner::Many(many) => many.size_hint(), + } + } + + fn count(self) -> usize + where + Self: Sized, + { + match self.inner { + OneOrManyWithParensIntoIterInner::One(one) => one.count(), + OneOrManyWithParensIntoIterInner::Many(many) => many.count(), + } + } + + fn fold(mut self, init: B, f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + match &mut self.inner { + OneOrManyWithParensIntoIterInner::One(one) => one.fold(init, f), + OneOrManyWithParensIntoIterInner::Many(many) => many.fold(init, f), + } + } +} + +impl core::iter::DoubleEndedIterator for OneOrManyWithParensIntoIter { + fn next_back(&mut self) -> Option { + match &mut self.inner { + OneOrManyWithParensIntoIterInner::One(one) => one.next_back(), + OneOrManyWithParensIntoIterInner::Many(many) => many.next_back(), + } + } +} + +impl IntoIterator for OneOrManyWithParens { + type Item = T; + + type IntoIter = OneOrManyWithParensIntoIter; + + fn into_iter(self) -> Self::IntoIter { + let inner = match self { + OneOrManyWithParens::One(one) => { + OneOrManyWithParensIntoIterInner::One(core::iter::once(one)) + } + OneOrManyWithParens::Many(many) => { + OneOrManyWithParensIntoIterInner::Many(many.into_iter()) + } + }; + + OneOrManyWithParensIntoIter { inner } + } +} + impl fmt::Display for OneOrManyWithParens where T: fmt::Display, @@ -6984,4 +7123,178 @@ mod tests { }); assert_eq!("INTERVAL '5' SECOND (1, 3)", format!("{interval}")); } + + #[test] + fn test_one_or_many_with_parens_deref() { + use core::ops::Index; + + let one = OneOrManyWithParens::One("a"); + + assert_eq!(one.deref(), &["a"]); + assert_eq!( as Deref>::deref(&one), &["a"]); + + assert_eq!(one[0], "a"); + assert_eq!(one.index(0), &"a"); + assert_eq!( + < as Deref>::Target as Index>::index(&one, 0), + &"a" + ); + + assert_eq!(one.len(), 1); + assert_eq!( as Deref>::Target::len(&one), 1); + + let many1 = OneOrManyWithParens::Many(vec!["b"]); + + assert_eq!(many1.deref(), &["b"]); + assert_eq!( as Deref>::deref(&many1), &["b"]); + + assert_eq!(many1[0], "b"); + assert_eq!(many1.index(0), &"b"); + assert_eq!( + < as Deref>::Target as Index>::index(&many1, 0), + &"b" + ); + + assert_eq!(many1.len(), 1); + assert_eq!( as Deref>::Target::len(&many1), 1); + + let many2 = OneOrManyWithParens::Many(vec!["c", "d"]); + + assert_eq!(many2.deref(), &["c", "d"]); + assert_eq!( + as Deref>::deref(&many2), + &["c", "d"] + ); + + assert_eq!(many2[0], "c"); + assert_eq!(many2.index(0), &"c"); + assert_eq!( + < as Deref>::Target as Index>::index(&many2, 0), + &"c" + ); + + assert_eq!(many2[1], "d"); + assert_eq!(many2.index(1), &"d"); + assert_eq!( + < as Deref>::Target as Index>::index(&many2, 1), + &"d" + ); + + assert_eq!(many2.len(), 2); + assert_eq!( as Deref>::Target::len(&many2), 2); + } + + #[test] + fn test_one_or_many_with_parens_as_ref() { + let one = OneOrManyWithParens::One("a"); + + assert_eq!(one.as_ref(), &["a"]); + assert_eq!( as AsRef<_>>::as_ref(&one), &["a"]); + + let many1 = OneOrManyWithParens::Many(vec!["b"]); + + assert_eq!(many1.as_ref(), &["b"]); + assert_eq!( as AsRef<_>>::as_ref(&many1), &["b"]); + + let many2 = OneOrManyWithParens::Many(vec!["c", "d"]); + + assert_eq!(many2.as_ref(), &["c", "d"]); + assert_eq!( + as AsRef<_>>::as_ref(&many2), + &["c", "d"] + ); + } + + #[test] + fn test_one_or_many_with_parens_ref_into_iter() { + let one = OneOrManyWithParens::One("a"); + + assert_eq!(Vec::from_iter(&one), vec![&"a"]); + + let many1 = OneOrManyWithParens::Many(vec!["b"]); + + assert_eq!(Vec::from_iter(&many1), vec![&"b"]); + + let many2 = OneOrManyWithParens::Many(vec!["c", "d"]); + + assert_eq!(Vec::from_iter(&many2), vec![&"c", &"d"]); + } + + #[test] + fn test_one_or_many_with_parens_value_into_iter() { + use core::iter::once; + + //tests that our iterator implemented methods behaves exactly as it's inner iterator, at every step up to n calls to next/next_back + fn test_steps(ours: OneOrManyWithParens, inner: I, n: usize) + where + I: IntoIterator + Clone, + { + fn checks(ours: OneOrManyWithParensIntoIter, inner: I) + where + I: Iterator + Clone + DoubleEndedIterator, + { + assert_eq!(ours.size_hint(), inner.size_hint()); + assert_eq!(ours.clone().count(), inner.clone().count()); + + assert_eq!( + ours.clone().fold(1, |a, v| a + v), + inner.clone().fold(1, |a, v| a + v) + ); + + assert_eq!(Vec::from_iter(ours.clone()), Vec::from_iter(inner.clone())); + assert_eq!( + Vec::from_iter(ours.clone().rev()), + Vec::from_iter(inner.clone().rev()) + ); + } + + let mut ours_next = ours.clone().into_iter(); + let mut inner_next = inner.clone().into_iter(); + + for _ in 0..n { + checks(ours_next.clone(), inner_next.clone()); + + assert_eq!(ours_next.next(), inner_next.next()); + } + + let mut ours_next_back = ours.clone().into_iter(); + let mut inner_next_back = inner.clone().into_iter(); + + for _ in 0..n { + checks(ours_next_back.clone(), inner_next_back.clone()); + + assert_eq!(ours_next_back.next_back(), inner_next_back.next_back()); + } + + let mut ours_mixed = ours.clone().into_iter(); + let mut inner_mixed = inner.clone().into_iter(); + + for i in 0..n { + checks(ours_mixed.clone(), inner_mixed.clone()); + + if i % 2 == 0 { + assert_eq!(ours_mixed.next_back(), inner_mixed.next_back()); + } else { + assert_eq!(ours_mixed.next(), inner_mixed.next()); + } + } + + let mut ours_mixed2 = ours.into_iter(); + let mut inner_mixed2 = inner.into_iter(); + + for i in 0..n { + checks(ours_mixed2.clone(), inner_mixed2.clone()); + + if i % 2 == 0 { + assert_eq!(ours_mixed2.next(), inner_mixed2.next()); + } else { + assert_eq!(ours_mixed2.next_back(), inner_mixed2.next_back()); + } + } + } + + test_steps(OneOrManyWithParens::One(1), once(1), 3); + test_steps(OneOrManyWithParens::Many(vec![2]), vec![2], 3); + test_steps(OneOrManyWithParens::Many(vec![3, 4]), vec![3, 4], 4); + } } From 8dbcbb3c277d81968b0843b03545de958ac9d2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Milenkovi=C4=87?= Date: Tue, 10 Sep 2024 20:56:26 +0100 Subject: [PATCH 541/806] minor: Add `databricks` dialect to `dialect_from_str` (#1416) Co-authored-by: Andrew Lamb --- src/dialect/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 0be8c17c7..6b80243ff 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -587,6 +587,7 @@ pub fn dialect_from_str(dialect_name: impl AsRef) -> Option Some(Box::new(BigQueryDialect)), "ansi" => Some(Box::new(AnsiDialect {})), "duckdb" => Some(Box::new(DuckDbDialect {})), + "databricks" => Some(Box::new(DatabricksDialect {})), _ => None, } } @@ -638,6 +639,8 @@ mod tests { assert!(parse_dialect("ANSI").is::()); assert!(parse_dialect("duckdb").is::()); assert!(parse_dialect("DuckDb").is::()); + assert!(parse_dialect("DataBricks").is::()); + assert!(parse_dialect("databricks").is::()); // error cases assert!(dialect_from_str("Unknown").is_none()); From cb0c511b05f4acc5867dce6e8b00ec38884cd058 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 10 Sep 2024 16:19:13 -0400 Subject: [PATCH 542/806] Add a test showing how negative constants are parsed (#1421) --- tests/sqlparser_common.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0f7a88d74..bebd33d19 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -42,7 +42,7 @@ mod test_utils; #[cfg(test)] use pretty_assertions::assert_eq; -use sqlparser::ast::Expr::Identifier; +use sqlparser::ast::Expr::{Identifier, UnaryOp}; use sqlparser::test_utils::all_dialects_except; #[test] @@ -4778,6 +4778,33 @@ fn parse_aggregate_with_group_by() { //TODO: assertions } +#[test] +fn parse_literal_integer() { + let sql = "SELECT 1, -10, +20"; + let select = verified_only_select(sql); + assert_eq!(3, select.projection.len()); + assert_eq!( + &Expr::Value(number("1")), + expr_from_projection(&select.projection[0]), + ); + // negative literal is parsed as a - and expr + assert_eq!( + &UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(number("10"))) + }, + expr_from_projection(&select.projection[1]), + ); + // positive literal is parsed as a + and expr + assert_eq!( + &UnaryOp { + op: UnaryOperator::Plus, + expr: Box::new(Expr::Value(number("20"))) + }, + expr_from_projection(&select.projection[2]), + ) +} + #[test] fn parse_literal_decimal() { // These numbers were explicitly chosen to not roundtrip if represented as From b9e77548866dc031f68dd0a0896d2e2b7a05d3f6 Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Wed, 11 Sep 2024 19:09:41 +0200 Subject: [PATCH 543/806] feat: Add support for MSSQL table options (#1414) --- src/ast/mod.rs | 120 ++++++++++++++++- src/keywords.rs | 2 + src/parser/mod.rs | 107 +++++++++++++-- tests/sqlparser_bigquery.rs | 28 ++-- tests/sqlparser_common.rs | 56 ++++---- tests/sqlparser_mssql.rs | 255 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 20 +-- 7 files changed, 523 insertions(+), 65 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 480442b1e..6c851906c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2082,6 +2082,15 @@ pub enum CreateTableOptions { /// e.g. `WITH (description = "123")` /// /// + /// + /// MSSQL supports more specific options that's not only key-value pairs. + /// + /// WITH ( + /// DISTRIBUTION = ROUND_ROBIN, + /// CLUSTERED INDEX (column_a DESC, column_b) + /// ) + /// + /// With(Vec), /// Options specified using the `OPTIONS` keyword. /// e.g. `OPTIONS(description = "123")` @@ -5728,14 +5737,119 @@ pub struct HiveFormat { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct SqlOption { +pub struct ClusteredIndex { pub name: Ident, - pub value: Expr, + pub asc: Option, +} + +impl fmt::Display for ClusteredIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name)?; + match self.asc { + Some(true) => write!(f, " ASC"), + Some(false) => write!(f, " DESC"), + _ => Ok(()), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableOptionsClustered { + ColumnstoreIndex, + ColumnstoreIndexOrder(Vec), + Index(Vec), +} + +impl fmt::Display for TableOptionsClustered { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TableOptionsClustered::ColumnstoreIndex => { + write!(f, "CLUSTERED COLUMNSTORE INDEX") + } + TableOptionsClustered::ColumnstoreIndexOrder(values) => { + write!( + f, + "CLUSTERED COLUMNSTORE INDEX ORDER ({})", + display_comma_separated(values) + ) + } + TableOptionsClustered::Index(values) => { + write!(f, "CLUSTERED INDEX ({})", display_comma_separated(values)) + } + } + } +} + +/// Specifies which partition the boundary values on table partitioning belongs to. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum PartitionRangeDirection { + Left, + Right, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SqlOption { + /// Clustered represents the clustered version of table storage for MSSQL. + /// + /// + Clustered(TableOptionsClustered), + /// Single identifier options, e.g. `HEAP` for MSSQL. + /// + /// + Ident(Ident), + /// Any option that consists of a key value pair where the value is an expression. e.g. + /// + /// WITH(DISTRIBUTION = ROUND_ROBIN) + KeyValue { key: Ident, value: Expr }, + /// One or more table partitions and represents which partition the boundary values belong to, + /// e.g. + /// + /// PARTITION (id RANGE LEFT FOR VALUES (10, 20, 30, 40)) + /// + /// + Partition { + column_name: Ident, + range_direction: Option, + for_values: Vec, + }, } impl fmt::Display for SqlOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} = {}", self.name, self.value) + match self { + SqlOption::Clustered(c) => write!(f, "{}", c), + SqlOption::Ident(ident) => { + write!(f, "{}", ident) + } + SqlOption::KeyValue { key: name, value } => { + write!(f, "{} = {}", name, value) + } + SqlOption::Partition { + column_name, + range_direction, + for_values, + } => { + let direction = match range_direction { + Some(PartitionRangeDirection::Left) => " LEFT", + Some(PartitionRangeDirection::Right) => " RIGHT", + None => "", + }; + + write!( + f, + "PARTITION ({} RANGE{} FOR VALUES ({}))", + column_name, + direction, + display_comma_separated(for_values) + ) + } + } } } diff --git a/src/keywords.rs b/src/keywords.rs index 3ee447cfe..68eaf050b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -166,6 +166,7 @@ define_keywords!( COLLECTION, COLUMN, COLUMNS, + COLUMNSTORE, COMMENT, COMMIT, COMMITTED, @@ -355,6 +356,7 @@ define_keywords!( HASH, HAVING, HEADER, + HEAP, HIGH_PRIORITY, HISTORY, HIVEVAR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b39685cbf..8d920b2ce 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6480,10 +6480,91 @@ impl<'a> Parser<'a> { } pub fn parse_sql_option(&mut self) -> Result { - let name = self.parse_identifier(false)?; - self.expect_token(&Token::Eq)?; - let value = self.parse_expr()?; - Ok(SqlOption { name, value }) + let is_mssql = dialect_of!(self is MsSqlDialect|GenericDialect); + + match self.peek_token().token { + Token::Word(w) if w.keyword == Keyword::HEAP && is_mssql => { + Ok(SqlOption::Ident(self.parse_identifier(false)?)) + } + Token::Word(w) if w.keyword == Keyword::PARTITION && is_mssql => { + self.parse_option_partition() + } + Token::Word(w) if w.keyword == Keyword::CLUSTERED && is_mssql => { + self.parse_option_clustered() + } + _ => { + let name = self.parse_identifier(false)?; + self.expect_token(&Token::Eq)?; + let value = self.parse_expr()?; + + Ok(SqlOption::KeyValue { key: name, value }) + } + } + } + + pub fn parse_option_clustered(&mut self) -> Result { + if self.parse_keywords(&[ + Keyword::CLUSTERED, + Keyword::COLUMNSTORE, + Keyword::INDEX, + Keyword::ORDER, + ]) { + Ok(SqlOption::Clustered( + TableOptionsClustered::ColumnstoreIndexOrder( + self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?, + ), + )) + } else if self.parse_keywords(&[Keyword::CLUSTERED, Keyword::COLUMNSTORE, Keyword::INDEX]) { + Ok(SqlOption::Clustered( + TableOptionsClustered::ColumnstoreIndex, + )) + } else if self.parse_keywords(&[Keyword::CLUSTERED, Keyword::INDEX]) { + self.expect_token(&Token::LParen)?; + + let columns = self.parse_comma_separated(|p| { + let name = p.parse_identifier(false)?; + let asc = p.parse_asc_desc(); + + Ok(ClusteredIndex { name, asc }) + })?; + + self.expect_token(&Token::RParen)?; + + Ok(SqlOption::Clustered(TableOptionsClustered::Index(columns))) + } else { + Err(ParserError::ParserError( + "invalid CLUSTERED sequence".to_string(), + )) + } + } + + pub fn parse_option_partition(&mut self) -> Result { + self.expect_keyword(Keyword::PARTITION)?; + self.expect_token(&Token::LParen)?; + let column_name = self.parse_identifier(false)?; + + self.expect_keyword(Keyword::RANGE)?; + let range_direction = if self.parse_keyword(Keyword::LEFT) { + Some(PartitionRangeDirection::Left) + } else if self.parse_keyword(Keyword::RIGHT) { + Some(PartitionRangeDirection::Right) + } else { + None + }; + + self.expect_keywords(&[Keyword::FOR, Keyword::VALUES])?; + self.expect_token(&Token::LParen)?; + + let for_values = self.parse_comma_separated(Parser::parse_expr)?; + + self.expect_token(&Token::RParen)?; + self.expect_token(&Token::RParen)?; + + Ok(SqlOption::Partition { + column_name, + range_direction, + for_values, + }) } pub fn parse_partition(&mut self) -> Result { @@ -11014,17 +11095,23 @@ impl<'a> Parser<'a> { }) } - /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) - pub fn parse_order_by_expr(&mut self) -> Result { - let expr = self.parse_expr()?; - - let asc = if self.parse_keyword(Keyword::ASC) { + /// Parse ASC or DESC, returns an Option with true if ASC, false of DESC or `None` if none of + /// them. + pub fn parse_asc_desc(&mut self) -> Option { + if self.parse_keyword(Keyword::ASC) { Some(true) } else if self.parse_keyword(Keyword::DESC) { Some(false) } else { None - }; + } + } + + /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) + pub fn parse_order_by_expr(&mut self) -> Result { + let expr = self.parse_expr()?; + + let asc = self.parse_asc_desc(); let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { Some(true) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 4f84b376d..e051baa8b 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -267,8 +267,8 @@ fn parse_create_view_with_options() { ViewColumnDef { name: Ident::new("age"), data_type: None, - options: Some(vec![SqlOption { - name: Ident::new("description"), + options: Some(vec![SqlOption::KeyValue { + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString("field age".to_string())), }]) }, @@ -287,8 +287,8 @@ fn parse_create_view_with_options() { unreachable!() }; assert_eq!( - &SqlOption { - name: Ident::new("description"), + &SqlOption::KeyValue { + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "a view that expires in 2 days".to_string() )), @@ -414,8 +414,8 @@ fn parse_create_table_with_options() { }, ColumnOptionDef { name: None, - option: ColumnOption::Options(vec![SqlOption { - name: Ident::new("description"), + option: ColumnOption::Options(vec![SqlOption::KeyValue { + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "field x".to_string() )), @@ -429,8 +429,8 @@ fn parse_create_table_with_options() { collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Options(vec![SqlOption { - name: Ident::new("description"), + option: ColumnOption::Options(vec![SqlOption::KeyValue { + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "field y".to_string() )), @@ -448,12 +448,12 @@ fn parse_create_table_with_options() { Ident::new("age"), ])), Some(vec![ - SqlOption { - name: Ident::new("partition_expiration_days"), + SqlOption::KeyValue { + key: Ident::new("partition_expiration_days"), value: Expr::Value(number("1")), }, - SqlOption { - name: Ident::new("description"), + SqlOption::KeyValue { + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "table option description".to_string() )), @@ -2005,8 +2005,8 @@ fn test_bigquery_create_function() { function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value(number( "42" )))), - options: Some(vec![SqlOption { - name: Ident::new("x"), + options: Some(vec![SqlOption::KeyValue { + key: Ident::new("x"), value: Expr::Value(Value::SingleQuotedString("y".into())), }]), behavior: None, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bebd33d19..ff45d7b6e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3637,12 +3637,12 @@ fn parse_create_table_with_options() { Statement::CreateTable(CreateTable { with_options, .. }) => { assert_eq!( vec![ - SqlOption { - name: "foo".into(), + SqlOption::KeyValue { + key: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, - SqlOption { - name: "a".into(), + SqlOption::KeyValue { + key: "a".into(), value: Expr::Value(number("123")), }, ], @@ -3870,8 +3870,8 @@ fn parse_alter_table() { AlterTableOperation::SetTblProperties { table_properties } => { assert_eq!( table_properties, - [SqlOption { - name: Ident { + [SqlOption::KeyValue { + key: Ident { value: "classification".to_string(), quote_style: Some('\'') }, @@ -3958,12 +3958,12 @@ fn parse_alter_view_with_options() { Statement::AlterView { with_options, .. } => { assert_eq!( vec![ - SqlOption { - name: "foo".into(), + SqlOption::KeyValue { + key: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, - SqlOption { - name: "a".into(), + SqlOption::KeyValue { + key: "a".into(), value: Expr::Value(number("123")), }, ], @@ -6729,12 +6729,12 @@ fn parse_create_view_with_options() { Statement::CreateView { options, .. } => { assert_eq!( CreateTableOptions::With(vec![ - SqlOption { - name: "foo".into(), + SqlOption::KeyValue { + key: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, - SqlOption { - name: "a".into(), + SqlOption::KeyValue { + key: "a".into(), value: Expr::Value(number("123")), }, ]), @@ -8827,12 +8827,12 @@ fn parse_cache_table() { table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![ - SqlOption { - name: Ident::with_quote('\'', "K1"), + SqlOption::KeyValue { + key: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, - SqlOption { - name: Ident::with_quote('\'', "K2"), + SqlOption::KeyValue { + key: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, ], @@ -8852,12 +8852,12 @@ fn parse_cache_table() { table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![ - SqlOption { - name: Ident::with_quote('\'', "K1"), + SqlOption::KeyValue { + key: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, - SqlOption { - name: Ident::with_quote('\'', "K2"), + SqlOption::KeyValue { + key: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, ], @@ -8877,12 +8877,12 @@ fn parse_cache_table() { table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), has_as: true, options: vec![ - SqlOption { - name: Ident::with_quote('\'', "K1"), + SqlOption::KeyValue { + key: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, - SqlOption { - name: Ident::with_quote('\'', "K2"), + SqlOption::KeyValue { + key: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, ], @@ -9695,8 +9695,8 @@ fn parse_unload() { value: "s3://...".to_string(), quote_style: Some('\'') }, - with: vec![SqlOption { - name: Ident { + with: vec![SqlOption::KeyValue { + key: Ident { value: "format".to_string(), quote_style: None }, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 5c2ec8763..0ab160f56 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -653,6 +653,261 @@ fn parse_use() { } } +#[test] +fn parse_create_table_with_valid_options() { + let options = [ + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, PARTITION (column_a RANGE FOR VALUES (10, 11)))", + vec![ + SqlOption::KeyValue { + key: Ident { + value: "DISTRIBUTION".to_string(), + quote_style: None, + }, + value: Expr::Identifier(Ident { + value: "ROUND_ROBIN".to_string(), + quote_style: None, + }) + }, + SqlOption::Partition { + column_name: "column_a".into(), + range_direction: None, + for_values: vec![Expr::Value(test_utils::number("10")), Expr::Value(test_utils::number("11"))] , + }, + ], + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (PARTITION (column_a RANGE LEFT FOR VALUES (10, 11)))", + vec![ + SqlOption::Partition { + column_name: "column_a".into(), + range_direction: Some(PartitionRangeDirection::Left), + for_values: vec![ + Expr::Value(test_utils::number("10")), + Expr::Value(test_utils::number("11")), + ], + } + ], + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX)", + vec![SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndex)], + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX ORDER (column_a, column_b))", + vec![ + SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndexOrder(vec![ + "column_a".into(), + "column_b".into(), + ])) + ], + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED INDEX (column_a ASC, column_b DESC, column_c))", + vec![ + SqlOption::Clustered(TableOptionsClustered::Index(vec![ + ClusteredIndex { + name: Ident { + value: "column_a".to_string(), + quote_style: None, + }, + asc: Some(true), + }, + ClusteredIndex { + name: Ident { + value: "column_b".to_string(), + quote_style: None, + }, + asc: Some(false), + }, + ClusteredIndex { + name: Ident { + value: "column_c".to_string(), + quote_style: None, + }, + asc: None, + }, + ])) + ], + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = HASH(column_a, column_b), HEAP)", + vec![ + SqlOption::KeyValue { + key: Ident { + value: "DISTRIBUTION".to_string(), + quote_style: None, + }, + value: Expr::Function( + Function { + name: ObjectName( + vec![ + Ident { + value: "HASH".to_string(), + quote_style: None, + }, + ], + ), + parameters: FunctionArguments::None, + args: FunctionArguments::List( + FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed( + FunctionArgExpr::Expr( + Expr::Identifier( + Ident { + value: "column_a".to_string(), + quote_style: None, + }, + ), + ), + ), + FunctionArg::Unnamed( + FunctionArgExpr::Expr( + Expr::Identifier( + Ident { + value: "column_b".to_string(), + quote_style: None, + }, + ), + ), + ), + ], + clauses: vec![], + }, + ), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }, + ), + }, + SqlOption::Ident("HEAP".into()), + ], + ), + ]; + + for (sql, with_options) in options { + assert_eq!( + ms_and_generic().verified_stmt(sql), + Statement::CreateTable(CreateTable { + or_replace: false, + temporary: false, + external: false, + global: None, + if_not_exists: false, + transient: false, + volatile: false, + name: ObjectName(vec![Ident { + value: "mytable".to_string(), + quote_style: None, + },],), + columns: vec![ + ColumnDef { + name: Ident { + value: "column_a".to_string(), + quote_style: None, + }, + data_type: Int(None,), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident { + value: "column_b".to_string(), + quote_style: None, + }, + data_type: Int(None,), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident { + value: "column_c".to_string(), + quote_style: None, + }, + data_type: Int(None,), + collation: None, + options: vec![], + }, + ], + constraints: vec![], + hive_distribution: HiveDistributionStyle::NONE, + hive_formats: Some(HiveFormat { + row_format: None, + serde_properties: None, + 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, + order_by: None, + partition_by: None, + cluster_by: None, + clustered_by: None, + options: None, + strict: false, + copy_grants: false, + enable_schema_evolution: None, + change_tracking: None, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + default_ddl_collation: None, + with_aggregation_policy: None, + with_row_access_policy: None, + with_tags: None, + }) + ); + } +} + +#[test] +fn parse_create_table_with_invalid_options() { + let invalid_cases = vec![ + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX ORDER ())", + "Expected: identifier, found: )", + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE)", + "invalid CLUSTERED sequence", + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (HEAP INDEX)", + "Expected: ), found: INDEX", + ), + ( + + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (PARTITION (RANGE LEFT FOR VALUES (10, 11)))", + "Expected: RANGE, found: LEFT", + ), + ]; + + for (sql, expected_error) in invalid_cases { + let res = ms_and_generic().parse_sql_statements(sql); + assert_eq!( + format!("sql parser error: {expected_error}"), + res.unwrap_err().to_string() + ); + } +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1ebb5d54c..ec1311f2c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -461,16 +461,16 @@ fn parse_create_table_with_defaults() { assert_eq!( with_options, vec![ - SqlOption { - name: "fillfactor".into(), + SqlOption::KeyValue { + key: "fillfactor".into(), value: Expr::Value(number("20")) }, - SqlOption { - name: "user_catalog_table".into(), + SqlOption::KeyValue { + key: "user_catalog_table".into(), value: Expr::Value(Value::Boolean(true)) }, - SqlOption { - name: "autovacuum_vacuum_threshold".into(), + SqlOption::KeyValue { + key: "autovacuum_vacuum_threshold".into(), value: Expr::Value(number("100")) }, ] @@ -4482,12 +4482,12 @@ fn parse_create_table_with_options() { Statement::CreateTable(CreateTable { with_options, .. }) => { assert_eq!( vec![ - SqlOption { - name: "foo".into(), + SqlOption::KeyValue { + key: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, - SqlOption { - name: "a".into(), + SqlOption::KeyValue { + key: "a".into(), value: Expr::Value(number("123")), }, ], From 70dbb115572252c2ba57b270fb9a612e8d2f887b Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 11 Sep 2024 14:17:03 -0400 Subject: [PATCH 544/806] CHANGELOG for 0.51.0 (#1422) --- CHANGELOG.md | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 526374714..c77755d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,37 @@ changes that break via addition as "Added". ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. + +## [0.51.0] 2024-09-11 +As always, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs 🙏. +Without them this project would not be possible. + +Reminder: we are in the final phases of moving sqlparser-rs into the Apache +DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 + +### Fixed +* Fix Hive table comment should be after table column definitions (#1413) - Thanks @git-hulk +* Fix stack overflow in `parse_subexpr` (#1410) - Thanks @eejbyfeldt +* Fix `INTERVAL` parsing to support expressions and units via dialect (#1398) - Thanks @samuelcolvin +* Fix identifiers starting with `$` should be regarded as a placeholder in SQLite (#1402) - Thanks @git-hulk + +### Added +* Support for MSSQL table options (#1414) - Thanks @bombsimon +* Test showing how negative constants are parsed (#1421) - Thanks @alamb +* Support databricks dialect to dialect_from_str (#1416) - Thanks @milenkovicmalamb +* Support `DROP|CLEAR|MATERIALIZE PROJECTION` syntax for ClickHouse (#1417) - Thanks @git-hulk +* Support postgres `TRUNCATE` syntax (#1406) - Thanks @tobyhede +* Support `CREATE INDEX` with clause (#1389) - Thanks @lewiszlw +* Support parsing `CLUSTERED BY` clause for Hive (#1397) - Thanks @git-hulk +* Support different `USE` statement syntaxes (#1387) - Thanks @kacpermuda +* Support `ADD PROJECTION` syntax for ClickHouse (#1390) - Thanks @git-hulk + +### Changed +* Implement common traits for OneOrManyWithParens (#1368) - Thanks @gstvg +* Cleanup parse_statement (#1407) - Thanks @samuelcolvin +* Allow `DateTimeField::Custom` with `EXTRACT` in Postgres (#1394) - Thanks @samuelcolvin + + ## [0.50.0] 2024-08-15 Again, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs 🙏. Without them this project would not be possible. @@ -17,10 +48,10 @@ Without them this project would not be possible. Reminder: are in the process of moving sqlparser to governed as part of the Apache DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 -# Fixed +### Fixed * Clippy 1.80 warnings (#1357) - Thanks @lovasoa -# Added +### Added * Support `STRUCT` and list of structs for DuckDB dialect (#1372) - Thanks @jayzhan211 * Support custom lexical precedence in PostgreSQL dialect (#1379) - Thanks @samuelcolvin * Support `FREEZE|UNFREEZE PARTITION` syntax for ClickHouse (#1380) - Thanks @git-hulk @@ -38,7 +69,7 @@ DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 * Support position with normal function call syntax for Snowflake (#1341) - Thanks @jmhain * Support `TABLE` keyword in `DESC|DESCRIBE|EXPLAIN TABLE` statement (#1351) - Thanks @git-hulk -# Changed +### Changed * Only require `DESCRIBE TABLE` for Snowflake and ClickHouse dialect (#1386) - Thanks @ alamb * Rename (unreleased) `get_next_precedence_full` to `get_next_precedence_default` (#1378) - Thanks @samuelcolvin * Use local GitHub Action to replace setup-rust-action (#1371) - Thanks @git-hulk From b9f67847146658aa7a01e39f69ce87d3852e2589 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 11 Sep 2024 14:19:35 -0400 Subject: [PATCH 545/806] chore: Release sqlparser version 0.51.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 30c40dd60..2448b67ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.50.0" +version = "0.51.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" From 246838a69f68f4cee9a46ec59d6557d77492f392 Mon Sep 17 00:00:00 2001 From: Agaev Guseyn <60943542+agscpp@users.noreply.github.com> Date: Tue, 17 Sep 2024 00:04:21 +0300 Subject: [PATCH 546/806] Fix parsing of negative values (#1419) Co-authored-by: Agaev Huseyn --- src/parser/mod.rs | 53 ++++++++++++++++++++----------------- tests/sqlparser_common.rs | 12 +++++++++ tests/sqlparser_postgres.rs | 20 ++++++++++++++ 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8d920b2ce..e2f4dd508 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7411,6 +7411,7 @@ impl<'a> Parser<'a> { } } + /// Parse an unsigned numeric literal pub fn parse_number_value(&mut self) -> Result { match self.parse_value()? { v @ Value::Number(_, _) => Ok(v), @@ -7422,6 +7423,26 @@ impl<'a> Parser<'a> { } } + /// Parse a numeric literal as an expression. Returns a [`Expr::UnaryOp`] if the number is signed, + /// otherwise returns a [`Expr::Value`] + pub fn parse_number(&mut self) -> Result { + let next_token = self.next_token(); + match next_token.token { + Token::Plus => Ok(Expr::UnaryOp { + op: UnaryOperator::Plus, + expr: Box::new(Expr::Value(self.parse_number_value()?)), + }), + Token::Minus => Ok(Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(self.parse_number_value()?)), + }), + _ => { + self.prev_token(); + Ok(Expr::Value(self.parse_number_value()?)) + } + } + } + fn parse_introduced_string_value(&mut self) -> Result { let next_token = self.next_token(); let location = next_token.location; @@ -11741,30 +11762,20 @@ impl<'a> Parser<'a> { //[ INCREMENT [ BY ] increment ] if self.parse_keywords(&[Keyword::INCREMENT]) { if self.parse_keywords(&[Keyword::BY]) { - sequence_options.push(SequenceOptions::IncrementBy( - Expr::Value(self.parse_number_value()?), - true, - )); + sequence_options.push(SequenceOptions::IncrementBy(self.parse_number()?, true)); } else { - sequence_options.push(SequenceOptions::IncrementBy( - Expr::Value(self.parse_number_value()?), - false, - )); + sequence_options.push(SequenceOptions::IncrementBy(self.parse_number()?, false)); } } //[ MINVALUE minvalue | NO MINVALUE ] if self.parse_keyword(Keyword::MINVALUE) { - sequence_options.push(SequenceOptions::MinValue(Some(Expr::Value( - self.parse_number_value()?, - )))); + sequence_options.push(SequenceOptions::MinValue(Some(self.parse_number()?))); } else if self.parse_keywords(&[Keyword::NO, Keyword::MINVALUE]) { sequence_options.push(SequenceOptions::MinValue(None)); } //[ MAXVALUE maxvalue | NO MAXVALUE ] if self.parse_keywords(&[Keyword::MAXVALUE]) { - sequence_options.push(SequenceOptions::MaxValue(Some(Expr::Value( - self.parse_number_value()?, - )))); + sequence_options.push(SequenceOptions::MaxValue(Some(self.parse_number()?))); } else if self.parse_keywords(&[Keyword::NO, Keyword::MAXVALUE]) { sequence_options.push(SequenceOptions::MaxValue(None)); } @@ -11772,22 +11783,14 @@ impl<'a> Parser<'a> { //[ START [ WITH ] start ] if self.parse_keywords(&[Keyword::START]) { if self.parse_keywords(&[Keyword::WITH]) { - sequence_options.push(SequenceOptions::StartWith( - Expr::Value(self.parse_number_value()?), - true, - )); + sequence_options.push(SequenceOptions::StartWith(self.parse_number()?, true)); } else { - sequence_options.push(SequenceOptions::StartWith( - Expr::Value(self.parse_number_value()?), - false, - )); + sequence_options.push(SequenceOptions::StartWith(self.parse_number()?, false)); } } //[ CACHE cache ] if self.parse_keywords(&[Keyword::CACHE]) { - sequence_options.push(SequenceOptions::Cache(Expr::Value( - self.parse_number_value()?, - ))); + sequence_options.push(SequenceOptions::Cache(self.parse_number()?)); } // [ [ NO ] CYCLE ] if self.parse_keywords(&[Keyword::NO, Keyword::CYCLE]) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ff45d7b6e..5525650f2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2819,6 +2819,18 @@ fn parse_window_function_null_treatment_arg() { ); } +#[test] +fn parse_negative_value() { + let sql1 = "SELECT -1"; + one_statement_parses_to(sql1, "SELECT -1"); + + let sql2 = "CREATE SEQUENCE name INCREMENT -10 MINVALUE -1000 MAXVALUE 15 START -100;"; + one_statement_parses_to( + sql2, + "CREATE SEQUENCE name INCREMENT -10 MINVALUE -1000 MAXVALUE 15 START -100", + ); +} + #[test] fn parse_create_table() { let sql = "CREATE TABLE uk_cities (\ diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ec1311f2c..91d365692 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -277,6 +277,26 @@ fn parse_create_sequence() { "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name3 INCREMENT 1 NO MINVALUE MAXVALUE 20 OWNED BY NONE", ); + let sql7 = "CREATE SEQUENCE name4 + AS BIGINT + INCREMENT -15 + MINVALUE - 2000 MAXVALUE -50 + START WITH - 60"; + pg().one_statement_parses_to( + sql7, + "CREATE SEQUENCE name4 AS BIGINT INCREMENT -15 MINVALUE -2000 MAXVALUE -50 START WITH -60", + ); + + let sql8 = "CREATE SEQUENCE name5 + AS BIGINT + INCREMENT +10 + MINVALUE + 30 MAXVALUE +5000 + START WITH + 45"; + pg().one_statement_parses_to( + sql8, + "CREATE SEQUENCE name5 AS BIGINT INCREMENT +10 MINVALUE +30 MAXVALUE +5000 START WITH +45", + ); + assert!(matches!( pg().parse_sql_statements("CREATE SEQUENCE foo INCREMENT 1 NO MINVALUE NO"), Err(ParserError::ParserError(_)) From 1c505ce736350e4e746d64168867d75d1a4cd6a7 Mon Sep 17 00:00:00 2001 From: hulk Date: Thu, 19 Sep 2024 18:56:00 +0800 Subject: [PATCH 547/806] Allow to use ON CLUSTER cluster_name in TRUNCATE syntax (#1428) --- src/ast/mod.rs | 9 +++++++++ src/parser/mod.rs | 3 +++ tests/sqlparser_common.rs | 21 +++++++++++++++++++++ tests/sqlparser_postgres.rs | 7 +++++-- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6c851906c..4e3cbcdba 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2172,6 +2172,11 @@ pub enum Statement { /// Postgres-specific option /// [ CASCADE | RESTRICT ] cascade: Option, + /// ClickHouse-specific option + /// [ ON CLUSTER cluster_name ] + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) + on_cluster: Option, }, /// ```sql /// MSCK @@ -3293,6 +3298,7 @@ impl fmt::Display for Statement { only, identity, cascade, + on_cluster, } => { let table = if *table { "TABLE " } else { "" }; let only = if *only { "ONLY " } else { "" }; @@ -3321,6 +3327,9 @@ impl fmt::Display for Statement { write!(f, " PARTITION ({})", display_comma_separated(parts))?; } } + if let Some(on_cluster) = on_cluster { + write!(f, " ON CLUSTER {on_cluster}")?; + } Ok(()) } Statement::AttachDatabase { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e2f4dd508..4e5ca2b5a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -708,6 +708,8 @@ impl<'a> Parser<'a> { }; }; + let on_cluster = self.parse_optional_on_cluster()?; + Ok(Statement::Truncate { table_names, partitions, @@ -715,6 +717,7 @@ impl<'a> Parser<'a> { only, identity, cascade, + on_cluster, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5525650f2..c4a52938d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10804,3 +10804,24 @@ fn test_extract_seconds_single_quote_err() { "sql parser error: Expected: date/time field, found: 'seconds'" ); } + +#[test] +fn test_truncate_table_with_on_cluster() { + let sql = "TRUNCATE TABLE t ON CLUSTER cluster_name"; + match all_dialects().verified_stmt(sql) { + Statement::Truncate { on_cluster, .. } => { + assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); + } + _ => panic!("Expected: TRUNCATE TABLE statement"), + } + + // Omit ON CLUSTER is allowed + all_dialects().verified_stmt("TRUNCATE TABLE t"); + + assert_eq!( + ParserError::ParserError("Expected: identifier, found: EOF".to_string()), + all_dialects() + .parse_sql_statements("TRUNCATE TABLE t ON CLUSTER") + .unwrap_err() + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 91d365692..069b61d05 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4010,6 +4010,7 @@ fn parse_truncate() { only: false, identity: None, cascade: None, + on_cluster: None, }, truncate ); @@ -4032,7 +4033,8 @@ fn parse_truncate_with_options() { table: true, only: true, identity: Some(TruncateIdentityOption::Restart), - cascade: Some(TruncateCascadeOption::Cascade) + cascade: Some(TruncateCascadeOption::Cascade), + on_cluster: None, }, truncate ); @@ -4063,7 +4065,8 @@ fn parse_truncate_with_table_list() { table: true, only: false, identity: Some(TruncateIdentityOption::Restart), - cascade: Some(TruncateCascadeOption::Cascade) + cascade: Some(TruncateCascadeOption::Cascade), + on_cluster: None, }, truncate ); From 04a53e57538524a9cda8ca7304312113849aec52 Mon Sep 17 00:00:00 2001 From: Siyuan Huang <73871299+kysshsy@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:28:02 +0800 Subject: [PATCH 548/806] feat: support explain options (#1426) --- src/ast/mod.rs | 48 +++++++++++ src/dialect/duckdb.rs | 6 ++ src/dialect/generic.rs | 4 + src/dialect/mod.rs | 4 + src/dialect/postgresql.rs | 5 ++ src/parser/mod.rs | 45 ++++++++++- tests/sqlparser_common.rs | 163 +++++++++++++++++++++++++++++++++++++- 7 files changed, 268 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4e3cbcdba..94dabb059 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3032,6 +3032,8 @@ pub enum Statement { statement: Box, /// Optional output format of explain format: Option, + /// Postgres style utility options, `(analyze, verbose true)` + options: Option>, }, /// ```sql /// SAVEPOINT @@ -3219,6 +3221,7 @@ impl fmt::Display for Statement { analyze, statement, format, + options, } => { write!(f, "{describe_alias} ")?; @@ -3234,6 +3237,10 @@ impl fmt::Display for Statement { write!(f, "FORMAT {format} ")?; } + if let Some(options) = options { + write!(f, "({}) ", display_comma_separated(options))?; + } + write!(f, "{statement}") } Statement::Query(s) => write!(f, "{s}"), @@ -7125,6 +7132,47 @@ where } } +/// Represents a single PostgreSQL utility option. +/// +/// A utility option is a key-value pair where the key is an identifier (IDENT) and the value +/// can be one of the following: +/// - A number with an optional sign (`+` or `-`). Example: `+10`, `-10.2`, `3` +/// - A non-keyword string. Example: `option1`, `'option2'`, `"option3"` +/// - keyword: `TRUE`, `FALSE`, `ON` (`off` is also accept). +/// - Empty. Example: `ANALYZE` (identifier only) +/// +/// Utility options are used in various PostgreSQL DDL statements, including statements such as +/// `CLUSTER`, `EXPLAIN`, `VACUUM`, and `REINDEX`. These statements format options as `( option [, ...] )`. +/// +/// [CLUSTER](https://www.postgresql.org/docs/current/sql-cluster.html) +/// [EXPLAIN](https://www.postgresql.org/docs/current/sql-explain.html) +/// [VACUUM](https://www.postgresql.org/docs/current/sql-vacuum.html) +/// [REINDEX](https://www.postgresql.org/docs/current/sql-reindex.html) +/// +/// For example, the `EXPLAIN` AND `VACUUM` statements with options might look like this: +/// ```sql +/// EXPLAIN (ANALYZE, VERBOSE TRUE, FORMAT TEXT) SELECT * FROM my_table; +/// +/// VACCUM (VERBOSE, ANALYZE ON, PARALLEL 10) my_table; +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct UtilityOption { + pub name: Ident, + pub arg: Option, +} + +impl Display for UtilityOption { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref arg) = self.arg { + write!(f, "{} {}", self.name, arg) + } else { + write!(f, "{}", self.name) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 1fc211685..811f25f72 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -55,4 +55,10 @@ impl Dialect for DuckDbDialect { fn support_map_literal_syntax(&self) -> bool { true } + + // DuckDB is compatible with PostgreSQL syntax for this statement, + // although not all features may be implemented. + fn supports_explain_with_utility_options(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index c8f1c00d9..1c638d8b0 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -90,4 +90,8 @@ impl Dialect for GenericDialect { fn supports_create_index_with_clause(&self) -> bool { true } + + fn supports_explain_with_utility_options(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6b80243ff..cf0af6329 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -536,6 +536,10 @@ pub trait Dialect: Debug + Any { fn require_interval_qualifier(&self) -> bool { false } + + fn supports_explain_with_utility_options(&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 eba3a6989..5a7db0216 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -166,6 +166,11 @@ impl Dialect for PostgreSqlDialect { fn supports_create_index_with_clause(&self) -> bool { true } + + /// see + fn supports_explain_with_utility_options(&self) -> bool { + true + } } pub fn parse_comment(parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4e5ca2b5a..751101fea 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1277,6 +1277,29 @@ impl<'a> Parser<'a> { } } + pub fn parse_utility_options(&mut self) -> Result, ParserError> { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Self::parse_utility_option)?; + self.expect_token(&Token::RParen)?; + + Ok(options) + } + + fn parse_utility_option(&mut self) -> Result { + let name = self.parse_identifier(false)?; + + let next_token = self.peek_token(); + if next_token == Token::Comma || next_token == Token::RParen { + return Ok(UtilityOption { name, arg: None }); + } + let arg = self.parse_expr()?; + + Ok(UtilityOption { + name, + arg: Some(arg), + }) + } + fn try_parse_expr_sub_query(&mut self) -> Result, ParserError> { if self .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) @@ -8464,11 +8487,24 @@ impl<'a> Parser<'a> { &mut self, describe_alias: DescribeAlias, ) -> Result { - let analyze = self.parse_keyword(Keyword::ANALYZE); - let verbose = self.parse_keyword(Keyword::VERBOSE); + let mut analyze = false; + let mut verbose = false; let mut format = None; - if self.parse_keyword(Keyword::FORMAT) { - format = Some(self.parse_analyze_format()?); + let mut options = None; + + // Note: DuckDB is compatible with PostgreSQL syntax for this statement, + // although not all features may be implemented. + if describe_alias == DescribeAlias::Explain + && self.dialect.supports_explain_with_utility_options() + && self.peek_token().token == Token::LParen + { + options = Some(self.parse_utility_options()?) + } else { + analyze = self.parse_keyword(Keyword::ANALYZE); + verbose = self.parse_keyword(Keyword::VERBOSE); + if self.parse_keyword(Keyword::FORMAT) { + format = Some(self.parse_analyze_format()?); + } } match self.maybe_parse(|parser| parser.parse_statement()) { @@ -8481,6 +8517,7 @@ impl<'a> Parser<'a> { verbose, statement: Box::new(statement), format, + options, }), _ => { let hive_format = diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c4a52938d..2f001e17e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4268,22 +4268,26 @@ fn parse_scalar_function_in_projection() { } fn run_explain_analyze( + dialect: TestedDialects, query: &str, expected_verbose: bool, expected_analyze: bool, expected_format: Option, + exepcted_options: Option>, ) { - match verified_stmt(query) { + match dialect.verified_stmt(query) { Statement::Explain { describe_alias: _, analyze, verbose, statement, format, + options, } => { assert_eq!(verbose, expected_verbose); assert_eq!(analyze, expected_analyze); assert_eq!(format, expected_format); + assert_eq!(options, exepcted_options); assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); } _ => panic!("Unexpected Statement, must be Explain"), @@ -4328,47 +4332,73 @@ fn explain_desc() { #[test] fn parse_explain_analyze_with_simple_select() { // Describe is an alias for EXPLAIN - run_explain_analyze("DESCRIBE SELECT sqrt(id) FROM foo", false, false, None); + run_explain_analyze( + all_dialects(), + "DESCRIBE SELECT sqrt(id) FROM foo", + false, + false, + None, + None, + ); - run_explain_analyze("EXPLAIN SELECT sqrt(id) FROM foo", false, false, None); run_explain_analyze( + all_dialects(), + "EXPLAIN SELECT sqrt(id) FROM foo", + false, + false, + None, + None, + ); + run_explain_analyze( + all_dialects(), "EXPLAIN VERBOSE SELECT sqrt(id) FROM foo", true, false, None, + None, ); run_explain_analyze( + all_dialects(), "EXPLAIN ANALYZE SELECT sqrt(id) FROM foo", false, true, None, + None, ); run_explain_analyze( + all_dialects(), "EXPLAIN ANALYZE VERBOSE SELECT sqrt(id) FROM foo", true, true, None, + None, ); run_explain_analyze( + all_dialects(), "EXPLAIN ANALYZE FORMAT GRAPHVIZ SELECT sqrt(id) FROM foo", false, true, Some(AnalyzeFormat::GRAPHVIZ), + None, ); run_explain_analyze( + all_dialects(), "EXPLAIN ANALYZE VERBOSE FORMAT JSON SELECT sqrt(id) FROM foo", true, true, Some(AnalyzeFormat::JSON), + None, ); run_explain_analyze( + all_dialects(), "EXPLAIN VERBOSE FORMAT TEXT SELECT sqrt(id) FROM foo", true, false, Some(AnalyzeFormat::TEXT), + None, ); } @@ -10825,3 +10855,130 @@ fn test_truncate_table_with_on_cluster() { .unwrap_err() ); } + +#[test] +fn parse_explain_with_option_list() { + run_explain_analyze( + all_dialects_where(|d| d.supports_explain_with_utility_options()), + "EXPLAIN (ANALYZE false, VERBOSE true) SELECT sqrt(id) FROM foo", + false, + false, + None, + Some(vec![ + UtilityOption { + name: Ident::new("ANALYZE"), + arg: Some(Expr::Value(Value::Boolean(false))), + }, + UtilityOption { + name: Ident::new("VERBOSE"), + arg: Some(Expr::Value(Value::Boolean(true))), + }, + ]), + ); + + run_explain_analyze( + all_dialects_where(|d| d.supports_explain_with_utility_options()), + "EXPLAIN (ANALYZE ON, VERBOSE OFF) SELECT sqrt(id) FROM foo", + false, + false, + None, + Some(vec![ + UtilityOption { + name: Ident::new("ANALYZE"), + arg: Some(Expr::Identifier(Ident::new("ON"))), + }, + UtilityOption { + name: Ident::new("VERBOSE"), + arg: Some(Expr::Identifier(Ident::new("OFF"))), + }, + ]), + ); + + run_explain_analyze( + all_dialects_where(|d| d.supports_explain_with_utility_options()), + r#"EXPLAIN (FORMAT1 TEXT, FORMAT2 'JSON', FORMAT3 "XML", FORMAT4 YAML) SELECT sqrt(id) FROM foo"#, + false, + false, + None, + Some(vec![ + UtilityOption { + name: Ident::new("FORMAT1"), + arg: Some(Expr::Identifier(Ident::new("TEXT"))), + }, + UtilityOption { + name: Ident::new("FORMAT2"), + arg: Some(Expr::Value(Value::SingleQuotedString("JSON".to_string()))), + }, + UtilityOption { + name: Ident::new("FORMAT3"), + arg: Some(Expr::Identifier(Ident::with_quote('"', "XML"))), + }, + UtilityOption { + name: Ident::new("FORMAT4"), + arg: Some(Expr::Identifier(Ident::new("YAML"))), + }, + ]), + ); + + run_explain_analyze( + all_dialects_where(|d| d.supports_explain_with_utility_options()), + r#"EXPLAIN (NUM1 10, NUM2 +10.1, NUM3 -10.2) SELECT sqrt(id) FROM foo"#, + false, + false, + None, + Some(vec![ + UtilityOption { + name: Ident::new("NUM1"), + arg: Some(Expr::Value(Value::Number("10".parse().unwrap(), false))), + }, + UtilityOption { + name: Ident::new("NUM2"), + arg: Some(Expr::UnaryOp { + op: UnaryOperator::Plus, + expr: Box::new(Expr::Value(Value::Number("10.1".parse().unwrap(), false))), + }), + }, + UtilityOption { + name: Ident::new("NUM3"), + arg: Some(Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(Value::Number("10.2".parse().unwrap(), false))), + }), + }, + ]), + ); + + let utility_options = vec![ + UtilityOption { + name: Ident::new("ANALYZE"), + arg: None, + }, + UtilityOption { + name: Ident::new("VERBOSE"), + arg: Some(Expr::Value(Value::Boolean(true))), + }, + UtilityOption { + name: Ident::new("WAL"), + arg: Some(Expr::Identifier(Ident::new("OFF"))), + }, + UtilityOption { + name: Ident::new("FORMAT"), + arg: Some(Expr::Identifier(Ident::new("YAML"))), + }, + UtilityOption { + name: Ident::new("USER_DEF_NUM"), + arg: Some(Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(Value::Number("100.1".parse().unwrap(), false))), + }), + }, + ]; + run_explain_analyze ( + all_dialects_where(|d| d.supports_explain_with_utility_options()), + "EXPLAIN (ANALYZE, VERBOSE true, WAL OFF, FORMAT YAML, USER_DEF_NUM -100.1) SELECT sqrt(id) FROM foo", + false, + false, + None, + Some(utility_options), + ); +} From 71318df8b9e168b5d7d288d11b9e13a3c58ff55d Mon Sep 17 00:00:00 2001 From: Fischer <89784872+Fischer0522@users.noreply.github.com> Date: Sat, 21 Sep 2024 00:39:17 +0800 Subject: [PATCH 549/806] chore: remove redundant punctuation (#1434) --- src/parser/mod.rs | 2 +- tests/sqlparser_clickhouse.rs | 12 ++++++------ tests/sqlparser_common.rs | 4 ++-- tests/sqlparser_databricks.rs | 2 +- tests/sqlparser_mssql.rs | 2 +- tests/sqlparser_snowflake.rs | 8 ++++---- tests/sqlparser_sqlite.rs | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 751101fea..da1b4d2a6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1264,7 +1264,7 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_duckdb_struct_literal() } - _ => self.expected("an expression:", next_token), + _ => self.expected("an expression", next_token), }?; if self.parse_keyword(Keyword::COLLATE) { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index f89da7ee9..ae769c763 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -276,13 +276,13 @@ fn parse_alter_table_attach_and_detach_partition() { clickhouse_and_generic() .parse_sql_statements(format!("ALTER TABLE t0 {operation} PARTITION").as_str()) .unwrap_err(), - ParserError("Expected: an expression:, found: EOF".to_string()) + ParserError("Expected: an expression, found: EOF".to_string()) ); assert_eq!( clickhouse_and_generic() .parse_sql_statements(format!("ALTER TABLE t0 {operation} PART").as_str()) .unwrap_err(), - ParserError("Expected: an expression:, found: EOF".to_string()) + ParserError("Expected: an expression, found: EOF".to_string()) ); } } @@ -355,7 +355,7 @@ fn parse_alter_table_add_projection() { clickhouse_and_generic() .parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name (SELECT)") .unwrap_err(), - ParserError("Expected: an expression:, found: )".to_string()) + ParserError("Expected: an expression, found: )".to_string()) ); } @@ -498,13 +498,13 @@ fn parse_optimize_table() { clickhouse_and_generic() .parse_sql_statements("OPTIMIZE TABLE t0 DEDUPLICATE BY") .unwrap_err(), - ParserError("Expected: an expression:, found: EOF".to_string()) + ParserError("Expected: an expression, found: EOF".to_string()) ); assert_eq!( clickhouse_and_generic() .parse_sql_statements("OPTIMIZE TABLE t0 PARTITION") .unwrap_err(), - ParserError("Expected: an expression:, found: EOF".to_string()) + ParserError("Expected: an expression, found: EOF".to_string()) ); assert_eq!( clickhouse_and_generic() @@ -1479,7 +1479,7 @@ fn parse_freeze_and_unfreeze_partition() { clickhouse_and_generic() .parse_sql_statements(format!("ALTER TABLE t0 {operation_name} PARTITION").as_str()) .unwrap_err(), - ParserError("Expected: an expression:, found: EOF".to_string()) + ParserError("Expected: an expression, found: EOF".to_string()) ); assert_eq!( clickhouse_and_generic() diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2f001e17e..ac89fc1ca 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2051,7 +2051,7 @@ fn parse_tuple_invalid() { let sql = "select (), 2"; let res = parse_sql_statements(sql); assert_eq!( - ParserError::ParserError("Expected: an expression:, found: )".to_string()), + ParserError::ParserError("Expected: an expression, found: )".to_string()), res.unwrap_err() ); } @@ -9556,7 +9556,7 @@ fn parse_projection_trailing_comma() { trailing_commas .parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,") .unwrap_err(), - ParserError::ParserError("Expected: an expression:, found: EOF".to_string()) + ParserError::ParserError("Expected: an expression, found: EOF".to_string()) ); assert_eq!( diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index ee0cf2d7d..095ab0b30 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -64,7 +64,7 @@ fn test_databricks_exists() { let res = databricks().parse_sql_statements("SELECT EXISTS ("); assert_eq!( // TODO: improve this error message... - ParserError::ParserError("Expected: an expression:, found: EOF".to_string()), + ParserError::ParserError("Expected: an expression, found: EOF".to_string()), res.unwrap_err(), ); } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 0ab160f56..97114aed0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -487,7 +487,7 @@ fn parse_convert() { let error_sql = "SELECT CONVERT(INT, 'foo',) FROM T"; assert_eq!( - ParserError::ParserError("Expected: an expression:, found: )".to_owned()), + ParserError::ParserError("Expected: an expression, found: )".to_owned()), ms().parse_sql_statements(error_sql).unwrap_err() ); } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index d0876fc50..5be5bfd3a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1230,13 +1230,13 @@ fn parse_snowflake_declare_result_set() { let error_sql = "DECLARE res RESULTSET DEFAULT"; assert_eq!( - ParserError::ParserError("Expected: an expression:, found: EOF".to_owned()), + ParserError::ParserError("Expected: an expression, found: EOF".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); let error_sql = "DECLARE res RESULTSET :="; assert_eq!( - ParserError::ParserError("Expected: an expression:, found: EOF".to_owned()), + ParserError::ParserError("Expected: an expression, found: EOF".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); } @@ -1328,13 +1328,13 @@ fn parse_snowflake_declare_variable() { let error_sql = "DECLARE profit INT DEFAULT"; assert_eq!( - ParserError::ParserError("Expected: an expression:, found: EOF".to_owned()), + ParserError::ParserError("Expected: an expression, found: EOF".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); let error_sql = "DECLARE profit DEFAULT"; assert_eq!( - ParserError::ParserError("Expected: an expression:, found: EOF".to_owned()), + ParserError::ParserError("Expected: an expression, found: EOF".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index ce11b015a..8e2afb8d7 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -432,7 +432,7 @@ fn invalid_empty_list() { let sql = "SELECT * FROM t1 WHERE a IN (,,)"; let sqlite = sqlite_with_options(ParserOptions::new().with_trailing_commas(true)); assert_eq!( - "sql parser error: Expected: an expression:, found: ,", + "sql parser error: Expected: an expression, found: ,", sqlite.parse_sql_statements(sql).unwrap_err().to_string() ); } From fb42425d51162cd57e5a0cbd49eafc5743d60daf Mon Sep 17 00:00:00 2001 From: Aleksei Piianin Date: Fri, 20 Sep 2024 18:44:24 +0200 Subject: [PATCH 550/806] MS SQL Server: add support for IDENTITY column option (#1432) --- src/ast/ddl.rs | 28 ++++++++++ src/ast/mod.rs | 4 +- src/parser/mod.rs | 14 +++++ tests/sqlparser_mssql.rs | 111 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index b5444b8da..3cb754dfc 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1050,6 +1050,20 @@ impl fmt::Display for ColumnOptionDef { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IdentityProperty { + pub seed: Expr, + pub increment: Expr, +} + +impl fmt::Display for IdentityProperty { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}, {}", self.seed, self.increment) + } +} + /// `ColumnOption`s are modifiers that follow a column definition in a `CREATE /// TABLE` statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -1120,6 +1134,13 @@ pub enum ColumnOption { /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#view_column_option_list /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#column_option_list Options(Vec), + /// MS SQL Server specific: Creates an identity column in a table. + /// Syntax + /// ```sql + /// IDENTITY [ (seed , increment) ] + /// ``` + /// [MS SQL Server]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql-identity-property + Identity(Option), } impl fmt::Display for ColumnOption { @@ -1221,6 +1242,13 @@ impl fmt::Display for ColumnOption { Options(options) => { write!(f, "OPTIONS({})", display_comma_separated(options)) } + Identity(parameters) => { + write!(f, "IDENTITY")?; + if let Some(parameters) = parameters { + write!(f, "({parameters})")?; + } + Ok(()) + } } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 94dabb059..4eba1e59f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -36,8 +36,8 @@ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue, pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ConstraintCharacteristics, Deduplicate, DeferrableInitial, - GeneratedAs, GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Owner, - Partition, ProcedureParam, ReferentialAction, TableConstraint, + GeneratedAs, GeneratedExpressionMode, IdentityProperty, IndexOption, IndexType, + KeyOrIndexDisplay, Owner, Partition, ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index da1b4d2a6..5cce97767 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6069,6 +6069,20 @@ impl<'a> Parser<'a> { && dialect_of!(self is MySqlDialect | SQLiteDialect | DuckDbDialect | GenericDialect) { self.parse_optional_column_option_as() + } else if self.parse_keyword(Keyword::IDENTITY) + && dialect_of!(self is MsSqlDialect | GenericDialect) + { + let property = if self.consume_token(&Token::LParen) { + let seed = self.parse_number()?; + self.expect_token(&Token::Comma)?; + let increment = self.parse_number()?; + self.expect_token(&Token::RParen)?; + + Some(IdentityProperty { seed, increment }) + } else { + None + }; + Ok(Some(ColumnOption::Identity(property))) } else { Ok(None) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 97114aed0..9df8ae351 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -908,6 +908,117 @@ fn parse_create_table_with_invalid_options() { } } +#[test] +fn parse_create_table_with_identity_column() { + let with_column_options = [ + ( + r#"CREATE TABLE mytable (columnA INT IDENTITY NOT NULL)"#, + vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Identity(None), + }, + ColumnOptionDef { + name: None, + option: ColumnOption::NotNull, + }, + ], + ), + ( + r#"CREATE TABLE mytable (columnA INT IDENTITY(1, 1) NOT NULL)"#, + vec![ + ColumnOptionDef { + name: None, + #[cfg(not(feature = "bigdecimal"))] + option: ColumnOption::Identity(Some(IdentityProperty { + seed: Expr::Value(Value::Number("1".to_string(), false)), + increment: Expr::Value(Value::Number("1".to_string(), false)), + })), + #[cfg(feature = "bigdecimal")] + option: ColumnOption::Identity(Some(IdentityProperty { + seed: Expr::Value(Value::Number(bigdecimal::BigDecimal::from(1), false)), + increment: Expr::Value(Value::Number( + bigdecimal::BigDecimal::from(1), + false, + )), + })), + }, + ColumnOptionDef { + name: None, + option: ColumnOption::NotNull, + }, + ], + ), + ]; + + for (sql, column_options) in with_column_options { + assert_eq!( + ms_and_generic().verified_stmt(sql), + Statement::CreateTable(CreateTable { + or_replace: false, + temporary: false, + external: false, + global: None, + if_not_exists: false, + transient: false, + volatile: false, + name: ObjectName(vec![Ident { + value: "mytable".to_string(), + quote_style: None, + },],), + columns: vec![ColumnDef { + name: Ident { + value: "columnA".to_string(), + quote_style: None, + }, + data_type: Int(None,), + collation: None, + options: column_options, + },], + constraints: vec![], + hive_distribution: HiveDistributionStyle::NONE, + hive_formats: Some(HiveFormat { + row_format: None, + serde_properties: None, + 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, + order_by: None, + partition_by: None, + cluster_by: None, + clustered_by: None, + options: None, + strict: false, + copy_grants: false, + enable_schema_evolution: None, + change_tracking: None, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + default_ddl_collation: None, + with_aggregation_policy: None, + with_row_access_policy: None, + with_tags: None, + }), + ); + } +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], From 9934f3d93100720a2b15290be6f767fb675bec11 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 21 Sep 2024 06:23:28 -0400 Subject: [PATCH 551/806] Update to ASF header / add when missing (#1437) --- CHANGELOG.md | 19 ++++++ Cargo.toml | 17 +++++ HEADER | 23 ++++--- SECURITY.md | 19 ++++++ derive/Cargo.toml | 17 +++++ derive/README.md | 19 ++++++ derive/src/lib.rs | 79 ++++++++++++++++------ docs/benchmarking.md | 19 ++++++ docs/custom_sql_parser.md | 19 ++++++ docs/fuzzing.md | 19 ++++++ docs/releasing.md | 19 ++++++ examples/cli.rs | 23 ++++--- examples/parse_select.rs | 23 ++++--- fuzz/Cargo.toml | 17 +++++ fuzz/fuzz_targets/fuzz_parse_sql.rs | 17 +++++ rustfmt.toml | 17 +++++ sqlparser_bench/Cargo.toml | 17 +++++ sqlparser_bench/benches/sqlparser_bench.rs | 23 ++++--- src/ast/data_type.rs | 23 ++++--- src/ast/dcl.rs | 23 ++++--- src/ast/ddl.rs | 23 ++++--- src/ast/dml.rs | 23 ++++--- src/ast/helpers/mod.rs | 16 +++++ src/ast/helpers/stmt_create_table.rs | 17 +++++ src/ast/helpers/stmt_data_loading.rs | 23 ++++--- src/ast/mod.rs | 23 ++++--- src/ast/operator.rs | 23 ++++--- src/ast/query.rs | 23 ++++--- src/ast/trigger.rs | 23 ++++--- src/ast/value.rs | 23 ++++--- src/ast/visitor.rs | 23 ++++--- src/dialect/ansi.rs | 23 ++++--- src/dialect/bigquery.rs | 23 ++++--- src/dialect/clickhouse.rs | 23 ++++--- src/dialect/databricks.rs | 17 +++++ src/dialect/duckdb.rs | 23 ++++--- src/dialect/generic.rs | 23 ++++--- src/dialect/hive.rs | 23 ++++--- src/dialect/mod.rs | 23 ++++--- src/dialect/mssql.rs | 23 ++++--- src/dialect/mysql.rs | 23 ++++--- src/dialect/postgresql.rs | 17 +++++ src/dialect/redshift.rs | 23 ++++--- src/dialect/snowflake.rs | 23 ++++--- src/dialect/sqlite.rs | 23 ++++--- src/keywords.rs | 23 ++++--- src/lib.rs | 23 ++++--- src/test_utils.rs | 23 ++++--- src/tokenizer.rs | 23 ++++--- tests/sqlparser_bigquery.rs | 23 ++++--- tests/sqlparser_clickhouse.rs | 23 ++++--- tests/sqlparser_common.rs | 23 ++++--- tests/sqlparser_custom_dialect.rs | 23 ++++--- tests/sqlparser_databricks.rs | 17 +++++ tests/sqlparser_duckdb.rs | 23 ++++--- tests/sqlparser_hive.rs | 23 ++++--- tests/sqlparser_mssql.rs | 23 ++++--- tests/sqlparser_mysql.rs | 23 ++++--- tests/sqlparser_postgres.rs | 23 ++++--- tests/sqlparser_redshift.rs | 23 ++++--- tests/sqlparser_regression.rs | 23 ++++--- tests/sqlparser_snowflake.rs | 23 ++++--- tests/sqlparser_sqlite.rs | 23 ++++--- tests/test_utils/mod.rs | 23 ++++--- 64 files changed, 1006 insertions(+), 427 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c77755d59..07142602d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ + + # Changelog All notable changes to this project will be documented in this file. diff --git a/Cargo.toml b/Cargo.toml index 2448b67ce..99546465b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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. + [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" diff --git a/HEADER b/HEADER index 79fe4603f..6e778edd7 100644 --- a/HEADER +++ b/HEADER @@ -1,11 +1,16 @@ -// Licensed 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 +// 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 +// 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. \ No newline at end of file +// 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. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index 0636b8fc2..662ab74f0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,3 +1,22 @@ + + # Security Policy ## Reporting a Vulnerability diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 02eb7c880..0c5852c4c 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -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. + [package] name = "sqlparser_derive" description = "proc macro for sqlparser" diff --git a/derive/README.md b/derive/README.md index ffb5d266e..aa70e7c71 100644 --- a/derive/README.md +++ b/derive/README.md @@ -1,3 +1,22 @@ + + # SQL Parser Derive Macro ## Visit diff --git a/derive/src/lib.rs b/derive/src/lib.rs index d19696aa4..5ad1607f9 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,31 +1,53 @@ +// 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 proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::spanned::Spanned; use syn::{ parse::{Parse, ParseStream}, - parse_macro_input, parse_quote, Attribute, Data, DeriveInput, - Fields, GenericParam, Generics, Ident, Index, LitStr, Meta, Token + parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Fields, GenericParam, Generics, + Ident, Index, LitStr, Meta, Token, }; - /// Implementation of `[#derive(Visit)]` #[proc_macro_derive(VisitMut, attributes(visit))] pub fn derive_visit_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - derive_visit(input, &VisitType { - visit_trait: quote!(VisitMut), - visitor_trait: quote!(VisitorMut), - modifier: Some(quote!(mut)), - }) + derive_visit( + input, + &VisitType { + visit_trait: quote!(VisitMut), + visitor_trait: quote!(VisitorMut), + modifier: Some(quote!(mut)), + }, + ) } /// Implementation of `[#derive(Visit)]` #[proc_macro_derive(Visit, attributes(visit))] pub fn derive_visit_immutable(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - derive_visit(input, &VisitType { - visit_trait: quote!(Visit), - visitor_trait: quote!(Visitor), - modifier: None, - }) + derive_visit( + input, + &VisitType { + visit_trait: quote!(Visit), + visitor_trait: quote!(Visitor), + modifier: None, + }, + ) } struct VisitType { @@ -34,15 +56,16 @@ struct VisitType { modifier: Option, } -fn derive_visit( - input: proc_macro::TokenStream, - visit_type: &VisitType, -) -> proc_macro::TokenStream { +fn derive_visit(input: proc_macro::TokenStream, visit_type: &VisitType) -> proc_macro::TokenStream { // Parse the input tokens into a syntax tree. let input = parse_macro_input!(input as DeriveInput); let name = input.ident; - let VisitType { visit_trait, visitor_trait, modifier } = visit_type; + let VisitType { + visit_trait, + visitor_trait, + modifier, + } = visit_type; let attributes = Attributes::parse(&input.attrs); // Add a bound `T: Visit` to every type parameter T. @@ -87,7 +110,10 @@ impl Parse for WithIdent { let mut result = WithIdent { with: None }; let ident = input.parse::()?; if ident != "with" { - return Err(syn::Error::new(ident.span(), "Expected identifier to be `with`")); + return Err(syn::Error::new( + ident.span(), + "Expected identifier to be `with`", + )); } input.parse::()?; let s = input.parse::()?; @@ -131,17 +157,26 @@ impl Attributes { } // Add a bound `T: Visit` to every type parameter T. -fn add_trait_bounds(mut generics: Generics, VisitType{visit_trait, ..}: &VisitType) -> Generics { +fn add_trait_bounds(mut generics: Generics, VisitType { visit_trait, .. }: &VisitType) -> Generics { for param in &mut generics.params { if let GenericParam::Type(ref mut type_param) = *param { - type_param.bounds.push(parse_quote!(sqlparser::ast::#visit_trait)); + type_param + .bounds + .push(parse_quote!(sqlparser::ast::#visit_trait)); } } generics } // Generate the body of the visit implementation for the given type -fn visit_children(data: &Data, VisitType{visit_trait, modifier, ..}: &VisitType) -> TokenStream { +fn visit_children( + data: &Data, + VisitType { + visit_trait, + modifier, + .. + }: &VisitType, +) -> TokenStream { match data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => { diff --git a/docs/benchmarking.md b/docs/benchmarking.md index feae53c84..bba67bc4d 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -1,3 +1,22 @@ + + # Benchmarking Run `cargo bench` in the project `sqlparser_bench` execute the queries. diff --git a/docs/custom_sql_parser.md b/docs/custom_sql_parser.md index 055ddbaa5..9e9a0dd87 100644 --- a/docs/custom_sql_parser.md +++ b/docs/custom_sql_parser.md @@ -1,3 +1,22 @@ + + # Writing a Custom SQL Parser I have explored many different ways of building this library to make it easy to extend it for custom SQL dialects. Most of my attempts ended in failure but I have now found a workable solution. It is not without downsides but this seems to be the most pragmatic solution. diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 72d3d96eb..cc4ca4d78 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -1,3 +1,22 @@ + + # Fuzzing ## Installing `honggfuzz` diff --git a/docs/releasing.md b/docs/releasing.md index 13ae8290f..c1b85a20c 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,3 +1,22 @@ + + # Releasing ## Prerequisites diff --git a/examples/cli.rs b/examples/cli.rs index 72f963b1e..8a5d6501e 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] diff --git a/examples/parse_select.rs b/examples/parse_select.rs index 71fe1fa1e..1d977fb05 100644 --- a/examples/parse_select.rs +++ b/examples/parse_select.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 72ab86ef6..c162854d4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -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. + [package] name = "fuzz" version = "0.1.0" diff --git a/fuzz/fuzz_targets/fuzz_parse_sql.rs b/fuzz/fuzz_targets/fuzz_parse_sql.rs index 629fa360b..446b036cd 100644 --- a/fuzz/fuzz_targets/fuzz_parse_sql.rs +++ b/fuzz/fuzz_targets/fuzz_parse_sql.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 honggfuzz::fuzz; use sqlparser::dialect::GenericDialect; use sqlparser::parser::Parser; diff --git a/rustfmt.toml b/rustfmt.toml index edd0eecba..b9037cc0b 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,18 @@ +# 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. + # We use rustfmt's default settings to format the source code \ No newline at end of file diff --git a/sqlparser_bench/Cargo.toml b/sqlparser_bench/Cargo.toml index f2cd93288..9c33658a2 100644 --- a/sqlparser_bench/Cargo.toml +++ b/sqlparser_bench/Cargo.toml @@ -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. + [package] name = "sqlparser_bench" version = "0.1.0" diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs index 5293c0f50..27c58b450 100644 --- a/sqlparser_bench/benches/sqlparser_bench.rs +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 criterion::{criterion_group, criterion_main, Criterion}; use sqlparser::dialect::GenericDialect; diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index f3ebd16da..bc48341c4 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[cfg(not(feature = "std"))] use alloc::{boxed::Box, format, string::String, vec::Vec}; diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 1b0a77095..d47476ffa 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! AST types specific to GRANT/REVOKE/ROLE variants of [`Statement`](crate::ast::Statement) //! (commonly referred to as Data Control Language, or DCL) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3cb754dfc..e0441d07a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! AST types specific to CREATE/ALTER variants of [`Statement`](crate::ast::Statement) //! (commonly referred to as Data Definition Language, or DDL) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index c0e58e21a..8121f2c5b 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[cfg(not(feature = "std"))] use alloc::{boxed::Box, string::String, vec::Vec}; diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index b54e59b6d..d6924ab88 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -1,2 +1,18 @@ +// 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. pub mod stmt_create_table; pub mod stmt_data_loading; diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 82532b291..364969c40 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.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. + #[cfg(not(feature = "std"))] use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index a259e664b..a17f8c68c 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! AST types specific to loading and unloading syntax, like one available in Snowflake which //! contains: STAGE ddl operations, PUT upload or COPY INTO diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4eba1e59f..00898939c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! SQL Abstract Syntax Tree (AST) types #[cfg(not(feature = "std"))] diff --git a/src/ast/operator.rs b/src/ast/operator.rs index db6ed0564..c3bb379d6 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 core::fmt; diff --git a/src/ast/query.rs b/src/ast/query.rs index c52d01105..ec0198674 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[cfg(not(feature = "std"))] use alloc::{boxed::Box, vec::Vec}; diff --git a/src/ast/trigger.rs b/src/ast/trigger.rs index a0913db94..cf1c8c466 100644 --- a/src/ast/trigger.rs +++ b/src/ast/trigger.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! SQL Abstract Syntax Tree (AST) for triggers. use super::*; diff --git a/src/ast/value.rs b/src/ast/value.rs index 17cdb839d..30d956a07 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[cfg(not(feature = "std"))] use alloc::string::String; diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 1b8a43802..418e0a299 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! Recursive visitors for ast Nodes. See [`Visitor`] for more details. diff --git a/src/dialect/ansi.rs b/src/dialect/ansi.rs index 61ae5829e..32ba7b32a 100644 --- a/src/dialect/ansi.rs +++ b/src/dialect/ansi.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::dialect::Dialect; diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 3bce6702b..96633552b 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::dialect::Dialect; diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 09735cbee..ee8ee94d2 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::dialect::Dialect; diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index d3661444b..4924e8077 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.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 crate::dialect::Dialect; /// A [`Dialect`] for [Databricks SQL](https://www.databricks.com/) diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 811f25f72..e1b8db118 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::dialect::Dialect; diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 1c638d8b0..bea56d0ef 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::dialect::Dialect; diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index b32d44cb9..63642b33c 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::dialect::Dialect; diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index cf0af6329..619b54713 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. mod ansi; mod bigquery; diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index ec247092f..a9d296be3 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::dialect::Dialect; diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index b8c4631fd..2b5e42e59 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[cfg(not(feature = "std"))] use alloc::boxed::Box; diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 5a7db0216..945c4fcc0 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.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. + // Licensed 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 diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index bd4dc817c..3bfdec3b0 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::dialect::Dialect; use core::iter::Peekable; diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 4f37004b1..e14746d9a 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index cc08d7961..d3813d91c 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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 crate::ast::Statement; use crate::dialect::Dialect; diff --git a/src/keywords.rs b/src/keywords.rs index 68eaf050b..d384062f2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! This module defines //! 1) a list of constants for every keyword diff --git a/src/lib.rs b/src/lib.rs index ba0132ed8..6c8987b63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! # SQL Parser for Rust //! diff --git a/src/test_utils.rs b/src/test_utils.rs index 5c05ec996..eb0352353 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. /// This module contains internal utilities used for testing the library. /// While technically public, the library's users are not supposed to rely diff --git a/src/tokenizer.rs b/src/tokenizer.rs index be11a3140..6d0c86ff2 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! SQL Tokenizer //! diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index e051baa8b..55afe473b 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[macro_use] mod test_utils; diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index ae769c763..e30c33678 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] //! Test SQL syntax specific to ClickHouse. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ac89fc1ca..9aa76882a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] //! Test SQL syntax, which all sqlparser dialects must parse in the same way. diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index 5b29047a4..e9ca82aba 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. //! Test the ability for dialects to override parsing diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 095ab0b30..7dcfee68a 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.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::ast::*; use sqlparser::dialect::{DatabricksDialect, GenericDialect}; use sqlparser::parser::ParserError; diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 12368a88c..4703f4b60 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[macro_use] mod test_utils; diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 5426dfbc4..069500bf6 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9df8ae351..5a2ef9e87 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] //! Test SQL syntax specific to Microsoft's T-SQL. The parser based on the diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 33587c35a..87468b933 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] //! Test SQL syntax specific to MySQL. The parser based on the generic dialect diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 069b61d05..9ba3e5dbc 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] //! Test SQL syntax specific to PostgreSQL. The parser based on the diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 440116e02..eeba37957 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #[macro_use] mod test_utils; diff --git a/tests/sqlparser_regression.rs b/tests/sqlparser_regression.rs index e869e0932..55e03c45b 100644 --- a/tests/sqlparser_regression.rs +++ b/tests/sqlparser_regression.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5be5bfd3a..50c5f740e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] //! Test SQL syntax specific to Snowflake. The parser based on the diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 8e2afb8d7..d7fd3b896 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. #![warn(clippy::all)] //! Test SQL syntax specific to SQLite. The parser based on the diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index d3a56ce46..4bb0b1151 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,14 +1,19 @@ -// Licensed 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 +// 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 +// 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. +// 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. // Re-export everything from `src/test_utils.rs`. pub use sqlparser::test_utils::*; From 2403f79f55d2e2bb6281f787198fc029d9775440 Mon Sep 17 00:00:00 2001 From: Thomas Dagenais Date: Sat, 21 Sep 2024 06:25:14 -0400 Subject: [PATCH 552/806] Some small optimizations (#1424) --- src/ast/helpers/stmt_data_loading.rs | 9 ++++++--- src/ast/mod.rs | 25 ++++++++----------------- src/parser/mod.rs | 6 +++--- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index a17f8c68c..cda6c6ea4 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -108,11 +108,14 @@ impl fmt::Display for StageParamsObject { impl fmt::Display for DataLoadingOptions { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if !self.options.is_empty() { + let mut first = false; for option in &self.options { - write!(f, "{}", option)?; - if !option.eq(self.options.last().unwrap()) { - write!(f, " ")?; + if !first { + first = true; + } else { + f.write_str(" ")?; } + write!(f, "{}", option)?; } } Ok(()) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 00898939c..9b7a66650 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4511,30 +4511,21 @@ impl fmt::Display for Statement { options, query, } => { - if table_flag.is_some() { - write!( - f, - "CACHE {table_flag} TABLE {table_name}", - table_flag = table_flag.clone().unwrap(), - table_name = table_name, - )?; + if let Some(table_flag) = table_flag { + write!(f, "CACHE {table_flag} TABLE {table_name}")?; } else { - write!(f, "CACHE TABLE {table_name}",)?; + write!(f, "CACHE TABLE {table_name}")?; } if !options.is_empty() { write!(f, " OPTIONS({})", display_comma_separated(options))?; } - let has_query = query.is_some(); - if *has_as && has_query { - write!(f, " AS {query}", query = query.clone().unwrap()) - } else if !has_as && has_query { - write!(f, " {query}", query = query.clone().unwrap()) - } else if *has_as && !has_query { - write!(f, " AS") - } else { - Ok(()) + match (*has_as, query) { + (true, Some(query)) => write!(f, " AS {query}"), + (true, None) => f.write_str(" AS"), + (false, Some(query)) => write!(f, " {query}"), + (false, None) => Ok(()), } } Statement::UNCache { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5cce97767..5d57347cf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10552,12 +10552,12 @@ impl<'a> Parser<'a> { return parser_err!("Unsupported statement REPLACE", self.peek_token().location); } - let insert = &mut self.parse_insert()?; - if let Statement::Insert(Insert { replace_into, .. }) = insert { + let mut insert = self.parse_insert()?; + if let Statement::Insert(Insert { replace_into, .. }) = &mut insert { *replace_into = true; } - Ok(insert.clone()) + Ok(insert) } /// Parse an INSERT statement, returning a `Box`ed SetExpr From affe8b549884a351ead4f35aa8bdf4cae8c93e4b Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 22 Sep 2024 07:32:37 -0400 Subject: [PATCH 553/806] Fix `codestyle` CI check (#1438) --- .github/workflows/rust.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 146ea3120..4aab9cee7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -10,11 +10,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Rust Toolchain uses: ./.github/actions/setup-builder - with: - # Note that `nightly` is required for `license_template_path`, as - # it's an unstable feature. - rust-version: nightly - - run: cargo +nightly fmt -- --check --config-path <(echo 'license_template_path = "HEADER"') + - run: cargo fmt -- --check lint: runs-on: ubuntu-latest From 8a534c0e279dde2fad06e55423fba9cbffcca0a2 Mon Sep 17 00:00:00 2001 From: hulk Date: Thu, 26 Sep 2024 01:32:04 +0800 Subject: [PATCH 554/806] Implements CREATE POLICY syntax for PostgreSQL (#1440) --- src/ast/mod.rs | 85 +++++++++++++++++++++++++++ src/keywords.rs | 2 + src/parser/mod.rs | 118 +++++++++++++++++++++++++++++++++----- tests/sqlparser_common.rs | 102 ++++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 15 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9b7a66650..83646d298 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2135,6 +2135,35 @@ pub enum FromTable { WithoutKeyword(Vec), } +/// Policy type for a `CREATE POLICY` statement. +/// ```sql +/// AS [ PERMISSIVE | RESTRICTIVE ] +/// ``` +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.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 CreatePolicyType { + Permissive, + Restrictive, +} + +/// Policy command for a `CREATE POLICY` statement. +/// ```sql +/// FOR [ALL | SELECT | INSERT | UPDATE | DELETE] +/// ``` +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.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 CreatePolicyCommand { + All, + Select, + Insert, + Update, + Delete, +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -2375,6 +2404,20 @@ pub enum Statement { options: Vec, }, /// ```sql + /// CREATE POLICY + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) + CreatePolicy { + name: Ident, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + table_name: ObjectName, + policy_type: Option, + command: Option, + to: Option>, + using: Option, + with_check: Option, + }, + /// ```sql /// ALTER TABLE /// ``` AlterTable { @@ -4052,6 +4095,48 @@ impl fmt::Display for Statement { write!(f, " )")?; Ok(()) } + Statement::CreatePolicy { + name, + table_name, + policy_type, + command, + to, + using, + with_check, + } => { + write!(f, "CREATE POLICY {name} ON {table_name}")?; + + if let Some(policy_type) = policy_type { + match policy_type { + CreatePolicyType::Permissive => write!(f, " AS PERMISSIVE")?, + CreatePolicyType::Restrictive => write!(f, " AS RESTRICTIVE")?, + } + } + + if let Some(command) = command { + match command { + CreatePolicyCommand::All => write!(f, " FOR ALL")?, + CreatePolicyCommand::Select => write!(f, " FOR SELECT")?, + CreatePolicyCommand::Insert => write!(f, " FOR INSERT")?, + CreatePolicyCommand::Update => write!(f, " FOR UPDATE")?, + CreatePolicyCommand::Delete => write!(f, " FOR DELETE")?, + } + } + + if let Some(to) = to { + write!(f, " TO {}", display_comma_separated(to))?; + } + + if let Some(using) = using { + write!(f, " USING ({using})")?; + } + + if let Some(with_check) = with_check { + write!(f, " WITH CHECK ({with_check})")?; + } + + Ok(()) + } Statement::AlterTable { name, if_exists, diff --git a/src/keywords.rs b/src/keywords.rs index d384062f2..49c6ce20f 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -568,6 +568,7 @@ define_keywords!( PERCENTILE_DISC, PERCENT_RANK, PERIOD, + PERMISSIVE, PERSISTENT, PIVOT, PLACING, @@ -634,6 +635,7 @@ define_keywords!( RESTART, RESTRICT, RESTRICTED, + RESTRICTIVE, RESULT, RESULTSET, RETAIN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5d57347cf..4c3f8788d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -32,6 +32,7 @@ use IsLateral::*; use IsOptional::*; use crate::ast::helpers::stmt_create_table::{CreateTableBuilder, CreateTableConfiguration}; +use crate::ast::Statement::CreatePolicy; use crate::ast::*; use crate::dialect::*; use crate::keywords::{Keyword, ALL_KEYWORDS}; @@ -3569,6 +3570,8 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::MATERIALIZED) || self.parse_keyword(Keyword::VIEW) { self.prev_token(); self.parse_create_view(or_replace, temporary) + } else if self.parse_keyword(Keyword::POLICY) { + self.parse_create_policy() } else if self.parse_keyword(Keyword::EXTERNAL) { self.parse_create_external_table(or_replace) } else if self.parse_keyword(Keyword::FUNCTION) { @@ -4762,6 +4765,105 @@ impl<'a> Parser<'a> { }) } + pub fn parse_owner(&mut self) -> Result { + let owner = match self.parse_one_of_keywords(&[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) { + Some(Keyword::CURRENT_USER) => Owner::CurrentUser, + Some(Keyword::CURRENT_ROLE) => Owner::CurrentRole, + Some(Keyword::SESSION_USER) => Owner::SessionUser, + Some(_) => unreachable!(), + None => { + match self.parse_identifier(false) { + Ok(ident) => Owner::Ident(ident), + Err(e) => { + return Err(ParserError::ParserError(format!("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. {e}"))) + } + } + }, + }; + Ok(owner) + } + + /// ```sql + /// CREATE POLICY name ON table_name [ AS { PERMISSIVE | RESTRICTIVE } ] + /// [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ] + /// [ TO { role_name | PUBLIC | CURRENT_USER | CURRENT_ROLE | SESSION_USER } [, ...] ] + /// [ USING ( using_expression ) ] + /// [ WITH CHECK ( with_check_expression ) ] + /// ``` + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createpolicy.html) + pub fn parse_create_policy(&mut self) -> Result { + let name = self.parse_identifier(false)?; + self.expect_keyword(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + + let policy_type = if self.parse_keyword(Keyword::AS) { + let keyword = + self.expect_one_of_keywords(&[Keyword::PERMISSIVE, Keyword::RESTRICTIVE])?; + Some(match keyword { + Keyword::PERMISSIVE => CreatePolicyType::Permissive, + Keyword::RESTRICTIVE => CreatePolicyType::Restrictive, + _ => unreachable!(), + }) + } else { + None + }; + + let command = if self.parse_keyword(Keyword::FOR) { + let keyword = self.expect_one_of_keywords(&[ + Keyword::ALL, + Keyword::SELECT, + Keyword::INSERT, + Keyword::UPDATE, + Keyword::DELETE, + ])?; + Some(match keyword { + Keyword::ALL => CreatePolicyCommand::All, + Keyword::SELECT => CreatePolicyCommand::Select, + Keyword::INSERT => CreatePolicyCommand::Insert, + Keyword::UPDATE => CreatePolicyCommand::Update, + Keyword::DELETE => CreatePolicyCommand::Delete, + _ => unreachable!(), + }) + } else { + None + }; + + let to = if self.parse_keyword(Keyword::TO) { + Some(self.parse_comma_separated(|p| p.parse_owner())?) + } else { + None + }; + + let using = if self.parse_keyword(Keyword::USING) { + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Some(expr) + } else { + None + }; + + let with_check = if self.parse_keywords(&[Keyword::WITH, Keyword::CHECK]) { + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Some(expr) + } else { + None + }; + + Ok(CreatePolicy { + name, + table_name, + policy_type, + command, + to, + using, + with_check, + }) + } + pub fn parse_drop(&mut self) -> Result { // MySQL dialect supports `TEMPORARY` let temporary = dialect_of!(self is MySqlDialect | GenericDialect | DuckDbDialect) @@ -6941,21 +7043,7 @@ impl<'a> Parser<'a> { } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) && self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { - let new_owner = match self.parse_one_of_keywords(&[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) { - Some(Keyword::CURRENT_USER) => Owner::CurrentUser, - Some(Keyword::CURRENT_ROLE) => Owner::CurrentRole, - Some(Keyword::SESSION_USER) => Owner::SessionUser, - Some(_) => unreachable!(), - None => { - match self.parse_identifier(false) { - Ok(ident) => Owner::Ident(ident), - Err(e) => { - return Err(ParserError::ParserError(format!("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. {e}"))) - } - } - }, - }; - + let new_owner = self.parse_owner()?; AlterTableOperation::OwnerTo { new_owner } } else if dialect_of!(self is ClickHouseDialect|GenericDialect) && self.parse_keyword(Keyword::ATTACH) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9aa76882a..711070034 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10987,3 +10987,105 @@ fn parse_explain_with_option_list() { Some(utility_options), ); } + +#[test] +fn test_create_policy() { + let sql = concat!( + "CREATE POLICY my_policy ON my_table ", + "AS PERMISSIVE FOR SELECT ", + "TO my_role, CURRENT_USER ", + "USING (c0 = 1) ", + "WITH CHECK (true)" + ); + + match all_dialects().verified_stmt(sql) { + Statement::CreatePolicy { + name, + table_name, + to, + using, + with_check, + .. + } => { + assert_eq!(name.to_string(), "my_policy"); + assert_eq!(table_name.to_string(), "my_table"); + assert_eq!( + to, + Some(vec![ + Owner::Ident(Ident::new("my_role")), + Owner::CurrentUser + ]) + ); + assert_eq!( + using, + Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("c0"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + }) + ); + assert_eq!(with_check, Some(Expr::Value(Value::Boolean(true)))); + } + _ => unreachable!(), + } + + // USING with SELECT query + all_dialects().verified_stmt(concat!( + "CREATE POLICY my_policy ON my_table ", + "AS PERMISSIVE FOR SELECT ", + "TO my_role, CURRENT_USER ", + "USING (c0 IN (SELECT column FROM t0)) ", + "WITH CHECK (true)" + )); + // omit AS / FOR / TO / USING / WITH CHECK clauses is allowed + all_dialects().verified_stmt("CREATE POLICY my_policy ON my_table"); + + // missing table name + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE POLICY my_policy") + .unwrap_err() + .to_string(), + "sql parser error: Expected: ON, found: EOF" + ); + // missing policy type + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE POLICY my_policy ON my_table AS") + .unwrap_err() + .to_string(), + "sql parser error: Expected: one of PERMISSIVE or RESTRICTIVE, found: EOF" + ); + // missing FOR command + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE POLICY my_policy ON my_table FOR") + .unwrap_err() + .to_string(), + "sql parser error: Expected: one of ALL or SELECT or INSERT or UPDATE or DELETE, found: EOF" + ); + // missing TO owners + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE POLICY my_policy ON my_table TO") + .unwrap_err() + .to_string(), + "sql parser error: Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: EOF" + ); + // missing USING expression + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE POLICY my_policy ON my_table USING") + .unwrap_err() + .to_string(), + "sql parser error: Expected: (, found: EOF" + ); + // missing WITH CHECK expression + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE POLICY my_policy ON my_table WITH CHECK") + .unwrap_err() + .to_string(), + "sql parser error: Expected: (, found: EOF" + ); +} From 08fc6d381397760156fc247985b25fa85741bf47 Mon Sep 17 00:00:00 2001 From: Eason <30045503+Eason0729@users.noreply.github.com> Date: Sun, 29 Sep 2024 17:58:00 +0800 Subject: [PATCH 555/806] make `parse_expr_with_alias` public (#1444) Co-authored-by: Andrew Lamb Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4c3f8788d..b163b4cfb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10377,8 +10377,31 @@ impl<'a> Parser<'a> { Ok(ExprWithAlias { expr, alias }) } + /// Parses an expression with an optional alias - fn parse_expr_with_alias(&mut self) -> Result { + /// Examples: + + /// ```sql + /// SUM(price) AS total_price + /// ``` + + /// ```sql + /// SUM(price) + /// ``` + /// + /// Example + /// ``` + /// # use sqlparser::parser::{Parser, ParserError}; + /// # use sqlparser::dialect::GenericDialect; + /// # fn main() ->Result<(), ParserError> { + /// let sql = r#"SUM("a") as "b""#; + /// let mut parser = Parser::new(&GenericDialect).try_with_sql(sql)?; + /// let expr_with_alias = parser.parse_expr_with_alias()?; + /// assert_eq!(Some("b".to_string()), expr_with_alias.alias.map(|x|x.value)); + /// # Ok(()) + /// # } + + pub fn parse_expr_with_alias(&mut self) -> Result { let expr = self.parse_expr()?; let alias = if self.parse_keyword(Keyword::AS) { Some(self.parse_identifier(false)?) From 2af981e4e6ad00d5550758b17fa3218f6e0a2b09 Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 29 Sep 2024 17:58:34 +0800 Subject: [PATCH 556/806] Implements DROP POLICY syntax for PostgreSQL (#1445) --- src/ast/mod.rs | 26 ++++++++++++++++++++++++ src/parser/mod.rs | 42 +++++++++++++++++++++++++++++---------- tests/sqlparser_common.rs | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 11 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 83646d298..03db24fcd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2546,6 +2546,16 @@ pub enum Statement { name: Ident, storage_specifier: Option, }, + ///```sql + /// DROP POLICY + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-droppolicy.html) + DropPolicy { + if_exists: bool, + name: Ident, + table_name: ObjectName, + option: Option, + }, /// ```sql /// DECLARE /// ``` @@ -4258,6 +4268,22 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DropPolicy { + if_exists, + name, + table_name, + option, + } => { + write!(f, "DROP POLICY")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {name} ON {table_name}")?; + if let Some(option) = option { + write!(f, " {option}")?; + } + Ok(()) + } Statement::Discard { object_type } => { write!(f, "DISCARD {object_type}")?; Ok(()) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b163b4cfb..404a16c48 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4887,6 +4887,8 @@ impl<'a> Parser<'a> { ObjectType::Stage } else if self.parse_keyword(Keyword::FUNCTION) { return self.parse_drop_function(); + } else if self.parse_keyword(Keyword::POLICY) { + return self.parse_drop_policy(); } else if self.parse_keyword(Keyword::PROCEDURE) { return self.parse_drop_procedure(); } else if self.parse_keyword(Keyword::SECRET) { @@ -4928,6 +4930,14 @@ impl<'a> Parser<'a> { }) } + fn parse_optional_referential_action(&mut self) -> Option { + match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { + Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade), + Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict), + _ => None, + } + } + /// ```sql /// DROP FUNCTION [ IF EXISTS ] name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] [, ...] /// [ CASCADE | RESTRICT ] @@ -4935,11 +4945,7 @@ impl<'a> Parser<'a> { fn parse_drop_function(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let func_desc = self.parse_comma_separated(Parser::parse_function_desc)?; - let option = match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { - Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade), - Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict), - _ => None, - }; + let option = self.parse_optional_referential_action(); Ok(Statement::DropFunction { if_exists, func_desc, @@ -4947,6 +4953,25 @@ impl<'a> Parser<'a> { }) } + /// ```sql + /// DROP POLICY [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ] + /// ``` + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-droppolicy.html) + fn parse_drop_policy(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier(false)?; + self.expect_keyword(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + let option = self.parse_optional_referential_action(); + Ok(Statement::DropPolicy { + if_exists, + name, + table_name, + option, + }) + } + /// ```sql /// DROP PROCEDURE [ IF EXISTS ] name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] [, ...] /// [ CASCADE | RESTRICT ] @@ -4954,12 +4979,7 @@ impl<'a> Parser<'a> { fn parse_drop_procedure(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let proc_desc = self.parse_comma_separated(Parser::parse_function_desc)?; - let option = match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { - Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade), - Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict), - Some(_) => unreachable!(), // parse_one_of_keywords does not return other keywords - None => None, - }; + let option = self.parse_optional_referential_action(); Ok(Statement::DropProcedure { if_exists, proc_desc, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 711070034..48c4aafe1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11089,3 +11089,44 @@ fn test_create_policy() { "sql parser error: Expected: (, found: EOF" ); } + +#[test] +fn test_drop_policy() { + let sql = "DROP POLICY IF EXISTS my_policy ON my_table RESTRICT"; + match all_dialects().verified_stmt(sql) { + Statement::DropPolicy { + if_exists, + name, + table_name, + option, + } => { + assert_eq!(if_exists, true); + assert_eq!(name.to_string(), "my_policy"); + assert_eq!(table_name.to_string(), "my_table"); + assert_eq!(option, Some(ReferentialAction::Restrict)); + } + _ => unreachable!(), + } + + // omit IF EXISTS is allowed + all_dialects().verified_stmt("DROP POLICY my_policy ON my_table CASCADE"); + // omit option is allowed + all_dialects().verified_stmt("DROP POLICY my_policy ON my_table"); + + // missing table name + assert_eq!( + all_dialects() + .parse_sql_statements("DROP POLICY my_policy") + .unwrap_err() + .to_string(), + "sql parser error: Expected: ON, found: EOF" + ); + // Wrong option name + assert_eq!( + all_dialects() + .parse_sql_statements("DROP POLICY my_policy ON my_table WRONG") + .unwrap_err() + .to_string(), + "sql parser error: Expected: end of statement, found: WRONG" + ); +} From 73dc8a352d7d9edad3def308042f4db2a3c9a027 Mon Sep 17 00:00:00 2001 From: Heran Lin Date: Sun, 29 Sep 2024 18:17:19 +0800 Subject: [PATCH 557/806] Support `DROP DATABASE` (#1443) --- src/ast/mod.rs | 2 ++ src/parser/mod.rs | 4 +++- tests/sqlparser_common.rs | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 03db24fcd..19354ab96 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5691,6 +5691,7 @@ pub enum ObjectType { View, Index, Schema, + Database, Role, Sequence, Stage, @@ -5703,6 +5704,7 @@ impl fmt::Display for ObjectType { ObjectType::View => "VIEW", ObjectType::Index => "INDEX", ObjectType::Schema => "SCHEMA", + ObjectType::Database => "DATABASE", ObjectType::Role => "ROLE", ObjectType::Sequence => "SEQUENCE", ObjectType::Stage => "STAGE", diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 404a16c48..e5fd53554 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4881,6 +4881,8 @@ impl<'a> Parser<'a> { ObjectType::Role } else if self.parse_keyword(Keyword::SCHEMA) { ObjectType::Schema + } else if self.parse_keyword(Keyword::DATABASE) { + ObjectType::Database } else if self.parse_keyword(Keyword::SEQUENCE) { ObjectType::Sequence } else if self.parse_keyword(Keyword::STAGE) { @@ -4897,7 +4899,7 @@ impl<'a> Parser<'a> { return self.parse_drop_trigger(); } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION, PROCEDURE, STAGE, TRIGGER, SECRET or SEQUENCE after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, DATABASE, FUNCTION, PROCEDURE, STAGE, TRIGGER, SECRET or SEQUENCE after DROP", self.peek_token(), ); }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 48c4aafe1..e066451df 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6734,6 +6734,43 @@ fn parse_create_database_ine() { } } +#[test] +fn parse_drop_database() { + let sql = "DROP DATABASE mycatalog.mydb"; + match verified_stmt(sql) { + Statement::Drop { + names, + object_type, + if_exists, + .. + } => { + assert_eq!( + vec!["mycatalog.mydb"], + names.iter().map(ToString::to_string).collect::>() + ); + assert_eq!(ObjectType::Database, object_type); + assert!(!if_exists); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_drop_database_if_exists() { + let sql = "DROP DATABASE IF EXISTS mydb"; + match verified_stmt(sql) { + Statement::Drop { + object_type, + if_exists, + .. + } => { + assert_eq!(ObjectType::Database, object_type); + assert!(if_exists); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_view() { let sql = "CREATE VIEW myschema.myview AS SELECT foo FROM bar"; From 51cbd5a3e69f9cc903f91d7df335658094b5e811 Mon Sep 17 00:00:00 2001 From: hulk Date: Sun, 29 Sep 2024 18:17:31 +0800 Subject: [PATCH 558/806] Implements ALTER POLICY syntax for PostgreSQL (#1446) Co-authored-by: Andrew Lamb --- src/ast/ddl.rs | 43 +++++++++++++++++++++ src/ast/mod.rs | 28 +++++++++++--- src/parser/alter.rs | 65 ++++++++++++++++++++++++++++++- src/parser/mod.rs | 2 + tests/sqlparser_common.rs | 81 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index e0441d07a..4578ae8f1 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -234,6 +234,49 @@ pub enum AlterTableOperation { OwnerTo { new_owner: Owner }, } +/// An `ALTER Policy` (`Statement::AlterPolicy`) operation +/// +/// [PostgreSQL Documentation](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 AlterPolicyOperation { + Rename { + new_name: Ident, + }, + Apply { + to: Option>, + using: Option, + with_check: Option, + }, +} + +impl fmt::Display for AlterPolicyOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterPolicyOperation::Rename { new_name } => { + write!(f, " RENAME TO {new_name}") + } + AlterPolicyOperation::Apply { + to, + using, + with_check, + } => { + if let Some(to) = to { + write!(f, " TO {}", display_comma_separated(to))?; + } + if let Some(using) = using { + write!(f, " USING ({using})")?; + } + if let Some(with_check) = with_check { + write!(f, " WITH CHECK ({with_check})")?; + } + 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 19354ab96..a11fa63f4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -39,11 +39,12 @@ pub use self::data_type::{ }; pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue, Use}; pub use self::ddl::{ - AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ClusteredBy, ColumnDef, - ColumnOption, ColumnOptionDef, ConstraintCharacteristics, Deduplicate, DeferrableInitial, - GeneratedAs, GeneratedExpressionMode, IdentityProperty, IndexOption, IndexType, - KeyOrIndexDisplay, Owner, Partition, ProcedureParam, ReferentialAction, TableConstraint, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + AlterColumnOperation, AlterIndexOperation, AlterPolicyOperation, AlterTableOperation, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ConstraintCharacteristics, Deduplicate, + DeferrableInitial, GeneratedAs, GeneratedExpressionMode, IdentityProperty, IndexOption, + IndexType, KeyOrIndexDisplay, Owner, Partition, ProcedureParam, ReferentialAction, + TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -2459,6 +2460,16 @@ pub enum Statement { operation: AlterRoleOperation, }, /// ```sql + /// ALTER POLICY ON
[] + /// ``` + /// (Postgresql-specific) + AlterPolicy { + name: Ident, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + table_name: ObjectName, + operation: AlterPolicyOperation, + }, + /// ```sql /// ATTACH DATABASE 'path/to/file' AS alias /// ``` /// (SQLite-specific) @@ -4197,6 +4208,13 @@ impl fmt::Display for Statement { Statement::AlterRole { name, operation } => { write!(f, "ALTER ROLE {name} {operation}") } + Statement::AlterPolicy { + name, + table_name, + operation, + } => { + write!(f, "ALTER POLICY {name} ON {table_name}{operation}") + } Statement::Drop { object_type, if_exists, diff --git a/src/parser/alter.rs b/src/parser/alter.rs index 7bf99af66..28fdaf764 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -17,7 +17,10 @@ use alloc::vec; use super::{Parser, ParserError}; use crate::{ - ast::{AlterRoleOperation, Expr, Password, ResetConfig, RoleOption, SetConfigValue, Statement}, + ast::{ + AlterPolicyOperation, AlterRoleOperation, Expr, Password, ResetConfig, RoleOption, + SetConfigValue, Statement, + }, dialect::{MsSqlDialect, PostgreSqlDialect}, keywords::Keyword, tokenizer::Token, @@ -36,6 +39,66 @@ impl<'a> Parser<'a> { )) } + /// Parse ALTER POLICY statement + /// ```sql + /// ALTER POLICY policy_name ON table_name [ RENAME TO new_name ] + /// or + /// ALTER POLICY policy_name ON table_name + /// [ TO { role_name | PUBLIC | CURRENT_ROLE | CURRENT_USER | SESSION_USER } [, ...] ] + /// [ USING ( using_expression ) ] + /// [ WITH CHECK ( check_expression ) ] + /// ``` + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterpolicy.html) + pub fn parse_alter_policy(&mut self) -> Result { + let name = self.parse_identifier(false)?; + self.expect_keyword(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + + if self.parse_keyword(Keyword::RENAME) { + self.expect_keyword(Keyword::TO)?; + let new_name = self.parse_identifier(false)?; + Ok(Statement::AlterPolicy { + name, + table_name, + operation: AlterPolicyOperation::Rename { new_name }, + }) + } else { + let to = if self.parse_keyword(Keyword::TO) { + Some(self.parse_comma_separated(|p| p.parse_owner())?) + } else { + None + }; + + let using = if self.parse_keyword(Keyword::USING) { + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Some(expr) + } else { + None + }; + + let with_check = if self.parse_keywords(&[Keyword::WITH, Keyword::CHECK]) { + self.expect_token(&Token::LParen)?; + let expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Some(expr) + } else { + None + }; + Ok(Statement::AlterPolicy { + name, + table_name, + operation: AlterPolicyOperation::Apply { + to, + using, + with_check, + }, + }) + } + } + fn parse_mssql_alter_role(&mut self) -> Result { let role_name = self.parse_identifier(false)?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e5fd53554..a60dea3b8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7140,6 +7140,7 @@ impl<'a> Parser<'a> { Keyword::TABLE, Keyword::INDEX, Keyword::ROLE, + Keyword::POLICY, ])?; match object_type { Keyword::VIEW => self.parse_alter_view(), @@ -7191,6 +7192,7 @@ impl<'a> Parser<'a> { }) } Keyword::ROLE => self.parse_alter_role(), + Keyword::POLICY => self.parse_alter_policy(), // unreachable because expect_one_of_keywords used above _ => unreachable!(), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e066451df..29352981a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11167,3 +11167,84 @@ fn test_drop_policy() { "sql parser error: Expected: end of statement, found: WRONG" ); } + +#[test] +fn test_alter_policy() { + match verified_stmt("ALTER POLICY old_policy ON my_table RENAME TO new_policy") { + Statement::AlterPolicy { + name, + table_name, + operation, + .. + } => { + assert_eq!(name.to_string(), "old_policy"); + assert_eq!(table_name.to_string(), "my_table"); + assert_eq!( + operation, + AlterPolicyOperation::Rename { + new_name: Ident::new("new_policy") + } + ); + } + _ => unreachable!(), + } + + match verified_stmt(concat!( + "ALTER POLICY my_policy ON my_table TO CURRENT_USER ", + "USING ((SELECT c0)) WITH CHECK (c0 > 0)" + )) { + Statement::AlterPolicy { + name, table_name, .. + } => { + assert_eq!(name.to_string(), "my_policy"); + assert_eq!(table_name.to_string(), "my_table"); + } + _ => unreachable!(), + } + + // omit TO / USING / WITH CHECK clauses is allowed + verified_stmt("ALTER POLICY my_policy ON my_table"); + + // mixing RENAME and APPLY expressions + assert_eq!( + parse_sql_statements("ALTER POLICY old_policy ON my_table TO public RENAME TO new_policy") + .unwrap_err() + .to_string(), + "sql parser error: Expected: end of statement, found: RENAME" + ); + assert_eq!( + parse_sql_statements("ALTER POLICY old_policy ON my_table RENAME TO new_policy TO public") + .unwrap_err() + .to_string(), + "sql parser error: Expected: end of statement, found: TO" + ); + // missing TO in RENAME TO + assert_eq!( + parse_sql_statements("ALTER POLICY old_policy ON my_table RENAME") + .unwrap_err() + .to_string(), + "sql parser error: Expected: TO, found: EOF" + ); + // missing new name in RENAME TO + assert_eq!( + parse_sql_statements("ALTER POLICY old_policy ON my_table RENAME TO") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" + ); + + // missing the expression in USING + assert_eq!( + parse_sql_statements("ALTER POLICY my_policy ON my_table USING") + .unwrap_err() + .to_string(), + "sql parser error: Expected: (, found: EOF" + ); + // missing the expression in WITH CHECK + assert_eq!( + parse_sql_statements("ALTER POLICY my_policy ON my_table WITH CHECK") + .unwrap_err() + .to_string(), + "sql parser error: Expected: (, found: EOF" + ); +} From ce2686a16997c17fd7729e378e21cc463d2e97d4 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 30 Sep 2024 07:08:13 -0400 Subject: [PATCH 559/806] Add a note discouraging new use of `dialect_of` macro (#1448) --- src/dialect/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 619b54713..90dffe82d 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -60,6 +60,13 @@ use alloc::boxed::Box; /// Convenience check if a [`Parser`] uses a certain dialect. /// +/// Note: when possible please the new style, adding a method to the [`Dialect`] +/// trait rather than using this macro. +/// +/// The benefits of adding a method on `Dialect` over this macro are: +/// 1. user defined [`Dialect`]s can customize the parsing behavior +/// 2. The differences between dialects can be clearly documented in the trait +/// /// `dialect_of!(parser Is SQLiteDialect | GenericDialect)` evaluates /// to `true` if `parser.dialect` is one of the [`Dialect`]s specified. macro_rules! dialect_of { From 1e0460a7dfad4a6767d718a9d849b23c8b61ee80 Mon Sep 17 00:00:00 2001 From: Joshua Warner Date: Mon, 30 Sep 2024 10:40:07 -0700 Subject: [PATCH 560/806] Expand handling of `LIMIT 1, 2` handling to include sqlite (#1447) --- src/dialect/clickhouse.rs | 4 ++++ src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 7 ++++++- src/dialect/mysql.rs | 4 ++++ src/dialect/sqlite.rs | 4 ++++ src/parser/mod.rs | 2 +- src/test_utils.rs | 41 ++++++++++++++++++++++----------------- tests/sqlparser_common.rs | 19 ++++++++++++++---- 8 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index ee8ee94d2..0c8f08040 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -46,4 +46,8 @@ impl Dialect for ClickHouseDialect { fn require_interval_qualifier(&self) -> bool { true } + + fn supports_limit_comma(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index bea56d0ef..8bec45b28 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -99,4 +99,8 @@ impl Dialect for GenericDialect { fn supports_explain_with_utility_options(&self) -> bool { true } + + fn supports_limit_comma(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 90dffe82d..29ad0b98a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -67,7 +67,7 @@ use alloc::boxed::Box; /// 1. user defined [`Dialect`]s can customize the parsing behavior /// 2. The differences between dialects can be clearly documented in the trait /// -/// `dialect_of!(parser Is SQLiteDialect | GenericDialect)` evaluates +/// `dialect_of!(parser is SQLiteDialect | GenericDialect)` evaluates /// to `true` if `parser.dialect` is one of the [`Dialect`]s specified. macro_rules! dialect_of { ( $parsed_dialect: ident is $($dialect_type: ty)|+ ) => { @@ -323,6 +323,11 @@ pub trait Dialect: Debug + Any { false } + /// Does the dialect support parsing `LIMIT 1, 2` as `LIMIT 2 OFFSET 1`? + fn supports_limit_comma(&self) -> bool { + false + } + /// Does the dialect support trailing commas in the projection list? fn supports_projection_trailing_commas(&self) -> bool { self.supports_trailing_commas() diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 2b5e42e59..d1bf33345 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -93,6 +93,10 @@ impl Dialect for MySqlDialect { fn require_interval_qualifier(&self) -> bool { true } + + fn supports_limit_comma(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index d3813d91c..5c563bf4a 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -73,4 +73,8 @@ impl Dialect for SQLiteDialect { fn supports_in_empty_list(&self) -> bool { true } + + fn supports_limit_comma(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a60dea3b8..a4445e04d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8739,7 +8739,7 @@ impl<'a> Parser<'a> { offset = Some(self.parse_offset()?) } - if dialect_of!(self is GenericDialect | MySqlDialect | ClickHouseDialect) + if self.dialect.supports_limit_comma() && limit.is_some() && offset.is_none() && self.consume_token(&Token::Comma) diff --git a/src/test_utils.rs b/src/test_utils.rs index eb0352353..e588b3506 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -47,6 +47,14 @@ pub struct TestedDialects { } impl TestedDialects { + /// Create a TestedDialects with default options and the given dialects. + pub fn new(dialects: Vec>) -> Self { + Self { + dialects, + options: None, + } + } + fn new_parser<'a>(&self, dialect: &'a dyn Dialect) -> Parser<'a> { let parser = Parser::new(dialect); if let Some(options) = &self.options { @@ -211,24 +219,21 @@ impl TestedDialects { /// Returns all available dialects. pub fn all_dialects() -> TestedDialects { - let all_dialects = vec![ - Box::new(GenericDialect {}) as Box, - Box::new(PostgreSqlDialect {}) as Box, - Box::new(MsSqlDialect {}) as Box, - Box::new(AnsiDialect {}) as Box, - Box::new(SnowflakeDialect {}) as Box, - Box::new(HiveDialect {}) as Box, - Box::new(RedshiftSqlDialect {}) as Box, - Box::new(MySqlDialect {}) as Box, - Box::new(BigQueryDialect {}) as Box, - Box::new(SQLiteDialect {}) as Box, - Box::new(DuckDbDialect {}) as Box, - Box::new(DatabricksDialect {}) as Box, - ]; - TestedDialects { - dialects: all_dialects, - options: None, - } + TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(HiveDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), + Box::new(DatabricksDialect {}), + Box::new(ClickHouseDialect {}), + ]) } /// Returns all dialects matching the given predicate. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 29352981a..2677c193c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6832,7 +6832,9 @@ fn parse_create_view_with_options() { #[test] fn parse_create_view_with_columns() { let sql = "CREATE VIEW v (has, cols) AS SELECT 1, 2"; - match verified_stmt(sql) { + // TODO: why does this fail for ClickHouseDialect? (#1449) + // match all_dialects().verified_stmt(sql) { + match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::CreateView { name, columns, @@ -8624,17 +8626,26 @@ fn verified_expr(query: &str) -> Expr { #[test] fn parse_offset_and_limit() { - let sql = "SELECT foo FROM bar LIMIT 2 OFFSET 2"; + let sql = "SELECT foo FROM bar LIMIT 1 OFFSET 2"; let expect = Some(Offset { value: Expr::Value(number("2")), rows: OffsetRows::None, }); let ast = verified_query(sql); assert_eq!(ast.offset, expect); - assert_eq!(ast.limit, Some(Expr::Value(number("2")))); + assert_eq!(ast.limit, Some(Expr::Value(number("1")))); // different order is OK - one_statement_parses_to("SELECT foo FROM bar OFFSET 2 LIMIT 2", sql); + 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); // expressions are allowed let sql = "SELECT foo FROM bar LIMIT 1 + 2 OFFSET 3 * 4"; From 32a126b27c75a8248bde75401b8fbdc157e3dfc2 Mon Sep 17 00:00:00 2001 From: hulk Date: Sat, 5 Oct 2024 04:03:02 +0800 Subject: [PATCH 561/806] Fix always uses CommentDef::WithoutEq while parsing the inline comment (#1453) --- src/dialect/snowflake.rs | 14 ++++---------- src/parser/mod.rs | 27 +++++++++++++++++++++------ tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++++++++ tests/sqlparser_mysql.rs | 6 +++--- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index e14746d9a..f256c9b53 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -22,9 +22,7 @@ use crate::ast::helpers::stmt_data_loading::{ DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageLoadSelectItem, StageParamsObject, }; -use crate::ast::{ - CommentDef, Ident, ObjectName, RowAccessPolicy, Statement, Tag, WrappedCollection, -}; +use crate::ast::{Ident, ObjectName, RowAccessPolicy, Statement, Tag, WrappedCollection}; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -210,13 +208,9 @@ pub fn parse_create_table( builder = builder.copy_grants(true); } Keyword::COMMENT => { - parser.expect_token(&Token::Eq)?; - let next_token = parser.next_token(); - let comment = match next_token.token { - Token::SingleQuotedString(str) => Some(CommentDef::WithEq(str)), - _ => parser.expected("comment", next_token)?, - }; - builder = builder.comment(comment); + // Rewind the COMMENT keyword + parser.prev_token(); + builder = builder.comment(parser.parse_optional_inline_comment()?); } Keyword::AS => { let query = parser.parse_boxed_query()?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a4445e04d..028bae705 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5871,12 +5871,9 @@ impl<'a> Parser<'a> { // Excludes Hive dialect here since it has been handled after table column definitions. if !dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { - let _ = self.consume_token(&Token::Eq); - let next_token = self.next_token(); - comment = match next_token.token { - Token::SingleQuotedString(str) => Some(CommentDef::WithoutEq(str)), - _ => self.expected("comment", next_token)?, - } + // rewind the COMMENT keyword + self.prev_token(); + comment = self.parse_optional_inline_comment()? }; // Parse optional `AS ( query )` @@ -5957,6 +5954,24 @@ 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)?, + } + } else { + None + }; + Ok(comment) + } + pub fn parse_optional_procedure_parameters( &mut self, ) -> Result>, ParserError> { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2677c193c..a06b47a56 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -47,6 +47,7 @@ mod test_utils; #[cfg(test)] use pretty_assertions::assert_eq; +use sqlparser::ast::ColumnOption::Comment; use sqlparser::ast::Expr::{Identifier, UnaryOp}; use sqlparser::test_utils::all_dialects_except; @@ -9841,6 +9842,37 @@ fn test_comment_hash_syntax() { dialects.verified_only_select_with_canonical(sql, canonical); } +#[test] +fn test_parse_inline_comment() { + let sql = + "CREATE TABLE t0 (id INT COMMENT 'comment without equal') COMMENT = 'comment with equal'"; + // Hive dialect doesn't support `=` in table comment, please refer: + // [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, .. + }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: Ident::new("id".to_string()), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: Comment("comment without equal".to_string()), + }] + }] + ); + assert_eq!( + comment.unwrap(), + CommentDef::WithEq("comment with equal".to_string()) + ); + } + _ => unreachable!(), + } +} + #[test] fn test_buffer_reuse() { let d = GenericDialect {}; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 87468b933..19dbda21f 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -713,11 +713,11 @@ fn parse_create_table_primary_and_unique_key_characteristic_test() { #[test] fn parse_create_table_comment() { - let canonical = "CREATE TABLE foo (bar INT) COMMENT 'baz'"; + let without_equal = "CREATE TABLE foo (bar INT) COMMENT 'baz'"; let with_equal = "CREATE TABLE foo (bar INT) COMMENT = 'baz'"; - for sql in [canonical, with_equal] { - match mysql().one_statement_parses_to(sql, canonical) { + for sql in [without_equal, with_equal] { + match mysql().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, comment, .. }) => { assert_eq!(name.to_string(), "foo"); assert_eq!(comment.expect("Should exist").to_string(), "baz"); From e849f7f14350b480fbbca6618da9ae7cea2df5a2 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:03:38 +0200 Subject: [PATCH 562/806] Add support for the LIKE ANY and ILIKE ANY pattern-matching condition (#1456) --- src/ast/mod.rs | 20 ++++++++++++++++---- src/parser/mod.rs | 2 ++ tests/sqlparser_common.rs | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a11fa63f4..57cd401d3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -613,6 +613,9 @@ pub enum Expr { /// `[NOT] LIKE [ESCAPE ]` Like { negated: bool, + // Snowflake supports the ANY keyword to match against a list of patterns + // https://docs.snowflake.com/en/sql-reference/functions/like_any + any: bool, expr: Box, pattern: Box, escape_char: Option, @@ -620,6 +623,9 @@ pub enum Expr { /// `ILIKE` (case-insensitive `LIKE`) ILike { negated: bool, + // Snowflake supports the ANY keyword to match against a list of patterns + // https://docs.snowflake.com/en/sql-reference/functions/like_any + any: bool, expr: Box, pattern: Box, escape_char: Option, @@ -1242,20 +1248,23 @@ impl fmt::Display for Expr { expr, pattern, escape_char, + any, } => match escape_char { Some(ch) => write!( f, - "{} {}LIKE {} ESCAPE '{}'", + "{} {}LIKE {}{} ESCAPE '{}'", expr, if *negated { "NOT " } else { "" }, + if *any { "ANY " } else { "" }, pattern, ch ), _ => write!( f, - "{} {}LIKE {}", + "{} {}LIKE {}{}", expr, if *negated { "NOT " } else { "" }, + if *any { "ANY " } else { "" }, pattern ), }, @@ -1264,20 +1273,23 @@ impl fmt::Display for Expr { expr, pattern, escape_char, + any, } => match escape_char { Some(ch) => write!( f, - "{} {}ILIKE {} ESCAPE '{}'", + "{} {}ILIKE {}{} ESCAPE '{}'", expr, if *negated { "NOT " } else { "" }, + if *any { "ANY" } else { "" }, pattern, ch ), _ => write!( f, - "{} {}ILIKE {}", + "{} {}ILIKE {}{}", expr, if *negated { "NOT " } else { "" }, + if *any { "ANY " } else { "" }, pattern ), }, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 028bae705..88c3bd19c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2749,6 +2749,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::LIKE) { Ok(Expr::Like { negated, + any: self.parse_keyword(Keyword::ANY), expr: Box::new(expr), pattern: Box::new( self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?, @@ -2758,6 +2759,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::ILIKE) { Ok(Expr::ILike { negated, + any: self.parse_keyword(Keyword::ANY), expr: Box::new(expr), pattern: Box::new( self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a06b47a56..5d5a17ca5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1550,6 +1550,7 @@ fn parse_not_precedence() { negated: true, pattern: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), escape_char: None, + any: false, }), }, ); @@ -1580,6 +1581,7 @@ fn parse_null_like() { SelectItem::ExprWithAlias { expr: Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("column1"))), + any: false, negated: false, pattern: Box::new(Expr::Value(Value::Null)), escape_char: None, @@ -1595,6 +1597,7 @@ fn parse_null_like() { SelectItem::ExprWithAlias { expr: Expr::Like { expr: Box::new(Expr::Value(Value::Null)), + any: false, negated: false, pattern: Box::new(Expr::Identifier(Ident::new("column1"))), escape_char: None, @@ -1622,6 +1625,7 @@ fn parse_ilike() { negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None, + any: false, }, select.selection.unwrap() ); @@ -1638,6 +1642,7 @@ fn parse_ilike() { negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('^'.to_string()), + any: false, }, select.selection.unwrap() ); @@ -1655,6 +1660,7 @@ fn parse_ilike() { negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None, + any: false, })), select.selection.unwrap() ); @@ -1677,6 +1683,7 @@ fn parse_like() { negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None, + any: false, }, select.selection.unwrap() ); @@ -1693,6 +1700,7 @@ fn parse_like() { negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('^'.to_string()), + any: false, }, select.selection.unwrap() ); @@ -1710,6 +1718,7 @@ fn parse_like() { negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None, + any: false, })), select.selection.unwrap() ); @@ -10130,6 +10139,7 @@ fn test_selective_aggregation() { expr: Box::new(Expr::Identifier(Ident::new("name"))), pattern: Box::new(Expr::Value(Value::SingleQuotedString("a%".to_owned()))), escape_char: None, + any: false, })), null_treatment: None, over: None, @@ -11291,3 +11301,11 @@ fn test_alter_policy() { "sql parser error: Expected: (, found: EOF" ); } + +#[test] +fn test_select_where_with_like_or_ilike_any() { + verified_stmt(r#"SELECT * FROM x WHERE a ILIKE ANY '%abc%'"#); + verified_stmt(r#"SELECT * FROM x WHERE a LIKE ANY '%abc%'"#); + verified_stmt(r#"SELECT * FROM x WHERE a ILIKE ANY ('%Jo%oe%', 'T%e')"#); + verified_stmt(r#"SELECT * FROM x WHERE a LIKE ANY ('%Jo%oe%', 'T%e')"#); +} From 8ccb87a8355858ccb06ffc20484657cd7980d78c Mon Sep 17 00:00:00 2001 From: Maxwell Knight <107858496+MaxwellKnight@users.noreply.github.com> Date: Fri, 4 Oct 2024 23:07:26 +0300 Subject: [PATCH 563/806] added ability to parse extension to parse_comment inside postgres dialect (#1451) --- src/ast/mod.rs | 2 ++ src/dialect/postgresql.rs | 4 ++++ tests/sqlparser_postgres.rs | 15 +++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 57cd401d3..8c4bc25af 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1855,6 +1855,7 @@ impl fmt::Display for ShowCreateObject { pub enum CommentObject { Column, Table, + Extension, } impl fmt::Display for CommentObject { @@ -1862,6 +1863,7 @@ impl fmt::Display for CommentObject { match self { CommentObject::Column => f.write_str("COLUMN"), CommentObject::Table => f.write_str("TABLE"), + CommentObject::Extension => f.write_str("EXTENSION"), } } } diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 945c4fcc0..2a66705bb 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -205,6 +205,10 @@ pub fn parse_comment(parser: &mut Parser) -> Result { let object_name = parser.parse_object_name(false)?; (CommentObject::Table, object_name) } + Token::Word(w) if w.keyword == Keyword::EXTENSION => { + let object_name = parser.parse_object_name(false)?; + (CommentObject::Extension, object_name) + } _ => parser.expected("comment object_type", token)?, }; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9ba3e5dbc..735b87b5d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2902,6 +2902,21 @@ fn parse_comments() { _ => unreachable!(), } + match pg().verified_stmt("COMMENT ON EXTENSION plpgsql IS 'comment'") { + Statement::Comment { + object_type, + object_name, + comment: Some(comment), + if_exists, + } => { + assert_eq!("comment", comment); + assert_eq!("plpgsql", object_name.to_string()); + assert_eq!(CommentObject::Extension, object_type); + assert!(!if_exists); + } + _ => unreachable!(), + } + match pg().verified_stmt("COMMENT ON TABLE public.tab IS 'comment'") { Statement::Comment { object_type, From 84348d483eafaf4b608c9869ab03984852c7a4d2 Mon Sep 17 00:00:00 2001 From: Aleksei Piianin Date: Mon, 7 Oct 2024 22:20:23 +0200 Subject: [PATCH 564/806] Snowflake: support of views column comment (#1441) --- src/ast/ddl.rs | 9 +++------ src/ast/mod.rs | 12 +++++------ src/parser/mod.rs | 9 ++++++--- tests/sqlparser_bigquery.rs | 4 ++-- tests/sqlparser_snowflake.rs | 39 ++++++++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 4578ae8f1..6eafc4da0 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1040,6 +1040,7 @@ impl fmt::Display for ColumnDef { /// ```sql /// name /// age OPTIONS(description = "age column", tag = "prod") +/// amount COMMENT 'The total amount for the order line' /// created_at DateTime64 /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -1048,7 +1049,7 @@ impl fmt::Display for ColumnDef { pub struct ViewColumnDef { pub name: Ident, pub data_type: Option, - pub options: Option>, + pub options: Option>, } impl fmt::Display for ViewColumnDef { @@ -1058,11 +1059,7 @@ impl fmt::Display for ViewColumnDef { write!(f, " {}", data_type)?; } if let Some(options) = self.options.as_ref() { - write!( - f, - " OPTIONS({})", - display_comma_separated(options.as_slice()) - )?; + write!(f, " {}", display_comma_separated(options.as_slice()))?; } Ok(()) } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8c4bc25af..995370f5b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3930,6 +3930,12 @@ impl fmt::Display for Statement { .map(|to| format!(" TO {to}")) .unwrap_or_default() )?; + if !columns.is_empty() { + write!(f, " ({})", display_comma_separated(columns))?; + } + if matches!(options, CreateTableOptions::With(_)) { + write!(f, " {options}")?; + } if let Some(comment) = comment { write!( f, @@ -3937,12 +3943,6 @@ impl fmt::Display for Statement { value::escape_single_quote_string(comment) )?; } - if matches!(options, CreateTableOptions::With(_)) { - write!(f, " {options}")?; - } - if !columns.is_empty() { - write!(f, " ({})", display_comma_separated(columns))?; - } if !cluster_by.is_empty() { write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 88c3bd19c..4e67524ab 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8361,11 +8361,14 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier(false)?; - let options = if dialect_of!(self is BigQueryDialect | GenericDialect) - && self.parse_keyword(Keyword::OPTIONS) + 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(); - Some(self.parse_options(Keyword::OPTIONS)?) + self.parse_optional_column_option()? + .map(|option| vec![option]) } else { None }; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 55afe473b..63517fe57 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -272,10 +272,10 @@ fn parse_create_view_with_options() { ViewColumnDef { name: Ident::new("age"), data_type: None, - options: Some(vec![SqlOption::KeyValue { + options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue { key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString("field age".to_string())), - }]) + }])]), }, ], columns diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 50c5f740e..e73b6999a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2405,3 +2405,42 @@ fn parse_use() { ); } } + +#[test] +fn view_comment_option_should_be_after_column_list() { + for sql in [ + "CREATE OR REPLACE VIEW v (a) COMMENT = 'Comment' AS SELECT a FROM t", + "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() + .verified_stmt(sql); + } +} + +#[test] +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) { + Statement::CreateView { name, columns, .. } => { + assert_eq!(name.to_string(), "v"); + assert_eq!( + columns, + vec![ + ViewColumnDef { + name: Ident::new("a"), + data_type: None, + options: Some(vec![ColumnOption::Comment("Comment".to_string())]), + }, + ViewColumnDef { + name: Ident::new("b"), + data_type: None, + options: None, + } + ] + ); + } + _ => unreachable!(), + }; +} From 8badcdc2008bea4b5973c9dd12351a7cef29d8ab Mon Sep 17 00:00:00 2001 From: nucccc <44038808+nucccc@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:20:55 +0200 Subject: [PATCH 565/806] Add SQLite "ON CONFLICT" column option in CREATE TABLE statements (#1442) Co-authored-by: hulk Co-authored-by: Ifeanyi Ubah --- src/ast/ddl.rs | 8 ++++++++ src/parser/mod.rs | 13 +++++++++++++ tests/sqlparser_sqlite.rs | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6eafc4da0..0677e63bf 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -33,6 +33,7 @@ use crate::ast::{ display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, ObjectName, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Value, }; +use crate::keywords::Keyword; use crate::tokenizer::Token; /// An `ALTER TABLE` (`Statement::AlterTable`) operation @@ -1186,6 +1187,9 @@ pub enum ColumnOption { /// ``` /// [MS SQL Server]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql-identity-property Identity(Option), + /// SQLite specific: ON CONFLICT option on column definition + /// + OnConflict(Keyword), } impl fmt::Display for ColumnOption { @@ -1294,6 +1298,10 @@ impl fmt::Display for ColumnOption { } Ok(()) } + OnConflict(keyword) => { + write!(f, "ON CONFLICT {:?}", keyword)?; + Ok(()) + } } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4e67524ab..39173b63c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6224,6 +6224,19 @@ impl<'a> Parser<'a> { None }; Ok(Some(ColumnOption::Identity(property))) + } else if dialect_of!(self is SQLiteDialect | GenericDialect) + && self.parse_keywords(&[Keyword::ON, Keyword::CONFLICT]) + { + // Support ON CONFLICT for SQLite + Ok(Some(ColumnOption::OnConflict( + self.expect_one_of_keywords(&[ + Keyword::ROLLBACK, + Keyword::ABORT, + Keyword::FAIL, + Keyword::IGNORE, + Keyword::REPLACE, + ])?, + ))) } else { Ok(None) } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index d7fd3b896..b5427fb3c 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -22,6 +22,7 @@ #[macro_use] mod test_utils; +use sqlparser::keywords::Keyword; use test_utils::*; use sqlparser::ast::SelectItem::UnnamedExpr; @@ -281,6 +282,46 @@ fn parse_create_table_gencol() { sqlite_and_generic().verified_stmt("CREATE TABLE t1 (a INT, b INT AS (a * 2) STORED)"); } +#[test] +fn parse_create_table_on_conflict_col() { + for keyword in [ + Keyword::ROLLBACK, + Keyword::ABORT, + Keyword::FAIL, + Keyword::IGNORE, + Keyword::REPLACE, + ] { + 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!( + vec![ColumnOptionDef { + name: None, + option: ColumnOption::OnConflict(keyword), + }], + columns[1].options + ); + } + _ => unreachable!(), + } + } +} + +#[test] +fn test_parse_create_table_on_conflict_col_err() { + let sql_err = "CREATE TABLE t1 (a INT, b INT ON CONFLICT BOH)"; + let err = sqlite_and_generic() + .parse_sql_statements(sql_err) + .unwrap_err(); + assert_eq!( + err, + ParserError::ParserError( + "Expected: one of ROLLBACK or ABORT or FAIL or IGNORE or REPLACE, found: BOH" + .to_string() + ) + ); +} + #[test] fn parse_create_table_untyped() { sqlite().verified_stmt("CREATE TABLE t1 (a, b AS (a * 2), c NOT NULL)"); From ac956dc963b9a857202b62b50c99f35313efbce2 Mon Sep 17 00:00:00 2001 From: David Caldwell Date: Tue, 8 Oct 2024 09:26:32 -0700 Subject: [PATCH 566/806] Add support for ASC and DESC in CREATE TABLE column constraints for SQLite. (#1462) --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 4 ++++ src/dialect/sqlite.rs | 4 ++++ src/parser/mod.rs | 14 ++++++++++++++ tests/sqlparser_sqlite.rs | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8bec45b28..92d720a0b 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -103,4 +103,8 @@ impl Dialect for GenericDialect { fn supports_limit_comma(&self) -> bool { true } + + fn supports_asc_desc_in_column_definition(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 29ad0b98a..744f5a8c8 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -557,6 +557,10 @@ pub trait Dialect: Debug + Any { fn supports_explain_with_utility_options(&self) -> bool { false } + + fn supports_asc_desc_in_column_definition(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 5c563bf4a..95717f9fd 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -77,4 +77,8 @@ impl Dialect for SQLiteDialect { fn supports_limit_comma(&self) -> bool { true } + + fn supports_asc_desc_in_column_definition(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 39173b63c..34574a6b3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6192,6 +6192,20 @@ impl<'a> Parser<'a> { Ok(Some(ColumnOption::DialectSpecific(vec![ Token::make_keyword("AUTOINCREMENT"), ]))) + } else if self.parse_keyword(Keyword::ASC) + && self.dialect.supports_asc_desc_in_column_definition() + { + // Support ASC for SQLite + Ok(Some(ColumnOption::DialectSpecific(vec![ + Token::make_keyword("ASC"), + ]))) + } else if self.parse_keyword(Keyword::DESC) + && self.dialect.supports_asc_desc_in_column_definition() + { + // Support DESC for SQLite + Ok(Some(ColumnOption::DialectSpecific(vec![ + Token::make_keyword("DESC"), + ]))) } else if self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) && dialect_of!(self is MySqlDialect | GenericDialect) { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index b5427fb3c..d3e670e32 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -238,6 +238,43 @@ fn parse_create_table_auto_increment() { } } +#[test] +fn parse_create_table_primary_key_asc_desc() { + let expected_column_def = |kind| ColumnDef { + name: "bar".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Unique { + is_primary: true, + characteristics: None, + }, + }, + ColumnOptionDef { + name: None, + option: ColumnOption::DialectSpecific(vec![Token::make_keyword(kind)]), + }, + ], + }; + + let sql = "CREATE TABLE foo (bar INT PRIMARY KEY ASC)"; + match sqlite_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!(vec![expected_column_def("ASC")], columns); + } + _ => unreachable!(), + } + let sql = "CREATE TABLE foo (bar INT PRIMARY KEY DESC)"; + match sqlite_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!(vec![expected_column_def("DESC")], columns); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_sqlite_quote() { let sql = "CREATE TABLE `PRIMARY` (\"KEY\" INT, [INDEX] INT)"; From 7905fb49054c28538a69dabd802c7bad6ef86fe8 Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 9 Oct 2024 00:27:07 +0800 Subject: [PATCH 567/806] Add support of `EXPLAIN QUERY PLAN` syntax for SQLite dialect (#1458) --- src/ast/mod.rs | 9 +++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 4 ++++ tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 995370f5b..bc559a660 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3111,6 +3111,11 @@ pub enum Statement { analyze: bool, // Display additional information regarding the plan. verbose: bool, + /// `EXPLAIN QUERY PLAN` + /// Display the query plan without running the query. + /// + /// [SQLite](https://sqlite.org/lang_explain.html) + query_plan: bool, /// A SQL query that specifies what to explain statement: Box, /// Optional output format of explain @@ -3302,12 +3307,16 @@ impl fmt::Display for Statement { describe_alias, verbose, analyze, + query_plan, statement, format, options, } => { write!(f, "{describe_alias} ")?; + if *query_plan { + write!(f, "QUERY PLAN ")?; + } if *analyze { write!(f, "ANALYZE ")?; } diff --git a/src/keywords.rs b/src/keywords.rs index 49c6ce20f..ecf4bd474 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -572,6 +572,7 @@ define_keywords!( PERSISTENT, PIVOT, PLACING, + PLAN, PLANS, POLICY, PORTION, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 34574a6b3..cc60bbbd7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8662,6 +8662,7 @@ impl<'a> Parser<'a> { ) -> Result { let mut analyze = false; let mut verbose = false; + let mut query_plan = false; let mut format = None; let mut options = None; @@ -8672,6 +8673,8 @@ impl<'a> Parser<'a> { && self.peek_token().token == Token::LParen { options = Some(self.parse_utility_options()?) + } else if self.parse_keywords(&[Keyword::QUERY, Keyword::PLAN]) { + query_plan = true; } else { analyze = self.parse_keyword(Keyword::ANALYZE); verbose = self.parse_keyword(Keyword::VERBOSE); @@ -8688,6 +8691,7 @@ impl<'a> Parser<'a> { describe_alias, analyze, verbose, + query_plan, statement: Box::new(statement), format, options, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5d5a17ca5..7068ffc30 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4295,6 +4295,7 @@ fn run_explain_analyze( describe_alias: _, analyze, verbose, + query_plan, statement, format, options, @@ -4303,6 +4304,7 @@ fn run_explain_analyze( assert_eq!(analyze, expected_analyze); assert_eq!(format, expected_format); assert_eq!(options, exepcted_options); + assert!(!query_plan); assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); } _ => panic!("Unexpected Statement, must be Explain"), @@ -4417,6 +4419,36 @@ fn parse_explain_analyze_with_simple_select() { ); } +#[test] +fn parse_explain_query_plan() { + match all_dialects().verified_stmt("EXPLAIN QUERY PLAN SELECT sqrt(id) FROM foo") { + Statement::Explain { + query_plan, + analyze, + verbose, + statement, + .. + } => { + assert!(query_plan); + assert!(!analyze); + assert!(!verbose); + assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); + } + _ => unreachable!(), + } + + // omit QUERY PLAN should be good + all_dialects().verified_stmt("EXPLAIN SELECT sqrt(id) FROM foo"); + + // missing PLAN keyword should return error + assert_eq!( + ParserError::ParserError("Expected: end of statement, found: SELECT".to_string()), + all_dialects() + .parse_sql_statements("EXPLAIN QUERY SELECT sqrt(id) FROM foo") + .unwrap_err() + ); +} + #[test] fn parse_named_argument_function() { let sql = "SELECT FUN(a => '1', b => '2') FROM foo"; From c01e054fd85a4dcd16f27ca949e72d6a226d2f66 Mon Sep 17 00:00:00 2001 From: David Caldwell Date: Wed, 9 Oct 2024 06:59:00 -0700 Subject: [PATCH 568/806] Add "DROP TYPE" support. (#1461) --- src/ast/mod.rs | 2 ++ src/parser/mod.rs | 4 ++- tests/sqlparser_common.rs | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bc559a660..39f9def8e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5736,6 +5736,7 @@ pub enum ObjectType { Role, Sequence, Stage, + Type, } impl fmt::Display for ObjectType { @@ -5749,6 +5750,7 @@ impl fmt::Display for ObjectType { ObjectType::Role => "ROLE", ObjectType::Sequence => "SEQUENCE", ObjectType::Stage => "STAGE", + ObjectType::Type => "TYPE", }) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cc60bbbd7..c9e66e1f3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4889,6 +4889,8 @@ impl<'a> Parser<'a> { ObjectType::Sequence } else if self.parse_keyword(Keyword::STAGE) { ObjectType::Stage + } else if self.parse_keyword(Keyword::TYPE) { + ObjectType::Type } else if self.parse_keyword(Keyword::FUNCTION) { return self.parse_drop_function(); } else if self.parse_keyword(Keyword::POLICY) { @@ -4901,7 +4903,7 @@ impl<'a> Parser<'a> { return self.parse_drop_trigger(); } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, SCHEMA, DATABASE, FUNCTION, PROCEDURE, STAGE, TRIGGER, SECRET or SEQUENCE after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, DATABASE, FUNCTION, PROCEDURE, STAGE, TRIGGER, SECRET, SEQUENCE, or TYPE after DROP", self.peek_token(), ); }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7068ffc30..15132903c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9690,6 +9690,60 @@ fn parse_create_type() { ); } +#[test] +fn parse_drop_type() { + let sql = "DROP TYPE abc"; + match verified_stmt(sql) { + Statement::Drop { + names, + object_type, + if_exists, + cascade, + .. + } => { + assert_eq_vec(&["abc"], &names); + assert_eq!(ObjectType::Type, object_type); + assert!(!if_exists); + assert!(!cascade); + } + _ => unreachable!(), + }; + + let sql = "DROP TYPE IF EXISTS def, magician, quaternion"; + match verified_stmt(sql) { + Statement::Drop { + names, + object_type, + if_exists, + cascade, + .. + } => { + assert_eq_vec(&["def", "magician", "quaternion"], &names); + assert_eq!(ObjectType::Type, object_type); + assert!(if_exists); + assert!(!cascade); + } + _ => unreachable!(), + } + + let sql = "DROP TYPE IF EXISTS my_type CASCADE"; + match verified_stmt(sql) { + Statement::Drop { + names, + object_type, + if_exists, + cascade, + .. + } => { + assert_eq_vec(&["my_type"], &names); + assert_eq!(ObjectType::Type, object_type); + assert!(if_exists); + assert!(cascade); + } + _ => unreachable!(), + } +} + #[test] fn parse_call() { all_dialects().verified_stmt("CALL my_procedure()"); From 0a941b87dccde2faa9775366f39062202018eef8 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Thu, 10 Oct 2024 01:01:06 +0800 Subject: [PATCH 569/806] chore: Add asf.yaml (#1463) Co-authored-by: Andrew Lamb --- .asf.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .asf.yaml diff --git a/.asf.yaml b/.asf.yaml new file mode 100644 index 000000000..8ad476f12 --- /dev/null +++ b/.asf.yaml @@ -0,0 +1,38 @@ +# 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. + +# This file controls the settings of this repository +# +# See more details at +# https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features + +notifications: + commits: commits@datafusion.apache.org + issues: github@datafusion.apache.org + pullrequests: github@datafusion.apache.org +github: + description: "Extensible SQL Lexer and Parser for Rust" + labels: + - big-data + - rust + - sql + enabled_merge_buttons: + squash: true + merge: false + rebase: false + features: + issues: true From a4fa9e08b7a73c2d6efc360f9d481352e3521b7f Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Wed, 9 Oct 2024 23:47:14 +0200 Subject: [PATCH 570/806] Add support for quantified comparison predicates (ALL/ANY/SOME) (#1459) --- src/ast/mod.rs | 26 ++++++++++++++++++-- src/parser/mod.rs | 51 ++++++++++++++++++++++++--------------- tests/sqlparser_common.rs | 9 +++++++ 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 39f9def8e..2fbe91afc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -646,12 +646,16 @@ pub enum Expr { regexp: bool, }, /// `ANY` operation e.g. `foo > ANY(bar)`, comparison operator is one of `[=, >, <, =>, =<, !=]` + /// AnyOp { left: Box, compare_op: BinaryOperator, right: Box, + // ANY and SOME are synonymous: https://docs.cloudera.com/cdw-runtime/cloud/using-hiveql/topics/hive_comparison_predicates.html + is_some: bool, }, /// `ALL` operation e.g. `foo > ALL(bar)`, comparison operator is one of `[=, >, <, =>, =<, !=]` + /// AllOp { left: Box, compare_op: BinaryOperator, @@ -1332,12 +1336,30 @@ impl fmt::Display for Expr { left, compare_op, right, - } => write!(f, "{left} {compare_op} ANY({right})"), + is_some, + } => { + let add_parens = !matches!(right.as_ref(), Expr::Subquery(_)); + write!( + f, + "{left} {compare_op} {}{}{right}{}", + if *is_some { "SOME" } else { "ANY" }, + if add_parens { "(" } else { "" }, + if add_parens { ")" } else { "" }, + ) + } Expr::AllOp { left, compare_op, right, - } => write!(f, "{left} {compare_op} ALL({right})"), + } => { + let add_parens = !matches!(right.as_ref(), Expr::Subquery(_)); + write!( + f, + "{left} {compare_op} ALL{}{right}{}", + if add_parens { "(" } else { "" }, + if add_parens { ")" } else { "" }, + ) + } Expr::UnaryOp { op, expr } => { if op == &UnaryOperator::PGPostfixFactorial { write!(f, "{expr}{op}") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c9e66e1f3..cd9be1d8f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1302,13 +1302,9 @@ impl<'a> Parser<'a> { } fn try_parse_expr_sub_query(&mut self) -> Result, ParserError> { - if self - .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) - .is_none() - { + if !self.peek_sub_query() { return Ok(None); } - self.prev_token(); Ok(Some(Expr::Subquery(self.parse_boxed_query()?))) } @@ -1334,12 +1330,7 @@ impl<'a> Parser<'a> { // Snowflake permits a subquery to be passed as an argument without // an enclosing set of parens if it's the only argument. - if dialect_of!(self is SnowflakeDialect) - && self - .parse_one_of_keywords(&[Keyword::WITH, Keyword::SELECT]) - .is_some() - { - self.prev_token(); + if dialect_of!(self is SnowflakeDialect) && self.peek_sub_query() { let subquery = self.parse_boxed_query()?; self.expect_token(&Token::RParen)?; return Ok(Expr::Function(Function { @@ -2639,10 +2630,21 @@ impl<'a> Parser<'a> { }; if let Some(op) = regular_binary_operator { - if let Some(keyword) = self.parse_one_of_keywords(&[Keyword::ANY, Keyword::ALL]) { + if let Some(keyword) = + self.parse_one_of_keywords(&[Keyword::ANY, Keyword::ALL, Keyword::SOME]) + { self.expect_token(&Token::LParen)?; - let right = self.parse_subexpr(precedence)?; - self.expect_token(&Token::RParen)?; + let right = if self.peek_sub_query() { + // We have a subquery ahead (SELECT\WITH ...) need to rewind and + // use the parenthesis for parsing the subquery as an expression. + self.prev_token(); // LParen + self.parse_subexpr(precedence)? + } else { + // Non-subquery expression + let right = self.parse_subexpr(precedence)?; + self.expect_token(&Token::RParen)?; + right + }; if !matches!( op, @@ -2667,10 +2669,11 @@ impl<'a> Parser<'a> { compare_op: op, right: Box::new(right), }, - Keyword::ANY => Expr::AnyOp { + Keyword::ANY | Keyword::SOME => Expr::AnyOp { left: Box::new(expr), compare_op: op, right: Box::new(right), + is_some: keyword == Keyword::SOME, }, _ => unreachable!(), }) @@ -10507,11 +10510,7 @@ impl<'a> Parser<'a> { vec![] }; PivotValueSource::Any(order_by) - } else if self - .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) - .is_some() - { - self.prev_token(); + } else if self.peek_sub_query() { PivotValueSource::Subquery(self.parse_query()?) } else { PivotValueSource::List(self.parse_comma_separated(Self::parse_expr_with_alias)?) @@ -12177,6 +12176,18 @@ impl<'a> Parser<'a> { pub fn into_tokens(self) -> Vec { self.tokens } + + /// Returns true if the next keyword indicates a sub query, i.e. SELECT or WITH + fn peek_sub_query(&mut self) -> bool { + if self + .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) + .is_some() + { + self.prev_token(); + return true; + } + false + } } impl Word { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 15132903c..5327880a4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1907,6 +1907,7 @@ fn parse_binary_any() { left: Box::new(Expr::Identifier(Ident::new("a"))), compare_op: BinaryOperator::Eq, right: Box::new(Expr::Identifier(Ident::new("b"))), + is_some: false, }), select.projection[0] ); @@ -11395,3 +11396,11 @@ fn test_select_where_with_like_or_ilike_any() { verified_stmt(r#"SELECT * FROM x WHERE a ILIKE ANY ('%Jo%oe%', 'T%e')"#); verified_stmt(r#"SELECT * FROM x WHERE a LIKE ANY ('%Jo%oe%', 'T%e')"#); } + +#[test] +fn test_any_some_all_comparison() { + verified_stmt("SELECT c1 FROM tbl WHERE c1 = ANY(SELECT c2 FROM tbl)"); + verified_stmt("SELECT c1 FROM tbl WHERE c1 >= ALL(SELECT c2 FROM tbl)"); + verified_stmt("SELECT c1 FROM tbl WHERE c1 <> SOME(SELECT c2 FROM tbl)"); + verified_stmt("SELECT 1 = ANY(WITH x AS (SELECT 1) SELECT * FROM x)"); +} From 749b061fbfcdd997f0097152ec4bf9b7376b8c4e Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 11 Oct 2024 17:15:18 +0200 Subject: [PATCH 571/806] MySQL dialect: Add support for hash comments (#1466) --- src/tokenizer.rs | 5 +++-- tests/sqlparser_common.rs | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 6d0c86ff2..4186ec824 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -43,7 +43,8 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::DollarQuotedString; use crate::dialect::Dialect; use crate::dialect::{ - BigQueryDialect, DuckDbDialect, GenericDialect, PostgreSqlDialect, SnowflakeDialect, + BigQueryDialect, DuckDbDialect, GenericDialect, MySqlDialect, PostgreSqlDialect, + SnowflakeDialect, }; use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX}; @@ -1140,7 +1141,7 @@ impl<'a> Tokenizer<'a> { } '{' => self.consume_and_return(chars, Token::LBrace), '}' => self.consume_and_return(chars, Token::RBrace), - '#' if dialect_of!(self is SnowflakeDialect | BigQueryDialect) => { + '#' if dialect_of!(self is SnowflakeDialect | BigQueryDialect | MySqlDialect) => { chars.next(); // consume the '#', starting a snowflake single-line comment let comment = self.tokenize_single_line_comment(chars); Ok(Some(Token::Whitespace(Whitespace::SingleLineComment { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5327880a4..7140109b2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9923,7 +9923,11 @@ fn test_release_savepoint() { #[test] fn test_comment_hash_syntax() { let dialects = TestedDialects { - dialects: vec![Box::new(BigQueryDialect {}), Box::new(SnowflakeDialect {})], + dialects: vec![ + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(MySqlDialect {}), + ], options: None, }; let sql = r#" From 7c20d4ae1f8a07c39a8e0e45c0cfe4134138953f Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 14 Oct 2024 19:14:40 +0200 Subject: [PATCH 572/806] Fix #1469 (SET ROLE regression) (#1474) --- src/parser/mod.rs | 40 +++++++++++++++++++++++---------------- tests/sqlparser_common.rs | 24 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cd9be1d8f..b4c0487b4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9416,27 +9416,35 @@ 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(Keyword::ROLE)?; + let context_modifier = match modifier { + Some(Keyword::LOCAL) => ContextModifier::Local, + Some(Keyword::SESSION) => ContextModifier::Session, + _ => ContextModifier::None, + }; + + let role_name = if self.parse_keyword(Keyword::NONE) { + None + } else { + Some(self.parse_identifier(false)?) + }; + Ok(Statement::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 self.parse_keyword(Keyword::ROLE) { - let context_modifier = match modifier { - Some(Keyword::LOCAL) => ContextModifier::Local, - Some(Keyword::SESSION) => ContextModifier::Session, - _ => ContextModifier::None, - }; - - let role_name = if self.parse_keyword(Keyword::NONE) { - None - } else { - Some(self.parse_identifier(false)?) - }; - return Ok(Statement::SetRole { - context_modifier, - role_name, - }); + } else if let Some(set_role_stmt) = + self.maybe_parse(|parser| parser.parse_set_role(modifier)) + { + return Ok(set_role_stmt); } let variables = if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7140109b2..55ab3ddc3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7665,6 +7665,30 @@ fn parse_set_variable() { one_statement_parses_to("SET SOMETHING TO '1'", "SET SOMETHING = '1'"); } +#[test] +fn parse_set_role_as_variable() { + match verified_stmt("SET role = 'foobar'") { + Statement::SetVariable { + local, + hivevar, + variables, + value, + } => { + assert!(!local); + assert!(!hivevar); + assert_eq!( + variables, + OneOrManyWithParens::One(ObjectName(vec!["role".into()])) + ); + assert_eq!( + value, + vec![Expr::Value(Value::SingleQuotedString("foobar".into()))] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_double_colon_cast_at_timezone() { let sql = "SELECT '2001-01-01T00:00:00.000Z'::TIMESTAMP AT TIME ZONE 'Europe/Brussels' FROM t"; From 1dd7d26fbbc53ccbc1a77bbe4d713d44f814b6eb Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:12:39 +0200 Subject: [PATCH 573/806] Add support for parsing MsSql alias with equals (#1467) --- src/dialect/mod.rs | 11 +++++++++++ src/dialect/mssql.rs | 4 ++++ src/parser/mod.rs | 18 ++++++++++++++++++ tests/sqlparser_common.rs | 17 +++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 744f5a8c8..871055685 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -561,6 +561,17 @@ pub trait Dialect: Debug + Any { fn supports_asc_desc_in_column_definition(&self) -> bool { false } + + /// Returns true if this dialect supports treating the equals operator `=` within a `SelectItem` + /// as an alias assignment operator, rather than a boolean expression. + /// For example: the following statements are equivalent for such a dialect: + /// ```sql + /// SELECT col_alias = col FROM tbl; + /// SELECT col_alias AS col FROM tbl; + /// ``` + fn supports_eq_alias_assigment(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index a9d296be3..cace372c0 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -49,4 +49,8 @@ impl Dialect for MsSqlDialect { fn supports_connect_by(&self) -> bool { true } + + fn supports_eq_alias_assigment(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b4c0487b4..842e85c12 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11181,6 +11181,24 @@ impl<'a> Parser<'a> { self.peek_token().location ) } + Expr::BinaryOp { + left, + op: BinaryOperator::Eq, + right, + } if self.dialect.supports_eq_alias_assigment() + && matches!(left.as_ref(), Expr::Identifier(_)) => + { + let Expr::Identifier(alias) = *left else { + return parser_err!( + "BUG: expected identifier expression as alias", + self.peek_token().location + ); + }; + Ok(SelectItem::ExprWithAlias { + expr: *right, + alias, + }) + } expr => self .parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) .map(|alias| match alias { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 55ab3ddc3..55725eeca 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11432,3 +11432,20 @@ fn test_any_some_all_comparison() { verified_stmt("SELECT c1 FROM tbl WHERE c1 <> SOME(SELECT c2 FROM tbl)"); verified_stmt("SELECT 1 = ANY(WITH x AS (SELECT 1) SELECT * FROM x)"); } + +#[test] +fn test_alias_equal_expr() { + let dialects = all_dialects_where(|d| d.supports_eq_alias_assigment()); + let sql = r#"SELECT some_alias = some_column FROM some_table"#; + let expected = r#"SELECT some_column AS some_alias FROM some_table"#; + let _ = dialects.one_statement_parses_to(sql, expected); + + let sql = r#"SELECT some_alias = (a*b) FROM some_table"#; + let expected = r#"SELECT (a * b) AS some_alias FROM some_table"#; + let _ = dialects.one_statement_parses_to(sql, expected); + + let dialects = all_dialects_where(|d| !d.supports_eq_alias_assigment()); + let sql = r#"SELECT x = (a * b) FROM some_table"#; + let expected = r#"SELECT x = (a * b) FROM some_table"#; + let _ = dialects.one_statement_parses_to(sql, expected); +} From 3421e1e4d432990889f4f9ece418420dbe9dc2d5 Mon Sep 17 00:00:00 2001 From: Aleksei Piianin Date: Sun, 20 Oct 2024 20:13:25 +0200 Subject: [PATCH 574/806] Snowflake: support for extended column options in `CREATE TABLE` (#1454) --- src/ast/ddl.rs | 244 +++++++++++++++++++++++++-- src/ast/mod.rs | 11 +- src/dialect/mod.rs | 15 +- src/dialect/snowflake.rs | 122 ++++++++++++-- src/keywords.rs | 2 + src/parser/mod.rs | 28 +++- tests/sqlparser_mssql.rs | 31 ++-- tests/sqlparser_snowflake.rs | 315 +++++++++++++++++++++++++++++++++++ 8 files changed, 723 insertions(+), 45 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0677e63bf..49f6ae4bd 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -31,7 +31,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, - ObjectName, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Value, + ObjectName, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, }; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -1096,17 +1096,221 @@ impl fmt::Display for ColumnOptionDef { } } +/// Identity is a column option for defining an identity or autoincrement column in a `CREATE TABLE` statement. +/// Syntax +/// ```sql +/// { IDENTITY | AUTOINCREMENT } [ (seed , increment) | START num INCREMENT num ] [ ORDER | NOORDER ] +/// ``` +/// [MS SQL Server]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql-identity-property +/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum IdentityPropertyKind { + /// An identity property declared via the `AUTOINCREMENT` key word + /// Example: + /// ```sql + /// AUTOINCREMENT(100, 1) NOORDER + /// AUTOINCREMENT START 100 INCREMENT 1 ORDER + /// ``` + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + Autoincrement(IdentityProperty), + /// An identity property declared via the `IDENTITY` key word + /// Example, [MS SQL Server] or [Snowflake]: + /// ```sql + /// IDENTITY(100, 1) + /// ``` + /// [Snowflake] + /// ```sql + /// IDENTITY(100, 1) ORDER + /// IDENTITY START 100 INCREMENT 1 NOORDER + /// ``` + /// [MS SQL Server]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql-identity-property + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + Identity(IdentityProperty), +} + +impl fmt::Display for IdentityPropertyKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let (command, property) = match self { + IdentityPropertyKind::Identity(property) => ("IDENTITY", property), + IdentityPropertyKind::Autoincrement(property) => ("AUTOINCREMENT", property), + }; + write!(f, "{command}")?; + if let Some(parameters) = &property.parameters { + write!(f, "{parameters}")?; + } + if let Some(order) = &property.order { + write!(f, "{order}")?; + } + 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 IdentityProperty { + pub parameters: Option, + pub order: Option, +} + +/// A format of parameters of identity column. +/// +/// It is [Snowflake] specific. +/// Syntax +/// ```sql +/// (seed , increment) | START num INCREMENT num +/// ``` +/// [MS SQL Server] uses one way of representing these parameters. +/// Syntax +/// ```sql +/// (seed , increment) +/// ``` +/// [MS SQL Server]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql-identity-property +/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum IdentityPropertyFormatKind { + /// A parameters of identity column declared like parameters of function call + /// Example: + /// ```sql + /// (100, 1) + /// ``` + /// [MS SQL Server]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql-identity-property + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + FunctionCall(IdentityParameters), + /// A parameters of identity column declared with keywords `START` and `INCREMENT` + /// Example: + /// ```sql + /// START 100 INCREMENT 1 + /// ``` + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + StartAndIncrement(IdentityParameters), +} + +impl fmt::Display for IdentityPropertyFormatKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + IdentityPropertyFormatKind::FunctionCall(parameters) => { + write!(f, "({}, {})", parameters.seed, parameters.increment) + } + IdentityPropertyFormatKind::StartAndIncrement(parameters) => { + write!( + f, + " START {} INCREMENT {}", + parameters.seed, parameters.increment + ) + } + } + } +} +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IdentityParameters { pub seed: Expr, pub increment: Expr, } -impl fmt::Display for IdentityProperty { +/// The identity column option specifies how values are generated for the auto-incremented column, either in increasing or decreasing order. +/// Syntax +/// ```sql +/// ORDER | NOORDER +/// ``` +/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum IdentityPropertyOrder { + Order, + NoOrder, +} + +impl fmt::Display for IdentityPropertyOrder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + IdentityPropertyOrder::Order => write!(f, " ORDER"), + IdentityPropertyOrder::NoOrder => write!(f, " NOORDER"), + } + } +} + +/// Column policy that identify a security policy of access to a column. +/// Syntax +/// ```sql +/// [ WITH ] MASKING POLICY [ USING ( , , ... ) ] +/// [ WITH ] PROJECTION POLICY +/// ``` +/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ColumnPolicy { + MaskingPolicy(ColumnPolicyProperty), + ProjectionPolicy(ColumnPolicyProperty), +} + +impl fmt::Display for ColumnPolicy { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}, {}", self.seed, self.increment) + let (command, property) = match self { + ColumnPolicy::MaskingPolicy(property) => ("MASKING POLICY", property), + ColumnPolicy::ProjectionPolicy(property) => ("PROJECTION POLICY", property), + }; + if property.with { + write!(f, "WITH ")?; + } + write!(f, "{command} {}", property.policy_name)?; + if let Some(using_columns) = &property.using_columns { + write!(f, " USING ({})", display_comma_separated(using_columns))?; + } + 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 ColumnPolicyProperty { + /// This flag indicates that the column policy option is declared using the `WITH` prefix. + /// Example + /// ```sql + /// WITH PROJECTION POLICY sample_policy + /// ``` + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + pub with: bool, + pub policy_name: Ident, + pub using_columns: Option>, +} + +/// Tags option of column +/// Syntax +/// ```sql +/// [ WITH ] TAG ( = '' [ , = '' , ... ] ) +/// ``` +/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TagsColumnOption { + /// This flag indicates that the tags option is declared using the `WITH` prefix. + /// Example: + /// ```sql + /// WITH TAG (A = 'Tag A') + /// ``` + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + pub with: bool, + pub tags: Vec, +} + +impl fmt::Display for TagsColumnOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.with { + write!(f, "WITH ")?; + } + write!(f, "TAG ({})", display_comma_separated(&self.tags))?; + Ok(()) } } @@ -1180,16 +1384,32 @@ pub enum ColumnOption { /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#view_column_option_list /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#column_option_list Options(Vec), - /// MS SQL Server specific: Creates an identity column in a table. + /// Creates an identity or an autoincrement column in a table. /// Syntax /// ```sql - /// IDENTITY [ (seed , increment) ] + /// { IDENTITY | AUTOINCREMENT } [ (seed , increment) | START num INCREMENT num ] [ ORDER | NOORDER ] /// ``` /// [MS SQL Server]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql-identity-property - Identity(Option), + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + Identity(IdentityPropertyKind), /// SQLite specific: ON CONFLICT option on column definition /// OnConflict(Keyword), + /// Snowflake specific: an option of specifying security masking or projection policy to set on a column. + /// Syntax: + /// ```sql + /// [ WITH ] MASKING POLICY [ USING ( , , ... ) ] + /// [ WITH ] PROJECTION POLICY + /// ``` + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + Policy(ColumnPolicy), + /// Snowflake specific: Specifies the tag name and the tag string value. + /// Syntax: + /// ```sql + /// [ WITH ] TAG ( = '' [ , = '' , ... ] ) + /// ``` + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table + Tags(TagsColumnOption), } impl fmt::Display for ColumnOption { @@ -1292,16 +1512,18 @@ impl fmt::Display for ColumnOption { write!(f, "OPTIONS({})", display_comma_separated(options)) } Identity(parameters) => { - write!(f, "IDENTITY")?; - if let Some(parameters) = parameters { - write!(f, "({parameters})")?; - } - Ok(()) + write!(f, "{parameters}") } OnConflict(keyword) => { write!(f, "ON CONFLICT {:?}", keyword)?; Ok(()) } + Policy(parameters) => { + write!(f, "{parameters}") + } + Tags(tags) => { + write!(f, "{tags}") + } } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2fbe91afc..2e46722fa 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,11 +40,12 @@ pub use self::data_type::{ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue, Use}; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterPolicyOperation, AlterTableOperation, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ConstraintCharacteristics, Deduplicate, - DeferrableInitial, GeneratedAs, GeneratedExpressionMode, IdentityProperty, IndexOption, - IndexType, KeyOrIndexDisplay, Owner, Partition, ProcedureParam, ReferentialAction, - TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, - ViewColumnDef, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, + ConstraintCharacteristics, Deduplicate, DeferrableInitial, GeneratedAs, + GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, Owner, + Partition, ProcedureParam, ReferentialAction, TableConstraint, TagsColumnOption, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 871055685..b34e22081 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::{Expr, Statement}; +use crate::ast::{ColumnOption, Expr, Statement}; pub use crate::keywords; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -478,6 +478,19 @@ pub trait Dialect: Debug + Any { None } + /// Dialect-specific column option parser override + /// + /// This method is called to parse the next column option. + /// + /// If `None` is returned, falls back to the default behavior. + fn parse_column_option( + &self, + _parser: &mut Parser, + ) -> Option, ParserError>> { + // return None to fall back to the default behavior + None + } + /// Decide the lexical Precedence of operators. /// /// Uses (APPROXIMATELY) as a reference diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index f256c9b53..7c80f0461 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -22,7 +22,11 @@ use crate::ast::helpers::stmt_data_loading::{ DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageLoadSelectItem, StageParamsObject, }; -use crate::ast::{Ident, ObjectName, RowAccessPolicy, Statement, Tag, WrappedCollection}; +use crate::ast::{ + ColumnOption, ColumnPolicy, ColumnPolicyProperty, Ident, IdentityParameters, IdentityProperty, + IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, + RowAccessPolicy, Statement, TagsColumnOption, WrappedCollection, +}; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -149,6 +153,36 @@ impl Dialect for SnowflakeDialect { None } + fn parse_column_option( + &self, + parser: &mut Parser, + ) -> Option, ParserError>> { + parser.maybe_parse(|parser| { + let with = parser.parse_keyword(Keyword::WITH); + + if parser.parse_keyword(Keyword::IDENTITY) { + Ok(parse_identity_property(parser) + .map(|p| Some(ColumnOption::Identity(IdentityPropertyKind::Identity(p))))) + } else if parser.parse_keyword(Keyword::AUTOINCREMENT) { + Ok(parse_identity_property(parser).map(|p| { + Some(ColumnOption::Identity(IdentityPropertyKind::Autoincrement( + p, + ))) + })) + } else if parser.parse_keywords(&[Keyword::MASKING, Keyword::POLICY]) { + Ok(parse_column_policy_property(parser, with) + .map(|p| Some(ColumnOption::Policy(ColumnPolicy::MaskingPolicy(p))))) + } else if parser.parse_keywords(&[Keyword::PROJECTION, Keyword::POLICY]) { + Ok(parse_column_policy_property(parser, with) + .map(|p| Some(ColumnOption::Policy(ColumnPolicy::ProjectionPolicy(p))))) + } else if parser.parse_keywords(&[Keyword::TAG]) { + Ok(parse_column_tags(parser, with).map(|p| Some(ColumnOption::Tags(p)))) + } else { + Err(ParserError::ParserError("not found match".to_string())) + } + }) + } + fn get_next_precedence(&self, parser: &Parser) -> Option> { let token = parser.peek_token(); // Snowflake supports the `:` cast operator unlike other dialects @@ -307,16 +341,8 @@ pub fn parse_create_table( builder.with_row_access_policy(Some(RowAccessPolicy::new(policy, columns))) } Keyword::TAG => { - fn parse_tag(parser: &mut Parser) -> Result { - let name = parser.parse_identifier(false)?; - parser.expect_token(&Token::Eq)?; - let value = parser.parse_literal_string()?; - - Ok(Tag::new(name, value)) - } - parser.expect_token(&Token::LParen)?; - let tags = parser.parse_comma_separated(parse_tag)?; + let tags = parser.parse_comma_separated(Parser::parse_tag)?; parser.expect_token(&Token::RParen)?; builder = builder.with_tags(Some(tags)); } @@ -776,3 +802,79 @@ fn parse_parentheses_options(parser: &mut Parser) -> Result Result { + let parameters = if parser.consume_token(&Token::LParen) { + let seed = parser.parse_number()?; + parser.expect_token(&Token::Comma)?; + let increment = parser.parse_number()?; + parser.expect_token(&Token::RParen)?; + + Some(IdentityPropertyFormatKind::FunctionCall( + IdentityParameters { seed, increment }, + )) + } else if parser.parse_keyword(Keyword::START) { + let seed = parser.parse_number()?; + parser.expect_keyword(Keyword::INCREMENT)?; + let increment = parser.parse_number()?; + + Some(IdentityPropertyFormatKind::StartAndIncrement( + IdentityParameters { seed, increment }, + )) + } else { + None + }; + let order = match parser.parse_one_of_keywords(&[Keyword::ORDER, Keyword::NOORDER]) { + Some(Keyword::ORDER) => Some(IdentityPropertyOrder::Order), + Some(Keyword::NOORDER) => Some(IdentityPropertyOrder::NoOrder), + _ => None, + }; + Ok(IdentityProperty { parameters, order }) +} + +/// Parsing a policy property of column option +/// Syntax: +/// ```sql +/// [ USING ( , , ... ) +/// ``` +/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table +fn parse_column_policy_property( + parser: &mut Parser, + with: bool, +) -> Result { + let policy_name = parser.parse_identifier(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(false))?; + parser.expect_token(&Token::RParen)?; + Some(columns) + } else { + None + }; + + Ok(ColumnPolicyProperty { + with, + policy_name, + using_columns, + }) +} + +/// Parsing tags list of column +/// Syntax: +/// ```sql +/// ( = '' [ , = '' , ... ] ) +/// ``` +/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/sql/create-table +fn parse_column_tags(parser: &mut Parser, with: bool) -> Result { + parser.expect_token(&Token::LParen)?; + let tags = parser.parse_comma_separated(Parser::parse_tag)?; + parser.expect_token(&Token::RParen)?; + + Ok(TagsColumnOption { with, tags }) +} diff --git a/src/keywords.rs b/src/keywords.rs index ecf4bd474..001de7484 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -453,6 +453,7 @@ define_keywords!( MACRO, MANAGEDLOCATION, MAP, + MASKING, MATCH, MATCHED, MATCHES, @@ -504,6 +505,7 @@ define_keywords!( NOINHERIT, NOLOGIN, NONE, + NOORDER, NOREPLICATION, NORMALIZE, NOSCAN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 842e85c12..5bd64392a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6065,7 +6065,7 @@ impl<'a> Parser<'a> { } } else if let Some(option) = self.parse_optional_column_option()? { options.push(ColumnOptionDef { name: None, option }); - } else if dialect_of!(self is MySqlDialect | GenericDialect) + } else if dialect_of!(self is MySqlDialect | SnowflakeDialect | GenericDialect) && self.parse_keyword(Keyword::COLLATE) { collation = Some(self.parse_object_name(false)?); @@ -6105,6 +6105,10 @@ impl<'a> Parser<'a> { } pub fn parse_optional_column_option(&mut self) -> Result, ParserError> { + if let Some(option) = self.dialect.parse_column_option(self) { + return option; + } + if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) { Ok(Some(ColumnOption::CharacterSet( self.parse_object_name(false)?, @@ -6232,17 +6236,24 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::IDENTITY) && dialect_of!(self is MsSqlDialect | GenericDialect) { - let property = if self.consume_token(&Token::LParen) { + let parameters = if self.consume_token(&Token::LParen) { let seed = self.parse_number()?; self.expect_token(&Token::Comma)?; let increment = self.parse_number()?; self.expect_token(&Token::RParen)?; - Some(IdentityProperty { seed, increment }) + Some(IdentityPropertyFormatKind::FunctionCall( + IdentityParameters { seed, increment }, + )) } else { None }; - Ok(Some(ColumnOption::Identity(property))) + Ok(Some(ColumnOption::Identity( + IdentityPropertyKind::Identity(IdentityProperty { + parameters, + order: None, + }), + ))) } else if dialect_of!(self is SQLiteDialect | GenericDialect) && self.parse_keywords(&[Keyword::ON, Keyword::CONFLICT]) { @@ -6260,6 +6271,15 @@ impl<'a> Parser<'a> { Ok(None) } } + + pub(crate) fn parse_tag(&mut self) -> Result { + let name = self.parse_identifier(false)?; + self.expect_token(&Token::Eq)?; + let value = self.parse_literal_string()?; + + Ok(Tag::new(name, value)) + } + fn parse_optional_column_option_generated( &mut self, ) -> Result, ParserError> { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 5a2ef9e87..ef89a4768 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -921,7 +921,12 @@ fn parse_create_table_with_identity_column() { vec![ ColumnOptionDef { name: None, - option: ColumnOption::Identity(None), + option: ColumnOption::Identity(IdentityPropertyKind::Identity( + IdentityProperty { + parameters: None, + order: None, + }, + )), }, ColumnOptionDef { name: None, @@ -934,19 +939,17 @@ fn parse_create_table_with_identity_column() { vec![ ColumnOptionDef { name: None, - #[cfg(not(feature = "bigdecimal"))] - option: ColumnOption::Identity(Some(IdentityProperty { - seed: Expr::Value(Value::Number("1".to_string(), false)), - increment: Expr::Value(Value::Number("1".to_string(), false)), - })), - #[cfg(feature = "bigdecimal")] - option: ColumnOption::Identity(Some(IdentityProperty { - seed: Expr::Value(Value::Number(bigdecimal::BigDecimal::from(1), false)), - increment: Expr::Value(Value::Number( - bigdecimal::BigDecimal::from(1), - false, - )), - })), + option: ColumnOption::Identity(IdentityPropertyKind::Identity( + IdentityProperty { + parameters: Some(IdentityPropertyFormatKind::FunctionCall( + IdentityParameters { + seed: Expr::Value(number("1")), + increment: Expr::Value(number("1")), + }, + )), + order: None, + }, + )), }, ColumnOptionDef { name: None, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e73b6999a..d7e967ffe 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -525,6 +525,321 @@ fn test_snowflake_single_line_tokenize() { assert_eq!(expected, tokens); } +#[test] +fn test_snowflake_create_table_with_autoincrement_columns() { + let sql = concat!( + "CREATE TABLE my_table (", + "a INT AUTOINCREMENT ORDER, ", + "b INT AUTOINCREMENT(100, 1) NOORDER, ", + "c INT IDENTITY, ", + "d INT IDENTITY START 100 INCREMENT 1 ORDER", + ")" + ); + // it is a snowflake specific options (AUTOINCREMENT/IDENTITY) + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ + ColumnDef { + name: "a".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Identity(IdentityPropertyKind::Autoincrement( + IdentityProperty { + parameters: None, + order: Some(IdentityPropertyOrder::Order), + } + )) + }] + }, + ColumnDef { + name: "b".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Identity(IdentityPropertyKind::Autoincrement( + IdentityProperty { + parameters: Some(IdentityPropertyFormatKind::FunctionCall( + IdentityParameters { + seed: Expr::Value(number("100")), + increment: Expr::Value(number("1")), + } + )), + order: Some(IdentityPropertyOrder::NoOrder), + } + )) + }] + }, + ColumnDef { + name: "c".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Identity(IdentityPropertyKind::Identity( + IdentityProperty { + parameters: None, + order: None, + } + )) + }] + }, + ColumnDef { + name: "d".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Identity(IdentityPropertyKind::Identity( + IdentityProperty { + parameters: Some( + IdentityPropertyFormatKind::StartAndIncrement( + IdentityParameters { + seed: Expr::Value(number("100")), + increment: Expr::Value(number("1")), + } + ) + ), + order: Some(IdentityPropertyOrder::Order), + } + )) + }] + }, + ] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_with_collated_column() { + match snowflake_and_generic().verified_stmt("CREATE TABLE my_table (a TEXT COLLATE 'de_DE')") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "a".into(), + data_type: DataType::Text, + collation: Some(ObjectName(vec![Ident::with_quote('\'', "de_DE")])), + options: vec![] + },] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_table_with_columns_masking_policy() { + for (sql, with, using_columns) in [ + ( + "CREATE TABLE my_table (a INT WITH MASKING POLICY p)", + true, + None, + ), + ( + "CREATE TABLE my_table (a INT MASKING POLICY p)", + false, + None, + ), + ( + "CREATE TABLE my_table (a INT WITH MASKING POLICY p USING (a, b))", + true, + Some(vec!["a".into(), "b".into()]), + ), + ( + "CREATE TABLE my_table (a INT MASKING POLICY p USING (a, b))", + false, + Some(vec!["a".into(), "b".into()]), + ), + ] { + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "a".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( + ColumnPolicyProperty { + with, + policy_name: "p".into(), + using_columns, + } + )) + }], + },] + ); + } + _ => unreachable!(), + } + } +} + +#[test] +fn test_snowflake_create_table_with_columns_projection_policy() { + for (sql, with) in [ + ( + "CREATE TABLE my_table (a INT WITH PROJECTION POLICY p)", + true, + ), + ("CREATE TABLE my_table (a INT PROJECTION POLICY p)", false), + ] { + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "a".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( + ColumnPolicyProperty { + with, + policy_name: "p".into(), + using_columns: None, + } + )) + }], + },] + ); + } + _ => unreachable!(), + } + } +} + +#[test] +fn test_snowflake_create_table_with_columns_tags() { + for (sql, with) in [ + ( + "CREATE TABLE my_table (a INT WITH TAG (A='TAG A', B='TAG B'))", + true, + ), + ( + "CREATE TABLE my_table (a INT TAG (A='TAG A', B='TAG B'))", + false, + ), + ] { + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ColumnDef { + name: "a".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Tags(TagsColumnOption { + with, + tags: vec![ + Tag::new("A".into(), "TAG A".into()), + Tag::new("B".into(), "TAG B".into()), + ] + }), + }], + },] + ); + } + _ => unreachable!(), + } + } +} + +#[test] +fn test_snowflake_create_table_with_several_column_options() { + let sql = concat!( + "CREATE TABLE my_table (", + "a INT IDENTITY WITH MASKING POLICY p1 USING (a, b) WITH TAG (A='TAG A', B='TAG B'), ", + "b TEXT COLLATE 'de_DE' PROJECTION POLICY p2 TAG (C='TAG C', D='TAG D')", + ")" + ); + match snowflake().verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ + ColumnDef { + name: "a".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Identity(IdentityPropertyKind::Identity( + IdentityProperty { + parameters: None, + order: None + } + )), + }, + ColumnOptionDef { + name: None, + option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( + ColumnPolicyProperty { + with: true, + policy_name: "p1".into(), + using_columns: Some(vec!["a".into(), "b".into()]), + } + )), + }, + ColumnOptionDef { + name: None, + option: ColumnOption::Tags(TagsColumnOption { + with: true, + tags: vec![ + Tag::new("A".into(), "TAG A".into()), + Tag::new("B".into(), "TAG B".into()), + ] + }), + } + ], + }, + ColumnDef { + name: "b".into(), + data_type: DataType::Text, + collation: Some(ObjectName(vec![Ident::with_quote('\'', "de_DE")])), + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( + ColumnPolicyProperty { + with: false, + policy_name: "p2".into(), + using_columns: None, + } + )), + }, + ColumnOptionDef { + name: None, + option: ColumnOption::Tags(TagsColumnOption { + with: false, + tags: vec![ + Tag::new("C".into(), "TAG C".into()), + Tag::new("D".into(), "TAG D".into()), + ] + }), + } + ], + }, + ] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_sf_create_or_replace_view_with_comment_missing_equal() { assert!(snowflake_and_generic() From 45c5d69b2298bf54db29810149587a28691063ed Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 20 Oct 2024 22:29:55 +0200 Subject: [PATCH 575/806] MsSQL TRY_CONVERT (#1477) --- src/ast/mod.rs | 6 +++++- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 5 +++++ src/dialect/mssql.rs | 4 ++++ src/keywords.rs | 1 + src/parser/mod.rs | 12 ++++++++---- tests/sqlparser_common.rs | 11 +++++++++++ tests/sqlparser_mssql.rs | 2 ++ 8 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2e46722fa..4900307e5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -669,6 +669,9 @@ pub enum Expr { }, /// CONVERT a value to a different data type or character encoding. e.g. `CONVERT(foo USING utf8mb4)` Convert { + /// CONVERT (false) or TRY_CONVERT (true) + /// + is_try: bool, /// The expression to convert expr: Box, /// The target data type @@ -1371,13 +1374,14 @@ impl fmt::Display for Expr { } } Expr::Convert { + is_try, expr, target_before_value, data_type, charset, styles, } => { - write!(f, "CONVERT(")?; + write!(f, "{}CONVERT(", if *is_try { "TRY_" } else { "" })?; if let Some(data_type) = data_type { if let Some(charset) = charset { write!(f, "{expr}, {data_type} CHARACTER SET {charset}") diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 92d720a0b..0a5464c9c 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -107,4 +107,8 @@ impl Dialect for GenericDialect { fn supports_asc_desc_in_column_definition(&self) -> bool { true } + + fn supports_try_convert(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b34e22081..be97f929b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -585,6 +585,11 @@ pub trait Dialect: Debug + Any { fn supports_eq_alias_assigment(&self) -> bool { false } + + /// Returns true if this dialect supports the `TRY_CONVERT` function + fn supports_try_convert(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index cace372c0..78ec621ed 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -53,4 +53,8 @@ impl Dialect for MsSqlDialect { fn supports_eq_alias_assigment(&self) -> bool { true } + + fn supports_try_convert(&self) -> bool { + true + } } diff --git a/src/keywords.rs b/src/keywords.rs index 001de7484..6182ae176 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -771,6 +771,7 @@ define_keywords!( TRUE, TRUNCATE, TRY_CAST, + TRY_CONVERT, TUPLE, TYPE, UESCAPE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5bd64392a..a1079f6f7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1023,7 +1023,8 @@ impl<'a> Parser<'a> { self.parse_time_functions(ObjectName(vec![w.to_ident()])) } Keyword::CASE => self.parse_case_expr(), - Keyword::CONVERT => self.parse_convert_expr(), + Keyword::CONVERT => self.parse_convert_expr(false), + Keyword::TRY_CONVERT if self.dialect.supports_try_convert() => self.parse_convert_expr(true), Keyword::CAST => self.parse_cast_expr(CastKind::Cast), Keyword::TRY_CAST => self.parse_cast_expr(CastKind::TryCast), Keyword::SAFE_CAST => self.parse_cast_expr(CastKind::SafeCast), @@ -1614,7 +1615,7 @@ impl<'a> Parser<'a> { } /// mssql-like convert function - fn parse_mssql_convert(&mut self) -> Result { + fn parse_mssql_convert(&mut self, is_try: bool) -> Result { self.expect_token(&Token::LParen)?; let data_type = self.parse_data_type()?; self.expect_token(&Token::Comma)?; @@ -1626,6 +1627,7 @@ impl<'a> Parser<'a> { }; self.expect_token(&Token::RParen)?; Ok(Expr::Convert { + is_try, expr: Box::new(expr), data_type: Some(data_type), charset: None, @@ -1638,9 +1640,9 @@ impl<'a> Parser<'a> { /// - `CONVERT('héhé' USING utf8mb4)` (MySQL) /// - `CONVERT('héhé', CHAR CHARACTER SET utf8mb4)` (MySQL) /// - `CONVERT(DECIMAL(10, 5), 42)` (MSSQL) - the type comes first - pub fn parse_convert_expr(&mut self) -> Result { + pub fn parse_convert_expr(&mut self, is_try: bool) -> Result { if self.dialect.convert_type_before_value() { - return self.parse_mssql_convert(); + return self.parse_mssql_convert(is_try); } self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; @@ -1648,6 +1650,7 @@ impl<'a> Parser<'a> { let charset = self.parse_object_name(false)?; self.expect_token(&Token::RParen)?; return Ok(Expr::Convert { + is_try, expr: Box::new(expr), data_type: None, charset: Some(charset), @@ -1664,6 +1667,7 @@ impl<'a> Parser<'a> { }; self.expect_token(&Token::RParen)?; Ok(Expr::Convert { + is_try, expr: Box::new(expr), data_type: Some(data_type), charset, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 55725eeca..5683bcf91 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11449,3 +11449,14 @@ fn test_alias_equal_expr() { let expected = r#"SELECT x = (a * b) FROM some_table"#; let _ = dialects.one_statement_parses_to(sql, expected); } + +#[test] +fn test_try_convert() { + let dialects = + all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value()); + dialects.verified_expr("TRY_CONVERT(VARCHAR(MAX), 'foo')"); + + let dialects = + all_dialects_where(|d| d.supports_try_convert() && !d.convert_type_before_value()); + dialects.verified_expr("TRY_CONVERT('foo', VARCHAR(MAX))"); +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ef89a4768..58765f6c0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -464,6 +464,7 @@ fn parse_cast_varchar_max() { fn parse_convert() { let sql = "CONVERT(INT, 1, 2, 3, NULL)"; let Expr::Convert { + is_try, expr, data_type, charset, @@ -473,6 +474,7 @@ fn parse_convert() { else { unreachable!() }; + assert!(!is_try); assert_eq!(Expr::Value(number("1")), *expr); assert_eq!(Some(DataType::Int(None)), data_type); assert!(charset.is_none()); From a8432b57db6b8efc635e4903d712e76f7adb6e6d Mon Sep 17 00:00:00 2001 From: David Caldwell Date: Mon, 21 Oct 2024 11:44:38 -0700 Subject: [PATCH 576/806] Add PostgreSQL specfic "CREATE TYPE t AS ENUM (...)" support. (#1460) --- src/ast/ddl.rs | 5 +++++ src/dialect/postgresql.rs | 35 ++++++++++++++++++++++++++++++++++- tests/sqlparser_postgres.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 49f6ae4bd..21a716d25 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1706,6 +1706,8 @@ pub enum UserDefinedTypeRepresentation { Composite { attributes: Vec, }, + /// Note: this is PostgreSQL-specific. See + Enum { labels: Vec }, } impl fmt::Display for UserDefinedTypeRepresentation { @@ -1714,6 +1716,9 @@ impl fmt::Display for UserDefinedTypeRepresentation { UserDefinedTypeRepresentation::Composite { attributes } => { write!(f, "({})", display_comma_separated(attributes)) } + UserDefinedTypeRepresentation::Enum { labels } => { + write!(f, "ENUM ({})", display_comma_separated(labels)) + } } } } diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 2a66705bb..51dc49849 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -28,7 +28,7 @@ // limitations under the License. use log::debug; -use crate::ast::{CommentObject, Statement}; +use crate::ast::{CommentObject, ObjectName, Statement, UserDefinedTypeRepresentation}; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -138,6 +138,9 @@ impl Dialect for PostgreSqlDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.parse_keyword(Keyword::COMMENT) { Some(parse_comment(parser)) + } else if parser.parse_keyword(Keyword::CREATE) { + parser.prev_token(); // unconsume the CREATE in case we don't end up parsing anything + parse_create(parser) } else { None } @@ -225,3 +228,33 @@ pub fn parse_comment(parser: &mut Parser) -> Result { if_exists, }) } + +pub fn parse_create(parser: &mut Parser) -> Option> { + let name = parser.maybe_parse(|parser| -> Result { + parser.expect_keyword(Keyword::CREATE)?; + parser.expect_keyword(Keyword::TYPE)?; + let name = parser.parse_object_name(false)?; + parser.expect_keyword(Keyword::AS)?; + parser.expect_keyword(Keyword::ENUM)?; + Ok(name) + }); + name.map(|name| parse_create_type_as_enum(parser, name)) +} + +// https://www.postgresql.org/docs/current/sql-createtype.html +pub fn parse_create_type_as_enum( + parser: &mut Parser, + name: ObjectName, +) -> Result { + if !parser.consume_token(&Token::LParen) { + return parser.expected("'(' after CREATE TYPE AS ENUM", parser.peek_token()); + } + + let labels = parser.parse_comma_separated0(|p| p.parse_identifier(false), Token::RParen)?; + parser.expect_token(&Token::RParen)?; + + Ok(Statement::CreateType { + name, + representation: UserDefinedTypeRepresentation::Enum { labels }, + }) +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 735b87b5d..bd37214ce 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5128,3 +5128,32 @@ fn arrow_cast_precedence() { } ) } + +#[test] +fn parse_create_type_as_enum() { + let statement = pg().one_statement_parses_to( + r#"CREATE TYPE public.my_type AS ENUM ( + 'label1', + 'label2', + 'label3', + 'label4' + );"#, + "CREATE TYPE public.my_type AS ENUM ('label1', 'label2', 'label3', 'label4')", + ); + match statement { + Statement::CreateType { + name, + representation: UserDefinedTypeRepresentation::Enum { labels }, + } => { + assert_eq!("public.my_type", name.to_string()); + assert_eq!( + vec!["label1", "label2", "label3", "label4"] + .into_iter() + .map(|l| Ident::with_quote('\'', l)) + .collect::>(), + labels + ); + } + _ => unreachable!(), + } +} From 38f1e573fe9e176582f0b03867439548e5fd9922 Mon Sep 17 00:00:00 2001 From: Seve Martinez <20816697+seve-martinez@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:52:17 -0700 Subject: [PATCH 577/806] feat: adding Display implementation to DELETE and INSERT (#1427) Co-authored-by: Andrew Lamb --- src/ast/dml.rs | 115 +++++++++++++++++++++++++++++++++++++++- src/ast/mod.rs | 140 +++++-------------------------------------------- 2 files changed, 128 insertions(+), 127 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 8121f2c5b..2932fafb5 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -16,7 +16,12 @@ // under the License. #[cfg(not(feature = "std"))] -use alloc::{boxed::Box, string::String, vec::Vec}; +use alloc::{ + boxed::Box, + format, + string::{String, ToString}, + vec::Vec, +}; use core::fmt::{self, Display}; #[cfg(feature = "serde")] @@ -492,6 +497,81 @@ pub struct Insert { pub insert_alias: Option, } +impl Display for Insert { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let table_name = if let Some(alias) = &self.table_alias { + format!("{0} AS {alias}", self.table_name) + } else { + self.table_name.to_string() + }; + + if let Some(action) = self.or { + write!(f, "INSERT OR {action} INTO {table_name} ")?; + } else { + write!( + f, + "{start}", + start = if self.replace_into { + "REPLACE" + } else { + "INSERT" + }, + )?; + if let Some(priority) = self.priority { + write!(f, " {priority}",)?; + } + + write!( + f, + "{ignore}{over}{int}{tbl} {table_name} ", + table_name = table_name, + ignore = if self.ignore { " IGNORE" } else { "" }, + over = if self.overwrite { " OVERWRITE" } else { "" }, + int = if self.into { " INTO" } else { "" }, + tbl = if self.table { " TABLE" } else { "" }, + )?; + } + if !self.columns.is_empty() { + write!(f, "({}) ", display_comma_separated(&self.columns))?; + } + if let Some(ref parts) = self.partitioned { + if !parts.is_empty() { + write!(f, "PARTITION ({}) ", display_comma_separated(parts))?; + } + } + if !self.after_columns.is_empty() { + write!(f, "({}) ", display_comma_separated(&self.after_columns))?; + } + + if let Some(source) = &self.source { + write!(f, "{source}")?; + } + + if self.source.is_none() && self.columns.is_empty() { + write!(f, "DEFAULT VALUES")?; + } + + if let Some(insert_alias) = &self.insert_alias { + write!(f, " AS {0}", insert_alias.row_alias)?; + + if let Some(col_aliases) = &insert_alias.col_aliases { + if !col_aliases.is_empty() { + write!(f, " ({})", display_comma_separated(col_aliases))?; + } + } + } + + if let Some(on) = &self.on { + write!(f, "{on}")?; + } + + if let Some(returning) = &self.returning { + write!(f, " RETURNING {}", display_comma_separated(returning))?; + } + Ok(()) + } +} + /// DELETE statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -512,3 +592,36 @@ pub struct Delete { /// LIMIT (MySQL) pub limit: Option, } + +impl Display for Delete { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DELETE ")?; + if !self.tables.is_empty() { + write!(f, "{} ", display_comma_separated(&self.tables))?; + } + match &self.from { + FromTable::WithFromKeyword(from) => { + write!(f, "FROM {}", display_comma_separated(from))?; + } + FromTable::WithoutKeyword(from) => { + write!(f, "{}", display_comma_separated(from))?; + } + } + if let Some(using) = &self.using { + write!(f, " USING {}", display_comma_separated(using))?; + } + if let Some(selection) = &self.selection { + write!(f, " WHERE {selection}")?; + } + if let Some(returning) = &self.returning { + write!(f, " RETURNING {}", display_comma_separated(returning))?; + } + if !self.order_by.is_empty() { + write!(f, " ORDER BY {}", display_comma_separated(&self.order_by))?; + } + if let Some(limit) = &self.limit { + write!(f, " LIMIT {limit}")?; + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4900307e5..790a39bdb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2176,6 +2176,18 @@ pub enum FromTable { /// WithoutKeyword(Vec), } +impl Display for FromTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FromTable::WithFromKeyword(tables) => { + write!(f, "FROM {}", display_comma_separated(tables)) + } + FromTable::WithoutKeyword(tables) => { + write!(f, "{}", display_comma_separated(tables)) + } + } + } +} /// Policy type for a `CREATE POLICY` statement. /// ```sql @@ -3533,93 +3545,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Insert(insert) => { - let Insert { - or, - ignore, - into, - table_name, - table_alias, - overwrite, - partitioned, - columns, - after_columns, - source, - table, - on, - returning, - replace_into, - priority, - insert_alias, - } = insert; - let table_name = if let Some(alias) = table_alias { - format!("{table_name} AS {alias}") - } else { - table_name.to_string() - }; - - if let Some(action) = or { - write!(f, "INSERT OR {action} INTO {table_name} ")?; - } else { - write!( - f, - "{start}", - start = if *replace_into { "REPLACE" } else { "INSERT" }, - )?; - if let Some(priority) = priority { - write!(f, " {priority}",)?; - } - - write!( - f, - "{ignore}{over}{int}{tbl} {table_name} ", - table_name = table_name, - ignore = if *ignore { " IGNORE" } else { "" }, - over = if *overwrite { " OVERWRITE" } else { "" }, - int = if *into { " INTO" } else { "" }, - tbl = if *table { " TABLE" } else { "" }, - )?; - } - if !columns.is_empty() { - write!(f, "({}) ", display_comma_separated(columns))?; - } - if let Some(ref parts) = partitioned { - if !parts.is_empty() { - write!(f, "PARTITION ({}) ", display_comma_separated(parts))?; - } - } - if !after_columns.is_empty() { - write!(f, "({}) ", display_comma_separated(after_columns))?; - } - - if let Some(source) = source { - write!(f, "{source}")?; - } - - if source.is_none() && columns.is_empty() { - write!(f, "DEFAULT VALUES")?; - } - - if let Some(insert_alias) = insert_alias { - write!(f, " AS {0}", insert_alias.row_alias)?; - - if let Some(col_aliases) = &insert_alias.col_aliases { - if !col_aliases.is_empty() { - write!(f, " ({})", display_comma_separated(col_aliases))?; - } - } - } - - if let Some(on) = on { - write!(f, "{on}")?; - } - - if let Some(returning) = returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; - } - - Ok(()) - } + Statement::Insert(insert) => write!(f, "{insert}"), Statement::Install { extension_name: name, } => write!(f, "INSTALL {name}"), @@ -3696,45 +3622,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Delete(delete) => { - let Delete { - tables, - from, - using, - selection, - returning, - order_by, - limit, - } = delete; - write!(f, "DELETE ")?; - if !tables.is_empty() { - write!(f, "{} ", display_comma_separated(tables))?; - } - match from { - FromTable::WithFromKeyword(from) => { - write!(f, "FROM {}", display_comma_separated(from))?; - } - FromTable::WithoutKeyword(from) => { - write!(f, "{}", display_comma_separated(from))?; - } - } - if let Some(using) = using { - write!(f, " USING {}", display_comma_separated(using))?; - } - if let Some(selection) = selection { - write!(f, " WHERE {selection}")?; - } - if let Some(returning) = returning { - write!(f, " RETURNING {}", display_comma_separated(returning))?; - } - if !order_by.is_empty() { - write!(f, " ORDER BY {}", display_comma_separated(order_by))?; - } - if let Some(limit) = limit { - write!(f, " LIMIT {limit}")?; - } - Ok(()) - } + Statement::Delete(delete) => write!(f, "{delete}"), Statement::Close { cursor } => { write!(f, "CLOSE {cursor}")?; From 8e0d26abb3c4dda2fa091dfd9611be1dbdc14755 Mon Sep 17 00:00:00 2001 From: tomershaniii <65544633+tomershaniii@users.noreply.github.com> Date: Mon, 21 Oct 2024 22:41:34 +0300 Subject: [PATCH 578/806] fix for maybe_parse preventing parser from erroring on recursion limit (#1464) --- src/ast/mod.rs | 4 +- src/ast/query.rs | 2 +- src/dialect/mod.rs | 4 +- src/dialect/snowflake.rs | 4 +- src/parser/alter.rs | 2 +- src/parser/mod.rs | 184 ++++++-------- src/test_utils.rs | 27 +- tests/sqlparser_bigquery.rs | 21 +- tests/sqlparser_clickhouse.rs | 13 +- tests/sqlparser_common.rs | 466 ++++++++++++++-------------------- tests/sqlparser_databricks.rs | 13 +- tests/sqlparser_duckdb.rs | 15 +- tests/sqlparser_hive.rs | 15 +- tests/sqlparser_mssql.rs | 10 +- tests/sqlparser_mysql.rs | 109 +++----- tests/sqlparser_postgres.rs | 13 +- tests/sqlparser_redshift.rs | 13 +- tests/sqlparser_snowflake.rs | 54 ++-- tests/sqlparser_sqlite.rs | 25 +- 19 files changed, 423 insertions(+), 571 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 790a39bdb..0573240a2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3208,7 +3208,7 @@ pub enum Statement { /// Table confs options: Vec, /// Cache table as a Query - query: Option, + query: Option>, }, /// ```sql /// UNCACHE TABLE [ IF EXISTS ] @@ -6883,7 +6883,7 @@ impl fmt::Display for MacroArg { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum MacroDefinition { Expr(Expr), - Table(Query), + Table(Box), } impl fmt::Display for MacroDefinition { diff --git a/src/ast/query.rs b/src/ast/query.rs index ec0198674..dc5966e5e 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1103,7 +1103,7 @@ pub enum PivotValueSource { /// Pivot on all values returned by a subquery. /// /// See . - Subquery(Query), + Subquery(Box), } impl fmt::Display for PivotValueSource { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index be97f929b..28e7ac7d1 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -486,9 +486,9 @@ pub trait Dialect: Debug + Any { fn parse_column_option( &self, _parser: &mut Parser, - ) -> Option, ParserError>> { + ) -> Result, ParserError>>, ParserError> { // return None to fall back to the default behavior - None + Ok(None) } /// Decide the lexical Precedence of operators. diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 7c80f0461..d9331d952 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -156,7 +156,7 @@ impl Dialect for SnowflakeDialect { fn parse_column_option( &self, parser: &mut Parser, - ) -> Option, ParserError>> { + ) -> Result, ParserError>>, ParserError> { parser.maybe_parse(|parser| { let with = parser.parse_keyword(Keyword::WITH); @@ -247,7 +247,7 @@ pub fn parse_create_table( builder = builder.comment(parser.parse_optional_inline_comment()?); } Keyword::AS => { - let query = parser.parse_boxed_query()?; + let query = parser.parse_query()?; builder = builder.query(Some(query)); break; } diff --git a/src/parser/alter.rs b/src/parser/alter.rs index 28fdaf764..534105790 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -192,7 +192,7 @@ impl<'a> Parser<'a> { let _ = self.parse_keyword(Keyword::WITH); // option let mut options = vec![]; - while let Some(opt) = self.maybe_parse(|parser| parser.parse_pg_role_option()) { + while let Some(opt) = self.maybe_parse(|parser| parser.parse_pg_role_option())? { options.push(opt); } // check option diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a1079f6f7..a9a5b1df4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -478,7 +478,7 @@ impl<'a> Parser<'a> { Keyword::ANALYZE => self.parse_analyze(), Keyword::SELECT | Keyword::WITH | Keyword::VALUES => { self.prev_token(); - self.parse_boxed_query().map(Statement::Query) + self.parse_query().map(Statement::Query) } Keyword::TRUNCATE => self.parse_truncate(), Keyword::ATTACH => { @@ -551,7 +551,7 @@ impl<'a> Parser<'a> { }, Token::LParen => { self.prev_token(); - self.parse_boxed_query().map(Statement::Query) + self.parse_query().map(Statement::Query) } _ => self.expected("an SQL statement", next_token), } @@ -662,7 +662,7 @@ impl<'a> Parser<'a> { }; parser.expect_keyword(Keyword::PARTITIONS)?; Ok(pa) - }) + })? .unwrap_or_default(); Ok(Statement::Msck { repair, @@ -829,7 +829,7 @@ impl<'a> Parser<'a> { columns = self .maybe_parse(|parser| { parser.parse_comma_separated(|p| p.parse_identifier(false)) - }) + })? .unwrap_or_default(); for_columns = true } @@ -986,7 +986,7 @@ impl<'a> Parser<'a> { value: parser.parse_literal_string()?, }), } - }); + })?; if let Some(expr) = opt_expr { return Ok(expr); @@ -1061,7 +1061,7 @@ impl<'a> Parser<'a> { && !dialect_of!(self is ClickHouseDialect | DatabricksDialect) => { self.expect_token(&Token::LParen)?; - let query = self.parse_boxed_query()?; + let query = self.parse_query()?; self.expect_token(&Token::RParen)?; Ok(Expr::Function(Function { name: ObjectName(vec![w.to_ident()]), @@ -1228,7 +1228,7 @@ impl<'a> Parser<'a> { Token::LParen => { let expr = if let Some(expr) = self.try_parse_expr_sub_query()? { expr - } else if let Some(lambda) = self.try_parse_lambda() { + } else if let Some(lambda) = self.try_parse_lambda()? { return Ok(lambda); } else { let exprs = self.parse_comma_separated(Parser::parse_expr)?; @@ -1307,12 +1307,12 @@ impl<'a> Parser<'a> { return Ok(None); } - Ok(Some(Expr::Subquery(self.parse_boxed_query()?))) + Ok(Some(Expr::Subquery(self.parse_query()?))) } - fn try_parse_lambda(&mut self) -> Option { + fn try_parse_lambda(&mut self) -> Result, ParserError> { if !self.dialect.supports_lambda_functions() { - return None; + return Ok(None); } self.maybe_parse(|p| { let params = p.parse_comma_separated(|p| p.parse_identifier(false))?; @@ -1332,7 +1332,7 @@ impl<'a> Parser<'a> { // Snowflake permits a subquery to be passed as an argument without // an enclosing set of parens if it's the only argument. if dialect_of!(self is SnowflakeDialect) && self.peek_sub_query() { - let subquery = self.parse_boxed_query()?; + let subquery = self.parse_query()?; self.expect_token(&Token::RParen)?; return Ok(Expr::Function(Function { name, @@ -1697,7 +1697,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let exists_node = Expr::Exists { negated, - subquery: self.parse_boxed_query()?, + subquery: self.parse_query()?, }; self.expect_token(&Token::RParen)?; Ok(exists_node) @@ -1777,7 +1777,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), r#in: Box::new(from), }) - }); + })?; match position_expr { Some(expr) => Ok(expr), // Snowflake supports `position` as an ordinary function call @@ -3032,7 +3032,7 @@ impl<'a> Parser<'a> { self.prev_token(); Expr::InSubquery { expr: Box::new(expr), - subquery: self.parse_boxed_query()?, + subquery: self.parse_query()?, negated, } } else { @@ -3513,17 +3513,19 @@ impl<'a> Parser<'a> { } /// Run a parser method `f`, reverting back to the current position if unsuccessful. - #[must_use] - pub fn maybe_parse(&mut self, mut f: F) -> Option + pub fn maybe_parse(&mut self, mut f: F) -> Result, ParserError> where F: FnMut(&mut Parser) -> Result, { let index = self.index; - if let Ok(t) = f(self) { - Some(t) - } else { - self.index = index; - None + match f(self) { + Ok(t) => Ok(Some(t)), + // Unwind stack if limit exceeded + Err(ParserError::RecursionLimitExceeded) => Err(ParserError::RecursionLimitExceeded), + Err(_) => { + self.index = index; + Ok(None) + } } } @@ -3759,7 +3761,7 @@ impl<'a> Parser<'a> { } /// Parse 'AS' before as query,such as `WITH XXX AS SELECT XXX` oer `CACHE TABLE AS SELECT XXX` - pub fn parse_as_query(&mut self) -> Result<(bool, Query), ParserError> { + pub fn parse_as_query(&mut self) -> Result<(bool, Box), ParserError> { match self.peek_token().token { Token::Word(word) => match word.keyword { Keyword::AS => { @@ -4523,7 +4525,7 @@ impl<'a> Parser<'a> { }; self.expect_keyword(Keyword::AS)?; - let query = self.parse_boxed_query()?; + let query = self.parse_query()?; // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. let with_no_schema_binding = dialect_of!(self is RedshiftSqlDialect | GenericDialect) @@ -5102,7 +5104,7 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::FOR)?; - let query = Some(self.parse_boxed_query()?); + let query = Some(self.parse_query()?); Ok(Statement::Declare { stmts: vec![Declare { @@ -5196,7 +5198,7 @@ impl<'a> Parser<'a> { match self.peek_token().token { Token::Word(w) if w.keyword == Keyword::SELECT => ( Some(DeclareType::Cursor), - Some(self.parse_boxed_query()?), + Some(self.parse_query()?), None, None, ), @@ -5889,7 +5891,7 @@ impl<'a> Parser<'a> { // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { - Some(self.parse_boxed_query()?) + Some(self.parse_query()?) } else { None }; @@ -6109,7 +6111,7 @@ impl<'a> Parser<'a> { } pub fn parse_optional_column_option(&mut self) -> Result, ParserError> { - if let Some(option) = self.dialect.parse_column_option(self) { + if let Some(option) = self.dialect.parse_column_option(self)? { return option; } @@ -6483,7 +6485,7 @@ impl<'a> Parser<'a> { } // optional index name - let index_name = self.parse_optional_indent(); + let index_name = self.parse_optional_indent()?; let index_type = self.parse_optional_using_then_index_type()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -6504,7 +6506,7 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::KEY)?; // optional index name - let index_name = self.parse_optional_indent(); + let index_name = self.parse_optional_indent()?; let index_type = self.parse_optional_using_then_index_type()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -6566,7 +6568,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_indent()?, }; let index_type = self.parse_optional_using_then_index_type()?; @@ -6597,7 +6599,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_indent()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -6679,7 +6681,7 @@ impl<'a> Parser<'a> { /// Parse `[ident]`, mostly `ident` is name, like: /// `window_name`, `index_name`, ... - pub fn parse_optional_indent(&mut self) -> Option { + pub fn parse_optional_indent(&mut self) -> Result, ParserError> { self.maybe_parse(|parser| parser.parse_identifier(false)) } @@ -7278,7 +7280,7 @@ impl<'a> Parser<'a> { let with_options = self.parse_options(Keyword::WITH)?; self.expect_keyword(Keyword::AS)?; - let query = self.parse_boxed_query()?; + let query = self.parse_query()?; Ok(Statement::AlterView { name, @@ -7317,7 +7319,7 @@ impl<'a> Parser<'a> { pub fn parse_copy(&mut self) -> Result { let source; if self.consume_token(&Token::LParen) { - source = CopySource::Query(self.parse_boxed_query()?); + source = CopySource::Query(self.parse_query()?); self.expect_token(&Token::RParen)?; } else { let table_name = self.parse_object_name(false)?; @@ -7361,7 +7363,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; } let mut legacy_options = vec![]; - while let Some(opt) = self.maybe_parse(|parser| parser.parse_copy_legacy_option()) { + while let Some(opt) = self.maybe_parse(|parser| parser.parse_copy_legacy_option())? { legacy_options.push(opt); } let values = if let CopyTarget::Stdin = target { @@ -7453,7 +7455,7 @@ impl<'a> Parser<'a> { Some(Keyword::CSV) => CopyLegacyOption::Csv({ let mut opts = vec![]; while let Some(opt) = - self.maybe_parse(|parser| parser.parse_copy_legacy_csv_option()) + self.maybe_parse(|parser| parser.parse_copy_legacy_csv_option())? { opts.push(opt); } @@ -8035,7 +8037,7 @@ impl<'a> Parser<'a> { // Keyword::ARRAY syntax from above while self.consume_token(&Token::LBracket) { let size = if dialect_of!(self is GenericDialect | DuckDbDialect | PostgreSqlDialect) { - self.maybe_parse(|p| p.parse_literal_uint()) + self.maybe_parse(|p| p.parse_literal_uint())? } else { None }; @@ -8712,7 +8714,7 @@ impl<'a> Parser<'a> { } } - match self.maybe_parse(|parser| parser.parse_statement()) { + match self.maybe_parse(|parser| parser.parse_statement())? { Some(Statement::Explain { .. }) | Some(Statement::ExplainTable { .. }) => Err( ParserError::ParserError("Explain must be root of the plan".to_string()), ), @@ -8751,20 +8753,11 @@ impl<'a> Parser<'a> { } } - /// Call's [`Self::parse_query`] returning a `Box`'ed result. - /// - /// This function can be used to reduce the stack size required in debug - /// builds. Instead of `sizeof(Query)` only a pointer (`Box`) - /// is used. - pub fn parse_boxed_query(&mut self) -> Result, ParserError> { - self.parse_query().map(Box::new) - } - /// Parse a query expression, i.e. a `SELECT` statement optionally /// preceded with some `WITH` CTE declarations and optionally followed /// by `ORDER BY`. Unlike some other parse_... methods, this one doesn't /// expect the initial keyword to be already consumed - pub fn parse_query(&mut self) -> Result { + pub fn parse_query(&mut self) -> Result, ParserError> { let _guard = self.recursion_counter.try_decrease()?; let with = if self.parse_keyword(Keyword::WITH) { Some(With { @@ -8787,7 +8780,8 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, - }) + } + .into()) } else if self.parse_keyword(Keyword::UPDATE) { Ok(Query { with, @@ -8801,9 +8795,10 @@ impl<'a> Parser<'a> { for_clause: None, settings: None, format_clause: None, - }) + } + .into()) } else { - let body = self.parse_boxed_query_body(self.dialect.prec_unknown())?; + let body = self.parse_query_body(self.dialect.prec_unknown())?; let order_by = self.parse_optional_order_by()?; @@ -8885,7 +8880,8 @@ impl<'a> Parser<'a> { for_clause, settings, format_clause, - }) + } + .into()) } } @@ -9022,7 +9018,7 @@ impl<'a> Parser<'a> { } } self.expect_token(&Token::LParen)?; - let query = self.parse_boxed_query()?; + let query = self.parse_query()?; self.expect_token(&Token::RParen)?; let alias = TableAlias { name, @@ -9046,7 +9042,7 @@ impl<'a> Parser<'a> { } } self.expect_token(&Token::LParen)?; - let query = self.parse_boxed_query()?; + let query = self.parse_query()?; self.expect_token(&Token::RParen)?; let alias = TableAlias { name, columns }; Cte { @@ -9062,15 +9058,6 @@ impl<'a> Parser<'a> { Ok(cte) } - /// Call's [`Self::parse_query_body`] returning a `Box`'ed result. - /// - /// This function can be used to reduce the stack size required in debug - /// builds. Instead of `sizeof(QueryBody)` only a pointer (`Box`) - /// is used. - fn parse_boxed_query_body(&mut self, precedence: u8) -> Result, ParserError> { - self.parse_query_body(precedence).map(Box::new) - } - /// Parse a "query body", which is an expression with roughly the /// following grammar: /// ```sql @@ -9079,17 +9066,14 @@ impl<'a> Parser<'a> { /// subquery ::= query_body [ order_by_limit ] /// set_operation ::= query_body { 'UNION' | 'EXCEPT' | 'INTERSECT' } [ 'ALL' ] query_body /// ``` - /// - /// If you need `Box` then maybe there is sense to use `parse_boxed_query_body` - /// due to prevent stack overflow in debug building(to reserve less memory on stack). - pub fn parse_query_body(&mut self, precedence: u8) -> Result { + pub fn parse_query_body(&mut self, precedence: u8) -> Result, ParserError> { // We parse the expression using a Pratt parser, as in `parse_expr()`. // Start by parsing a restricted SELECT or a `(subquery)`: let expr = if self.parse_keyword(Keyword::SELECT) { SetExpr::Select(self.parse_select().map(Box::new)?) } else if self.consume_token(&Token::LParen) { // CTEs are not allowed here, but the parser currently accepts them - let subquery = self.parse_boxed_query()?; + let subquery = self.parse_query()?; self.expect_token(&Token::RParen)?; SetExpr::Query(subquery) } else if self.parse_keyword(Keyword::VALUES) { @@ -9114,7 +9098,7 @@ impl<'a> Parser<'a> { &mut self, mut expr: SetExpr, precedence: u8, - ) -> Result { + ) -> Result, ParserError> { loop { // The query can be optionally followed by a set operator: let op = self.parse_set_operator(&self.peek_token().token); @@ -9135,11 +9119,11 @@ impl<'a> Parser<'a> { left: Box::new(expr), op: op.unwrap(), set_quantifier, - right: self.parse_boxed_query_body(next_precedence)?, + right: self.parse_query_body(next_precedence)?, }; } - Ok(expr) + Ok(expr.into()) } pub fn parse_set_operator(&mut self, token: &Token) -> Option { @@ -9466,7 +9450,7 @@ impl<'a> Parser<'a> { 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)) + self.maybe_parse(|parser| parser.parse_set_role(modifier))? { return Ok(set_role_stmt); } @@ -9932,7 +9916,7 @@ impl<'a> Parser<'a> { // subquery, followed by the closing ')', and the alias of the derived table. // In the example above this is case (3). if let Some(mut table) = - self.maybe_parse(|parser| parser.parse_derived_table_factor(NotLateral)) + self.maybe_parse(|parser| parser.parse_derived_table_factor(NotLateral))? { while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { @@ -10462,7 +10446,7 @@ impl<'a> Parser<'a> { &mut self, lateral: IsLateral, ) -> Result { - let subquery = self.parse_boxed_query()?; + let subquery = self.parse_query()?; self.expect_token(&Token::RParen)?; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; Ok(TableFactor::Derived { @@ -10836,7 +10820,7 @@ impl<'a> Parser<'a> { } else { None }; - let source = self.parse_boxed_query()?; + let source = self.parse_query()?; Ok(Statement::Directory { local, path, @@ -10872,7 +10856,7 @@ impl<'a> Parser<'a> { vec![] }; - let source = Some(self.parse_boxed_query()?); + let source = Some(self.parse_query()?); (columns, partitioned, after_columns, source) }; @@ -11786,7 +11770,7 @@ impl<'a> Parser<'a> { pub fn parse_unload(&mut self) -> Result { self.expect_token(&Token::LParen)?; - let query = self.parse_boxed_query()?; + let query = self.parse_query()?; self.expect_token(&Token::RParen)?; self.expect_keyword(Keyword::TO)?; @@ -12130,7 +12114,9 @@ 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(), + Token::Word(word) if word.keyword == Keyword::NoKeyword => { + self.parse_optional_indent()? + } _ => None, }; @@ -12342,10 +12328,8 @@ mod tests { #[test] fn test_ansii_character_string_types() { // Character string types: - let dialect = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], - options: None, - }; + let dialect = + TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})]); test_parse_data_type!(dialect, "CHARACTER", DataType::Character(None)); @@ -12472,10 +12456,8 @@ mod tests { #[test] fn test_ansii_character_large_object_types() { // Character large object types: - let dialect = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], - options: None, - }; + let dialect = + TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})]); test_parse_data_type!( dialect, @@ -12505,10 +12487,9 @@ mod tests { #[test] fn test_parse_custom_types() { - let dialect = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], - options: None, - }; + let dialect = + TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})]); + test_parse_data_type!( dialect, "GEOMETRY", @@ -12537,10 +12518,8 @@ mod tests { #[test] fn test_ansii_exact_numeric_types() { // Exact numeric types: - let dialect = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], - options: None, - }; + let dialect = + TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})]); test_parse_data_type!(dialect, "NUMERIC", DataType::Numeric(ExactNumberInfo::None)); @@ -12588,10 +12567,8 @@ mod tests { #[test] fn test_ansii_date_type() { // Datetime types: - let dialect = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], - options: None, - }; + let dialect = + TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})]); test_parse_data_type!(dialect, "DATE", DataType::Date); @@ -12700,10 +12677,8 @@ mod tests { }}; } - let dialect = TestedDialects { - dialects: vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})], - options: None, - }; + let dialect = + TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})]); test_parse_table_constraint!( dialect, @@ -12822,10 +12797,7 @@ mod tests { #[test] fn test_parse_multipart_identifier_positive() { - let dialect = TestedDialects { - dialects: vec![Box::new(GenericDialect {})], - options: None, - }; + let dialect = TestedDialects::new(vec![Box::new(GenericDialect {})]); // parse multipart with quotes let expected = vec![ diff --git a/src/test_utils.rs b/src/test_utils.rs index e588b3506..b35fc45c2 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -44,6 +44,7 @@ use pretty_assertions::assert_eq; pub struct TestedDialects { pub dialects: Vec>, pub options: Option, + pub recursion_limit: Option, } impl TestedDialects { @@ -52,16 +53,38 @@ impl TestedDialects { Self { dialects, options: None, + recursion_limit: None, } } + pub fn new_with_options(dialects: Vec>, options: ParserOptions) -> Self { + Self { + dialects, + options: Some(options), + recursion_limit: None, + } + } + + pub fn with_recursion_limit(mut self, recursion_limit: usize) -> Self { + self.recursion_limit = Some(recursion_limit); + self + } + fn new_parser<'a>(&self, dialect: &'a dyn Dialect) -> Parser<'a> { let parser = Parser::new(dialect); - if let Some(options) = &self.options { + let parser = if let Some(options) = &self.options { parser.with_options(options.clone()) } else { parser - } + }; + + let parser = if let Some(recursion_limit) = &self.recursion_limit { + parser.with_recursion_limit(*recursion_limit) + } else { + parser + }; + + parser } /// Run the given function for all of `self.dialects`, assert that they diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 63517fe57..2bf470f71 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -40,10 +40,10 @@ fn parse_literal_string() { r#""""triple-double\"escaped""", "#, r#""""triple-double"unescaped""""#, ); - let dialect = TestedDialects { - dialects: vec![Box::new(BigQueryDialect {})], - options: Some(ParserOptions::new().with_unescape(false)), - }; + let dialect = TestedDialects::new_with_options( + vec![Box::new(BigQueryDialect {})], + ParserOptions::new().with_unescape(false), + ); let select = dialect.verified_only_select(sql); assert_eq!(10, select.projection.len()); assert_eq!( @@ -1936,17 +1936,14 @@ fn parse_big_query_declare() { } fn bigquery() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(BigQueryDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(BigQueryDialect {})]) } fn bigquery_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(BigQueryDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(BigQueryDialect {}), + Box::new(GenericDialect {}), + ]) } #[test] diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index e30c33678..f8c349a37 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1613,15 +1613,12 @@ fn parse_explain_table() { } fn clickhouse() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(ClickHouseDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) } fn clickhouse_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(ClickHouseDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(ClickHouseDialect {}), + Box::new(GenericDialect {}), + ]) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5683bcf91..a2eb5070d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -341,19 +341,16 @@ fn parse_update() { #[test] fn parse_update_set_from() { let sql = "UPDATE t1 SET name = t2.name FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 WHERE t1.id = t2.id"; - let dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(DuckDbDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(BigQueryDialect {}), - Box::new(SnowflakeDialect {}), - Box::new(RedshiftSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(SQLiteDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(SQLiteDialect {}), + ]); let stmt = dialects.verified_stmt(sql); assert_eq!( stmt, @@ -1051,10 +1048,7 @@ fn test_eof_after_as() { #[test] fn test_no_infix_error() { - let dialects = TestedDialects { - dialects: vec![Box::new(ClickHouseDialect {})], - options: None, - }; + let dialects = TestedDialects::new(vec![Box::new(ClickHouseDialect {})]); let res = dialects.parse_sql_statements("ASSERT-URA<<"); assert_eq!( @@ -1182,23 +1176,20 @@ fn parse_null_in_select() { #[test] fn parse_exponent_in_select() -> Result<(), ParserError> { // all except Hive, as it allows numbers to start an identifier - let dialects = TestedDialects { - dialects: vec![ - Box::new(AnsiDialect {}), - Box::new(BigQueryDialect {}), - Box::new(ClickHouseDialect {}), - Box::new(DuckDbDialect {}), - Box::new(GenericDialect {}), - // Box::new(HiveDialect {}), - Box::new(MsSqlDialect {}), - Box::new(MySqlDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(RedshiftSqlDialect {}), - Box::new(SnowflakeDialect {}), - Box::new(SQLiteDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(AnsiDialect {}), + Box::new(BigQueryDialect {}), + Box::new(ClickHouseDialect {}), + Box::new(DuckDbDialect {}), + Box::new(GenericDialect {}), + // Box::new(HiveDialect {}), + Box::new(MsSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(SQLiteDialect {}), + ]); let sql = "SELECT 10e-20, 1e3, 1e+3, 1e3a, 1e, 0.5e2"; let mut select = dialects.parse_sql_statements(sql)?; @@ -1271,14 +1262,12 @@ fn parse_escaped_single_quote_string_predicate_with_no_escape() { let sql = "SELECT id, fname, lname FROM customer \ WHERE salary <> 'Jim''s salary'"; - let ast = TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: Some( - ParserOptions::new() - .with_trailing_commas(true) - .with_unescape(false), - ), - } + let ast = TestedDialects::new_with_options( + vec![Box::new(MySqlDialect {})], + ParserOptions::new() + .with_trailing_commas(true) + .with_unescape(false), + ) .verified_only_select(sql); assert_eq!( @@ -1400,10 +1389,10 @@ fn parse_mod() { } fn pg_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(PostgreSqlDialect {}), + Box::new(GenericDialect {}), + ]) } #[test] @@ -1868,14 +1857,13 @@ fn parse_string_agg() { /// selects all dialects but PostgreSQL pub fn all_dialects_but_pg() -> TestedDialects { - TestedDialects { - dialects: all_dialects() + TestedDialects::new( + all_dialects() .dialects .into_iter() .filter(|x| !x.is::()) .collect(), - options: None, - } + ) } #[test] @@ -2691,17 +2679,14 @@ fn parse_listagg() { #[test] fn parse_array_agg_func() { - let supported_dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(DuckDbDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(AnsiDialect {}), - Box::new(HiveDialect {}), - ], - options: None, - }; + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(HiveDialect {}), + ]); for sql in [ "SELECT ARRAY_AGG(x ORDER BY x) AS a FROM T", @@ -2716,16 +2701,13 @@ fn parse_array_agg_func() { #[test] fn parse_agg_with_order_by() { - let supported_dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(AnsiDialect {}), - Box::new(HiveDialect {}), - ], - options: None, - }; + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(HiveDialect {}), + ]); for sql in [ "SELECT FIRST_VALUE(x ORDER BY x) AS a FROM T", @@ -2739,17 +2721,14 @@ fn parse_agg_with_order_by() { #[test] fn parse_window_rank_function() { - let supported_dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(AnsiDialect {}), - Box::new(HiveDialect {}), - Box::new(SnowflakeDialect {}), - ], - options: None, - }; + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(HiveDialect {}), + Box::new(SnowflakeDialect {}), + ]); for sql in [ "SELECT column1, column2, FIRST_VALUE(column2) OVER (PARTITION BY column1 ORDER BY column2 NULLS LAST) AS column2_first FROM t1", @@ -2761,10 +2740,10 @@ fn parse_window_rank_function() { supported_dialects.verified_stmt(sql); } - let supported_dialects_nulls = TestedDialects { - dialects: vec![Box::new(MsSqlDialect {}), Box::new(SnowflakeDialect {})], - options: None, - }; + let supported_dialects_nulls = TestedDialects::new(vec![ + Box::new(MsSqlDialect {}), + Box::new(SnowflakeDialect {}), + ]); for sql in [ "SELECT column1, column2, FIRST_VALUE(column2) IGNORE NULLS OVER (PARTITION BY column1 ORDER BY column2 NULLS LAST) AS column2_first FROM t1", @@ -3321,10 +3300,7 @@ fn parse_create_table_hive_array() { true, ), ] { - let dialects = TestedDialects { - dialects, - options: None, - }; + let dialects = TestedDialects::new(dialects); let sql = format!( "CREATE TABLE IF NOT EXISTS something (name INT, val {})", @@ -3374,14 +3350,11 @@ fn parse_create_table_hive_array() { } // SnowflakeDialect using array different - let dialects = TestedDialects { - dialects: vec![ - Box::new(PostgreSqlDialect {}), - Box::new(HiveDialect {}), - Box::new(MySqlDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(PostgreSqlDialect {}), + Box::new(HiveDialect {}), + Box::new(MySqlDialect {}), + ]); let sql = "CREATE TABLE IF NOT EXISTS something (name int, val array Result<(), Par #[test] fn parse_create_table_with_options() { - let generic = TestedDialects { - dialects: vec![Box::new(GenericDialect {})], - options: None, - }; + let generic = TestedDialects::new(vec![Box::new(GenericDialect {})]); let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)"; match generic.verified_stmt(sql) { @@ -3695,10 +3662,7 @@ fn parse_create_table_clone() { #[test] fn parse_create_table_trailing_comma() { - let dialect = TestedDialects { - dialects: vec![Box::new(DuckDbDialect {})], - options: None, - }; + let dialect = TestedDialects::new(vec![Box::new(DuckDbDialect {})]); let sql = "CREATE TABLE foo (bar int,);"; dialect.one_statement_parses_to(sql, "CREATE TABLE foo (bar INT)"); @@ -4040,15 +4004,12 @@ fn parse_alter_table_add_column() { #[test] fn parse_alter_table_add_column_if_not_exists() { - let dialects = TestedDialects { - dialects: vec![ - Box::new(PostgreSqlDialect {}), - Box::new(BigQueryDialect {}), - Box::new(GenericDialect {}), - Box::new(DuckDbDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(PostgreSqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + ]); match alter_table_op(dialects.verified_stmt("ALTER TABLE tab ADD IF NOT EXISTS foo TEXT")) { AlterTableOperation::AddColumn { if_not_exists, .. } => { @@ -4191,10 +4152,7 @@ fn parse_alter_table_alter_column_type() { _ => unreachable!(), } - let dialect = TestedDialects { - dialects: vec![Box::new(GenericDialect {})], - options: None, - }; + let dialect = TestedDialects::new(vec![Box::new(GenericDialect {})]); let res = dialect.parse_sql_statements(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT")); @@ -4611,15 +4569,12 @@ fn parse_window_functions() { #[test] fn parse_named_window_functions() { - let supported_dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MySqlDialect {}), - Box::new(BigQueryDialect {}), - ], - options: None, - }; + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + ]); let sql = "SELECT row_number() OVER (w ORDER BY dt DESC), \ sum(foo) OVER (win PARTITION BY a, b ORDER BY c, d \ @@ -5684,10 +5639,10 @@ fn parse_unnest_in_from_clause() { let select = dialects.verified_only_select(sql); assert_eq!(select.from, want); } - let dialects = TestedDialects { - dialects: vec![Box::new(BigQueryDialect {}), Box::new(GenericDialect {})], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(BigQueryDialect {}), + Box::new(GenericDialect {}), + ]); // 1. both Alias and WITH OFFSET clauses. chk( "expr", @@ -6670,22 +6625,20 @@ fn parse_trim() { ); //keep Snowflake/BigQuery TRIM syntax failing - let all_expected_snowflake = TestedDialects { - dialects: vec![ - //Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(AnsiDialect {}), - //Box::new(SnowflakeDialect {}), - Box::new(HiveDialect {}), - Box::new(RedshiftSqlDialect {}), - Box::new(MySqlDialect {}), - //Box::new(BigQueryDialect {}), - Box::new(SQLiteDialect {}), - Box::new(DuckDbDialect {}), - ], - options: None, - }; + let all_expected_snowflake = TestedDialects::new(vec![ + //Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + //Box::new(SnowflakeDialect {}), + Box::new(HiveDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(MySqlDialect {}), + //Box::new(BigQueryDialect {}), + Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), + ]); + assert_eq!( ParserError::ParserError("Expected: ), found: 'a'".to_owned()), all_expected_snowflake @@ -8582,20 +8535,17 @@ fn test_lock_nonblock() { #[test] fn test_placeholder() { - let dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(DuckDbDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(AnsiDialect {}), - Box::new(BigQueryDialect {}), - Box::new(SnowflakeDialect {}), - // Note: `$` is the starting word for the HiveDialect identifier - // Box::new(sqlparser::dialect::HiveDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + // Note: `$` is the starting word for the HiveDialect identifier + // Box::new(sqlparser::dialect::HiveDialect {}), + ]); let sql = "SELECT * FROM student WHERE id = $Id1"; let ast = dialects.verified_only_select(sql); assert_eq!( @@ -8621,21 +8571,18 @@ fn test_placeholder() { }), ); - let dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(DuckDbDialect {}), - // Note: `?` is for jsonb operators in PostgreSqlDialect - // Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(AnsiDialect {}), - Box::new(BigQueryDialect {}), - Box::new(SnowflakeDialect {}), - // Note: `$` is the starting word for the HiveDialect identifier - // Box::new(sqlparser::dialect::HiveDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + // Note: `?` is for jsonb operators in PostgreSqlDialect + // Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + // Note: `$` is the starting word for the HiveDialect identifier + // Box::new(sqlparser::dialect::HiveDialect {}), + ]); let sql = "SELECT * FROM student WHERE id = ?"; let ast = dialects.verified_only_select(sql); assert_eq!( @@ -9023,7 +8970,7 @@ fn parse_cache_table() { value: Expr::Value(number("0.88")), }, ], - query: Some(query.clone()), + query: Some(query.clone().into()), } ); @@ -9048,7 +8995,7 @@ fn parse_cache_table() { value: Expr::Value(number("0.88")), }, ], - query: Some(query.clone()), + query: Some(query.clone().into()), } ); @@ -9059,7 +9006,7 @@ fn parse_cache_table() { table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![], - query: Some(query.clone()), + query: Some(query.clone().into()), } ); @@ -9070,7 +9017,7 @@ fn parse_cache_table() { table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), has_as: true, options: vec![], - query: Some(query), + query: Some(query.into()), } ); @@ -9243,14 +9190,11 @@ fn parse_with_recursion_limit() { #[test] fn parse_escaped_string_with_unescape() { fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = TestedDialects { - dialects: vec![ - Box::new(MySqlDialect {}), - Box::new(BigQueryDialect {}), - Box::new(SnowflakeDialect {}), - ], - options: None, - } + let stmt = TestedDialects::new(vec![ + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + ]) .one_statement_parses_to(sql, ""); match stmt { @@ -9283,14 +9227,14 @@ fn parse_escaped_string_with_unescape() { #[test] fn parse_escaped_string_without_unescape() { fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = TestedDialects { - dialects: vec![ + let stmt = TestedDialects::new_with_options( + vec![ Box::new(MySqlDialect {}), Box::new(BigQueryDialect {}), Box::new(SnowflakeDialect {}), ], - options: Some(ParserOptions::new().with_unescape(false)), - } + ParserOptions::new().with_unescape(false), + ) .one_statement_parses_to(sql, ""); match stmt { @@ -9558,17 +9502,14 @@ fn make_where_clause(num: usize) -> String { #[test] fn parse_non_latin_identifiers() { - let supported_dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(DuckDbDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(RedshiftSqlDialect {}), - Box::new(MySqlDialect {}), - ], - options: None, - }; + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + 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.説明"); @@ -9582,10 +9523,7 @@ fn parse_non_latin_identifiers() { fn parse_trailing_comma() { // At the moment, DuckDB is the only dialect that allows // trailing commas anywhere in the query - let trailing_commas = TestedDialects { - dialects: vec![Box::new(DuckDbDialect {})], - options: None, - }; + let trailing_commas = TestedDialects::new(vec![Box::new(DuckDbDialect {})]); trailing_commas.one_statement_parses_to( "SELECT album_id, name, FROM track", @@ -9624,10 +9562,7 @@ fn parse_trailing_comma() { trailing_commas.verified_stmt(r#"SELECT "from" FROM "from""#); // doesn't allow any trailing commas - let trailing_commas = TestedDialects { - dialects: vec![Box::new(GenericDialect {})], - options: None, - }; + let trailing_commas = TestedDialects::new(vec![Box::new(GenericDialect {})]); assert_eq!( trailing_commas @@ -9656,10 +9591,10 @@ fn parse_trailing_comma() { #[test] fn parse_projection_trailing_comma() { // Some dialects allow trailing commas only in the projection - let trailing_commas = TestedDialects { - dialects: vec![Box::new(SnowflakeDialect {}), Box::new(BigQueryDialect {})], - options: None, - }; + let trailing_commas = TestedDialects::new(vec![ + Box::new(SnowflakeDialect {}), + Box::new(BigQueryDialect {}), + ]); trailing_commas.one_statement_parses_to( "SELECT album_id, name, FROM track", @@ -9946,14 +9881,11 @@ fn test_release_savepoint() { #[test] fn test_comment_hash_syntax() { - let dialects = TestedDialects { - dialects: vec![ - Box::new(BigQueryDialect {}), - Box::new(SnowflakeDialect {}), - Box::new(MySqlDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(BigQueryDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(MySqlDialect {}), + ]); let sql = r#" # comment SELECT a, b, c # , d, e @@ -10013,10 +9945,10 @@ fn test_buffer_reuse() { #[test] fn parse_map_access_expr() { let sql = "users[-1][safe_offset(2)]"; - let dialects = TestedDialects { - dialects: vec![Box::new(BigQueryDialect {}), Box::new(ClickHouseDialect {})], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(BigQueryDialect {}), + Box::new(ClickHouseDialect {}), + ]); let expr = dialects.verified_expr(sql); let expected = Expr::MapAccess { column: Expr::Identifier(Ident::new("users")).into(), @@ -10591,16 +10523,13 @@ fn test_match_recognize_patterns() { #[test] fn test_select_wildcard_with_replace() { let sql = r#"SELECT * REPLACE (lower(city) AS city) FROM addresses"#; - let dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(BigQueryDialect {}), - Box::new(ClickHouseDialect {}), - Box::new(SnowflakeDialect {}), - Box::new(DuckDbDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(BigQueryDialect {}), + Box::new(ClickHouseDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(DuckDbDialect {}), + ]); let select = dialects.verified_only_select(sql); let expected = SelectItem::Wildcard(WildcardAdditionalOptions { opt_replace: Some(ReplaceSelectItem { @@ -10657,14 +10586,11 @@ fn test_select_wildcard_with_replace() { #[test] fn parse_sized_list() { - let dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(DuckDbDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(DuckDbDialect {}), + ]); let sql = r#"CREATE TABLE embeddings (data FLOAT[1536])"#; dialects.verified_stmt(sql); let sql = r#"CREATE TABLE embeddings (data FLOAT[1536][3])"#; @@ -10675,14 +10601,11 @@ fn parse_sized_list() { #[test] fn insert_into_with_parentheses() { - let dialects = TestedDialects { - dialects: vec![ - Box::new(SnowflakeDialect {}), - Box::new(RedshiftSqlDialect {}), - Box::new(GenericDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(SnowflakeDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(GenericDialect {}), + ]); dialects.verified_stmt("INSERT INTO t1 (id, name) (SELECT t2.id, t2.name FROM t2)"); } @@ -10850,14 +10773,11 @@ fn parse_within_group() { #[test] fn tests_select_values_without_parens() { - let dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(SnowflakeDialect {}), - Box::new(DatabricksDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(DatabricksDialect {}), + ]); let sql = "SELECT * FROM VALUES (1, 2), (2,3) AS tbl (id, val)"; let canonical = "SELECT * FROM (VALUES (1, 2), (2, 3)) AS tbl (id, val)"; dialects.verified_only_select_with_canonical(sql, canonical); @@ -10865,14 +10785,12 @@ fn tests_select_values_without_parens() { #[test] fn tests_select_values_without_parens_and_set_op() { - let dialects = TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(SnowflakeDialect {}), - Box::new(DatabricksDialect {}), - ], - options: None, - }; + let dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(SnowflakeDialect {}), + Box::new(DatabricksDialect {}), + ]); + let sql = "SELECT id + 1, name FROM VALUES (1, 'Apple'), (2, 'Banana'), (3, 'Orange') AS fruits (id, name) UNION ALL SELECT 5, 'Strawberry'"; let canonical = "SELECT id + 1, name FROM (VALUES (1, 'Apple'), (2, 'Banana'), (3, 'Orange')) AS fruits (id, name) UNION ALL SELECT 5, 'Strawberry'"; let query = dialects.verified_query_with_canonical(sql, canonical); diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 7dcfee68a..7b917bd06 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -24,17 +24,14 @@ use test_utils::*; mod test_utils; fn databricks() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(DatabricksDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(DatabricksDialect {})]) } fn databricks_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(DatabricksDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(DatabricksDialect {}), + Box::new(GenericDialect {}), + ]) } #[test] diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 4703f4b60..a4109b0a3 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -24,17 +24,14 @@ use sqlparser::ast::*; use sqlparser::dialect::{DuckDbDialect, GenericDialect}; fn duckdb() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(DuckDbDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(DuckDbDialect {})]) } fn duckdb_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(DuckDbDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(DuckDbDialect {}), + Box::new(GenericDialect {}), + ]) } #[test] @@ -242,7 +239,7 @@ fn test_create_table_macro() { MacroArg::new("col1_value"), MacroArg::new("col2_value"), ]), - definition: MacroDefinition::Table(duckdb().verified_query(query)), + definition: MacroDefinition::Table(duckdb().verified_query(query).into()), }; assert_eq!(expected, macro_); } diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 069500bf6..10bd374c0 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -418,10 +418,7 @@ fn parse_create_function() { } // Test error in dialect that doesn't support parsing CREATE FUNCTION - let unsupported_dialects = TestedDialects { - dialects: vec![Box::new(MsSqlDialect {})], - options: None, - }; + let unsupported_dialects = TestedDialects::new(vec![Box::new(MsSqlDialect {})]); assert_eq!( unsupported_dialects.parse_sql_statements(sql).unwrap_err(), @@ -538,15 +535,9 @@ fn parse_use() { } fn hive() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(HiveDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(HiveDialect {})]) } fn hive_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(HiveDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(HiveDialect {}), Box::new(GenericDialect {})]) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 58765f6c0..0223e2915 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1030,14 +1030,8 @@ fn parse_create_table_with_identity_column() { } fn ms() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(MsSqlDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } fn ms_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 19dbda21f..db5b9ec8d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -944,11 +944,7 @@ fn parse_quote_identifiers() { fn parse_escaped_quote_identifiers_with_escape() { let sql = "SELECT `quoted `` identifier`"; assert_eq!( - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: None, - } - .verified_stmt(sql), + TestedDialects::new(vec![Box::new(MySqlDialect {})]).verified_stmt(sql), Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { @@ -991,13 +987,13 @@ fn parse_escaped_quote_identifiers_with_escape() { fn parse_escaped_quote_identifiers_with_no_escape() { let sql = "SELECT `quoted `` identifier`"; assert_eq!( - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: Some(ParserOptions { + TestedDialects::new_with_options( + vec![Box::new(MySqlDialect {})], + ParserOptions { trailing_commas: false, unescape: false, - }), - } + } + ) .verified_stmt(sql), Statement::Query(Box::new(Query { with: None, @@ -1041,11 +1037,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { fn parse_escaped_backticks_with_escape() { let sql = "SELECT ```quoted identifier```"; assert_eq!( - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: None, - } - .verified_stmt(sql), + TestedDialects::new(vec![Box::new(MySqlDialect {})]).verified_stmt(sql), Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { @@ -1088,10 +1080,10 @@ fn parse_escaped_backticks_with_escape() { fn parse_escaped_backticks_with_no_escape() { let sql = "SELECT ```quoted identifier```"; assert_eq!( - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: Some(ParserOptions::new().with_unescape(false)), - } + TestedDialects::new_with_options( + vec![Box::new(MySqlDialect {})], + ParserOptions::new().with_unescape(false) + ) .verified_stmt(sql), Statement::Query(Box::new(Query { with: None, @@ -1144,55 +1136,26 @@ fn parse_unterminated_escape() { #[test] fn check_roundtrip_of_escaped_string() { - let options = Some(ParserOptions::new().with_unescape(false)); - - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: options.clone(), - } - .verified_stmt(r"SELECT 'I\'m fine'"); - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: options.clone(), - } - .verified_stmt(r#"SELECT 'I''m fine'"#); - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: options.clone(), - } - .verified_stmt(r"SELECT 'I\\\'m fine'"); - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: options.clone(), - } - .verified_stmt(r"SELECT 'I\\\'m fine'"); - - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: options.clone(), - } - .verified_stmt(r#"SELECT "I\"m fine""#); - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: options.clone(), - } - .verified_stmt(r#"SELECT "I""m fine""#); - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: options.clone(), - } - .verified_stmt(r#"SELECT "I\\\"m fine""#); - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: options.clone(), - } - .verified_stmt(r#"SELECT "I\\\"m fine""#); - - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options, - } - .verified_stmt(r#"SELECT "I'm ''fine''""#); + let options = ParserOptions::new().with_unescape(false); + + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r"SELECT 'I\'m fine'"); + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r#"SELECT 'I''m fine'"#); + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r"SELECT 'I\\\'m fine'"); + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r"SELECT 'I\\\'m fine'"); + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r#"SELECT "I\"m fine""#); + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r#"SELECT "I""m fine""#); + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r#"SELECT "I\\\"m fine""#); + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r#"SELECT "I\\\"m fine""#); + TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone()) + .verified_stmt(r#"SELECT "I'm ''fine''""#); } #[test] @@ -2624,17 +2587,11 @@ fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name } fn mysql() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(MySqlDialect {})]) } fn mysql_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})]) } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index bd37214ce..b9b3811ba 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2973,17 +2973,14 @@ fn parse_on_commit() { } fn pg() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(PostgreSqlDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]) } fn pg_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(PostgreSqlDialect {}), + Box::new(GenericDialect {}), + ]) } #[test] diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index eeba37957..a25d50605 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -171,17 +171,14 @@ fn parse_delimited_identifiers() { } fn redshift() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(RedshiftSqlDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(RedshiftSqlDialect {})]) } fn redshift_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(RedshiftSqlDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(RedshiftSqlDialect {}), + Box::new(GenericDialect {}), + ]) } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index d7e967ffe..c17c7b958 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -854,10 +854,8 @@ fn parse_sf_create_or_replace_view_with_comment_missing_equal() { #[test] fn parse_sf_create_or_replace_with_comment_for_snowflake() { let sql = "CREATE OR REPLACE VIEW v COMMENT = 'hello, world' AS SELECT 1"; - let dialect = test_utils::TestedDialects { - dialects: vec![Box::new(SnowflakeDialect {}) as Box], - options: None, - }; + let dialect = + test_utils::TestedDialects::new(vec![Box::new(SnowflakeDialect {}) as Box]); match dialect.verified_stmt(sql) { Statement::CreateView { @@ -1250,24 +1248,25 @@ fn test_array_agg_func() { } fn snowflake() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(SnowflakeDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(SnowflakeDialect {})]) +} + +fn snowflake_with_recursion_limit(recursion_limit: usize) -> TestedDialects { + TestedDialects::new(vec![Box::new(SnowflakeDialect {})]).with_recursion_limit(recursion_limit) } fn snowflake_without_unescape() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(SnowflakeDialect {})], - options: Some(ParserOptions::new().with_unescape(false)), - } + TestedDialects::new_with_options( + vec![Box::new(SnowflakeDialect {})], + ParserOptions::new().with_unescape(false), + ) } fn snowflake_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(SnowflakeDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(SnowflakeDialect {}), + Box::new(GenericDialect {}), + ]) } #[test] @@ -2759,3 +2758,26 @@ fn parse_view_column_descriptions() { _ => unreachable!(), }; } + +#[test] +fn test_parentheses_overflow() { + let max_nesting_level: usize = 30; + + // Verify the recursion check is not too wasteful... (num of parentheses - 2 is acceptable) + let slack = 2; + let l_parens = "(".repeat(max_nesting_level - slack); + let r_parens = ")".repeat(max_nesting_level - slack); + let sql = format!("SELECT * FROM {l_parens}a.b.c{r_parens}"); + let parsed = + snowflake_with_recursion_limit(max_nesting_level).parse_sql_statements(sql.as_str()); + assert_eq!(parsed.err(), None); + + // Verify the recursion check triggers... (num of parentheses - 1 is acceptable) + let slack = 1; + let l_parens = "(".repeat(max_nesting_level - slack); + let r_parens = ")".repeat(max_nesting_level - slack); + let sql = format!("SELECT * FROM {l_parens}a.b.c{r_parens}"); + let parsed = + snowflake_with_recursion_limit(max_nesting_level).parse_sql_statements(sql.as_str()); + assert_eq!(parsed.err(), Some(ParserError::RecursionLimitExceeded)); +} diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index d3e670e32..6f8bbb2d8 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -529,14 +529,13 @@ fn parse_start_transaction_with_modifier() { sqlite_and_generic().one_statement_parses_to("BEGIN IMMEDIATE", "BEGIN IMMEDIATE TRANSACTION"); sqlite_and_generic().one_statement_parses_to("BEGIN EXCLUSIVE", "BEGIN EXCLUSIVE TRANSACTION"); - let unsupported_dialects = TestedDialects { - dialects: all_dialects() + let unsupported_dialects = TestedDialects::new( + all_dialects() .dialects .into_iter() .filter(|x| !(x.is::() || x.is::())) .collect(), - options: None, - }; + ); let res = unsupported_dialects.parse_sql_statements("BEGIN DEFERRED"); assert_eq!( ParserError::ParserError("Expected: end of statement, found: DEFERRED".to_string()), @@ -571,22 +570,16 @@ fn test_dollar_identifier_as_placeholder() { } fn sqlite() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(SQLiteDialect {})], - options: None, - } + TestedDialects::new(vec![Box::new(SQLiteDialect {})]) } fn sqlite_with_options(options: ParserOptions) -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(SQLiteDialect {})], - options: Some(options), - } + TestedDialects::new_with_options(vec![Box::new(SQLiteDialect {})], options) } fn sqlite_and_generic() -> TestedDialects { - TestedDialects { - dialects: vec![Box::new(SQLiteDialect {}), Box::new(GenericDialect {})], - options: None, - } + TestedDialects::new(vec![ + Box::new(SQLiteDialect {}), + Box::new(GenericDialect {}), + ]) } From ee90373d35e47823c7c0b3afc39beafd1d29f9ca Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:36:47 +0100 Subject: [PATCH 579/806] Fix build (#1483) --- src/dialect/postgresql.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 51dc49849..361cc74be 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -230,15 +230,17 @@ pub fn parse_comment(parser: &mut Parser) -> Result { } pub fn parse_create(parser: &mut Parser) -> Option> { - let name = parser.maybe_parse(|parser| -> Result { + match parser.maybe_parse(|parser| -> Result { parser.expect_keyword(Keyword::CREATE)?; parser.expect_keyword(Keyword::TYPE)?; let name = parser.parse_object_name(false)?; parser.expect_keyword(Keyword::AS)?; parser.expect_keyword(Keyword::ENUM)?; Ok(name) - }); - name.map(|name| parse_create_type_as_enum(parser, name)) + }) { + Ok(name) => name.map(|name| parse_create_type_as_enum(parser, name)), + Err(e) => Some(Err(e)), + } } // https://www.postgresql.org/docs/current/sql-createtype.html From 8de3cb00742242974ead0632ecfdcd136cd6abd7 Mon Sep 17 00:00:00 2001 From: hulk Date: Fri, 1 Nov 2024 23:20:19 +0800 Subject: [PATCH 580/806] Fix complex blocks warning when running clippy (#1488) --- src/dialect/postgresql.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 361cc74be..dc458ec5d 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -230,14 +230,16 @@ pub fn parse_comment(parser: &mut Parser) -> Result { } pub fn parse_create(parser: &mut Parser) -> Option> { - match parser.maybe_parse(|parser| -> Result { + let name = parser.maybe_parse(|parser| -> Result { parser.expect_keyword(Keyword::CREATE)?; parser.expect_keyword(Keyword::TYPE)?; let name = parser.parse_object_name(false)?; parser.expect_keyword(Keyword::AS)?; parser.expect_keyword(Keyword::ENUM)?; Ok(name) - }) { + }); + + match name { Ok(name) => name.map(|name| parse_create_type_as_enum(parser, name)), Err(e) => Some(Err(e)), } From e2197eeca9ef2d51a26f29ac23c15515aa668a0f Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 3 Nov 2024 14:07:06 +0100 Subject: [PATCH 581/806] Add support for SHOW DATABASES/SCHEMAS/TABLES/VIEWS in Hive (#1487) --- src/ast/mod.rs | 81 ++++++++++++++++++++++++++++++++++++++- src/keywords.rs | 3 ++ src/parser/mod.rs | 50 ++++++++++++++++++++++-- tests/sqlparser_common.rs | 21 ++++++++++ tests/sqlparser_mysql.rs | 9 ++++- 5 files changed, 157 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0573240a2..b2672552e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2782,12 +2782,29 @@ pub enum Statement { filter: Option, }, /// ```sql + /// SHOW DATABASES [LIKE 'pattern'] + /// ``` + ShowDatabases { filter: Option }, + /// ```sql + /// SHOW SCHEMAS [LIKE 'pattern'] + /// ``` + ShowSchemas { filter: Option }, + /// ```sql /// SHOW TABLES /// ``` - /// Note: this is a MySQL-specific statement. ShowTables { extended: bool, full: bool, + clause: Option, + db_name: Option, + filter: Option, + }, + /// ```sql + /// SHOW VIEWS + /// ``` + ShowViews { + materialized: bool, + clause: Option, db_name: Option, filter: Option, }, @@ -4363,9 +4380,24 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::ShowDatabases { filter } => { + write!(f, "SHOW DATABASES")?; + if let Some(filter) = filter { + write!(f, " {filter}")?; + } + Ok(()) + } + Statement::ShowSchemas { filter } => { + write!(f, "SHOW SCHEMAS")?; + if let Some(filter) = filter { + write!(f, " {filter}")?; + } + Ok(()) + } Statement::ShowTables { extended, full, + clause: show_clause, db_name, filter, } => { @@ -4375,8 +4407,33 @@ impl fmt::Display for Statement { extended = if *extended { "EXTENDED " } else { "" }, full = if *full { "FULL " } else { "" }, )?; + if let Some(show_clause) = show_clause { + write!(f, " {show_clause}")?; + } + if let Some(db_name) = db_name { + write!(f, " {db_name}")?; + } + if let Some(filter) = filter { + write!(f, " {filter}")?; + } + Ok(()) + } + Statement::ShowViews { + materialized, + clause: show_clause, + db_name, + filter, + } => { + write!( + f, + "SHOW {}VIEWS", + if *materialized { "MATERIALIZED " } else { "" } + )?; + if let Some(show_clause) = show_clause { + write!(f, " {show_clause}")?; + } if let Some(db_name) = db_name { - write!(f, " FROM {db_name}")?; + write!(f, " {db_name}")?; } if let Some(filter) = filter { write!(f, " {filter}")?; @@ -6057,6 +6114,7 @@ pub enum ShowStatementFilter { Like(String), ILike(String), Where(Expr), + NoKeyword(String), } impl fmt::Display for ShowStatementFilter { @@ -6066,6 +6124,25 @@ impl fmt::Display for ShowStatementFilter { Like(pattern) => write!(f, "LIKE '{}'", value::escape_single_quote_string(pattern)), ILike(pattern) => write!(f, "ILIKE {}", value::escape_single_quote_string(pattern)), Where(expr) => write!(f, "WHERE {expr}"), + NoKeyword(pattern) => write!(f, "'{}'", value::escape_single_quote_string(pattern)), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ShowClause { + IN, + FROM, +} + +impl fmt::Display for ShowClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ShowClause::*; + match self { + FROM => write!(f, "FROM"), + IN => write!(f, "IN"), } } } diff --git a/src/keywords.rs b/src/keywords.rs index 6182ae176..e98309681 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -217,6 +217,7 @@ define_keywords!( CYCLE, DATA, DATABASE, + DATABASES, DATA_RETENTION_TIME_IN_DAYS, DATE, DATE32, @@ -662,6 +663,7 @@ define_keywords!( SAFE_CAST, SAVEPOINT, SCHEMA, + SCHEMAS, SCOPE, SCROLL, SEARCH, @@ -822,6 +824,7 @@ define_keywords!( VERSION, VERSIONING, VIEW, + VIEWS, VIRTUAL, VOLATILE, WAREHOUSE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a9a5b1df4..c4b92ba4e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9579,6 +9579,10 @@ impl<'a> Parser<'a> { Ok(self.parse_show_columns(extended, full)?) } else if self.parse_keyword(Keyword::TABLES) { Ok(self.parse_show_tables(extended, full)?) + } else if self.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEWS]) { + Ok(self.parse_show_views(true)?) + } else if self.parse_keyword(Keyword::VIEWS) { + Ok(self.parse_show_views(false)?) } else if self.parse_keyword(Keyword::FUNCTIONS) { Ok(self.parse_show_functions()?) } else if extended || full { @@ -9605,6 +9609,10 @@ impl<'a> Parser<'a> { session, global, }) + } else if self.parse_keyword(Keyword::DATABASES) { + self.parse_show_databases() + } else if self.parse_keyword(Keyword::SCHEMAS) { + self.parse_show_schemas() } else { Ok(Statement::ShowVariable { variable: self.parse_identifiers()?, @@ -9612,6 +9620,18 @@ impl<'a> Parser<'a> { } } + fn parse_show_databases(&mut self) -> Result { + Ok(Statement::ShowDatabases { + filter: self.parse_show_statement_filter()?, + }) + } + + fn parse_show_schemas(&mut self) -> Result { + Ok(Statement::ShowSchemas { + filter: self.parse_show_statement_filter()?, + }) + } + pub fn parse_show_create(&mut self) -> Result { let obj_type = match self.expect_one_of_keywords(&[ Keyword::TABLE, @@ -9667,14 +9687,31 @@ impl<'a> Parser<'a> { extended: bool, full: bool, ) -> Result { - let db_name = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { - Some(_) => Some(self.parse_identifier(false)?), - None => None, + let (clause, db_name) = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { + Some(Keyword::FROM) => (Some(ShowClause::FROM), Some(self.parse_identifier(false)?)), + Some(Keyword::IN) => (Some(ShowClause::IN), Some(self.parse_identifier(false)?)), + _ => (None, None), }; let filter = self.parse_show_statement_filter()?; Ok(Statement::ShowTables { extended, full, + clause, + db_name, + filter, + }) + } + + fn parse_show_views(&mut self, materialized: bool) -> Result { + let (clause, db_name) = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { + Some(Keyword::FROM) => (Some(ShowClause::FROM), Some(self.parse_identifier(false)?)), + Some(Keyword::IN) => (Some(ShowClause::IN), Some(self.parse_identifier(false)?)), + _ => (None, None), + }; + let filter = self.parse_show_statement_filter()?; + Ok(Statement::ShowViews { + materialized, + clause, db_name, filter, }) @@ -9704,7 +9741,12 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::WHERE) { Ok(Some(ShowStatementFilter::Where(self.parse_expr()?))) } else { - Ok(None) + self.maybe_parse(|parser| -> Result { + parser.parse_literal_string() + })? + .map_or(Ok(None), |filter| { + Ok(Some(ShowStatementFilter::NoKeyword(filter))) + }) } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a2eb5070d..4016e5a69 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11378,3 +11378,24 @@ fn test_try_convert() { all_dialects_where(|d| d.supports_try_convert() && !d.convert_type_before_value()); dialects.verified_expr("TRY_CONVERT('foo', VARCHAR(MAX))"); } + +#[test] +fn test_show_dbs_schemas_tables_views() { + verified_stmt("SHOW DATABASES"); + verified_stmt("SHOW DATABASES LIKE '%abc'"); + verified_stmt("SHOW SCHEMAS"); + verified_stmt("SHOW SCHEMAS LIKE '%abc'"); + verified_stmt("SHOW TABLES"); + verified_stmt("SHOW TABLES IN db1"); + verified_stmt("SHOW TABLES IN db1 'abc'"); + verified_stmt("SHOW VIEWS"); + verified_stmt("SHOW VIEWS IN db1"); + verified_stmt("SHOW VIEWS IN db1 'abc'"); + verified_stmt("SHOW VIEWS FROM db1"); + verified_stmt("SHOW VIEWS FROM db1 'abc'"); + verified_stmt("SHOW MATERIALIZED VIEWS"); + verified_stmt("SHOW MATERIALIZED VIEWS IN db1"); + verified_stmt("SHOW MATERIALIZED VIEWS IN db1 'abc'"); + verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); + verified_stmt("SHOW MATERIALIZED VIEWS FROM db1 'abc'"); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index db5b9ec8d..4b9354e85 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -329,6 +329,7 @@ fn parse_show_tables() { Statement::ShowTables { extended: false, full: false, + clause: None, db_name: None, filter: None, } @@ -338,6 +339,7 @@ fn parse_show_tables() { Statement::ShowTables { extended: false, full: false, + clause: Some(ShowClause::FROM), db_name: Some(Ident::new("mydb")), filter: None, } @@ -347,6 +349,7 @@ fn parse_show_tables() { Statement::ShowTables { extended: true, full: false, + clause: None, db_name: None, filter: None, } @@ -356,6 +359,7 @@ fn parse_show_tables() { Statement::ShowTables { extended: false, full: true, + clause: None, db_name: None, filter: None, } @@ -365,6 +369,7 @@ fn parse_show_tables() { Statement::ShowTables { extended: false, full: false, + clause: None, db_name: None, filter: Some(ShowStatementFilter::Like("pattern".into())), } @@ -374,13 +379,15 @@ fn parse_show_tables() { Statement::ShowTables { extended: false, full: false, + clause: None, db_name: None, filter: Some(ShowStatementFilter::Where( mysql_and_generic().verified_expr("1 = 2") )), } ); - mysql_and_generic().one_statement_parses_to("SHOW TABLES IN mydb", "SHOW TABLES FROM mydb"); + mysql_and_generic().verified_stmt("SHOW TABLES IN mydb"); + mysql_and_generic().verified_stmt("SHOW TABLES FROM mydb"); } #[test] From a9a9d58c389a77f4ea3fc4ee2395b34739b64c62 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 6 Nov 2024 10:47:01 -0500 Subject: [PATCH 582/806] Fix typo in `Dialect::supports_eq_alias_assigment` (#1478) --- src/dialect/mod.rs | 2 +- src/dialect/mssql.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 28e7ac7d1..7e43439a4 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -582,7 +582,7 @@ pub trait Dialect: Debug + Any { /// SELECT col_alias = col FROM tbl; /// SELECT col_alias AS col FROM tbl; /// ``` - fn supports_eq_alias_assigment(&self) -> bool { + fn supports_eq_alias_assignment(&self) -> bool { false } diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 78ec621ed..a5ee0bf75 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -50,7 +50,7 @@ impl Dialect for MsSqlDialect { true } - fn supports_eq_alias_assigment(&self) -> bool { + fn supports_eq_alias_assignment(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c4b92ba4e..1ad637f90 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11235,7 +11235,7 @@ impl<'a> Parser<'a> { left, op: BinaryOperator::Eq, right, - } if self.dialect.supports_eq_alias_assigment() + } if self.dialect.supports_eq_alias_assignment() && matches!(left.as_ref(), Expr::Identifier(_)) => { let Expr::Identifier(alias) = *left else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4016e5a69..94dfcfec1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11353,7 +11353,7 @@ fn test_any_some_all_comparison() { #[test] fn test_alias_equal_expr() { - let dialects = all_dialects_where(|d| d.supports_eq_alias_assigment()); + let dialects = all_dialects_where(|d| d.supports_eq_alias_assignment()); let sql = r#"SELECT some_alias = some_column FROM some_table"#; let expected = r#"SELECT some_column AS some_alias FROM some_table"#; let _ = dialects.one_statement_parses_to(sql, expected); @@ -11362,7 +11362,7 @@ fn test_alias_equal_expr() { let expected = r#"SELECT (a * b) AS some_alias FROM some_table"#; let _ = dialects.one_statement_parses_to(sql, expected); - let dialects = all_dialects_where(|d| !d.supports_eq_alias_assigment()); + let dialects = all_dialects_where(|d| !d.supports_eq_alias_assignment()); let sql = r#"SELECT x = (a * b) FROM some_table"#; let expected = r#"SELECT x = (a * b) FROM some_table"#; let _ = dialects.one_statement_parses_to(sql, expected); From 05821cc7db7b499528ac47a93d8d86a43c889933 Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Wed, 6 Nov 2024 23:51:08 +0800 Subject: [PATCH 583/806] Add support for PostgreSQL `LISTEN/NOTIFY` syntax (#1485) --- src/ast/mod.rs | 28 +++++++++++++++ src/dialect/mod.rs | 10 ++++++ src/dialect/postgresql.rs | 10 ++++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 19 ++++++++++ tests/sqlparser_common.rs | 76 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b2672552e..2ef3a460b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3295,6 +3295,23 @@ pub enum Statement { include_final: bool, deduplicate: Option, }, + /// ```sql + /// LISTEN + /// ``` + /// listen for a notification channel + /// + /// See Postgres + LISTEN { channel: Ident }, + /// ```sql + /// NOTIFY channel [ , payload ] + /// ``` + /// send a notification event together with an optional “payload” string to channel + /// + /// See Postgres + NOTIFY { + channel: Ident, + payload: Option, + }, } impl fmt::Display for Statement { @@ -4839,6 +4856,17 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::LISTEN { channel } => { + write!(f, "LISTEN {channel}")?; + Ok(()) + } + Statement::NOTIFY { channel, payload } => { + write!(f, "NOTIFY {channel}")?; + if let Some(payload) = payload { + write!(f, ", '{payload}'")?; + } + Ok(()) + } } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 7e43439a4..5abddba38 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -590,6 +590,16 @@ pub trait Dialect: Debug + Any { fn supports_try_convert(&self) -> bool { false } + + /// Returns true if the dialect supports the `LISTEN` statement + fn supports_listen(&self) -> bool { + false + } + + /// Returns true if the dialect supports the `NOTIFY` statement + fn supports_notify(&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 dc458ec5d..c40c826c4 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -191,6 +191,16 @@ impl Dialect for PostgreSqlDialect { fn supports_explain_with_utility_options(&self) -> bool { true } + + /// see + fn supports_listen(&self) -> bool { + true + } + + /// see + fn supports_notify(&self) -> bool { + true + } } pub fn parse_comment(parser: &mut Parser) -> Result { diff --git a/src/keywords.rs b/src/keywords.rs index e98309681..d60227c99 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -438,6 +438,7 @@ define_keywords!( LIKE_REGEX, LIMIT, LINES, + LISTEN, LN, LOAD, LOCAL, @@ -513,6 +514,7 @@ define_keywords!( NOSUPERUSER, NOT, NOTHING, + NOTIFY, NOWAIT, NO_WRITE_TO_BINLOG, NTH_VALUE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1ad637f90..fd7d1c578 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -532,6 +532,10 @@ impl<'a> Parser<'a> { Keyword::EXECUTE => self.parse_execute(), Keyword::PREPARE => self.parse_prepare(), Keyword::MERGE => self.parse_merge(), + // `LISTEN` and `NOTIFY` are Postgres-specific + // syntaxes. They are used for Postgres statement. + Keyword::LISTEN if self.dialect.supports_listen() => self.parse_listen(), + Keyword::NOTIFY if self.dialect.supports_notify() => self.parse_notify(), // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html Keyword::PRAGMA => self.parse_pragma(), Keyword::UNLOAD => self.parse_unload(), @@ -946,6 +950,21 @@ impl<'a> Parser<'a> { Ok(Statement::ReleaseSavepoint { name }) } + pub fn parse_listen(&mut self) -> Result { + let channel = self.parse_identifier(false)?; + Ok(Statement::LISTEN { channel }) + } + + pub fn parse_notify(&mut self) -> Result { + let channel = self.parse_identifier(false)?; + let payload = if self.consume_token(&Token::Comma) { + Some(self.parse_literal_string()?) + } else { + None + }; + Ok(Statement::NOTIFY { channel, payload }) + } + /// Parse an expression prefix. pub fn parse_prefix(&mut self) -> Result { // allow the dialect to override prefix parsing diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 94dfcfec1..334dae2b3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11399,3 +11399,79 @@ fn test_show_dbs_schemas_tables_views() { verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); verified_stmt("SHOW MATERIALIZED VIEWS FROM db1 'abc'"); } + +#[test] +fn parse_listen_channel() { + let dialects = all_dialects_where(|d| d.supports_listen()); + + match dialects.verified_stmt("LISTEN test1") { + Statement::LISTEN { channel } => { + assert_eq!(Ident::new("test1"), channel); + } + _ => unreachable!(), + }; + + assert_eq!( + dialects.parse_sql_statements("LISTEN *").unwrap_err(), + ParserError::ParserError("Expected: identifier, found: *".to_string()) + ); + + let dialects = all_dialects_where(|d| !d.supports_listen()); + + assert_eq!( + dialects.parse_sql_statements("LISTEN test1").unwrap_err(), + ParserError::ParserError("Expected: an SQL statement, found: LISTEN".to_string()) + ); +} + +#[test] +fn parse_notify_channel() { + let dialects = all_dialects_where(|d| d.supports_notify()); + + match dialects.verified_stmt("NOTIFY test1") { + Statement::NOTIFY { channel, payload } => { + assert_eq!(Ident::new("test1"), channel); + assert_eq!(payload, None); + } + _ => unreachable!(), + }; + + match dialects.verified_stmt("NOTIFY test1, 'this is a test notification'") { + Statement::NOTIFY { + channel, + payload: Some(payload), + } => { + assert_eq!(Ident::new("test1"), channel); + assert_eq!("this is a test notification", payload); + } + _ => unreachable!(), + }; + + assert_eq!( + dialects.parse_sql_statements("NOTIFY *").unwrap_err(), + ParserError::ParserError("Expected: identifier, found: *".to_string()) + ); + assert_eq!( + dialects + .parse_sql_statements("NOTIFY test1, *") + .unwrap_err(), + ParserError::ParserError("Expected: literal string, found: *".to_string()) + ); + + let sql_statements = [ + "NOTIFY test1", + "NOTIFY test1, 'this is a test notification'", + ]; + let dialects = all_dialects_where(|d| !d.supports_notify()); + + for &sql in &sql_statements { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected: an SQL statement, found: NOTIFY".to_string()) + ); + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected: an SQL statement, found: NOTIFY".to_string()) + ); + } +} From a5b0092506afd871c42a7ff2421d42091600f0d3 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:09:55 +0100 Subject: [PATCH 584/806] Add support for TOP before ALL/DISTINCT (#1495) --- src/ast/query.rs | 12 +++++++++++- src/dialect/mod.rs | 6 ++++++ src/dialect/redshift.rs | 6 ++++++ src/parser/mod.rs | 16 ++++++++++------ tests/sqlparser_clickhouse.rs | 1 + tests/sqlparser_common.rs | 18 ++++++++++++++++++ tests/sqlparser_duckdb.rs | 2 ++ tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_mysql.rs | 8 ++++++++ tests/sqlparser_postgres.rs | 3 +++ 10 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index dc5966e5e..6767662d5 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -279,6 +279,8 @@ pub struct Select { pub distinct: Option, /// MSSQL syntax: `TOP () [ PERCENT ] [ WITH TIES ]` pub top: Option, + /// Whether the top was located before `ALL`/`DISTINCT` + pub top_before_distinct: bool, /// projection expressions pub projection: Vec, /// INTO @@ -327,12 +329,20 @@ impl fmt::Display for Select { write!(f, " {value_table_mode}")?; } + if let Some(ref top) = self.top { + if self.top_before_distinct { + write!(f, " {top}")?; + } + } if let Some(ref distinct) = self.distinct { write!(f, " {distinct}")?; } if let Some(ref top) = self.top { - write!(f, " {top}")?; + if !self.top_before_distinct { + write!(f, " {top}")?; + } } + write!(f, " {}", display_comma_separated(&self.projection))?; if let Some(ref into) = self.into { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 5abddba38..453fee3de 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -600,6 +600,12 @@ pub trait Dialect: Debug + Any { fn supports_notify(&self) -> bool { false } + + /// Returns true if this dialect expects the the `TOP` option + /// before the `ALL`/`DISTINCT` options in a `SELECT` statement. + fn supports_top_before_distinct(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 3bfdec3b0..4d0773843 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -68,4 +68,10 @@ impl Dialect for RedshiftSqlDialect { fn supports_connect_by(&self) -> bool { true } + + /// Redshift expects the `TOP` option before the `ALL/DISTINCT` option: + /// + fn supports_top_before_distinct(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fd7d1c578..de11ba7c9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9193,13 +9193,16 @@ impl<'a> Parser<'a> { None }; + let mut top_before_distinct = false; + let mut top = None; + if self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) { + top = Some(self.parse_top()?); + top_before_distinct = true; + } let distinct = self.parse_all_or_distinct()?; - - let top = if self.parse_keyword(Keyword::TOP) { - Some(self.parse_top()?) - } else { - None - }; + if !self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) { + top = Some(self.parse_top()?); + } let projection = self.parse_projection()?; @@ -9342,6 +9345,7 @@ impl<'a> Parser<'a> { Ok(Select { distinct, top, + top_before_distinct, projection, into, from, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index f8c349a37..a71871115 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -40,6 +40,7 @@ fn parse_map_access_expr() { Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![UnnamedExpr(MapAccess { column: Box::new(Identifier(Ident { value: "string_values".to_string(), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 334dae2b3..49753a1f4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -379,6 +379,7 @@ fn parse_update_set_from() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![ SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("name"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))), @@ -4649,6 +4650,7 @@ fn test_parse_named_window() { let expected = Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![ SelectItem::ExprWithAlias { expr: Expr::Function(Function { @@ -5289,6 +5291,7 @@ fn parse_interval_and_or_xor() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![UnnamedExpr(Expr::Identifier(Ident { value: "col".to_string(), quote_style: None, @@ -7367,6 +7370,7 @@ fn lateral_function() { let expected = Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { opt_ilike: None, opt_exclude: None, @@ -8215,6 +8219,7 @@ fn parse_merge() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::Wildcard( WildcardAdditionalOptions::default() )], @@ -9803,6 +9808,7 @@ fn parse_unload() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),], into: None, from: vec![TableWithJoins { @@ -9978,6 +9984,7 @@ fn parse_connect_by() { let expect_query = Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![ SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("employee_id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), @@ -10064,6 +10071,7 @@ fn parse_connect_by() { Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![ SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("employee_id"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), @@ -11475,3 +11483,13 @@ fn parse_notify_channel() { ); } } + +#[test] +fn test_select_top() { + let dialects = all_dialects_where(|d| d.supports_top_before_distinct()); + dialects.one_statement_parses_to("SELECT ALL * FROM tbl", "SELECT * FROM tbl"); + dialects.verified_stmt("SELECT TOP 3 * FROM tbl"); + dialects.one_statement_parses_to("SELECT TOP 3 ALL * FROM tbl", "SELECT TOP 3 * FROM tbl"); + dialects.verified_stmt("SELECT TOP 3 DISTINCT * FROM tbl"); + dialects.verified_stmt("SELECT TOP 3 DISTINCT a, b, c FROM tbl"); +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a4109b0a3..d68f37713 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -261,6 +261,7 @@ fn test_select_union_by_name() { left: Box::::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { opt_ilike: None, opt_exclude: None, @@ -301,6 +302,7 @@ fn test_select_union_by_name() { right: Box::::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { opt_ilike: None, opt_exclude: None, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 0223e2915..c5f43b072 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -114,6 +114,7 @@ fn parse_create_procedure() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))], into: None, from: vec![], @@ -514,6 +515,7 @@ fn parse_substring_in_select() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: Some(Distinct::Distinct), top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Substring { expr: Box::new(Expr::Identifier(Ident { value: "description".to_string(), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 4b9354e85..6cd08df18 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -957,6 +957,7 @@ fn parse_escaped_quote_identifiers_with_escape() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { value: "quoted ` identifier".into(), quote_style: Some('`'), @@ -1007,6 +1008,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { value: "quoted `` identifier".into(), quote_style: Some('`'), @@ -1050,6 +1052,7 @@ fn parse_escaped_backticks_with_escape() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { value: "`quoted identifier`".into(), quote_style: Some('`'), @@ -1097,6 +1100,7 @@ fn parse_escaped_backticks_with_no_escape() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { value: "``quoted identifier``".into(), quote_style: Some('`'), @@ -1741,6 +1745,7 @@ fn parse_select_with_numeric_prefix_column_name() { Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new( "123col_$@123abc" )))], @@ -1795,6 +1800,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![ SelectItem::UnnamedExpr(Expr::Value(number("123e4"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("123col_$@123abc"))) @@ -2295,6 +2301,7 @@ fn parse_substring_in_select() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: Some(Distinct::Distinct), top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Substring { expr: Box::new(Expr::Identifier(Ident { value: "description".to_string(), @@ -2616,6 +2623,7 @@ fn parse_hex_string_introducer() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::IntroducedString { introducer: "_latin1".to_string(), value: Value::HexStringLiteral("4D7953514C".to_string()) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b9b3811ba..c30603baa 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1165,6 +1165,7 @@ fn parse_copy_to() { body: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![ SelectItem::ExprWithAlias { expr: Expr::Value(number("42")), @@ -2505,6 +2506,7 @@ fn parse_array_subquery_expr() { left: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))], into: None, from: vec![], @@ -2525,6 +2527,7 @@ fn parse_array_subquery_expr() { right: Box::new(SetExpr::Select(Box::new(Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("2")))], into: None, from: vec![], From fc0e13b80ea76274891e05290be34b0478075245 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Wed, 6 Nov 2024 22:04:13 +0100 Subject: [PATCH 585/806] add support for `FOR ORDINALITY` and `NESTED` in JSON_TABLE (#1493) --- src/ast/mod.rs | 15 +++++---- src/ast/query.rs | 71 ++++++++++++++++++++++++++++++++++++++-- src/parser/mod.rs | 20 +++++++++-- tests/sqlparser_mysql.rs | 10 ++++-- 4 files changed, 102 insertions(+), 14 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2ef3a460b..a24739a60 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -54,13 +54,14 @@ pub use self::query::{ ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, - JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern, - MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, - OffsetRows, OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, - RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, - ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, + JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, LateralView, + LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, + NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderBy, OrderByExpr, + PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, + ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, + SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, + TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, Values, + WildcardAdditionalOptions, With, WithFill, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index 6767662d5..7af472430 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2286,19 +2286,84 @@ impl fmt::Display for ForJson { } /// A single column definition in MySQL's `JSON_TABLE` table valued function. +/// +/// See +/// - [MySQL's JSON_TABLE documentation](https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html#function_json-table) +/// - [Oracle's JSON_TABLE documentation](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/JSON_TABLE.html) +/// - [MariaDB's JSON_TABLE documentation](https://mariadb.com/kb/en/json_table/) +/// /// ```sql /// SELECT * /// FROM JSON_TABLE( /// '["a", "b"]', /// '$[*]' COLUMNS ( -/// value VARCHAR(20) PATH '$' +/// name FOR ORDINALITY, +/// value VARCHAR(20) PATH '$', +/// NESTED PATH '$[*]' COLUMNS ( +/// value VARCHAR(20) PATH '$' +/// ) /// ) /// ) AS jt; /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct JsonTableColumn { +pub enum JsonTableColumn { + /// A named column with a JSON path + Named(JsonTableNamedColumn), + /// The FOR ORDINALITY column, which is a special column that returns the index of the current row in a JSON array. + ForOrdinality(Ident), + /// A set of nested columns, which extracts data from a nested JSON array. + Nested(JsonTableNestedColumn), +} + +impl fmt::Display for JsonTableColumn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsonTableColumn::Named(json_table_named_column) => { + write!(f, "{json_table_named_column}") + } + JsonTableColumn::ForOrdinality(ident) => write!(f, "{} FOR ORDINALITY", ident), + JsonTableColumn::Nested(json_table_nested_column) => { + write!(f, "{json_table_nested_column}") + } + } + } +} + +/// A nested column in a JSON_TABLE column list +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct JsonTableNestedColumn { + pub path: Value, + pub columns: Vec, +} + +impl fmt::Display for JsonTableNestedColumn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "NESTED PATH {} COLUMNS ({})", + self.path, + display_comma_separated(&self.columns) + ) + } +} + +/// A single column definition in MySQL's `JSON_TABLE` table valued function. +/// +/// See +/// +/// ```sql +/// value VARCHAR(20) PATH '$' +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct JsonTableNamedColumn { /// The name of the column to be extracted. pub name: Ident, /// The type of the column to be extracted. @@ -2313,7 +2378,7 @@ pub struct JsonTableColumn { pub on_error: Option, } -impl fmt::Display for JsonTableColumn { +impl fmt::Display for JsonTableNamedColumn { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index de11ba7c9..2bd454369 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10466,7 +10466,23 @@ impl<'a> Parser<'a> { /// Parses MySQL's JSON_TABLE column definition. /// For example: `id INT EXISTS PATH '$' DEFAULT '0' ON EMPTY ERROR ON ERROR` pub fn parse_json_table_column_def(&mut self) -> Result { + if self.parse_keyword(Keyword::NESTED) { + let _has_path_keyword = self.parse_keyword(Keyword::PATH); + let path = self.parse_value()?; + self.expect_keyword(Keyword::COLUMNS)?; + let columns = self.parse_parenthesized(|p| { + p.parse_comma_separated(Self::parse_json_table_column_def) + })?; + return Ok(JsonTableColumn::Nested(JsonTableNestedColumn { + path, + columns, + })); + } let name = self.parse_identifier(false)?; + if self.parse_keyword(Keyword::FOR) { + self.expect_keyword(Keyword::ORDINALITY)?; + return Ok(JsonTableColumn::ForOrdinality(name)); + } let r#type = self.parse_data_type()?; let exists = self.parse_keyword(Keyword::EXISTS); self.expect_keyword(Keyword::PATH)?; @@ -10481,14 +10497,14 @@ impl<'a> Parser<'a> { on_error = Some(error_handling); } } - Ok(JsonTableColumn { + Ok(JsonTableColumn::Named(JsonTableNamedColumn { name, r#type, path, exists, on_empty, on_error, - }) + })) } fn parse_json_table_column_error_handling( diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 6cd08df18..47f7f5b4b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2773,6 +2773,12 @@ fn parse_json_table() { r#"SELECT * FROM JSON_TABLE('[1,2]', '$[*]' COLUMNS(x INT PATH '$' ERROR ON EMPTY)) AS t"#, ); mysql().verified_only_select(r#"SELECT * FROM JSON_TABLE('[1,2]', '$[*]' COLUMNS(x INT PATH '$' ERROR ON EMPTY DEFAULT '0' ON ERROR)) AS t"#); + mysql().verified_only_select( + r#"SELECT jt.* FROM JSON_TABLE('["Alice", "Bob", "Charlie"]', '$[*]' COLUMNS(row_num FOR ORDINALITY, name VARCHAR(50) PATH '$')) AS jt"#, + ); + mysql().verified_only_select( + r#"SELECT * FROM JSON_TABLE('[ {"a": 1, "b": [11,111]}, {"a": 2, "b": [22,222]}, {"a":3}]', '$[*]' COLUMNS(a INT PATH '$.a', NESTED PATH '$.b[*]' COLUMNS (b INT PATH '$'))) AS jt"#, + ); assert_eq!( mysql() .verified_only_select( @@ -2784,14 +2790,14 @@ fn parse_json_table() { json_expr: Expr::Value(Value::SingleQuotedString("[1,2]".to_string())), json_path: Value::SingleQuotedString("$[*]".to_string()), columns: vec![ - JsonTableColumn { + JsonTableColumn::Named(JsonTableNamedColumn { name: Ident::new("x"), r#type: DataType::Int(None), path: Value::SingleQuotedString("$".to_string()), exists: false, on_empty: Some(JsonTableColumnErrorHandling::Default(Value::SingleQuotedString("0".to_string()))), on_error: Some(JsonTableColumnErrorHandling::Null), - }, + }), ], alias: Some(TableAlias { name: Ident::new("t"), From 9394ad4c0cfa7f9a264ba2109764f3309c39c41d Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 6 Nov 2024 16:48:49 -0500 Subject: [PATCH 586/806] Add Apache License to additional files (#1502) --- .github/dependabot.yml | 17 +++++++++++++++++ .github/workflows/rust.yml | 17 +++++++++++++++++ README.md | 19 +++++++++++++++++++ tests/queries/tpch/1.sql | 18 ++++++++++++++++++ tests/queries/tpch/10.sql | 17 +++++++++++++++++ tests/queries/tpch/11.sql | 17 +++++++++++++++++ tests/queries/tpch/12.sql | 17 +++++++++++++++++ tests/queries/tpch/13.sql | 17 +++++++++++++++++ tests/queries/tpch/14.sql | 17 +++++++++++++++++ tests/queries/tpch/15.sql | 17 +++++++++++++++++ tests/queries/tpch/16.sql | 17 +++++++++++++++++ tests/queries/tpch/17.sql | 17 +++++++++++++++++ tests/queries/tpch/18.sql | 17 +++++++++++++++++ tests/queries/tpch/19.sql | 17 +++++++++++++++++ tests/queries/tpch/2.sql | 18 +++++++++++++++++- tests/queries/tpch/20.sql | 17 +++++++++++++++++ tests/queries/tpch/21.sql | 17 +++++++++++++++++ tests/queries/tpch/22.sql | 17 +++++++++++++++++ tests/queries/tpch/3.sql | 17 +++++++++++++++++ tests/queries/tpch/4.sql | 17 +++++++++++++++++ tests/queries/tpch/5.sql | 17 +++++++++++++++++ tests/queries/tpch/6.sql | 19 ++++++++++++++++++- tests/queries/tpch/7.sql | 17 +++++++++++++++++ tests/queries/tpch/8.sql | 17 +++++++++++++++++ tests/queries/tpch/9.sql | 17 +++++++++++++++++ 25 files changed, 429 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a73d1dc4e..7d2903d29 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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. + version: 2 updates: - package-ecosystem: cargo diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4aab9cee7..253d8ab27 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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. + name: Rust on: [push, pull_request] diff --git a/README.md b/README.md index 3226b9549..934d9d06d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ + + # Extensible SQL Lexer and Parser for Rust [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) diff --git a/tests/queries/tpch/1.sql b/tests/queries/tpch/1.sql index ae44c94d2..89232181e 100644 --- a/tests/queries/tpch/1.sql +++ b/tests/queries/tpch/1.sql @@ -1,3 +1,21 @@ +-- 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. + + select l_returnflag, l_linestatus, diff --git a/tests/queries/tpch/10.sql b/tests/queries/tpch/10.sql index a8de12995..06f6256b4 100644 --- a/tests/queries/tpch/10.sql +++ b/tests/queries/tpch/10.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/11.sql b/tests/queries/tpch/11.sql index f9cf254b5..6115eb59d 100644 --- a/tests/queries/tpch/11.sql +++ b/tests/queries/tpch/11.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/12.sql b/tests/queries/tpch/12.sql index ca9c494e7..0ffdb6668 100644 --- a/tests/queries/tpch/12.sql +++ b/tests/queries/tpch/12.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/13.sql b/tests/queries/tpch/13.sql index 32b0ebeb9..64572aba8 100644 --- a/tests/queries/tpch/13.sql +++ b/tests/queries/tpch/13.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/14.sql b/tests/queries/tpch/14.sql index 74f9643ef..abe7f2ea5 100644 --- a/tests/queries/tpch/14.sql +++ b/tests/queries/tpch/14.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/15.sql b/tests/queries/tpch/15.sql index 8b3b8c1ef..16646fb60 100644 --- a/tests/queries/tpch/15.sql +++ b/tests/queries/tpch/15.sql @@ -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. + -- using default substitutions create view revenue0 (supplier_no, total_revenue) as diff --git a/tests/queries/tpch/16.sql b/tests/queries/tpch/16.sql index a0412fcbb..e7e489796 100644 --- a/tests/queries/tpch/16.sql +++ b/tests/queries/tpch/16.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/17.sql b/tests/queries/tpch/17.sql index d59bc18ab..d895fb27f 100644 --- a/tests/queries/tpch/17.sql +++ b/tests/queries/tpch/17.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/18.sql b/tests/queries/tpch/18.sql index e07956fed..54b8b85d3 100644 --- a/tests/queries/tpch/18.sql +++ b/tests/queries/tpch/18.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/19.sql b/tests/queries/tpch/19.sql index 908e08297..1aca74329 100644 --- a/tests/queries/tpch/19.sql +++ b/tests/queries/tpch/19.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/2.sql b/tests/queries/tpch/2.sql index f04c1d497..9426b91d6 100644 --- a/tests/queries/tpch/2.sql +++ b/tests/queries/tpch/2.sql @@ -1,5 +1,21 @@ --- using default substitutions +-- 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. +-- using default substitutions select s_acctbal, diff --git a/tests/queries/tpch/20.sql b/tests/queries/tpch/20.sql index 7aaabc2d5..6f06dcb7a 100644 --- a/tests/queries/tpch/20.sql +++ b/tests/queries/tpch/20.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/21.sql b/tests/queries/tpch/21.sql index 5a287f9a3..58879495f 100644 --- a/tests/queries/tpch/21.sql +++ b/tests/queries/tpch/21.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/22.sql b/tests/queries/tpch/22.sql index 1fc8523ad..2a7f45e95 100644 --- a/tests/queries/tpch/22.sql +++ b/tests/queries/tpch/22.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/3.sql b/tests/queries/tpch/3.sql index 710aac520..df50cf760 100644 --- a/tests/queries/tpch/3.sql +++ b/tests/queries/tpch/3.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/4.sql b/tests/queries/tpch/4.sql index e5adb9349..e4a84f79c 100644 --- a/tests/queries/tpch/4.sql +++ b/tests/queries/tpch/4.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/5.sql b/tests/queries/tpch/5.sql index ea376576f..9b92b30ed 100644 --- a/tests/queries/tpch/5.sql +++ b/tests/queries/tpch/5.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/6.sql b/tests/queries/tpch/6.sql index 949b7b16a..709fd201f 100644 --- a/tests/queries/tpch/6.sql +++ b/tests/queries/tpch/6.sql @@ -1,5 +1,22 @@ --- using default substitutions +-- 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. + +-- using default substitutions select sum(l_extendedprice * l_discount) as revenue diff --git a/tests/queries/tpch/7.sql b/tests/queries/tpch/7.sql index 85dd8f9c8..1379dbb47 100644 --- a/tests/queries/tpch/7.sql +++ b/tests/queries/tpch/7.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/8.sql b/tests/queries/tpch/8.sql index e6e4d30b8..22ae8b906 100644 --- a/tests/queries/tpch/8.sql +++ b/tests/queries/tpch/8.sql @@ -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. + -- using default substitutions diff --git a/tests/queries/tpch/9.sql b/tests/queries/tpch/9.sql index f9eaf65ee..fa957b052 100644 --- a/tests/queries/tpch/9.sql +++ b/tests/queries/tpch/9.sql @@ -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. + -- using default substitutions From 543ec6c584b9dbb5855f7364cd5d80b0b4ed563e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 6 Nov 2024 16:50:27 -0500 Subject: [PATCH 587/806] Move CHANGELOG content (#1503) --- CHANGELOG.md | 1180 +------------------------------------- changelog/0.51.0-pre.md | 1188 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 1192 insertions(+), 1176 deletions(-) create mode 100644 changelog/0.51.0-pre.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 07142602d..e047515ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,1181 +20,9 @@ # Changelog All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project aims to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Given that the parser produces a typed AST, any changes to the AST will -technically be breaking and thus will result in a `0.(N+1)` version. We document -changes that break via addition as "Added". - -## [Unreleased] -Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. - - -## [0.51.0] 2024-09-11 -As always, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs 🙏. -Without them this project would not be possible. - -Reminder: we are in the final phases of moving sqlparser-rs into the Apache -DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 - -### Fixed -* Fix Hive table comment should be after table column definitions (#1413) - Thanks @git-hulk -* Fix stack overflow in `parse_subexpr` (#1410) - Thanks @eejbyfeldt -* Fix `INTERVAL` parsing to support expressions and units via dialect (#1398) - Thanks @samuelcolvin -* Fix identifiers starting with `$` should be regarded as a placeholder in SQLite (#1402) - Thanks @git-hulk - -### Added -* Support for MSSQL table options (#1414) - Thanks @bombsimon -* Test showing how negative constants are parsed (#1421) - Thanks @alamb -* Support databricks dialect to dialect_from_str (#1416) - Thanks @milenkovicmalamb -* Support `DROP|CLEAR|MATERIALIZE PROJECTION` syntax for ClickHouse (#1417) - Thanks @git-hulk -* Support postgres `TRUNCATE` syntax (#1406) - Thanks @tobyhede -* Support `CREATE INDEX` with clause (#1389) - Thanks @lewiszlw -* Support parsing `CLUSTERED BY` clause for Hive (#1397) - Thanks @git-hulk -* Support different `USE` statement syntaxes (#1387) - Thanks @kacpermuda -* Support `ADD PROJECTION` syntax for ClickHouse (#1390) - Thanks @git-hulk - -### Changed -* Implement common traits for OneOrManyWithParens (#1368) - Thanks @gstvg -* Cleanup parse_statement (#1407) - Thanks @samuelcolvin -* Allow `DateTimeField::Custom` with `EXTRACT` in Postgres (#1394) - Thanks @samuelcolvin - - -## [0.50.0] 2024-08-15 -Again, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs 🙏. -Without them this project would not be possible. - -Reminder: are in the process of moving sqlparser to governed as part of the Apache -DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 - -### Fixed -* Clippy 1.80 warnings (#1357) - Thanks @lovasoa - -### Added -* Support `STRUCT` and list of structs for DuckDB dialect (#1372) - Thanks @jayzhan211 -* Support custom lexical precedence in PostgreSQL dialect (#1379) - Thanks @samuelcolvin -* Support `FREEZE|UNFREEZE PARTITION` syntax for ClickHouse (#1380) - Thanks @git-hulk -* Support scale in `CEIL` and `FLOOR` functions (#1377) - Thanks @seve-martinez -* Support `CREATE TRIGGER` and `DROP TRIGGER` statements (#1352) - Thanks @LucaCappelletti94 -* Support `EXTRACT` syntax for snowflake (#1374) - Thanks @seve-martinez -* Support `ATTACH` / `DETACH PARTITION` for ClickHouse (#1362) - Thanks @git-hulk -* Support Dialect level precedence, update Postgres Dialect to match Postgres (#1360) - Thanks @samuelcolvin -* Support parsing empty map literal syntax for DuckDB and Generic dialects (#1361) - Thanks @goldmedal -* Support `SETTINGS` clause for ClickHouse table-valued functions (#1358) - Thanks @Jesse-Bakker -* Support `OPTIMIZE TABLE` statement for ClickHouse (#1359) - Thanks @git-hulk -* Support `ON CLUSTER` in `ALTER TABLE` for ClickHouse (#1342) - Thanks @git-hulk -* Support `GLOBAL` keyword before the join operator (#1353) - Thanks @git-hulk -* Support postgres String Constants with Unicode Escapes (#1355) - Thanks @lovasoa -* Support position with normal function call syntax for Snowflake (#1341) - Thanks @jmhain -* Support `TABLE` keyword in `DESC|DESCRIBE|EXPLAIN TABLE` statement (#1351) - Thanks @git-hulk - -### Changed -* Only require `DESCRIBE TABLE` for Snowflake and ClickHouse dialect (#1386) - Thanks @ alamb -* Rename (unreleased) `get_next_precedence_full` to `get_next_precedence_default` (#1378) - Thanks @samuelcolvin -* Use local GitHub Action to replace setup-rust-action (#1371) - Thanks @git-hulk -* Simplify arrow_cast tests (#1367) - Thanks @alamb -* Update version of GitHub Actions (#1363) - Thanks @git-hulk -* Make `Parser::maybe_parse` pub (#1364) - Thanks @Jesse-Bakker -* Improve comments on 1Dialect` (#1366) - Thanks @alamb - - -## [0.49.0] 2024-07-23 -As always, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs! - -We are in the process of moving sqlparser to governed as part of the Apache -DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 - -### Fixed -* Fix quoted identifier regression edge-case with "from" in SELECT (#1346) - Thanks @alexander-beedie -* Fix `AS` query clause should be after the create table options (#1339) - Thanks @git-hulk - -### Added - -* Support `MATERIALIZED`/`ALIAS`/`EPHERMERAL` default column options for ClickHouse (#1348) - Thanks @git-hulk -* Support `()` as the `GROUP BY` nothing (#1347) - Thanks @git-hulk -* Support Map literal syntax for DuckDB and Generic (#1344) - Thanks @goldmedal -* Support subquery expression in `SET` expressions (#1343) - Thanks @iffyio -* Support `WITH FILL` for ClickHouse (#1330) - Thanks @nickpresta -* Support `PARTITION BY` for PostgreSQL in `CREATE TABLE` statement (#1338) - Thanks @git-hulk -* Support of table function `WITH ORDINALITY` modifier for Postgres (#1337) - Thanks @git-hulk - - -## [0.48.0] 2024-07-09 - -Huge shout out to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs! - -### Fixed -* Fix CI error message in CI (#1333) - Thanks @alamb -* Fix typo in sqlparser-derive README (#1310) - Thanks @leoyvens -* Re-enable trailing commas in DCL (#1318) - Thanks @MohamedAbdeen21 -* Fix a few typos in comment lines (#1316) - Thanks @git-hulk -* Fix Snowflake `SELECT * wildcard REPLACE ... RENAME` order (#1321) - Thanks @alexander-beedie -* Allow semi-colon at the end of UNCACHE statement (#1320) - Thanks @LorrensP-2158466 -* Return errors, not panic, when integers fail to parse in `AUTO_INCREMENT` and `TOP` (#1305) - Thanks @eejbyfeldt - -### Added -* Support `OWNER TO` clause in Postgres (#1314) - Thanks @gainings -* Support `FORMAT` clause for ClickHouse (#1335) - Thanks @git-hulk -* Support `DROP PROCEDURE` statement (#1324) - Thanks @LorrensP-2158466 -* Support `PREWHERE` condition for ClickHouse dialect (#1328) - Thanks @git-hulk -* Support `SETTINGS` pairs for ClickHouse dialect (#1327) - Thanks @git-hulk -* Support `GROUP BY WITH MODIFIER` for ClickHouse dialect (#1323) - Thanks @git-hulk -* Support DuckDB Union datatype (#1322) - Thanks @gstvg -* Support parametric arguments to `FUNCTION` for ClickHouse dialect (#1315) - Thanks @git-hulk -* Support `TO` in `CREATE VIEW` clause for Clickhouse (#1313) - Thanks @Bidaya0 -* Support `UPDATE` statements that contain tuple assignments (#1317) - Thanks @lovasoa -* Support `BY NAME quantifier across all set ops (#1309) - Thanks @alexander-beedie -* Support SnowFlake exclusive `CREATE TABLE` options (#1233) - Thanks @balliegojr -* Support ClickHouse `CREATE TABLE` with primary key and parametrised table engine (#1289) - Thanks @7phs -* Support custom operators in Postgres (#1302) - Thanks @lovasoa -* Support ClickHouse data types (#1285) - Thanks @7phs - -### Changed -* Add stale PR github workflow (#1331) - Thanks @alamb -* Refine docs (#1326) - Thanks @emilsivervik -* Improve error messages with additional colons (#1319) - Thanks @LorrensP-2158466 -* Move Display fmt to struct for `CreateIndex` (#1307) - Thanks @philipcristiano -* Enhancing Trailing Comma Option (#1212) - Thanks @MohamedAbdeen21 -* Encapsulate `CreateTable`, `CreateIndex` into specific structs (#1291) - Thanks @philipcristiano - -## [0.47.0] 2024-06-01 - -### Fixed -* Re-support Postgres array slice syntax (#1290) - Thanks @jmhain -* Fix DoubleColon cast skipping AT TIME ZONE #1266 (#1267) - Thanks @dmitrybugakov -* Fix for values as table name in Databricks and generic (#1278) - Thanks @jmhain - -### Added -* Support `ASOF` joins in Snowflake (#1288) - Thanks @jmhain -* Support `CREATE VIEW` with fields and data types ClickHouse (#1292) - Thanks @7phs -* Support view comments for Snowflake (#1287) - Thanks @bombsimon -* Support dynamic pivot in Snowflake (#1280) - Thanks @jmhain -* Support `CREATE FUNCTION` for BigQuery, generalize AST (#1253) - Thanks @iffyio -* Support expression in `AT TIME ZONE` and fix precedence (#1272) - Thanks @jmhain -* Support `IGNORE/RESPECT NULLS` inside function argument list for Databricks (#1263) - Thanks @jmhain -* Support `SELECT * EXCEPT` Databricks (#1261) - Thanks @jmhain -* Support triple quoted strings (#1262) - Thanks @iffyio -* Support array indexing for duckdb (#1265) - Thanks @JichaoS -* Support multiple SET variables (#1252) - Thanks @iffyio -* Support `ANY_VALUE` `HAVING` clause (#1258) in BigQuery - Thanks @jmhain -* Support keywords as field names in BigQuery struct syntax (#1254) - Thanks @iffyio -* Support `GROUP_CONCAT()` in MySQL (#1256) - Thanks @jmhain -* Support lambda functions in Databricks (#1257) - Thanks @jmhain -* Add const generic peek_tokens method to parser (#1255) - Thanks @jmhain - - -## [0.46.0] 2024-05-03 - -### Changed -* Consolidate representation of function calls, remove `AggregateExpressionWithFilter`, `ArraySubquery`, `ListAgg` and `ArrayAgg` (#1247) - Thanks jmhain -* Extended dialect trait to support numeric prefixed identifiers (#1188) - Thanks @groobyming -* Update simple_logger requirement from 4.0 to 5.0 (#1246) - Thanks @dependabot -* Improve parsing of JSON accesses on Postgres and Snowflake (#1215) - Thanks @jmhain -* Encapsulate Insert and Delete into specific structs (#1224) - Thanks @tisonkun -* Preserve double colon casts (and simplify cast representations) (#1221) - Thanks @jmhain - -### Fixed -* Fix redundant brackets in Hive/Snowflake/Redshift (#1229) - Thanks @yuval-illumex - -### Added -* Support values without parens in Snowflake and DataBricks (#1249) - Thanks @HiranmayaGundu -* Support WINDOW clause after QUALIFY when parsing (#1248) - Thanks @iffyio -* Support `DECLARE` parsing for mssql (#1235) - Thanks @devanbenz -* Support `?`-based jsonb operators in Postgres (#1242) - THanks @ReppCodes -* Support Struct datatype parsing for GenericDialect (#1241) - Thanks @duongcongtoai -* Support BigQuery window function null treatment (#1239) - Thanks @iffyio -* Support extend pivot operator - Thanks @iffyio -* Support Databricks SQL dialect (#1220) - Thanks @jmhain -* Support for MSSQL CONVERT styles (#1219) - Thanks @iffyio -* Support window clause using named window in BigQuery (#1237) - Thanks @iffyio -* Support for CONNECT BY (#1138) - Thanks @jmhain -* Support object constants in Snowflake (#1223) - Thanks @jmhain -* Support BigQuery MERGE syntax (#1217) - Thanks @iffyio -* Support for MAX for NVARCHAR (#1232) - Thanks @ bombsimon -* Support fixed size list types (#1231) - @@universalmind303 -* Support Snowflake MATCH_RECOGNIZE syntax (#1222) - Thanks @jmhain -* Support quoted string backslash escaping (#1177) - Thanks @iffyio -* Support Modify Column for MySQL dialect (#1216) - Thanks @KKould -* Support `select * ilike` for snowflake (#1228) - Thanks @HiranmayaGundu -* Support wildcard replace in duckdb and snowflake syntax (#1226) - Thanks @HiranmayaGundu - - - -## [0.45.0] 2024-04-12 - -### Added -* Support `DateTimeField` variants: `CUSTOM` and `WEEK(MONDAY)` (#1191) - Thanks @iffyio -* Support for arbitrary expr in `MapAccessSyntax` (#1179) - Thanks @iffyio -* Support unquoted hyphen in table/view declaration for BigQuery (#1178) - Thanks @iffyio -* Support `CREATE/DROP SECRET` for duckdb dialect (#1208) - Thanks @JichaoS -* Support MySQL `UNIQUE` table constraint (#1164) - Thanks @Nikita-str -* Support tailing commas on Snowflake. (#1205) - Thanks @yassun7010 -* Support `[FIRST | AFTER column_name]` in `ALTER TABLE` for MySQL (#1180) - Thanks @xring -* Support inline comment with hash syntax for BigQuery (#1192) - Thanks @iffyio -* Support named windows in OVER (window_definition) clause (#1166) - Thanks @Nikita-str -* Support PARALLEL ... and for ..ON NULL INPUT ... to CREATE FUNCTION` (#1202) - Thanks @dimfeld -* Support DuckDB functions named arguments with assignment operator (#1195) - Thanks @alamb -* Support DuckDB struct literal syntax (#1194) - Thanks @gstvg -* Support `$$` in generic dialect ... (#1185)- Thanks @milenkovicm -* Support row_alias and col_aliases in `INSERT` statement for MySQL and Generic dialects (#1136) - Thanks @emin100 - -### Fixed -* Fix dollar quoted string tokenizer (#1193) - Thanks @ZacJW -* Do not allocate in `impl Display` for `DateTimeField` (#1209) - Thanks @alamb -* Fix parse `COPY INTO` stage names without parens for SnowFlake (#1187) - Thanks @mobuchowski -* Solve stack overflow on RecursionLimitExceeded on debug builds (#1171) - Thanks @Nikita-str -* Fix parsing of equality binary operator in function argument (#1182) - Thanks @jmhain -* Fix some comments (#1184) - Thanks @sunxunle - -### Changed -* Cleanup `CREATE FUNCTION` tests (#1203) - Thanks @alamb -* Parse `SUBSTRING FROM` syntax in all dialects, reflect change in the AST (#1173) - Thanks @lovasoa -* Add identifier quote style to Dialect trait (#1170) - Thanks @backkem - -## [0.44.0] 2024-03-02 - -### Added -* Support EXPLAIN / DESCR / DESCRIBE [FORMATTED | EXTENDED] (#1156) - Thanks @jonathanlehtoalamb -* Support ALTER TABLE ... SET LOCATION (#1154) - Thanks @jonathanlehto -* Support `ROW FORMAT DELIMITED` in Hive (#1155) - Thanks @jonathanlehto -* Support `SERDEPROPERTIES` for `CREATE TABLE` with Hive (#1152) - Thanks @jonathanlehto -* Support `EXECUTE ... USING` for Postgres (#1153) - Thanks @jonathanlehto -* Support Postgres style `CREATE FUNCTION` in GenericDialect (#1159) - Thanks @alamb -* Support `SET TBLPROPERTIES` (#1151) - Thanks @jonathanlehto -* Support `UNLOAD` statement (#1150) - Thanks @jonathanlehto -* Support `MATERIALIZED CTEs` (#1148) - Thanks @ReppCodes -* Support `DECLARE` syntax for snowflake and bigquery (#1122) - Thanks @iffyio -* Support `SELECT AS VALUE` and `SELECT AS STRUCT` for BigQuery (#1135) - Thanks @lustefaniak -* Support `(+)` outer join syntax (#1145) - Thanks @jmhain -* Support `INSERT INTO ... SELECT ... RETURNING`(#1132) - Thanks @lovasoa -* Support DuckDB `INSTALL` and `LOAD` (#1127) - Thanks @universalmind303 -* Support `=` operator in function args (#1128) - Thanks @universalmind303 -* Support `CREATE VIEW IF NOT EXISTS` (#1118) - Thanks @7phs -* Support `UPDATE FROM` for SQLite (further to #694) (#1117) - Thanks @ggaughan -* Support optional `DELETE FROM` statement (#1120) - Thanks @iffyio -* Support MySQL `SHOW STATUS` statement (#1119) - Thanks invm - -### Fixed -* Clean up nightly clippy lints (#1158) - Thanks @alamb -* Handle escape, unicode, and hex in tokenize_escaped_single_quoted_string (#1146) - Thanks @JasonLi-cn -* Fix panic while parsing `REPLACE` (#1140) - THanks @jjbayer -* Fix clippy warning from rust 1.76 (#1130) - Thanks @alamb -* Fix release instructions (#1115) - Thanks @alamb - -### Changed -* Add `parse_keyword_with_tokens` for paring keyword and tokens combination (#1141) - Thanks @viirya -* Add ParadeDB to list of known users (#1142) - Thanks @philippemnoel -* Accept JSON_TABLE both as an unquoted table name and a table-valued function (#1134) - Thanks @lovasoa - - -## [0.43.1] 2024-01-22 -### Changes -* Fixed CHANGELOG - - -## [0.43.0] 2024-01-22 -* NO CHANGES - -## [0.42.0] 2024-01-22 - -### Added -* Support for constraint `CHARACTERISTICS` clause (#1099) - Thanks @dimfeld -* Support for unquoted hyphenated identifiers on bigquery (#1109) - Thanks @jmhain -* Support `BigQuery` table and view options (#1061) - Thanks @iffyio -* Support Postgres operators for the LIKE expression variants (#1096) - Thanks @gruuya -* Support "timezone_region" and "timezone_abbr" for `EXTRACT` (and `DATE_PART`) (#1090) - Thanks @alexander-beedie -* Support `JSONB` datatype (#1089) - Thanks @alexander-beedie -* Support PostgreSQL `^@` starts-with operator (#1091) - Thanks @alexander-beedie -* Support PostgreSQL Insert table aliases (#1069) (#1084) - Thanks @boydjohnson -* Support PostgreSQL `CREATE EXTENSION` (#1078) - Thanks @tobyhede -* Support PostgreSQL `ADD GENERATED` in `ALTER COLUMN` statements (#1079) - Thanks @tobyhede -* Support SQLite column definitions with no type (#1075) - Thanks @takluyver -* Support PostgreSQL `ENABLE` and `DISABLE` on `ALTER TABLE` (#1077) - Thanks @tobyhede -* Support MySQL `FLUSH` statement (#1076) - Thanks @emin100 -* Support Mysql `REPLACE` statement and `PRIORITY` clause of `INSERT` (#1072) - Thanks @emin100 - -### Fixed -* Fix `:start` and `:end` json accesses on SnowFlake (#1110) - Thanks @jmhain -* Fix array_agg wildcard behavior (#1093) - Thanks @ReppCodes -* Error on dangling `NO` in `CREATE SEQUENCE` options (#1104) - Thanks @PartiallyTyped -* Allow string values in `PRAGMA` commands (#1101) - Thanks @invm - -### Changed -* Use `Option` for Min and Max vals in Seq Opts, fix alter col seq display (#1106) - Thanks @PartiallyTyped -* Replace `AtomicUsize` with Cell in the recursion counter (#1098) - Thanks @wzzzzd -* Add Qrlew as a user in README.md (#1107) - Thanks @ngrislain -* Add APIs to reuse token buffers in `Tokenizer` (#1094) - Thanks @0rphon -* Bump version of `sqlparser-derive` to 0.2.2 (#1083) - Thanks @alamb - -## [0.41.0] 2023-12-22 - -### Added -* Support `DEFERRED`, `IMMEDIATE`, and `EXCLUSIVE` in SQLite's `BEGIN TRANSACTION` command (#1067) - Thanks @takaebato -* Support generated columns skipping `GENERATED ALWAYS` keywords (#1058) - Thanks @takluyver -* Support `LOCK/UNLOCK TABLES` for MySQL (#1059) - Thanks @zzzdong -* Support `JSON_TABLE` (#1062) - Thanks @lovasoa -* Support `CALL` statements (#1063) - Thanks @lovasoa - -### Fixed -* fix rendering of SELECT TOP (#1070) for Snowflake - Thanks jmhain - -### Changed -* Improve documentation formatting (#1068) - Thanks @alamb -* Replace type_id() by trait method to allow wrapping dialects (#1065) - Thanks @jjbayer -* Document that comments aren't preserved for round trip (#1060) - Thanks @takluyver -* Update sqlparser-derive to use `syn 2.0` (#1040) - Thanks @serprex - -## [0.40.0] 2023-11-27 - -### Added -* Add `{pre,post}_visit_query` to `Visitor` (#1044) - Thanks @jmhain -* Support generated virtual columns with expression (#1051) - Thanks @takluyver -* Support PostgreSQL `END` (#1035) - Thanks @tobyhede -* Support `INSERT INTO ... DEFAULT VALUES ...` (#1036) - Thanks @CDThomas -* Support `RELEASE` and `ROLLBACK TO SAVEPOINT` (#1045) - Thanks @CDThomas -* Support `CONVERT` expressions (#1048) - Thanks @lovasoa -* Support `GLOBAL` and `SESSION` parts in `SHOW VARIABLES` for mysql and generic - Thanks @emin100 -* Support snowflake `PIVOT` on derived table factors (#1027) - Thanks @lustefaniak -* Support mssql json and xml extensions (#1043) - Thanks @lovasoa -* Support for `MAX` as a character length (#1038) - Thanks @lovasoa -* Support `IN ()` syntax of SQLite (#1028) - Thanks @alamb - -### Fixed -* Fix extra whitespace printed before `ON CONFLICT` (#1037) - Thanks @CDThomas - -### Changed -* Document round trip ability (#1052) - Thanks @alamb -* Add PRQL to list of users (#1031) - Thanks @vanillajonathan - -## [0.39.0] 2023-10-27 - -### Added -* Support for `LATERAL FLATTEN` and similar (#1026) - Thanks @lustefaniak -* Support BigQuery struct, array and bytes , int64, `float64` datatypes (#1003) - Thanks @iffyio -* Support numbers as placeholders in Snowflake (e.g. `:1)` (#1001) - Thanks @yuval-illumex -* Support date 'key' when using semi structured data (#1023) @yuval-illumex -* Support IGNORE|RESPECT NULLs clause in window functions (#998) - Thanks @yuval-illumex -* Support for single-quoted identifiers (#1021) - Thanks @lovasoa -* Support multiple PARTITION statements in ALTER TABLE ADD statement (#1011) - Thanks @bitemyapp -* Support "with" identifiers surrounded by backticks in GenericDialect (#1010) - Thanks @bitemyapp -* Support INSERT IGNORE in MySql and GenericDialect (#1004) - Thanks @emin100 -* Support SQLite `pragma` statement (#969) - Thanks @marhoily -* Support `position` as a column name (#1022) - Thanks @lustefaniak -* Support `FILTER` in Functions (for `OVER`) clause (#1007) - Thanks @lovasoa -* Support `SELECT * EXCEPT/REPLACE` syntax from ClickHouse (#1013) - Thanks @lustefaniak -* Support subquery as function arg w/o parens in Snowflake dialect (#996) - Thanks @jmhain -* Support `UNION DISTINCT BY NAME` syntax (#997) - Thanks @alexander-beedie -* Support mysql `RLIKE` and `REGEXP` binary operators (#1017) - Thanks @lovasoa -* Support bigquery `CAST AS x [STRING|DATE] FORMAT` syntax (#978) - Thanks @lustefaniak -* Support Snowflake/BigQuery `TRIM`. (#975) - Thanks @zdenal -* Support `CREATE [TEMPORARY|TEMP] VIEW [IF NOT EXISTS] `(#993) - Thanks @gabivlj -* Support for `CREATE VIEW … WITH NO SCHEMA BINDING` Redshift (#979) - Thanks @lustefaniak -* Support `UNPIVOT` and a fix for chained PIVOTs (#983) - @jmhain -* Support for `LIMIT BY` (#977) - Thanks @lustefaniak -* Support for mixed BigQuery table name quoting (#971) - Thanks @iffyio -* Support `DELETE` with `ORDER BY` and `LIMIT` (MySQL) (#992) - Thanks @ulrichsg -* Support `EXTRACT` for `DAYOFWEEK`, `DAYOFYEAR`, `ISOWEEK`, `TIME` (#980) - Thanks @lustefaniak -* Support `ATTACH DATABASE` (#989) - Thanks @lovasoa - -### Fixed -* Fix handling of `/~%` in Snowflake stage name (#1009) - Thanks @lustefaniak -* Fix column `COLLATE` not displayed (#1012) - Thanks @lustefaniak -* Fix for clippy 1.73 (#995) - Thanks @alamb - -### Changed -* Test to ensure `+ - * / %` binary operators work the same in all dialects (#1025) - Thanks @lustefaniak -* Improve documentation on Parser::consume_token and friends (#994) - Thanks @alamb -* Test that regexp can be used as an identifier in postgres (#1018) - Thanks @lovasoa -* Add docstrings for Dialects, update README (#1016) - Thanks @alamb -* Add JumpWire to users in README (#990) - Thanks @hexedpackets -* Add tests for clickhouse: `tokenize == as Token::DoubleEq` (#981)- Thanks @lustefaniak - -## [0.38.0] 2023-09-21 - -### Added - -* Support `==`operator for Sqlite (#970) - Thanks @marhoily -* Support mysql `PARTITION` to table selection (#959) - Thanks @chunshao90 -* Support `UNNEST` as a table factor for PostgreSQL (#968) @hexedpackets -* Support MySQL `UNIQUE KEY` syntax (#962) - Thanks @artorias1024 -* Support` `GROUP BY ALL` (#964) - @berkaysynnada -* Support multiple actions in one ALTER TABLE statement (#960) - Thanks @ForbesLindesay -* Add `--sqlite param` to CLI (#956) - Thanks @ddol - -### Fixed -* Fix Rust 1.72 clippy lints (#957) - Thanks @alamb - -### Changed -* Add missing token loc in parse err msg (#965) - Thanks @ding-young -* Change how `ANY` and `ALL` expressions are represented in AST (#963) - Thanks @SeanTroyUWO -* Show location info in parse errors (#958) - Thanks @MartinNowak -* Update release documentation (#954) - Thanks @alamb -* Break test and coverage test into separate jobs (#949) - Thanks @alamb - - -## [0.37.0] 2023-08-22 - -### Added -* Support `FOR SYSTEM_TIME AS OF` table time travel clause support, `visit_table_factor` to Visitor (#951) - Thanks @gruuya -* Support MySQL `auto_increment` offset in table definition (#950) - Thanks @ehoeve -* Test for mssql table name in square brackets (#952) - Thanks @lovasoa -* Support additional Postgres `CREATE INDEX` syntax (#943) - Thanks @ForbesLindesay -* Support `ALTER ROLE` syntax of PostgreSQL and MS SQL Server (#942) - Thanks @r4ntix -* Support table-level comments (#946) - Thanks @ehoeve -* Support `DROP TEMPORARY TABLE`, MySQL syntax (#916) - Thanks @liadgiladi -* Support posgres type alias (#933) - Thanks @Kikkon - -### Fixed -* Clarify the value of the special flag (#948) - Thanks @alamb -* Fix `SUBSTRING` from/to argument construction for mssql (#947) - Thanks @jmaness -* Fix: use Rust idiomatic capitalization for newly added DataType enums (#939) - Thanks @Kikkon -* Fix `BEGIN TRANSACTION` being serialized as `START TRANSACTION` (#935) - Thanks @lovasoa -* Fix parsing of datetime functions without parenthesis (#930) - Thanks @lovasoa - -## [0.36.1] 2023-07-19 - -### Fixed -* Fix parsing of identifiers after '%' symbol (#927) - Thanks @alamb - -## [0.36.0] 2023-07-19 - -### Added -* Support toggling "unescape" mode to retain original escaping (#870) - Thanks @canalun -* Support UNION (ALL) BY NAME syntax (#915) - Thanks @parkma99 -* Add doc comment for all operators (#917) - Thanks @izveigor -* Support `PGOverlap` operator (#912) - Thanks @izveigor -* Support multi args for unnest (#909) - Thanks @jayzhan211 -* Support `ALTER VIEW`, MySQL syntax (#907) - Thanks @liadgiladi -* Add DeltaLake keywords (#906) - Thanks @roeap - -### Fixed -* Parse JsonOperators correctly (#913) - Thanks @izveigor -* Fix dependabot by removing rust-toolchain toml (#922) - Thanks @alamb - -### Changed -* Clean up JSON operator tokenizing code (#923) - Thanks @alamb -* Upgrade bigdecimal to 0.4.1 (#921) - Thanks @jinlee0 -* Remove most instances of #[cfg(feature(bigdecimal))] in tests (#910) - Thanks @alamb - -## [0.35.0] 2023-06-23 - -### Added -* Support `CREATE PROCEDURE` of MSSQL (#900) - Thanks @delsehi -* Support DuckDB's `CREATE MACRO` statements (#897) - Thanks @MartinNowak -* Support for `CREATE TYPE (AS)` statements (#888) - Thanks @srijs -* Support `STRICT` tables of sqlite (#903) - Thanks @parkma99 - -### Fixed -* Fixed precedence of unary negation operator with operators: Mul, Div and Mod (#902) - Thanks @izveigor - -### Changed -* Add `support_group_by_expr` to `Dialect` trait (#896) - Thanks @jdye64 -* Update criterion requirement from `0.4` to `0.5` in `/sqlparser_bench` (#890) - Thanks @dependabot (!!) - -## [0.34.0] 2023-05-19 - -### Added - -* Support named window frames (#881) - Thanks @berkaysynnada, @mustafasrepo, and @ozankabak -* Support for `ORDER BY` clauses in aggregate functions (#882) - Thanks @mustafasrepo -* Support `DuckDB` dialect (#878) - Thanks @eitsupi -* Support optional `TABLE` keyword for `TRUNCATE TABLE` (#883) - Thanks @mobuchowski -* Support MySQL's `DIV` operator (#876) - Thanks @eitsupi -* Support Custom operators (#868) - Thanks @max-sixty -* Add `Parser::parse_multipart_identifier` (#860) - Thanks @Jefffrey -* Support for multiple expressions, order by in `ARRAY_AGG` (#879) - Thanks @mustafasrepo -* Support for query source in `COPY .. TO` statement (#858) - Thanks @aprimadi -* Support `DISTINCT ON (...)` (#852) - Thanks @aljazerzen -* Support multiple-table `DELETE` syntax (#855) - Thanks @AviRaboah -* Support `COPY INTO` in `SnowflakeDialect` (#841) - Thanks @pawel-big-lebowski -* Support identifiers beginning with digits in MySQL (#856) - Thanks @AviRaboah - -### Changed -* Include license file in published crate (#871) - Thanks @ankane -* Make `Expr::Interval` its own struct (#872) - Thanks @aprimadi -* Add dialect_from_str and improve Dialect documentation (#848) - Thanks @alamb -* Add clickhouse to example (#849) - Thanks @anglinb - -### Fixed -* Fix merge conflict (#885) - Thanks @alamb -* Fix tiny typo in custom_sql_parser.md (#864) - Thanks @okue -* Fix logical merge conflict (#865) - Thanks @alamb -* Test trailing commas (#859) - Thanks @aljazerzen - - -## [0.33.0] 2023-04-10 - -### Added -* Support for Mysql Backslash escapes (enabled by default) (#844) - Thanks @cobyge -* Support "UPDATE" statement in "WITH" subquery (#842) - Thanks @nicksrandall -* Support PIVOT table syntax (#836) - Thanks @pawel-big-lebowski -* Support CREATE/DROP STAGE for Snowflake (#833) - Thanks @pawel-big-lebowski -* Support Non-Latin characters (#840) - Thanks @mskrzypkows -* Support PostgreSQL: GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY and GENERATED - Thanks @sam-mmm -* Support IF EXISTS in COMMENT statements (#831) - Thanks @pawel-big-lebowski -* Support snowflake alter table swap with (#825) - Thanks @pawel-big-lebowski - -### Changed -* Move tests from parser.rs to appropriate parse_XX tests (#845) - Thanks @alamb -* Correct typos in parser.rs (#838) - Thanks @felixonmars -* Improve documentation on verified_* methods (#828) - Thanks @alamb - -## [0.32.0] 2023-03-6 - -### Added -* Support ClickHouse `CREATE TABLE` with `ORDER BY` (#824) - Thanks @ankrgyl -* Support PostgreSQL exponentiation `^` operator (#813) - Thanks @michael-2956 -* Support `BIGNUMERIC` type in BigQuery (#811) - Thanks @togami2864 -* Support for optional trailing commas (#810) - Thanks @ankrgyl - -### Fixed -* Fix table alias parsing regression by backing out redshift column definition list (#827) - Thanks @alamb -* Fix typo in `ReplaceSelectElement` `colum_name` --> `column_name` (#822) - Thanks @togami2864 - -## [0.31.0] 2023-03-1 - -### Added -* Support raw string literals for BigQuery dialect (#812) - Thanks @togami2864 -* Support `SELECT * REPLACE AS ` in BigQuery dialect (#798) - Thanks @togami2864 -* Support byte string literals for BigQuery dialect (#802) - Thanks @togami2864 -* Support columns definition list for system information functions in RedShift dialect (#769) - Thanks @mskrzypkows -* Support `TRANSIENT` keyword in Snowflake dialect (#807) - Thanks @mobuchowski -* Support `JSON` keyword (#799) - Thanks @togami2864 -* Support MySQL Character Set Introducers (#788) - Thanks @mskrzypkows - -### Fixed -* Fix clippy error in ci (#803) - Thanks @togami2864 -* Handle offset in map key in BigQuery dialect (#797) - Thanks @Ziinc -* Fix a typo (precendence -> precedence) (#794) - Thanks @SARDONYX-sard -* use post_* visitors for mutable visits (#789) - Thanks @lovasoa - -### Changed -* Add another known user (#787) - Thanks @joocer - -## [0.30.0] 2023-01-02 - -### Added -* Support `RENAME` for wildcard `SELECTs` (#784) - Thanks @Jefffrey -* Add a mutable visitor (#782) - Thanks @lovasoa - -### Changed -* Allow parsing of mysql empty row inserts (#783) - Thanks @Jefffrey - -### Fixed -* Fix logical conflict (#785) - Thanks @alamb - -## [0.29.0] 2022-12-29 - -### Highlights -* Partial source location tracking: see #710 -* Recursion limit to prevent stack overflows: #764 -* AST visitor: #765 - -### Added -feat: dollar-quoted strings support (#772) - Thanks @vasilev-alex -* Add derive based AST visitor (#765) - Thanks @tustvold -* Support `ALTER INDEX {INDEX_NAME} RENAME TO {NEW_INDEX_NAME}` (#767) - Thanks @devgony -* Support `CREATE TABLE ON UPDATE ` Function (#685) - Thanks @CEOJINSUNG -* Support `CREATE FUNCTION` definition with `$$` (#755)- Thanks @zidaye -* Add location tracking in the tokenizer and parser (#710) - Thanks @ankrgyl -* Add configurable recursion limit to parser, to protect against stackoverflows (#764) - Thanks @alamb -* Support parsing scientific notation (such as `10e5`) (#768) - Thanks @Jefffrey -* Support `DROP FUNCTION` syntax (#752) - Thanks @zidaye -* Support json operators `@>` `<@`, `@?` and `@@` - Thanks @audunska -* Support the type key (#750)- Thanks @yuval-illumex - -### Changed -* Improve docs and add examples for Visitor (#778) - Thanks @alamb -* Add a backlink from sqlparse_derive to sqlparser and publishing instructions (#779) - Thanks @alamb -* Document new features, update authors (#776) - Thanks @alamb -* Improve Readme (#774) - Thanks @alamb -* Standardize comments on parsing optional keywords (#773) - Thanks @alamb -* Enable grouping sets parsing for `GenericDialect` (#771) - Thanks @Jefffrey -* Generalize conflict target (#762) - Thanks @audunska -* Generalize locking clause (#759) - Thanks @audunska -* Add negative test for except clause on wildcards (#746)- Thanks @alamb -* Add `NANOSECOND` keyword (#749)- Thanks @waitingkuo - -### Fixed -* ParserError if nested explain (#781) - Thanks @Jefffrey -* Fix cargo docs / warnings and add CI check (#777) - Thanks @alamb -* unnest join constraint with alias parsing for BigQuery dialect (#732)- Thanks @Ziinc - -## [0.28.0] 2022-12-05 - -### Added -* Support for `EXCEPT` clause on wildcards (#745) - Thanks @AugustoFKL -* Support `CREATE FUNCTION` Postgres options (#722) - Thanks @wangrunji0408 -* Support `CREATE TABLE x AS TABLE y` (#704) - Thanks @sarahyurick -* Support MySQL `ROWS` syntax for `VALUES` (#737) - Thanks @aljazerzen -* Support `WHERE` condition for `UPDATE ON CONFLICT` (#735) - Thanks @zidaye -* Support `CLUSTER BY` when creating Materialized View (#736) - Thanks @yuval-illumex -* Support nested comments (#726) - Thanks @yang-han -* Support `USING` method when creating indexes. (#731) - Thanks @step-baby and @yangjiaxin01 -* Support `SEMI`/`ANTI` `JOIN` syntax (#723) - Thanks @mingmwang -* Support `EXCLUDE` support for snowflake and generic dialect (#721) - Thanks @AugustoFKL -* Support `MATCH AGAINST` (#708) - Thanks @AugustoFKL -* Support `IF NOT EXISTS` in `ALTER TABLE ADD COLUMN` (#707) - Thanks @AugustoFKL -* Support `SET TIME ZONE ` (#727) - Thanks @waitingkuo -* Support `UPDATE ... FROM ( subquery )` (#694) - Thanks @unvalley - -### Changed -* Add `Parser::index()` method to get current parsing index (#728) - Thanks @neverchanje -* Add `COMPRESSION` as keyword (#720)- Thanks @AugustoFKL -* Derive `PartialOrd`, `Ord`, and `Copy` whenever possible (#717) - Thanks @AugustoFKL -* Fixed `INTERVAL` parsing logic and precedence (#705) - Thanks @sarahyurick -* Support updating multiple column names whose names are the same as(#725) - Thanks @step-baby - -### Fixed -* Clean up some redundant code in parser (#741) - Thanks @alamb -* Fix logical conflict - Thanks @alamb -* Cleanup to avoid is_ok() (#740) - Thanks @alamb -* Cleanup to avoid using unreachable! when parsing semi/anti join (#738) - Thanks @alamb -* Add an example to docs to clarify semantic analysis (#739) - Thanks @alamb -* Add information about parting semantic logic to README.md (#724) - Thanks @AugustoFKL -* Logical conflicts - Thanks @alamb -* Tiny typo in docs (#709) - Thanks @pmcgee69 - - -## [0.27.0] 2022-11-11 - -### Added -* Support `ON CONFLICT` and `RETURNING` in `UPDATE` statement (#666) - Thanks @main and @gamife -* Support `FULLTEXT` option on create table for MySQL and Generic dialects (#702) - Thanks @AugustoFKL -* Support `ARRAY_AGG` for Bigquery and Snowflake (#662) - Thanks @SuperBo -* Support DISTINCT for SetOperator (#689) - Thanks @unvalley -* Support the ARRAY type of Snowflake (#699) - Thanks @yuval-illumex -* Support create sequence with options INCREMENT, MINVALUE, MAXVALUE, START etc. (#681) - Thanks @sam-mmm -* Support `:` operator for semi-structured data in Snowflake(#693) - Thanks @yuval-illumex -* Support ALTER TABLE DROP PRIMARY KEY (#682) - Thanks @ding-young -* Support `NUMERIC` and `DEC` ANSI data types (#695) - Thanks @AugustoFKL -* Support modifiers for Custom Datatype (#680) - Thanks @sunng87 - -### Changed -* Add precision for TIME, DATETIME, and TIMESTAMP data types (#701) - Thanks @AugustoFKL -* add Date keyword (#691) - Thanks @sarahyurick -* Update simple_logger requirement from 2.1 to 4.0 - Thanks @dependabot - -### Fixed -* Fix broken DataFusion link (#703) - Thanks @jmg-duarte -* Add MySql, BigQuery to all dialects tests, fixed bugs (#697) - Thanks @omer-shtivi - - -## [0.26.0] 2022-10-19 - -### Added -* Support MySQL table option `{INDEX | KEY}` in CREATE TABLE definiton (#665) - Thanks @AugustoFKL -* Support `CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] ` (#678) - Thanks @sam-mmm -* Support `DROP SEQUENCE` statement (#673) - Thanks @sam-mmm -* Support for ANSI types `CHARACTER LARGE OBJECT[(p)]` and `CHAR LARGE OBJECT[(p)]` (#671) - Thanks @AugustoFKL -* Support `[CACHE|UNCACHE] TABLE` (#670) - Thanks @francis-du -* Support `CEIL(expr TO DateTimeField)` and `FLOOR(expr TO DateTimeField)` - Thanks @sarahyurick -* Support all ansii character string types, (#648) - Thanks @AugustoFKL - -### Changed -* Support expressions inside window frames (#655) - Thanks @mustafasrepo and @ozankabak -* Support unit on char length units for small character strings (#663) - Thanks @AugustoFKL -* Replace booleans on `SET ROLE` with a single enum. (#664) - Thanks @AugustoFKL -* Replace `Option`s with enum for `DECIMAL` precision (#654) - Thanks @AugustoFKL - -## [0.25.0] 2022-10-03 - -### Added - -* Support `AUTHORIZATION` clause in `CREATE SCHEMA` statements (#641) - Thanks @AugustoFKL -* Support optional precision for `CLOB` and `BLOB` (#639) - Thanks @AugustoFKL -* Support optional precision in `VARBINARY` and `BINARY` (#637) - Thanks @AugustoFKL - - -### Changed -* `TIMESTAMP` and `TIME` parsing preserve zone information (#646) - Thanks @AugustoFKL - -### Fixed - -* Correct order of arguments when parsing `LIMIT x,y` , restrict to `MySql` and `Generic` dialects - Thanks @AugustoFKL - - -## [0.24.0] 2022-09-29 - -### Added - -* Support `MILLENNIUM` (2 Ns) (#633) - Thanks @sarahyurick -* Support `MEDIUMINT` (#630) - Thanks @AugustoFKL -* Support `DOUBLE PRECISION` (#629) - Thanks @AugustoFKL -* Support precision in `CLOB`, `BINARY`, `VARBINARY`, `BLOB` data type (#618) - Thanks @ding-young -* Support `CREATE ROLE` and `DROP ROLE` (#598) - Thanks @blx -* Support full range of sqlite prepared statement placeholders (#604) - Thanks @lovasoa -* Support National string literal with lower case `n` (#612) - Thanks @mskrzypkows -* Support SHOW FUNCTIONS (#620) - Thanks @joocer -* Support `set time zone to 'some-timezone'` (#617) - Thanks @waitingkuo - -### Changed -* Move `Value::Interval` to `Expr::Interval` (#609) - Thanks @ding-young -* Update `criterion` dev-requirement from 0.3 to 0.4 in /sqlparser_bench (#611) - Thanks @dependabot -* Box `Query` in `Cte` (#572) - Thanks @MazterQyou - -### Other -* Disambiguate CREATE ROLE ... USER and GROUP (#628) - Thanks @alamb -* Add test for optional WITH in CREATE ROLE (#627) - Thanks @alamb - -## [0.23.0] 2022-09-08 - -### Added -* Add support for aggregate expressions with filters (#585) - Thanks @andygrove -* Support `LOCALTIME` and `LOCALTIMESTAMP` time functions (#592) - Thanks @MazterQyou - -## [0.22.0] 2022-08-26 - -### Added -* Support `OVERLAY` expressions (#594) - Thanks @ayushg -* Support `WITH TIMEZONE` and `WITHOUT TIMEZONE` when parsing `TIMESTAMP` expressions (#589) - Thanks @waitingkuo -* Add ability for dialects to override prefix, infix, and statement parsing (#581) - Thanks @andygrove - -## [0.21.0] 2022-08-18 - -### Added -* Support `IS [NOT] TRUE`, `IS [NOT] FALSE`, and `IS [NOT] UNKNOWN` - Thanks (#583) @sarahyurick -* Support `SIMILAR TO` syntax (#569) - Thanks @ayushdg -* Support `SHOW COLLATION` (#564) - Thanks @MazterQyou -* Support `SHOW TABLES` (#563) - Thanks @MazterQyou -* Support `SET NAMES literal [COLLATE literal]` (#558) - Thanks @ovr -* Support trailing commas (#557) in `BigQuery` dialect - Thanks @komukomo -* Support `USE ` (#565) - Thanks @MazterQyou -* Support `SHOW COLUMNS FROM tbl FROM db` (#562) - Thanks @MazterQyou -* Support `SHOW VARIABLES` for `MySQL` dialect (#559) - Thanks @ovr and @vasilev-alex - -### Changed -* Support arbitrary expression in `SET` statement (#574) - Thanks @ovr and @vasilev-alex -* Parse LIKE patterns as Expr not Value (#579) - Thanks @andygrove -* Update Ballista link in README (#576) - Thanks @sanxiyn -* Parse `TRIM` from with optional expr and `FROM` expr (#573) - Thanks @ayushdg -* Support PostgreSQL array subquery constructor (#566) - Thanks @MazterQyou -* Clarify contribution licensing (#570) - Thanks @alamb -* Update for new clippy ints (#571) - Thanks @alamb -* Change `Like` and `ILike` to `Expr` variants, allow escape char (#569) - Thanks @ayushdg -* Parse special keywords as functions (`current_user`, `user`, etc) (#561) - Thanks @ovr -* Support expressions in `LIMIT`/`OFFSET` (#567) - Thanks @MazterQyou - -## [0.20.0] 2022-08-05 - -### Added -* Support custom `OPERATOR` postgres syntax (#548) - Thanks @iskakaushik -* Support `SAFE_CAST` for BigQuery (#552) - Thanks @togami2864 - -### Changed -* Added SECURITY.md (#546) - Thanks @JamieSlome -* Allow `>>` and `<<` binary operators in Generic dialect (#553) - Thanks @ovr -* Allow `NestedJoin` with an alias (#551) - Thanks @waitingkuo - -## [0.19.0] 2022-07-28 - -### Added - -* Support `ON CLUSTER` for `CREATE TABLE` statement (ClickHouse DDL) (#527) - Thanks @andyrichardson -* Support empty `ARRAY` literals (#532) - Thanks @bitemyapp -* Support `AT TIME ZONE` clause (#539) - Thanks @bitemyapp -* Support `USING` clause and table aliases in `DELETE` (#541) - Thanks @mobuchowski -* Support `SHOW CREATE VIEW` statement (#536) - Thanks @mrob95 -* Support `CLONE` clause in `CREATE TABLE` statements (#542) - Thanks @mobuchowski -* Support `WITH OFFSET Alias` in table references (#528) - Thanks @sivchari -* Support double quoted (`"`) literal strings: (#530) - Thanks @komukomo -* Support `ON UPDATE` clause on column definitions in `CREATE TABLE` statements (#522) - Thanks @frolovdev - - -### Changed: - -* `Box`ed `Query` body to save stack space (#540) - Thanks @5tan -* Distinguish between `INT` and `INTEGER` types (#525) - Thanks @frolovdev -* Parse `WHERE NOT EXISTS` as `Expr::Exists` rather than `Expr::UnaryOp` for consistency (#523) - Thanks @frolovdev -* Support `Expr` instead of `String` for argument to `INTERVAL` (#517) - Thanks @togami2864 - -### Fixed: - -* Report characters instead of bytes in error messages (#529) - Thanks @michael-2956 - - -## [0.18.0] 2022-06-06 - -### Added - -* Support `CLOSE` (cursors) (#515) - Thanks @ovr -* Support `DECLARE` (cursors) (#509) - Thanks @ovr -* Support `FETCH` (cursors) (#510) - Thanks @ovr -* Support `DATETIME` keyword (#512) - Thanks @komukomo -* Support `UNNEST` as a table factor (#493) - Thanks @sivchari -* Support `CREATE FUNCTION` (hive flavor) (#496) - Thanks @mobuchowski -* Support placeholders (`$` or `?`) in `LIMIT` clause (#494) - Thanks @step-baby -* Support escaped string literals (PostgreSQL) (#502) - Thanks @ovr -* Support `IS TRUE` and `IS FALSE` (#499) - Thanks @ovr -* Support `DISCARD [ALL | PLANS | SEQUENCES | TEMPORARY | TEMP]` (#500) - Thanks @gandronchik -* Support `array<..>` HIVE data types (#491) - Thanks @mobuchowski -* Support `SET` values that begin with `-` #495 - Thanks @mobuchowski -* Support unicode whitespace (#482) - Thanks @alexsatori -* Support `BigQuery` dialect (#490) - Thanks @komukomo - -### Changed: -* Add docs for MapAccess (#489) - Thanks @alamb -* Rename `ArrayIndex::indexs` to `ArrayIndex::indexes` (#492) - Thanks @alamb - -### Fixed: -* Fix escaping of trailing quote in quoted identifiers (#505) - Thanks @razzolini-qpq -* Fix parsing of `COLLATE` after parentheses in expressions (#507) - Thanks @razzolini-qpq -* Distinguish tables and nullary functions in `FROM` (#506) - Thanks @razzolini-qpq -* Fix `MERGE INTO` semicolon handling (#508) - Thanks @mskrzypkows - -## [0.17.0] 2022-05-09 - -### Added - -* Support `#` as first character in field name for `RedShift` dialect (#485) - Thanks @yuval-illumex -* Support for postgres composite types (#466) - Thanks @poonai -* Support `TABLE` keyword with SELECT INTO (#487) - Thanks @MazterQyou -* Support `ANY`/`ALL` operators (#477) - Thanks @ovr -* Support `ArrayIndex` in `GenericDialect` (#480) - Thanks @ovr -* Support `Redshift` dialect, handle square brackets properly (#471) - Thanks @mskrzypkows -* Support `KILL` statement (#479) - Thanks @ovr -* Support `QUALIFY` clause on `SELECT` for `Snowflake` dialect (#465) - Thanks @mobuchowski -* Support `POSITION(x IN y)` function syntax (#463) @yuval-illumex -* Support `global`,`local`, `on commit` for `create temporary table` (#456) - Thanks @gandronchik -* Support `NVARCHAR` data type (#462) - Thanks @yuval-illumex -* Support for postgres json operators `->`, `->>`, `#>`, and `#>>` (#458) - Thanks @poonai -* Support `SET ROLE` statement (#455) - Thanks @slhmy - -### Changed: -* Improve docstrings for `KILL` statement (#481) - Thanks @alamb -* Add negative tests for `POSITION` (#469) - Thanks @alamb -* Add negative tests for `IN` parsing (#468) - Thanks @alamb -* Suppport table names (as well as subqueries) as source in `MERGE` statements (#483) - Thanks @mskrzypkows - - -### Fixed: -* `INTO` keyword is optional for `INSERT`, `MERGE` (#473) - Thanks @mobuchowski -* Support `IS TRUE` and `IS FALSE` expressions in boolean filter (#474) - Thanks @yuval-illumex -* Support fully qualified object names in `SET VARIABLE` (#484) - Thanks mobuchowski - -## [0.16.0] 2022-04-03 - -### Added - -* Support `WEEK` keyword in `EXTRACT` (#436) - Thanks @Ted-Jiang -* Support `MERGE` statement (#430) - Thanks @mobuchowski -* Support `SAVEPOINT` statement (#438) - Thanks @poonai -* Support `TO` clause in `COPY` (#441) - Thanks @matthewmturner -* Support `CREATE DATABASE` statement (#451) - Thanks @matthewmturner -* Support `FROM` clause in `UPDATE` statement (#450) - Thanks @slhmy -* Support additional `COPY` options (#446) - Thanks @wangrunji0408 - -### Fixed: -* Bug in array / map access parsing (#433) - Thanks @monadbobo - -## [0.15.0] 2022-03-07 - -### Added - -* Support for ClickHouse array types (e.g. [1,2,3]) (#429) - Thanks @monadbobo -* Support for `unsigned tinyint`, `unsigned int`, `unsigned smallint` and `unsigned bigint` datatypes (#428) - Thanks @watarukura -* Support additional keywords for `EXTRACT` (#427) - Thanks @mobuchowski -* Support IN UNNEST(expression) (#426) - Thanks @komukomo -* Support COLLATION keywork on CREATE TABLE (#424) - Thanks @watarukura -* Support FOR UPDATE/FOR SHARE clause (#418) - Thanks @gamife -* Support prepared statement placeholder arg `?` and `$` (#420) - Thanks @gamife -* Support array expressions such as `ARRAY[1,2]` , `foo[1]` and `INT[][]` (#419) - Thanks @gamife - -### Changed: -* remove Travis CI (#421) - Thanks @efx - -### Fixed: -* Allow `array` to be used as a function name again (#432) - @alamb -* Update docstring reference to `Query` (#423) - Thanks @max-sixty - -## [0.14.0] 2022-02-09 - -### Added -* Support `CURRENT_TIMESTAMP`, `CURRENT_TIME`, and `CURRENT_DATE` (#391) - Thanks @yuval-illumex -* SUPPORT `SUPER` keyword (#387) - Thanks @flaneur2020 -* Support differing orders of `OFFSET` `LIMIT` as well as `LIMIT` `OFFSET` (#413) - Thanks @yuval-illumex -* Support for `FROM `, `DELIMITER`, and `CSV HEADER` options for `COPY` command (#409) - Thanks @poonai -* Support `CHARSET` and `ENGINE` clauses on `CREATE TABLE` for mysql (#392) - Thanks @antialize -* Support `DROP CONSTRAINT [ IF EXISTS ] [ CASCADE ]` (#396) - Thanks @tvallotton -* Support parsing tuples and add `Expr::Tuple` (#414) - @alamb -* Support MySQL style `LIMIT X, Y` (#415) - @alamb -* Support `SESSION TRANSACTION` and `TRANSACTION SNAPSHOT`. (#379) - Thanks @poonai -* Support `ALTER COLUMN` and `RENAME CONSTRAINT` (#381) - Thanks @zhamlin -* Support for Map access, add ClickHouse dialect (#382) - Thanks @monadbobo - -### Changed -* Restrict where wildcard (`*`) can appear, add to `FunctionArgExpr` remove `Expr::[Qualified]Wildcard`, (#378) - Thanks @panarch -* Update simple_logger requirement from 1.9 to 2.1 (#403) -* export all methods of parser (#397) - Thanks @neverchanje! -* Clarify maintenance status on README (#416) - @alamb - -### Fixed -* Fix new clippy errors (#412) - @alamb -* Fix panic with `GRANT/REVOKE` in `CONNECT`, `CREATE`, `EXECUTE` or `TEMPORARY` - Thanks @evgenyx00 -* Handle double quotes inside quoted identifiers correctly (#411) - Thanks @Marwes -* Handle mysql backslash escaping (#373) - Thanks @vasilev-alex - -## [0.13.0] 2021-12-10 - -### Added -* Add ALTER TABLE CHANGE COLUMN, extend the UPDATE statement with ON clause (#375) - Thanks @0xA537FD! -* Add support for GROUPIING SETS, ROLLUP and CUBE - Thanks @Jimexist! -* Add basic support for GRANT and REVOKE (#365) - Thanks @blx! - -### Changed -* Use Rust 2021 edition (#368) - Thanks @Jimexist! - -### Fixed -* Fix clippy errors (#367, #374) - Thanks @Jimexist! - - -## [0.12.0] 2021-10-14 - -### Added -* Add support for [NOT] IS DISTINCT FROM (#306) - @Dandandan - -### Changed -* Move the keywords module - Thanks @koushiro! - - -## [0.11.0] 2021-09-24 - -### Added -* Support minimum display width for integer data types (#337) Thanks @vasilev-alex! -* Add logical XOR operator (#357) - Thanks @xzmrdltl! -* Support DESCRIBE table_name (#340) - Thanks @ovr! -* Support SHOW CREATE TABLE|EVENT|FUNCTION (#338) - Thanks @ovr! -* Add referential actions to TableConstraint foreign key (#306) - Thanks @joshwd36! - -### Changed -* Enable map access for numbers, multiple nesting levels (#356) - Thanks @Igosuki! -* Rename Token::Mult to Token::Mul (#353) - Thanks @koushiro! -* Use derive(Default) for HiveFormat (#348) - Thanks @koushiro! -* Improve tokenizer error (#347) - Thanks @koushiro! -* Eliminate redundant string copy in Tokenizer (#343) - Thanks @koushiro! -* Update bigdecimal requirement from 0.2 to 0.3 dependencies (#341) -* Support parsing hexadecimal literals that start with `0x` (#324) - Thanks @TheSchemm! - - -## [0.10.0] 2021-08-23 - -### Added -* Support for `no_std` (#332) - Thanks @koushiro! -* Postgres regular expression operators (`~`, `~*`, `!~`, `!~*`) (#328) - Thanks @b41sh! -* tinyint (#320) - Thanks @sundy-li -* ILIKE (#300) - Thanks @maxcountryman! -* TRIM syntax (#331, #334) - Thanks ever0de - - -### Fixed -* Return error instead of panic (#316) - Thanks @BohuTANG! - -### Changed -- Rename `Modulus` to `Modulo` (#335) - Thanks @RGRAVITY817! -- Update links to reflect repository move to `sqlparser-rs` GitHub org (#333) - Thanks @andygrove -- Add default value for `WindowFrame` (#313) - Thanks @Jimexist! - -## [0.9.0] 2021-03-21 - -### Added -* Add support for `TRY_CAST` syntax (#299) - Thanks @seddonm1! - -## [0.8.0] 2021-02-20 - -### Added -* Introduce Hive QL dialect `HiveDialect` and syntax (#235) - Thanks @hntd187! -* Add `SUBSTRING(col [FROM ] [FOR ])` syntax (#293) -* Support parsing floats without leading digits `.01` (#294) -* Support parsing multiple show variables (#290) - Thanks @francis-du! -* Support SQLite `INSERT OR [..]` syntax (#281) - Thanks @zhangli-pear! - -## [0.7.0] 2020-12-28 - -### Changed -- Change the MySQL dialect to support `` `identifiers` `` quoted with backticks instead of the standard `"double-quoted"` identifiers (#247) - thanks @mashuai! -- Update bigdecimal requirement from 0.1 to 0.2 (#268) - -### Added -- Enable dialect-specific behaviours in the parser (`dialect_of!()`) (#254) - thanks @eyalleshem! -- Support named arguments in function invocations (`ARG_NAME => val`) (#250) - thanks @eyalleshem! -- Support `TABLE()` functions in `FROM` (#253) - thanks @eyalleshem! -- Support Snowflake's single-line comments starting with '#' or '//' (#264) - thanks @eyalleshem! -- Support PostgreSQL `PREPARE`, `EXECUTE`, and `DEALLOCATE` (#243) - thanks @silathdiir! -- Support PostgreSQL math operators (#267) - thanks @alex-dukhno! -- Add SQLite dialect (#248) - thanks @mashuai! -- Add Snowflake dialect (#259) - thanks @eyalleshem! -- Support for Recursive CTEs - thanks @rhanqtl! -- Support `FROM (table_name) alias` syntax - thanks @eyalleshem! -- Support for `EXPLAIN [ANALYZE] VERBOSE` - thanks @ovr! -- Support `ANALYZE TABLE` -- DDL: - - Support `OR REPLACE` in `CREATE VIEW`/`TABLE` (#239) - thanks @Dandandan! - - Support specifying `ASC`/`DESC` in index columns (#249) - thanks @mashuai! - - Support SQLite `AUTOINCREMENT` and MySQL `AUTO_INCREMENT` column option in `CREATE TABLE` (#234) - thanks @mashuai! - - Support PostgreSQL `IF NOT EXISTS` for `CREATE SCHEMA` (#276) - thanks @alex-dukhno! - -### Fixed -- Fix a typo in `JSONFILE` serialization, introduced in 0.3.1 (#237) -- Change `CREATE INDEX` serialization to not end with a semicolon, introduced in 0.5.1 (#245) -- Don't fail parsing `ALTER TABLE ADD COLUMN` ending with a semicolon, introduced in 0.5.1 (#246) - thanks @mashuai - -## [0.6.1] - 2020-07-20 - -### Added -- Support BigQuery `ASSERT` statement (#226) - -## [0.6.0] - 2020-07-20 - -### Added -- Support SQLite's `CREATE TABLE (...) WITHOUT ROWID` (#208) - thanks @mashuai! -- Support SQLite's `CREATE VIRTUAL TABLE` (#209) - thanks @mashuai! - -## [0.5.1] - 2020-06-26 -This release should have been called `0.6`, as it introduces multiple incompatible changes to the API. If you don't want to upgrade yet, you can revert to the previous version by changing your `Cargo.toml` to: - - sqlparser = "= 0.5.0" - - -### Changed -- **`Parser::parse_sql` now accepts a `&str` instead of `String` (#182)** - thanks @Dandandan! -- Change `Ident` (previously a simple `String`) to store the parsed (unquoted) `value` of the identifier and the `quote_style` separately (#143) - thanks @apparebit! -- Support Snowflake's `FROM (table_name)` (#155) - thanks @eyalleshem! -- Add line and column number to TokenizerError (#194) - thanks @Dandandan! -- Use Token::EOF instead of Option (#195) -- Make the units keyword following `INTERVAL '...'` optional (#184) - thanks @maxcountryman! -- Generalize `DATE`/`TIME`/`TIMESTAMP` literals representation in the AST (`TypedString { data_type, value }`) and allow `DATE` and other keywords to be used as identifiers when not followed by a string (#187) - thanks @maxcountryman! -- Output DataType capitalized (`fmt::Display`) (#202) - thanks @Dandandan! - -### Added -- Support MSSQL `TOP () [ PERCENT ] [ WITH TIES ]` (#150) - thanks @alexkyllo! -- Support MySQL `LIMIT row_count OFFSET offset` (not followed by `ROW` or `ROWS`) and remember which variant was parsed (#158) - thanks @mjibson! -- Support PostgreSQL `CREATE TABLE IF NOT EXISTS table_name` (#163) - thanks @alex-dukhno! -- Support basic forms of `CREATE INDEX` and `DROP INDEX` (#167) - thanks @mashuai! -- Support `ON { UPDATE | DELETE } { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` in `FOREIGN KEY` constraints (#170) - thanks @c7hm4r! -- Support basic forms of `CREATE SCHEMA` and `DROP SCHEMA` (#173) - thanks @alex-dukhno! -- Support `NULLS FIRST`/`LAST` in `ORDER BY` expressions (#176) - thanks @houqp! -- Support `LISTAGG()` (#174) - thanks @maxcountryman! -- Support the string concatentation operator `||` (#178) - thanks @Dandandan! -- Support bitwise AND (`&`), OR (`|`), XOR (`^`) (#181) - thanks @Dandandan! -- Add serde support to AST structs and enums (#196) - thanks @panarch! -- Support `ALTER TABLE ADD COLUMN`, `RENAME COLUMN`, and `RENAME TO` (#203) - thanks @mashuai! -- Support `ALTER TABLE DROP COLUMN` (#148) - thanks @ivanceras! -- Support `CREATE TABLE ... AS ...` (#206) - thanks @Dandandan! - -### Fixed -- Report an error for unterminated string literals (#165) -- Make file format (`STORED AS`) case insensitive (#200) and don't allow quoting it (#201) - thanks @Dandandan! - -## [0.5.0] - 2019-10-10 - -### Changed -- Replace the `Value::Long(u64)` and `Value::Double(f64)` variants with `Value::Number(String)` to avoid losing precision when parsing decimal literals (#130) - thanks @benesch! -- `--features bigdecimal` can be enabled to work with `Value::Number(BigDecimal)` instead, at the cost of an additional dependency. - -### Added -- Support MySQL `SHOW COLUMNS`, `SET =`, and `SHOW ` statements (#135) - thanks @quodlibetor and @benesch! - -### Fixed -- Don't fail to parse `START TRANSACTION` followed by a semicolon (#139) - thanks @gaffneyk! - - -## [0.4.0] - 2019-07-02 -This release brings us closer to SQL-92 support, mainly thanks to the improvements contributed back from @MaterializeInc's fork and other work by @benesch. - -### Changed -- Remove "SQL" from type and enum variant names, `SQLType` -> `DataType`, remove "sql" prefix from module names (#105, #122) -- Rename `ASTNode` -> `Expr` (#119) -- Improve consistency of binary/unary op nodes (#112): - - `ASTNode::SQLBinaryExpr` is now `Expr::BinaryOp` and `ASTNode::SQLUnary` is `Expr::UnaryOp`; - - The `op: SQLOperator` field is now either a `BinaryOperator` or an `UnaryOperator`. -- Change the representation of JOINs to match the standard (#109): `SQLSelect`'s `relation` and `joins` are replaced with `from: Vec`. Before this change `FROM foo NATURAL JOIN bar, baz` was represented as "foo" as the `relation` followed by two joins (`Inner(Natural)` and `Implicit`); now it's two `TableWithJoins` (`foo NATURAL JOIN bar` and `baz`). -- Extract a `SQLFunction` struct (#89) -- Replace `Option>` with `Vec` in the AST structs (#73) -- Change `Value::Long()` to be unsigned, use u64 consistently (#65) - -### Added -- Infra: - - Implement `fmt::Display` on AST nodes (#124) - thanks @vemoo! - - Implement `Hash` (#88) and `Eq` (#123) on all AST nodes - - Implement `std::error::Error` for `ParserError` (#72) - - Handle Windows line-breaks (#54) -- Expressions: - - Support `INTERVAL` literals (#103) - - Support `DATE` / `TIME` / `TIMESTAMP` literals (#99) - - Support `EXTRACT` (#96) - - Support `X'hex value'` literals (#95) - - Support `EXISTS` subqueries (#90) - - Support nested expressions in `BETWEEN` (#80) - - Support `COUNT(DISTINCT x)` and similar (#77) - - Support `CASE operand WHEN expected_value THEN ..` and table-valued functions (#59) - - Support analytic (window) functions (`OVER` clause) (#50) -- Queries / DML: - - Support nested joins (#100) and derived tables with set operations (#111) - - Support `UPDATE` statements (#97) - - Support `INSERT INTO foo SELECT * FROM bar` and `FROM VALUES (...)` (#91) - - Support `SELECT ALL` (#76) - - Add `FETCH` and `OFFSET` support, and `LATERAL` (#69) - thanks @thomas-jeepe! - - Support `COLLATE`, optional column list in CTEs (#64) -- DDL/TCL: - - Support `START/SET/COMMIT/ROLLBACK TRANSACTION` (#106) - thanks @SamuelMarks! - - Parse column constraints in any order (#93) - - Parse `DECIMAL` and `DEC` aliases for `NUMERIC` type (#92) - - Support `DROP [TABLE|VIEW]` (#75) - - Support arbitrary `WITH` options for `CREATE [TABLE|VIEW]` (#74) - - Support constraints in `CREATE TABLE` (#65) -- Add basic MSSQL dialect (#61) and some MSSQL-specific features: - - `CROSS`/`OUTER APPLY` (#120) - - MSSQL identifier and alias parsing rules (#66) - - `WITH` hints (#59) - -### Fixed -- Report an error for `SELECT * FROM a OUTER JOIN b` instead of parsing `OUTER` as an alias (#118) -- Fix the precedence of `NOT LIKE` (#82) and unary `NOT` (#107) -- Do not panic when `NOT` is not followed by an expected keyword (#71) -successfully instead of returning a parse error - thanks @ivanceras! (#67) - and similar fixes for queries with no `FROM` (#116) -- Fix issues with `ALTER TABLE ADD CONSTRAINT` parsing (#65) -- Serialize the "not equals" operator as `<>` instead of `!=` (#64) -- Remove dependencies on `uuid` (#59) and `chrono` (#61) -- Make `SELECT` query with `LIMIT` clause but no `WHERE` parse - Fix incorrect behavior of `ASTNode::SQLQualifiedWildcard::to_string()` (returned `foo*` instead of `foo.*`) - thanks @thomas-jeepe! (#52) - -## [0.3.1] - 2019-04-20 -### Added -- Extended `SQLStatement::SQLCreateTable` to support Hive's EXTERNAL TABLES (`CREATE EXTERNAL TABLE .. STORED AS .. LOCATION '..'`) - thanks @zhzy0077! (#46) -- Parse `SELECT DISTINCT` to `SQLSelect::distinct` (#49) - -## [0.3.0] - 2019-04-03 -### Changed -This release includes major changes to the AST structs to add a number of features, as described in #37 and #43. In particular: -- `ASTNode` variants that represent statements were extracted from `ASTNode` into a separate `SQLStatement` enum; - - `Parser::parse_sql` now returns a `Vec` of parsed statements. - - `ASTNode` now represents an expression (renamed to `Expr` in 0.4.0) -- The query representation (formerly `ASTNode::SQLSelect`) became more complicated to support: - - `WITH` and `UNION`/`EXCEPT`/`INTERSECT` (via `SQLQuery`, `Cte`, and `SQLSetExpr`), - - aliases and qualified wildcards in `SELECT` (via `SQLSelectItem`), - - and aliases in `FROM`/`JOIN` (via `TableFactor`). -- A new `SQLObjectName` struct is used instead of `String` or `ASTNode::SQLCompoundIdentifier` - for objects like tables, custom types, etc. -- Added support for "delimited identifiers" and made keywords context-specific (thus accepting them as valid identifiers in most contexts) - **this caused a regression in parsing `SELECT .. FROM .. LIMIT ..` (#67), fixed in 0.4.0** - -### Added -Other than the changes listed above, some less intrusive additions include: -- Support `CREATE [MATERIALIZED] VIEW` statement -- Support `IN`, `BETWEEN`, unary +/- in epressions -- Support `CHAR` data type and `NUMERIC` not followed by `(p,s)`. -- Support national string literals (`N'...'`) - -## [0.2.4] - 2019-03-08 -Same as 0.2.2. - -## [0.2.3] - 2019-03-08 [YANKED] - -## [0.2.2] - 2019-03-08 -### Changed -- Removed `Value::String`, `Value::DoubleQuotedString`, and `Token::String`, making - - `'...'` parse as a string literal (`Value::SingleQuotedString`), and - - `"..."` fail to parse until version 0.3.0 (#36) - -## [0.2.1] - 2019-01-13 -We don't have a changelog for the changes made in 2018, but thanks to @crw5996, @cswinter, @fredrikroos, @ivanceras, @nickolay, @virattara for their contributions in the early stages of the project! +technically be breaking and thus will result in a `0.(N+1)` version. -## [0.1.0] - 2018-09-03 -Initial release +- Unreleased: Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. +- `0.51.0` and earlier: [changelog/0.51.0-pre.md](changelog/0.51.0-pre.md) \ No newline at end of file diff --git a/changelog/0.51.0-pre.md b/changelog/0.51.0-pre.md new file mode 100644 index 000000000..18c7aefb6 --- /dev/null +++ b/changelog/0.51.0-pre.md @@ -0,0 +1,1188 @@ + + + +## [0.51.0] 2024-09-11 +As always, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs 🙏. +Without them this project would not be possible. + +Reminder: we are in the final phases of moving sqlparser-rs into the Apache +DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 + +### Fixed +* Fix Hive table comment should be after table column definitions (#1413) - Thanks @git-hulk +* Fix stack overflow in `parse_subexpr` (#1410) - Thanks @eejbyfeldt +* Fix `INTERVAL` parsing to support expressions and units via dialect (#1398) - Thanks @samuelcolvin +* Fix identifiers starting with `$` should be regarded as a placeholder in SQLite (#1402) - Thanks @git-hulk + +### Added +* Support for MSSQL table options (#1414) - Thanks @bombsimon +* Test showing how negative constants are parsed (#1421) - Thanks @alamb +* Support databricks dialect to dialect_from_str (#1416) - Thanks @milenkovicmalamb +* Support `DROP|CLEAR|MATERIALIZE PROJECTION` syntax for ClickHouse (#1417) - Thanks @git-hulk +* Support postgres `TRUNCATE` syntax (#1406) - Thanks @tobyhede +* Support `CREATE INDEX` with clause (#1389) - Thanks @lewiszlw +* Support parsing `CLUSTERED BY` clause for Hive (#1397) - Thanks @git-hulk +* Support different `USE` statement syntaxes (#1387) - Thanks @kacpermuda +* Support `ADD PROJECTION` syntax for ClickHouse (#1390) - Thanks @git-hulk + +### Changed +* Implement common traits for OneOrManyWithParens (#1368) - Thanks @gstvg +* Cleanup parse_statement (#1407) - Thanks @samuelcolvin +* Allow `DateTimeField::Custom` with `EXTRACT` in Postgres (#1394) - Thanks @samuelcolvin + + +## [0.50.0] 2024-08-15 +Again, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs 🙏. +Without them this project would not be possible. + +Reminder: are in the process of moving sqlparser to governed as part of the Apache +DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 + +### Fixed +* Clippy 1.80 warnings (#1357) - Thanks @lovasoa + +### Added +* Support `STRUCT` and list of structs for DuckDB dialect (#1372) - Thanks @jayzhan211 +* Support custom lexical precedence in PostgreSQL dialect (#1379) - Thanks @samuelcolvin +* Support `FREEZE|UNFREEZE PARTITION` syntax for ClickHouse (#1380) - Thanks @git-hulk +* Support scale in `CEIL` and `FLOOR` functions (#1377) - Thanks @seve-martinez +* Support `CREATE TRIGGER` and `DROP TRIGGER` statements (#1352) - Thanks @LucaCappelletti94 +* Support `EXTRACT` syntax for snowflake (#1374) - Thanks @seve-martinez +* Support `ATTACH` / `DETACH PARTITION` for ClickHouse (#1362) - Thanks @git-hulk +* Support Dialect level precedence, update Postgres Dialect to match Postgres (#1360) - Thanks @samuelcolvin +* Support parsing empty map literal syntax for DuckDB and Generic dialects (#1361) - Thanks @goldmedal +* Support `SETTINGS` clause for ClickHouse table-valued functions (#1358) - Thanks @Jesse-Bakker +* Support `OPTIMIZE TABLE` statement for ClickHouse (#1359) - Thanks @git-hulk +* Support `ON CLUSTER` in `ALTER TABLE` for ClickHouse (#1342) - Thanks @git-hulk +* Support `GLOBAL` keyword before the join operator (#1353) - Thanks @git-hulk +* Support postgres String Constants with Unicode Escapes (#1355) - Thanks @lovasoa +* Support position with normal function call syntax for Snowflake (#1341) - Thanks @jmhain +* Support `TABLE` keyword in `DESC|DESCRIBE|EXPLAIN TABLE` statement (#1351) - Thanks @git-hulk + +### Changed +* Only require `DESCRIBE TABLE` for Snowflake and ClickHouse dialect (#1386) - Thanks @ alamb +* Rename (unreleased) `get_next_precedence_full` to `get_next_precedence_default` (#1378) - Thanks @samuelcolvin +* Use local GitHub Action to replace setup-rust-action (#1371) - Thanks @git-hulk +* Simplify arrow_cast tests (#1367) - Thanks @alamb +* Update version of GitHub Actions (#1363) - Thanks @git-hulk +* Make `Parser::maybe_parse` pub (#1364) - Thanks @Jesse-Bakker +* Improve comments on 1Dialect` (#1366) - Thanks @alamb + + +## [0.49.0] 2024-07-23 +As always, huge props to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs! + +We are in the process of moving sqlparser to governed as part of the Apache +DataFusion project: https://github.com/sqlparser-rs/sqlparser-rs/issues/1294 + +### Fixed +* Fix quoted identifier regression edge-case with "from" in SELECT (#1346) - Thanks @alexander-beedie +* Fix `AS` query clause should be after the create table options (#1339) - Thanks @git-hulk + +### Added + +* Support `MATERIALIZED`/`ALIAS`/`EPHERMERAL` default column options for ClickHouse (#1348) - Thanks @git-hulk +* Support `()` as the `GROUP BY` nothing (#1347) - Thanks @git-hulk +* Support Map literal syntax for DuckDB and Generic (#1344) - Thanks @goldmedal +* Support subquery expression in `SET` expressions (#1343) - Thanks @iffyio +* Support `WITH FILL` for ClickHouse (#1330) - Thanks @nickpresta +* Support `PARTITION BY` for PostgreSQL in `CREATE TABLE` statement (#1338) - Thanks @git-hulk +* Support of table function `WITH ORDINALITY` modifier for Postgres (#1337) - Thanks @git-hulk + + +## [0.48.0] 2024-07-09 + +Huge shout out to @iffyio @jmhain and @lovasoa for their help reviewing and merging PRs! + +### Fixed +* Fix CI error message in CI (#1333) - Thanks @alamb +* Fix typo in sqlparser-derive README (#1310) - Thanks @leoyvens +* Re-enable trailing commas in DCL (#1318) - Thanks @MohamedAbdeen21 +* Fix a few typos in comment lines (#1316) - Thanks @git-hulk +* Fix Snowflake `SELECT * wildcard REPLACE ... RENAME` order (#1321) - Thanks @alexander-beedie +* Allow semi-colon at the end of UNCACHE statement (#1320) - Thanks @LorrensP-2158466 +* Return errors, not panic, when integers fail to parse in `AUTO_INCREMENT` and `TOP` (#1305) - Thanks @eejbyfeldt + +### Added +* Support `OWNER TO` clause in Postgres (#1314) - Thanks @gainings +* Support `FORMAT` clause for ClickHouse (#1335) - Thanks @git-hulk +* Support `DROP PROCEDURE` statement (#1324) - Thanks @LorrensP-2158466 +* Support `PREWHERE` condition for ClickHouse dialect (#1328) - Thanks @git-hulk +* Support `SETTINGS` pairs for ClickHouse dialect (#1327) - Thanks @git-hulk +* Support `GROUP BY WITH MODIFIER` for ClickHouse dialect (#1323) - Thanks @git-hulk +* Support DuckDB Union datatype (#1322) - Thanks @gstvg +* Support parametric arguments to `FUNCTION` for ClickHouse dialect (#1315) - Thanks @git-hulk +* Support `TO` in `CREATE VIEW` clause for Clickhouse (#1313) - Thanks @Bidaya0 +* Support `UPDATE` statements that contain tuple assignments (#1317) - Thanks @lovasoa +* Support `BY NAME quantifier across all set ops (#1309) - Thanks @alexander-beedie +* Support SnowFlake exclusive `CREATE TABLE` options (#1233) - Thanks @balliegojr +* Support ClickHouse `CREATE TABLE` with primary key and parametrised table engine (#1289) - Thanks @7phs +* Support custom operators in Postgres (#1302) - Thanks @lovasoa +* Support ClickHouse data types (#1285) - Thanks @7phs + +### Changed +* Add stale PR github workflow (#1331) - Thanks @alamb +* Refine docs (#1326) - Thanks @emilsivervik +* Improve error messages with additional colons (#1319) - Thanks @LorrensP-2158466 +* Move Display fmt to struct for `CreateIndex` (#1307) - Thanks @philipcristiano +* Enhancing Trailing Comma Option (#1212) - Thanks @MohamedAbdeen21 +* Encapsulate `CreateTable`, `CreateIndex` into specific structs (#1291) - Thanks @philipcristiano + +## [0.47.0] 2024-06-01 + +### Fixed +* Re-support Postgres array slice syntax (#1290) - Thanks @jmhain +* Fix DoubleColon cast skipping AT TIME ZONE #1266 (#1267) - Thanks @dmitrybugakov +* Fix for values as table name in Databricks and generic (#1278) - Thanks @jmhain + +### Added +* Support `ASOF` joins in Snowflake (#1288) - Thanks @jmhain +* Support `CREATE VIEW` with fields and data types ClickHouse (#1292) - Thanks @7phs +* Support view comments for Snowflake (#1287) - Thanks @bombsimon +* Support dynamic pivot in Snowflake (#1280) - Thanks @jmhain +* Support `CREATE FUNCTION` for BigQuery, generalize AST (#1253) - Thanks @iffyio +* Support expression in `AT TIME ZONE` and fix precedence (#1272) - Thanks @jmhain +* Support `IGNORE/RESPECT NULLS` inside function argument list for Databricks (#1263) - Thanks @jmhain +* Support `SELECT * EXCEPT` Databricks (#1261) - Thanks @jmhain +* Support triple quoted strings (#1262) - Thanks @iffyio +* Support array indexing for duckdb (#1265) - Thanks @JichaoS +* Support multiple SET variables (#1252) - Thanks @iffyio +* Support `ANY_VALUE` `HAVING` clause (#1258) in BigQuery - Thanks @jmhain +* Support keywords as field names in BigQuery struct syntax (#1254) - Thanks @iffyio +* Support `GROUP_CONCAT()` in MySQL (#1256) - Thanks @jmhain +* Support lambda functions in Databricks (#1257) - Thanks @jmhain +* Add const generic peek_tokens method to parser (#1255) - Thanks @jmhain + + +## [0.46.0] 2024-05-03 + +### Changed +* Consolidate representation of function calls, remove `AggregateExpressionWithFilter`, `ArraySubquery`, `ListAgg` and `ArrayAgg` (#1247) - Thanks jmhain +* Extended dialect trait to support numeric prefixed identifiers (#1188) - Thanks @groobyming +* Update simple_logger requirement from 4.0 to 5.0 (#1246) - Thanks @dependabot +* Improve parsing of JSON accesses on Postgres and Snowflake (#1215) - Thanks @jmhain +* Encapsulate Insert and Delete into specific structs (#1224) - Thanks @tisonkun +* Preserve double colon casts (and simplify cast representations) (#1221) - Thanks @jmhain + +### Fixed +* Fix redundant brackets in Hive/Snowflake/Redshift (#1229) - Thanks @yuval-illumex + +### Added +* Support values without parens in Snowflake and DataBricks (#1249) - Thanks @HiranmayaGundu +* Support WINDOW clause after QUALIFY when parsing (#1248) - Thanks @iffyio +* Support `DECLARE` parsing for mssql (#1235) - Thanks @devanbenz +* Support `?`-based jsonb operators in Postgres (#1242) - THanks @ReppCodes +* Support Struct datatype parsing for GenericDialect (#1241) - Thanks @duongcongtoai +* Support BigQuery window function null treatment (#1239) - Thanks @iffyio +* Support extend pivot operator - Thanks @iffyio +* Support Databricks SQL dialect (#1220) - Thanks @jmhain +* Support for MSSQL CONVERT styles (#1219) - Thanks @iffyio +* Support window clause using named window in BigQuery (#1237) - Thanks @iffyio +* Support for CONNECT BY (#1138) - Thanks @jmhain +* Support object constants in Snowflake (#1223) - Thanks @jmhain +* Support BigQuery MERGE syntax (#1217) - Thanks @iffyio +* Support for MAX for NVARCHAR (#1232) - Thanks @ bombsimon +* Support fixed size list types (#1231) - @@universalmind303 +* Support Snowflake MATCH_RECOGNIZE syntax (#1222) - Thanks @jmhain +* Support quoted string backslash escaping (#1177) - Thanks @iffyio +* Support Modify Column for MySQL dialect (#1216) - Thanks @KKould +* Support `select * ilike` for snowflake (#1228) - Thanks @HiranmayaGundu +* Support wildcard replace in duckdb and snowflake syntax (#1226) - Thanks @HiranmayaGundu + + + +## [0.45.0] 2024-04-12 + +### Added +* Support `DateTimeField` variants: `CUSTOM` and `WEEK(MONDAY)` (#1191) - Thanks @iffyio +* Support for arbitrary expr in `MapAccessSyntax` (#1179) - Thanks @iffyio +* Support unquoted hyphen in table/view declaration for BigQuery (#1178) - Thanks @iffyio +* Support `CREATE/DROP SECRET` for duckdb dialect (#1208) - Thanks @JichaoS +* Support MySQL `UNIQUE` table constraint (#1164) - Thanks @Nikita-str +* Support tailing commas on Snowflake. (#1205) - Thanks @yassun7010 +* Support `[FIRST | AFTER column_name]` in `ALTER TABLE` for MySQL (#1180) - Thanks @xring +* Support inline comment with hash syntax for BigQuery (#1192) - Thanks @iffyio +* Support named windows in OVER (window_definition) clause (#1166) - Thanks @Nikita-str +* Support PARALLEL ... and for ..ON NULL INPUT ... to CREATE FUNCTION` (#1202) - Thanks @dimfeld +* Support DuckDB functions named arguments with assignment operator (#1195) - Thanks @alamb +* Support DuckDB struct literal syntax (#1194) - Thanks @gstvg +* Support `$$` in generic dialect ... (#1185)- Thanks @milenkovicm +* Support row_alias and col_aliases in `INSERT` statement for MySQL and Generic dialects (#1136) - Thanks @emin100 + +### Fixed +* Fix dollar quoted string tokenizer (#1193) - Thanks @ZacJW +* Do not allocate in `impl Display` for `DateTimeField` (#1209) - Thanks @alamb +* Fix parse `COPY INTO` stage names without parens for SnowFlake (#1187) - Thanks @mobuchowski +* Solve stack overflow on RecursionLimitExceeded on debug builds (#1171) - Thanks @Nikita-str +* Fix parsing of equality binary operator in function argument (#1182) - Thanks @jmhain +* Fix some comments (#1184) - Thanks @sunxunle + +### Changed +* Cleanup `CREATE FUNCTION` tests (#1203) - Thanks @alamb +* Parse `SUBSTRING FROM` syntax in all dialects, reflect change in the AST (#1173) - Thanks @lovasoa +* Add identifier quote style to Dialect trait (#1170) - Thanks @backkem + +## [0.44.0] 2024-03-02 + +### Added +* Support EXPLAIN / DESCR / DESCRIBE [FORMATTED | EXTENDED] (#1156) - Thanks @jonathanlehtoalamb +* Support ALTER TABLE ... SET LOCATION (#1154) - Thanks @jonathanlehto +* Support `ROW FORMAT DELIMITED` in Hive (#1155) - Thanks @jonathanlehto +* Support `SERDEPROPERTIES` for `CREATE TABLE` with Hive (#1152) - Thanks @jonathanlehto +* Support `EXECUTE ... USING` for Postgres (#1153) - Thanks @jonathanlehto +* Support Postgres style `CREATE FUNCTION` in GenericDialect (#1159) - Thanks @alamb +* Support `SET TBLPROPERTIES` (#1151) - Thanks @jonathanlehto +* Support `UNLOAD` statement (#1150) - Thanks @jonathanlehto +* Support `MATERIALIZED CTEs` (#1148) - Thanks @ReppCodes +* Support `DECLARE` syntax for snowflake and bigquery (#1122) - Thanks @iffyio +* Support `SELECT AS VALUE` and `SELECT AS STRUCT` for BigQuery (#1135) - Thanks @lustefaniak +* Support `(+)` outer join syntax (#1145) - Thanks @jmhain +* Support `INSERT INTO ... SELECT ... RETURNING`(#1132) - Thanks @lovasoa +* Support DuckDB `INSTALL` and `LOAD` (#1127) - Thanks @universalmind303 +* Support `=` operator in function args (#1128) - Thanks @universalmind303 +* Support `CREATE VIEW IF NOT EXISTS` (#1118) - Thanks @7phs +* Support `UPDATE FROM` for SQLite (further to #694) (#1117) - Thanks @ggaughan +* Support optional `DELETE FROM` statement (#1120) - Thanks @iffyio +* Support MySQL `SHOW STATUS` statement (#1119) - Thanks invm + +### Fixed +* Clean up nightly clippy lints (#1158) - Thanks @alamb +* Handle escape, unicode, and hex in tokenize_escaped_single_quoted_string (#1146) - Thanks @JasonLi-cn +* Fix panic while parsing `REPLACE` (#1140) - THanks @jjbayer +* Fix clippy warning from rust 1.76 (#1130) - Thanks @alamb +* Fix release instructions (#1115) - Thanks @alamb + +### Changed +* Add `parse_keyword_with_tokens` for paring keyword and tokens combination (#1141) - Thanks @viirya +* Add ParadeDB to list of known users (#1142) - Thanks @philippemnoel +* Accept JSON_TABLE both as an unquoted table name and a table-valued function (#1134) - Thanks @lovasoa + + +## [0.43.1] 2024-01-22 +### Changes +* Fixed CHANGELOG + + +## [0.43.0] 2024-01-22 +* NO CHANGES + +## [0.42.0] 2024-01-22 + +### Added +* Support for constraint `CHARACTERISTICS` clause (#1099) - Thanks @dimfeld +* Support for unquoted hyphenated identifiers on bigquery (#1109) - Thanks @jmhain +* Support `BigQuery` table and view options (#1061) - Thanks @iffyio +* Support Postgres operators for the LIKE expression variants (#1096) - Thanks @gruuya +* Support "timezone_region" and "timezone_abbr" for `EXTRACT` (and `DATE_PART`) (#1090) - Thanks @alexander-beedie +* Support `JSONB` datatype (#1089) - Thanks @alexander-beedie +* Support PostgreSQL `^@` starts-with operator (#1091) - Thanks @alexander-beedie +* Support PostgreSQL Insert table aliases (#1069) (#1084) - Thanks @boydjohnson +* Support PostgreSQL `CREATE EXTENSION` (#1078) - Thanks @tobyhede +* Support PostgreSQL `ADD GENERATED` in `ALTER COLUMN` statements (#1079) - Thanks @tobyhede +* Support SQLite column definitions with no type (#1075) - Thanks @takluyver +* Support PostgreSQL `ENABLE` and `DISABLE` on `ALTER TABLE` (#1077) - Thanks @tobyhede +* Support MySQL `FLUSH` statement (#1076) - Thanks @emin100 +* Support Mysql `REPLACE` statement and `PRIORITY` clause of `INSERT` (#1072) - Thanks @emin100 + +### Fixed +* Fix `:start` and `:end` json accesses on SnowFlake (#1110) - Thanks @jmhain +* Fix array_agg wildcard behavior (#1093) - Thanks @ReppCodes +* Error on dangling `NO` in `CREATE SEQUENCE` options (#1104) - Thanks @PartiallyTyped +* Allow string values in `PRAGMA` commands (#1101) - Thanks @invm + +### Changed +* Use `Option` for Min and Max vals in Seq Opts, fix alter col seq display (#1106) - Thanks @PartiallyTyped +* Replace `AtomicUsize` with Cell in the recursion counter (#1098) - Thanks @wzzzzd +* Add Qrlew as a user in README.md (#1107) - Thanks @ngrislain +* Add APIs to reuse token buffers in `Tokenizer` (#1094) - Thanks @0rphon +* Bump version of `sqlparser-derive` to 0.2.2 (#1083) - Thanks @alamb + +## [0.41.0] 2023-12-22 + +### Added +* Support `DEFERRED`, `IMMEDIATE`, and `EXCLUSIVE` in SQLite's `BEGIN TRANSACTION` command (#1067) - Thanks @takaebato +* Support generated columns skipping `GENERATED ALWAYS` keywords (#1058) - Thanks @takluyver +* Support `LOCK/UNLOCK TABLES` for MySQL (#1059) - Thanks @zzzdong +* Support `JSON_TABLE` (#1062) - Thanks @lovasoa +* Support `CALL` statements (#1063) - Thanks @lovasoa + +### Fixed +* fix rendering of SELECT TOP (#1070) for Snowflake - Thanks jmhain + +### Changed +* Improve documentation formatting (#1068) - Thanks @alamb +* Replace type_id() by trait method to allow wrapping dialects (#1065) - Thanks @jjbayer +* Document that comments aren't preserved for round trip (#1060) - Thanks @takluyver +* Update sqlparser-derive to use `syn 2.0` (#1040) - Thanks @serprex + +## [0.40.0] 2023-11-27 + +### Added +* Add `{pre,post}_visit_query` to `Visitor` (#1044) - Thanks @jmhain +* Support generated virtual columns with expression (#1051) - Thanks @takluyver +* Support PostgreSQL `END` (#1035) - Thanks @tobyhede +* Support `INSERT INTO ... DEFAULT VALUES ...` (#1036) - Thanks @CDThomas +* Support `RELEASE` and `ROLLBACK TO SAVEPOINT` (#1045) - Thanks @CDThomas +* Support `CONVERT` expressions (#1048) - Thanks @lovasoa +* Support `GLOBAL` and `SESSION` parts in `SHOW VARIABLES` for mysql and generic - Thanks @emin100 +* Support snowflake `PIVOT` on derived table factors (#1027) - Thanks @lustefaniak +* Support mssql json and xml extensions (#1043) - Thanks @lovasoa +* Support for `MAX` as a character length (#1038) - Thanks @lovasoa +* Support `IN ()` syntax of SQLite (#1028) - Thanks @alamb + +### Fixed +* Fix extra whitespace printed before `ON CONFLICT` (#1037) - Thanks @CDThomas + +### Changed +* Document round trip ability (#1052) - Thanks @alamb +* Add PRQL to list of users (#1031) - Thanks @vanillajonathan + +## [0.39.0] 2023-10-27 + +### Added +* Support for `LATERAL FLATTEN` and similar (#1026) - Thanks @lustefaniak +* Support BigQuery struct, array and bytes , int64, `float64` datatypes (#1003) - Thanks @iffyio +* Support numbers as placeholders in Snowflake (e.g. `:1)` (#1001) - Thanks @yuval-illumex +* Support date 'key' when using semi structured data (#1023) @yuval-illumex +* Support IGNORE|RESPECT NULLs clause in window functions (#998) - Thanks @yuval-illumex +* Support for single-quoted identifiers (#1021) - Thanks @lovasoa +* Support multiple PARTITION statements in ALTER TABLE ADD statement (#1011) - Thanks @bitemyapp +* Support "with" identifiers surrounded by backticks in GenericDialect (#1010) - Thanks @bitemyapp +* Support INSERT IGNORE in MySql and GenericDialect (#1004) - Thanks @emin100 +* Support SQLite `pragma` statement (#969) - Thanks @marhoily +* Support `position` as a column name (#1022) - Thanks @lustefaniak +* Support `FILTER` in Functions (for `OVER`) clause (#1007) - Thanks @lovasoa +* Support `SELECT * EXCEPT/REPLACE` syntax from ClickHouse (#1013) - Thanks @lustefaniak +* Support subquery as function arg w/o parens in Snowflake dialect (#996) - Thanks @jmhain +* Support `UNION DISTINCT BY NAME` syntax (#997) - Thanks @alexander-beedie +* Support mysql `RLIKE` and `REGEXP` binary operators (#1017) - Thanks @lovasoa +* Support bigquery `CAST AS x [STRING|DATE] FORMAT` syntax (#978) - Thanks @lustefaniak +* Support Snowflake/BigQuery `TRIM`. (#975) - Thanks @zdenal +* Support `CREATE [TEMPORARY|TEMP] VIEW [IF NOT EXISTS] `(#993) - Thanks @gabivlj +* Support for `CREATE VIEW … WITH NO SCHEMA BINDING` Redshift (#979) - Thanks @lustefaniak +* Support `UNPIVOT` and a fix for chained PIVOTs (#983) - @jmhain +* Support for `LIMIT BY` (#977) - Thanks @lustefaniak +* Support for mixed BigQuery table name quoting (#971) - Thanks @iffyio +* Support `DELETE` with `ORDER BY` and `LIMIT` (MySQL) (#992) - Thanks @ulrichsg +* Support `EXTRACT` for `DAYOFWEEK`, `DAYOFYEAR`, `ISOWEEK`, `TIME` (#980) - Thanks @lustefaniak +* Support `ATTACH DATABASE` (#989) - Thanks @lovasoa + +### Fixed +* Fix handling of `/~%` in Snowflake stage name (#1009) - Thanks @lustefaniak +* Fix column `COLLATE` not displayed (#1012) - Thanks @lustefaniak +* Fix for clippy 1.73 (#995) - Thanks @alamb + +### Changed +* Test to ensure `+ - * / %` binary operators work the same in all dialects (#1025) - Thanks @lustefaniak +* Improve documentation on Parser::consume_token and friends (#994) - Thanks @alamb +* Test that regexp can be used as an identifier in postgres (#1018) - Thanks @lovasoa +* Add docstrings for Dialects, update README (#1016) - Thanks @alamb +* Add JumpWire to users in README (#990) - Thanks @hexedpackets +* Add tests for clickhouse: `tokenize == as Token::DoubleEq` (#981)- Thanks @lustefaniak + +## [0.38.0] 2023-09-21 + +### Added + +* Support `==`operator for Sqlite (#970) - Thanks @marhoily +* Support mysql `PARTITION` to table selection (#959) - Thanks @chunshao90 +* Support `UNNEST` as a table factor for PostgreSQL (#968) @hexedpackets +* Support MySQL `UNIQUE KEY` syntax (#962) - Thanks @artorias1024 +* Support` `GROUP BY ALL` (#964) - @berkaysynnada +* Support multiple actions in one ALTER TABLE statement (#960) - Thanks @ForbesLindesay +* Add `--sqlite param` to CLI (#956) - Thanks @ddol + +### Fixed +* Fix Rust 1.72 clippy lints (#957) - Thanks @alamb + +### Changed +* Add missing token loc in parse err msg (#965) - Thanks @ding-young +* Change how `ANY` and `ALL` expressions are represented in AST (#963) - Thanks @SeanTroyUWO +* Show location info in parse errors (#958) - Thanks @MartinNowak +* Update release documentation (#954) - Thanks @alamb +* Break test and coverage test into separate jobs (#949) - Thanks @alamb + + +## [0.37.0] 2023-08-22 + +### Added +* Support `FOR SYSTEM_TIME AS OF` table time travel clause support, `visit_table_factor` to Visitor (#951) - Thanks @gruuya +* Support MySQL `auto_increment` offset in table definition (#950) - Thanks @ehoeve +* Test for mssql table name in square brackets (#952) - Thanks @lovasoa +* Support additional Postgres `CREATE INDEX` syntax (#943) - Thanks @ForbesLindesay +* Support `ALTER ROLE` syntax of PostgreSQL and MS SQL Server (#942) - Thanks @r4ntix +* Support table-level comments (#946) - Thanks @ehoeve +* Support `DROP TEMPORARY TABLE`, MySQL syntax (#916) - Thanks @liadgiladi +* Support posgres type alias (#933) - Thanks @Kikkon + +### Fixed +* Clarify the value of the special flag (#948) - Thanks @alamb +* Fix `SUBSTRING` from/to argument construction for mssql (#947) - Thanks @jmaness +* Fix: use Rust idiomatic capitalization for newly added DataType enums (#939) - Thanks @Kikkon +* Fix `BEGIN TRANSACTION` being serialized as `START TRANSACTION` (#935) - Thanks @lovasoa +* Fix parsing of datetime functions without parenthesis (#930) - Thanks @lovasoa + +## [0.36.1] 2023-07-19 + +### Fixed +* Fix parsing of identifiers after '%' symbol (#927) - Thanks @alamb + +## [0.36.0] 2023-07-19 + +### Added +* Support toggling "unescape" mode to retain original escaping (#870) - Thanks @canalun +* Support UNION (ALL) BY NAME syntax (#915) - Thanks @parkma99 +* Add doc comment for all operators (#917) - Thanks @izveigor +* Support `PGOverlap` operator (#912) - Thanks @izveigor +* Support multi args for unnest (#909) - Thanks @jayzhan211 +* Support `ALTER VIEW`, MySQL syntax (#907) - Thanks @liadgiladi +* Add DeltaLake keywords (#906) - Thanks @roeap + +### Fixed +* Parse JsonOperators correctly (#913) - Thanks @izveigor +* Fix dependabot by removing rust-toolchain toml (#922) - Thanks @alamb + +### Changed +* Clean up JSON operator tokenizing code (#923) - Thanks @alamb +* Upgrade bigdecimal to 0.4.1 (#921) - Thanks @jinlee0 +* Remove most instances of #[cfg(feature(bigdecimal))] in tests (#910) - Thanks @alamb + +## [0.35.0] 2023-06-23 + +### Added +* Support `CREATE PROCEDURE` of MSSQL (#900) - Thanks @delsehi +* Support DuckDB's `CREATE MACRO` statements (#897) - Thanks @MartinNowak +* Support for `CREATE TYPE (AS)` statements (#888) - Thanks @srijs +* Support `STRICT` tables of sqlite (#903) - Thanks @parkma99 + +### Fixed +* Fixed precedence of unary negation operator with operators: Mul, Div and Mod (#902) - Thanks @izveigor + +### Changed +* Add `support_group_by_expr` to `Dialect` trait (#896) - Thanks @jdye64 +* Update criterion requirement from `0.4` to `0.5` in `/sqlparser_bench` (#890) - Thanks @dependabot (!!) + +## [0.34.0] 2023-05-19 + +### Added + +* Support named window frames (#881) - Thanks @berkaysynnada, @mustafasrepo, and @ozankabak +* Support for `ORDER BY` clauses in aggregate functions (#882) - Thanks @mustafasrepo +* Support `DuckDB` dialect (#878) - Thanks @eitsupi +* Support optional `TABLE` keyword for `TRUNCATE TABLE` (#883) - Thanks @mobuchowski +* Support MySQL's `DIV` operator (#876) - Thanks @eitsupi +* Support Custom operators (#868) - Thanks @max-sixty +* Add `Parser::parse_multipart_identifier` (#860) - Thanks @Jefffrey +* Support for multiple expressions, order by in `ARRAY_AGG` (#879) - Thanks @mustafasrepo +* Support for query source in `COPY .. TO` statement (#858) - Thanks @aprimadi +* Support `DISTINCT ON (...)` (#852) - Thanks @aljazerzen +* Support multiple-table `DELETE` syntax (#855) - Thanks @AviRaboah +* Support `COPY INTO` in `SnowflakeDialect` (#841) - Thanks @pawel-big-lebowski +* Support identifiers beginning with digits in MySQL (#856) - Thanks @AviRaboah + +### Changed +* Include license file in published crate (#871) - Thanks @ankane +* Make `Expr::Interval` its own struct (#872) - Thanks @aprimadi +* Add dialect_from_str and improve Dialect documentation (#848) - Thanks @alamb +* Add clickhouse to example (#849) - Thanks @anglinb + +### Fixed +* Fix merge conflict (#885) - Thanks @alamb +* Fix tiny typo in custom_sql_parser.md (#864) - Thanks @okue +* Fix logical merge conflict (#865) - Thanks @alamb +* Test trailing commas (#859) - Thanks @aljazerzen + + +## [0.33.0] 2023-04-10 + +### Added +* Support for Mysql Backslash escapes (enabled by default) (#844) - Thanks @cobyge +* Support "UPDATE" statement in "WITH" subquery (#842) - Thanks @nicksrandall +* Support PIVOT table syntax (#836) - Thanks @pawel-big-lebowski +* Support CREATE/DROP STAGE for Snowflake (#833) - Thanks @pawel-big-lebowski +* Support Non-Latin characters (#840) - Thanks @mskrzypkows +* Support PostgreSQL: GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY and GENERATED - Thanks @sam-mmm +* Support IF EXISTS in COMMENT statements (#831) - Thanks @pawel-big-lebowski +* Support snowflake alter table swap with (#825) - Thanks @pawel-big-lebowski + +### Changed +* Move tests from parser.rs to appropriate parse_XX tests (#845) - Thanks @alamb +* Correct typos in parser.rs (#838) - Thanks @felixonmars +* Improve documentation on verified_* methods (#828) - Thanks @alamb + +## [0.32.0] 2023-03-6 + +### Added +* Support ClickHouse `CREATE TABLE` with `ORDER BY` (#824) - Thanks @ankrgyl +* Support PostgreSQL exponentiation `^` operator (#813) - Thanks @michael-2956 +* Support `BIGNUMERIC` type in BigQuery (#811) - Thanks @togami2864 +* Support for optional trailing commas (#810) - Thanks @ankrgyl + +### Fixed +* Fix table alias parsing regression by backing out redshift column definition list (#827) - Thanks @alamb +* Fix typo in `ReplaceSelectElement` `colum_name` --> `column_name` (#822) - Thanks @togami2864 + +## [0.31.0] 2023-03-1 + +### Added +* Support raw string literals for BigQuery dialect (#812) - Thanks @togami2864 +* Support `SELECT * REPLACE AS ` in BigQuery dialect (#798) - Thanks @togami2864 +* Support byte string literals for BigQuery dialect (#802) - Thanks @togami2864 +* Support columns definition list for system information functions in RedShift dialect (#769) - Thanks @mskrzypkows +* Support `TRANSIENT` keyword in Snowflake dialect (#807) - Thanks @mobuchowski +* Support `JSON` keyword (#799) - Thanks @togami2864 +* Support MySQL Character Set Introducers (#788) - Thanks @mskrzypkows + +### Fixed +* Fix clippy error in ci (#803) - Thanks @togami2864 +* Handle offset in map key in BigQuery dialect (#797) - Thanks @Ziinc +* Fix a typo (precendence -> precedence) (#794) - Thanks @SARDONYX-sard +* use post_* visitors for mutable visits (#789) - Thanks @lovasoa + +### Changed +* Add another known user (#787) - Thanks @joocer + +## [0.30.0] 2023-01-02 + +### Added +* Support `RENAME` for wildcard `SELECTs` (#784) - Thanks @Jefffrey +* Add a mutable visitor (#782) - Thanks @lovasoa + +### Changed +* Allow parsing of mysql empty row inserts (#783) - Thanks @Jefffrey + +### Fixed +* Fix logical conflict (#785) - Thanks @alamb + +## [0.29.0] 2022-12-29 + +### Highlights +* Partial source location tracking: see #710 +* Recursion limit to prevent stack overflows: #764 +* AST visitor: #765 + +### Added +feat: dollar-quoted strings support (#772) - Thanks @vasilev-alex +* Add derive based AST visitor (#765) - Thanks @tustvold +* Support `ALTER INDEX {INDEX_NAME} RENAME TO {NEW_INDEX_NAME}` (#767) - Thanks @devgony +* Support `CREATE TABLE ON UPDATE ` Function (#685) - Thanks @CEOJINSUNG +* Support `CREATE FUNCTION` definition with `$$` (#755)- Thanks @zidaye +* Add location tracking in the tokenizer and parser (#710) - Thanks @ankrgyl +* Add configurable recursion limit to parser, to protect against stackoverflows (#764) - Thanks @alamb +* Support parsing scientific notation (such as `10e5`) (#768) - Thanks @Jefffrey +* Support `DROP FUNCTION` syntax (#752) - Thanks @zidaye +* Support json operators `@>` `<@`, `@?` and `@@` - Thanks @audunska +* Support the type key (#750)- Thanks @yuval-illumex + +### Changed +* Improve docs and add examples for Visitor (#778) - Thanks @alamb +* Add a backlink from sqlparse_derive to sqlparser and publishing instructions (#779) - Thanks @alamb +* Document new features, update authors (#776) - Thanks @alamb +* Improve Readme (#774) - Thanks @alamb +* Standardize comments on parsing optional keywords (#773) - Thanks @alamb +* Enable grouping sets parsing for `GenericDialect` (#771) - Thanks @Jefffrey +* Generalize conflict target (#762) - Thanks @audunska +* Generalize locking clause (#759) - Thanks @audunska +* Add negative test for except clause on wildcards (#746)- Thanks @alamb +* Add `NANOSECOND` keyword (#749)- Thanks @waitingkuo + +### Fixed +* ParserError if nested explain (#781) - Thanks @Jefffrey +* Fix cargo docs / warnings and add CI check (#777) - Thanks @alamb +* unnest join constraint with alias parsing for BigQuery dialect (#732)- Thanks @Ziinc + +## [0.28.0] 2022-12-05 + +### Added +* Support for `EXCEPT` clause on wildcards (#745) - Thanks @AugustoFKL +* Support `CREATE FUNCTION` Postgres options (#722) - Thanks @wangrunji0408 +* Support `CREATE TABLE x AS TABLE y` (#704) - Thanks @sarahyurick +* Support MySQL `ROWS` syntax for `VALUES` (#737) - Thanks @aljazerzen +* Support `WHERE` condition for `UPDATE ON CONFLICT` (#735) - Thanks @zidaye +* Support `CLUSTER BY` when creating Materialized View (#736) - Thanks @yuval-illumex +* Support nested comments (#726) - Thanks @yang-han +* Support `USING` method when creating indexes. (#731) - Thanks @step-baby and @yangjiaxin01 +* Support `SEMI`/`ANTI` `JOIN` syntax (#723) - Thanks @mingmwang +* Support `EXCLUDE` support for snowflake and generic dialect (#721) - Thanks @AugustoFKL +* Support `MATCH AGAINST` (#708) - Thanks @AugustoFKL +* Support `IF NOT EXISTS` in `ALTER TABLE ADD COLUMN` (#707) - Thanks @AugustoFKL +* Support `SET TIME ZONE ` (#727) - Thanks @waitingkuo +* Support `UPDATE ... FROM ( subquery )` (#694) - Thanks @unvalley + +### Changed +* Add `Parser::index()` method to get current parsing index (#728) - Thanks @neverchanje +* Add `COMPRESSION` as keyword (#720)- Thanks @AugustoFKL +* Derive `PartialOrd`, `Ord`, and `Copy` whenever possible (#717) - Thanks @AugustoFKL +* Fixed `INTERVAL` parsing logic and precedence (#705) - Thanks @sarahyurick +* Support updating multiple column names whose names are the same as(#725) - Thanks @step-baby + +### Fixed +* Clean up some redundant code in parser (#741) - Thanks @alamb +* Fix logical conflict - Thanks @alamb +* Cleanup to avoid is_ok() (#740) - Thanks @alamb +* Cleanup to avoid using unreachable! when parsing semi/anti join (#738) - Thanks @alamb +* Add an example to docs to clarify semantic analysis (#739) - Thanks @alamb +* Add information about parting semantic logic to README.md (#724) - Thanks @AugustoFKL +* Logical conflicts - Thanks @alamb +* Tiny typo in docs (#709) - Thanks @pmcgee69 + + +## [0.27.0] 2022-11-11 + +### Added +* Support `ON CONFLICT` and `RETURNING` in `UPDATE` statement (#666) - Thanks @main and @gamife +* Support `FULLTEXT` option on create table for MySQL and Generic dialects (#702) - Thanks @AugustoFKL +* Support `ARRAY_AGG` for Bigquery and Snowflake (#662) - Thanks @SuperBo +* Support DISTINCT for SetOperator (#689) - Thanks @unvalley +* Support the ARRAY type of Snowflake (#699) - Thanks @yuval-illumex +* Support create sequence with options INCREMENT, MINVALUE, MAXVALUE, START etc. (#681) - Thanks @sam-mmm +* Support `:` operator for semi-structured data in Snowflake(#693) - Thanks @yuval-illumex +* Support ALTER TABLE DROP PRIMARY KEY (#682) - Thanks @ding-young +* Support `NUMERIC` and `DEC` ANSI data types (#695) - Thanks @AugustoFKL +* Support modifiers for Custom Datatype (#680) - Thanks @sunng87 + +### Changed +* Add precision for TIME, DATETIME, and TIMESTAMP data types (#701) - Thanks @AugustoFKL +* add Date keyword (#691) - Thanks @sarahyurick +* Update simple_logger requirement from 2.1 to 4.0 - Thanks @dependabot + +### Fixed +* Fix broken DataFusion link (#703) - Thanks @jmg-duarte +* Add MySql, BigQuery to all dialects tests, fixed bugs (#697) - Thanks @omer-shtivi + + +## [0.26.0] 2022-10-19 + +### Added +* Support MySQL table option `{INDEX | KEY}` in CREATE TABLE definiton (#665) - Thanks @AugustoFKL +* Support `CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] ` (#678) - Thanks @sam-mmm +* Support `DROP SEQUENCE` statement (#673) - Thanks @sam-mmm +* Support for ANSI types `CHARACTER LARGE OBJECT[(p)]` and `CHAR LARGE OBJECT[(p)]` (#671) - Thanks @AugustoFKL +* Support `[CACHE|UNCACHE] TABLE` (#670) - Thanks @francis-du +* Support `CEIL(expr TO DateTimeField)` and `FLOOR(expr TO DateTimeField)` - Thanks @sarahyurick +* Support all ansii character string types, (#648) - Thanks @AugustoFKL + +### Changed +* Support expressions inside window frames (#655) - Thanks @mustafasrepo and @ozankabak +* Support unit on char length units for small character strings (#663) - Thanks @AugustoFKL +* Replace booleans on `SET ROLE` with a single enum. (#664) - Thanks @AugustoFKL +* Replace `Option`s with enum for `DECIMAL` precision (#654) - Thanks @AugustoFKL + +## [0.25.0] 2022-10-03 + +### Added + +* Support `AUTHORIZATION` clause in `CREATE SCHEMA` statements (#641) - Thanks @AugustoFKL +* Support optional precision for `CLOB` and `BLOB` (#639) - Thanks @AugustoFKL +* Support optional precision in `VARBINARY` and `BINARY` (#637) - Thanks @AugustoFKL + + +### Changed +* `TIMESTAMP` and `TIME` parsing preserve zone information (#646) - Thanks @AugustoFKL + +### Fixed + +* Correct order of arguments when parsing `LIMIT x,y` , restrict to `MySql` and `Generic` dialects - Thanks @AugustoFKL + + +## [0.24.0] 2022-09-29 + +### Added + +* Support `MILLENNIUM` (2 Ns) (#633) - Thanks @sarahyurick +* Support `MEDIUMINT` (#630) - Thanks @AugustoFKL +* Support `DOUBLE PRECISION` (#629) - Thanks @AugustoFKL +* Support precision in `CLOB`, `BINARY`, `VARBINARY`, `BLOB` data type (#618) - Thanks @ding-young +* Support `CREATE ROLE` and `DROP ROLE` (#598) - Thanks @blx +* Support full range of sqlite prepared statement placeholders (#604) - Thanks @lovasoa +* Support National string literal with lower case `n` (#612) - Thanks @mskrzypkows +* Support SHOW FUNCTIONS (#620) - Thanks @joocer +* Support `set time zone to 'some-timezone'` (#617) - Thanks @waitingkuo + +### Changed +* Move `Value::Interval` to `Expr::Interval` (#609) - Thanks @ding-young +* Update `criterion` dev-requirement from 0.3 to 0.4 in /sqlparser_bench (#611) - Thanks @dependabot +* Box `Query` in `Cte` (#572) - Thanks @MazterQyou + +### Other +* Disambiguate CREATE ROLE ... USER and GROUP (#628) - Thanks @alamb +* Add test for optional WITH in CREATE ROLE (#627) - Thanks @alamb + +## [0.23.0] 2022-09-08 + +### Added +* Add support for aggregate expressions with filters (#585) - Thanks @andygrove +* Support `LOCALTIME` and `LOCALTIMESTAMP` time functions (#592) - Thanks @MazterQyou + +## [0.22.0] 2022-08-26 + +### Added +* Support `OVERLAY` expressions (#594) - Thanks @ayushg +* Support `WITH TIMEZONE` and `WITHOUT TIMEZONE` when parsing `TIMESTAMP` expressions (#589) - Thanks @waitingkuo +* Add ability for dialects to override prefix, infix, and statement parsing (#581) - Thanks @andygrove + +## [0.21.0] 2022-08-18 + +### Added +* Support `IS [NOT] TRUE`, `IS [NOT] FALSE`, and `IS [NOT] UNKNOWN` - Thanks (#583) @sarahyurick +* Support `SIMILAR TO` syntax (#569) - Thanks @ayushdg +* Support `SHOW COLLATION` (#564) - Thanks @MazterQyou +* Support `SHOW TABLES` (#563) - Thanks @MazterQyou +* Support `SET NAMES literal [COLLATE literal]` (#558) - Thanks @ovr +* Support trailing commas (#557) in `BigQuery` dialect - Thanks @komukomo +* Support `USE ` (#565) - Thanks @MazterQyou +* Support `SHOW COLUMNS FROM tbl FROM db` (#562) - Thanks @MazterQyou +* Support `SHOW VARIABLES` for `MySQL` dialect (#559) - Thanks @ovr and @vasilev-alex + +### Changed +* Support arbitrary expression in `SET` statement (#574) - Thanks @ovr and @vasilev-alex +* Parse LIKE patterns as Expr not Value (#579) - Thanks @andygrove +* Update Ballista link in README (#576) - Thanks @sanxiyn +* Parse `TRIM` from with optional expr and `FROM` expr (#573) - Thanks @ayushdg +* Support PostgreSQL array subquery constructor (#566) - Thanks @MazterQyou +* Clarify contribution licensing (#570) - Thanks @alamb +* Update for new clippy ints (#571) - Thanks @alamb +* Change `Like` and `ILike` to `Expr` variants, allow escape char (#569) - Thanks @ayushdg +* Parse special keywords as functions (`current_user`, `user`, etc) (#561) - Thanks @ovr +* Support expressions in `LIMIT`/`OFFSET` (#567) - Thanks @MazterQyou + +## [0.20.0] 2022-08-05 + +### Added +* Support custom `OPERATOR` postgres syntax (#548) - Thanks @iskakaushik +* Support `SAFE_CAST` for BigQuery (#552) - Thanks @togami2864 + +### Changed +* Added SECURITY.md (#546) - Thanks @JamieSlome +* Allow `>>` and `<<` binary operators in Generic dialect (#553) - Thanks @ovr +* Allow `NestedJoin` with an alias (#551) - Thanks @waitingkuo + +## [0.19.0] 2022-07-28 + +### Added + +* Support `ON CLUSTER` for `CREATE TABLE` statement (ClickHouse DDL) (#527) - Thanks @andyrichardson +* Support empty `ARRAY` literals (#532) - Thanks @bitemyapp +* Support `AT TIME ZONE` clause (#539) - Thanks @bitemyapp +* Support `USING` clause and table aliases in `DELETE` (#541) - Thanks @mobuchowski +* Support `SHOW CREATE VIEW` statement (#536) - Thanks @mrob95 +* Support `CLONE` clause in `CREATE TABLE` statements (#542) - Thanks @mobuchowski +* Support `WITH OFFSET Alias` in table references (#528) - Thanks @sivchari +* Support double quoted (`"`) literal strings: (#530) - Thanks @komukomo +* Support `ON UPDATE` clause on column definitions in `CREATE TABLE` statements (#522) - Thanks @frolovdev + + +### Changed: + +* `Box`ed `Query` body to save stack space (#540) - Thanks @5tan +* Distinguish between `INT` and `INTEGER` types (#525) - Thanks @frolovdev +* Parse `WHERE NOT EXISTS` as `Expr::Exists` rather than `Expr::UnaryOp` for consistency (#523) - Thanks @frolovdev +* Support `Expr` instead of `String` for argument to `INTERVAL` (#517) - Thanks @togami2864 + +### Fixed: + +* Report characters instead of bytes in error messages (#529) - Thanks @michael-2956 + + +## [0.18.0] 2022-06-06 + +### Added + +* Support `CLOSE` (cursors) (#515) - Thanks @ovr +* Support `DECLARE` (cursors) (#509) - Thanks @ovr +* Support `FETCH` (cursors) (#510) - Thanks @ovr +* Support `DATETIME` keyword (#512) - Thanks @komukomo +* Support `UNNEST` as a table factor (#493) - Thanks @sivchari +* Support `CREATE FUNCTION` (hive flavor) (#496) - Thanks @mobuchowski +* Support placeholders (`$` or `?`) in `LIMIT` clause (#494) - Thanks @step-baby +* Support escaped string literals (PostgreSQL) (#502) - Thanks @ovr +* Support `IS TRUE` and `IS FALSE` (#499) - Thanks @ovr +* Support `DISCARD [ALL | PLANS | SEQUENCES | TEMPORARY | TEMP]` (#500) - Thanks @gandronchik +* Support `array<..>` HIVE data types (#491) - Thanks @mobuchowski +* Support `SET` values that begin with `-` #495 - Thanks @mobuchowski +* Support unicode whitespace (#482) - Thanks @alexsatori +* Support `BigQuery` dialect (#490) - Thanks @komukomo + +### Changed: +* Add docs for MapAccess (#489) - Thanks @alamb +* Rename `ArrayIndex::indexs` to `ArrayIndex::indexes` (#492) - Thanks @alamb + +### Fixed: +* Fix escaping of trailing quote in quoted identifiers (#505) - Thanks @razzolini-qpq +* Fix parsing of `COLLATE` after parentheses in expressions (#507) - Thanks @razzolini-qpq +* Distinguish tables and nullary functions in `FROM` (#506) - Thanks @razzolini-qpq +* Fix `MERGE INTO` semicolon handling (#508) - Thanks @mskrzypkows + +## [0.17.0] 2022-05-09 + +### Added + +* Support `#` as first character in field name for `RedShift` dialect (#485) - Thanks @yuval-illumex +* Support for postgres composite types (#466) - Thanks @poonai +* Support `TABLE` keyword with SELECT INTO (#487) - Thanks @MazterQyou +* Support `ANY`/`ALL` operators (#477) - Thanks @ovr +* Support `ArrayIndex` in `GenericDialect` (#480) - Thanks @ovr +* Support `Redshift` dialect, handle square brackets properly (#471) - Thanks @mskrzypkows +* Support `KILL` statement (#479) - Thanks @ovr +* Support `QUALIFY` clause on `SELECT` for `Snowflake` dialect (#465) - Thanks @mobuchowski +* Support `POSITION(x IN y)` function syntax (#463) @yuval-illumex +* Support `global`,`local`, `on commit` for `create temporary table` (#456) - Thanks @gandronchik +* Support `NVARCHAR` data type (#462) - Thanks @yuval-illumex +* Support for postgres json operators `->`, `->>`, `#>`, and `#>>` (#458) - Thanks @poonai +* Support `SET ROLE` statement (#455) - Thanks @slhmy + +### Changed: +* Improve docstrings for `KILL` statement (#481) - Thanks @alamb +* Add negative tests for `POSITION` (#469) - Thanks @alamb +* Add negative tests for `IN` parsing (#468) - Thanks @alamb +* Suppport table names (as well as subqueries) as source in `MERGE` statements (#483) - Thanks @mskrzypkows + + +### Fixed: +* `INTO` keyword is optional for `INSERT`, `MERGE` (#473) - Thanks @mobuchowski +* Support `IS TRUE` and `IS FALSE` expressions in boolean filter (#474) - Thanks @yuval-illumex +* Support fully qualified object names in `SET VARIABLE` (#484) - Thanks mobuchowski + +## [0.16.0] 2022-04-03 + +### Added + +* Support `WEEK` keyword in `EXTRACT` (#436) - Thanks @Ted-Jiang +* Support `MERGE` statement (#430) - Thanks @mobuchowski +* Support `SAVEPOINT` statement (#438) - Thanks @poonai +* Support `TO` clause in `COPY` (#441) - Thanks @matthewmturner +* Support `CREATE DATABASE` statement (#451) - Thanks @matthewmturner +* Support `FROM` clause in `UPDATE` statement (#450) - Thanks @slhmy +* Support additional `COPY` options (#446) - Thanks @wangrunji0408 + +### Fixed: +* Bug in array / map access parsing (#433) - Thanks @monadbobo + +## [0.15.0] 2022-03-07 + +### Added + +* Support for ClickHouse array types (e.g. [1,2,3]) (#429) - Thanks @monadbobo +* Support for `unsigned tinyint`, `unsigned int`, `unsigned smallint` and `unsigned bigint` datatypes (#428) - Thanks @watarukura +* Support additional keywords for `EXTRACT` (#427) - Thanks @mobuchowski +* Support IN UNNEST(expression) (#426) - Thanks @komukomo +* Support COLLATION keywork on CREATE TABLE (#424) - Thanks @watarukura +* Support FOR UPDATE/FOR SHARE clause (#418) - Thanks @gamife +* Support prepared statement placeholder arg `?` and `$` (#420) - Thanks @gamife +* Support array expressions such as `ARRAY[1,2]` , `foo[1]` and `INT[][]` (#419) - Thanks @gamife + +### Changed: +* remove Travis CI (#421) - Thanks @efx + +### Fixed: +* Allow `array` to be used as a function name again (#432) - @alamb +* Update docstring reference to `Query` (#423) - Thanks @max-sixty + +## [0.14.0] 2022-02-09 + +### Added +* Support `CURRENT_TIMESTAMP`, `CURRENT_TIME`, and `CURRENT_DATE` (#391) - Thanks @yuval-illumex +* SUPPORT `SUPER` keyword (#387) - Thanks @flaneur2020 +* Support differing orders of `OFFSET` `LIMIT` as well as `LIMIT` `OFFSET` (#413) - Thanks @yuval-illumex +* Support for `FROM `, `DELIMITER`, and `CSV HEADER` options for `COPY` command (#409) - Thanks @poonai +* Support `CHARSET` and `ENGINE` clauses on `CREATE TABLE` for mysql (#392) - Thanks @antialize +* Support `DROP CONSTRAINT [ IF EXISTS ] [ CASCADE ]` (#396) - Thanks @tvallotton +* Support parsing tuples and add `Expr::Tuple` (#414) - @alamb +* Support MySQL style `LIMIT X, Y` (#415) - @alamb +* Support `SESSION TRANSACTION` and `TRANSACTION SNAPSHOT`. (#379) - Thanks @poonai +* Support `ALTER COLUMN` and `RENAME CONSTRAINT` (#381) - Thanks @zhamlin +* Support for Map access, add ClickHouse dialect (#382) - Thanks @monadbobo + +### Changed +* Restrict where wildcard (`*`) can appear, add to `FunctionArgExpr` remove `Expr::[Qualified]Wildcard`, (#378) - Thanks @panarch +* Update simple_logger requirement from 1.9 to 2.1 (#403) +* export all methods of parser (#397) - Thanks @neverchanje! +* Clarify maintenance status on README (#416) - @alamb + +### Fixed +* Fix new clippy errors (#412) - @alamb +* Fix panic with `GRANT/REVOKE` in `CONNECT`, `CREATE`, `EXECUTE` or `TEMPORARY` - Thanks @evgenyx00 +* Handle double quotes inside quoted identifiers correctly (#411) - Thanks @Marwes +* Handle mysql backslash escaping (#373) - Thanks @vasilev-alex + +## [0.13.0] 2021-12-10 + +### Added +* Add ALTER TABLE CHANGE COLUMN, extend the UPDATE statement with ON clause (#375) - Thanks @0xA537FD! +* Add support for GROUPIING SETS, ROLLUP and CUBE - Thanks @Jimexist! +* Add basic support for GRANT and REVOKE (#365) - Thanks @blx! + +### Changed +* Use Rust 2021 edition (#368) - Thanks @Jimexist! + +### Fixed +* Fix clippy errors (#367, #374) - Thanks @Jimexist! + + +## [0.12.0] 2021-10-14 + +### Added +* Add support for [NOT] IS DISTINCT FROM (#306) - @Dandandan + +### Changed +* Move the keywords module - Thanks @koushiro! + + +## [0.11.0] 2021-09-24 + +### Added +* Support minimum display width for integer data types (#337) Thanks @vasilev-alex! +* Add logical XOR operator (#357) - Thanks @xzmrdltl! +* Support DESCRIBE table_name (#340) - Thanks @ovr! +* Support SHOW CREATE TABLE|EVENT|FUNCTION (#338) - Thanks @ovr! +* Add referential actions to TableConstraint foreign key (#306) - Thanks @joshwd36! + +### Changed +* Enable map access for numbers, multiple nesting levels (#356) - Thanks @Igosuki! +* Rename Token::Mult to Token::Mul (#353) - Thanks @koushiro! +* Use derive(Default) for HiveFormat (#348) - Thanks @koushiro! +* Improve tokenizer error (#347) - Thanks @koushiro! +* Eliminate redundant string copy in Tokenizer (#343) - Thanks @koushiro! +* Update bigdecimal requirement from 0.2 to 0.3 dependencies (#341) +* Support parsing hexadecimal literals that start with `0x` (#324) - Thanks @TheSchemm! + + +## [0.10.0] 2021-08-23 + +### Added +* Support for `no_std` (#332) - Thanks @koushiro! +* Postgres regular expression operators (`~`, `~*`, `!~`, `!~*`) (#328) - Thanks @b41sh! +* tinyint (#320) - Thanks @sundy-li +* ILIKE (#300) - Thanks @maxcountryman! +* TRIM syntax (#331, #334) - Thanks ever0de + + +### Fixed +* Return error instead of panic (#316) - Thanks @BohuTANG! + +### Changed +- Rename `Modulus` to `Modulo` (#335) - Thanks @RGRAVITY817! +- Update links to reflect repository move to `sqlparser-rs` GitHub org (#333) - Thanks @andygrove +- Add default value for `WindowFrame` (#313) - Thanks @Jimexist! + +## [0.9.0] 2021-03-21 + +### Added +* Add support for `TRY_CAST` syntax (#299) - Thanks @seddonm1! + +## [0.8.0] 2021-02-20 + +### Added +* Introduce Hive QL dialect `HiveDialect` and syntax (#235) - Thanks @hntd187! +* Add `SUBSTRING(col [FROM ] [FOR ])` syntax (#293) +* Support parsing floats without leading digits `.01` (#294) +* Support parsing multiple show variables (#290) - Thanks @francis-du! +* Support SQLite `INSERT OR [..]` syntax (#281) - Thanks @zhangli-pear! + +## [0.7.0] 2020-12-28 + +### Changed +- Change the MySQL dialect to support `` `identifiers` `` quoted with backticks instead of the standard `"double-quoted"` identifiers (#247) - thanks @mashuai! +- Update bigdecimal requirement from 0.1 to 0.2 (#268) + +### Added +- Enable dialect-specific behaviours in the parser (`dialect_of!()`) (#254) - thanks @eyalleshem! +- Support named arguments in function invocations (`ARG_NAME => val`) (#250) - thanks @eyalleshem! +- Support `TABLE()` functions in `FROM` (#253) - thanks @eyalleshem! +- Support Snowflake's single-line comments starting with '#' or '//' (#264) - thanks @eyalleshem! +- Support PostgreSQL `PREPARE`, `EXECUTE`, and `DEALLOCATE` (#243) - thanks @silathdiir! +- Support PostgreSQL math operators (#267) - thanks @alex-dukhno! +- Add SQLite dialect (#248) - thanks @mashuai! +- Add Snowflake dialect (#259) - thanks @eyalleshem! +- Support for Recursive CTEs - thanks @rhanqtl! +- Support `FROM (table_name) alias` syntax - thanks @eyalleshem! +- Support for `EXPLAIN [ANALYZE] VERBOSE` - thanks @ovr! +- Support `ANALYZE TABLE` +- DDL: + - Support `OR REPLACE` in `CREATE VIEW`/`TABLE` (#239) - thanks @Dandandan! + - Support specifying `ASC`/`DESC` in index columns (#249) - thanks @mashuai! + - Support SQLite `AUTOINCREMENT` and MySQL `AUTO_INCREMENT` column option in `CREATE TABLE` (#234) - thanks @mashuai! + - Support PostgreSQL `IF NOT EXISTS` for `CREATE SCHEMA` (#276) - thanks @alex-dukhno! + +### Fixed +- Fix a typo in `JSONFILE` serialization, introduced in 0.3.1 (#237) +- Change `CREATE INDEX` serialization to not end with a semicolon, introduced in 0.5.1 (#245) +- Don't fail parsing `ALTER TABLE ADD COLUMN` ending with a semicolon, introduced in 0.5.1 (#246) - thanks @mashuai + +## [0.6.1] - 2020-07-20 + +### Added +- Support BigQuery `ASSERT` statement (#226) + +## [0.6.0] - 2020-07-20 + +### Added +- Support SQLite's `CREATE TABLE (...) WITHOUT ROWID` (#208) - thanks @mashuai! +- Support SQLite's `CREATE VIRTUAL TABLE` (#209) - thanks @mashuai! + +## [0.5.1] - 2020-06-26 +This release should have been called `0.6`, as it introduces multiple incompatible changes to the API. If you don't want to upgrade yet, you can revert to the previous version by changing your `Cargo.toml` to: + + sqlparser = "= 0.5.0" + + +### Changed +- **`Parser::parse_sql` now accepts a `&str` instead of `String` (#182)** - thanks @Dandandan! +- Change `Ident` (previously a simple `String`) to store the parsed (unquoted) `value` of the identifier and the `quote_style` separately (#143) - thanks @apparebit! +- Support Snowflake's `FROM (table_name)` (#155) - thanks @eyalleshem! +- Add line and column number to TokenizerError (#194) - thanks @Dandandan! +- Use Token::EOF instead of Option (#195) +- Make the units keyword following `INTERVAL '...'` optional (#184) - thanks @maxcountryman! +- Generalize `DATE`/`TIME`/`TIMESTAMP` literals representation in the AST (`TypedString { data_type, value }`) and allow `DATE` and other keywords to be used as identifiers when not followed by a string (#187) - thanks @maxcountryman! +- Output DataType capitalized (`fmt::Display`) (#202) - thanks @Dandandan! + +### Added +- Support MSSQL `TOP () [ PERCENT ] [ WITH TIES ]` (#150) - thanks @alexkyllo! +- Support MySQL `LIMIT row_count OFFSET offset` (not followed by `ROW` or `ROWS`) and remember which variant was parsed (#158) - thanks @mjibson! +- Support PostgreSQL `CREATE TABLE IF NOT EXISTS table_name` (#163) - thanks @alex-dukhno! +- Support basic forms of `CREATE INDEX` and `DROP INDEX` (#167) - thanks @mashuai! +- Support `ON { UPDATE | DELETE } { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` in `FOREIGN KEY` constraints (#170) - thanks @c7hm4r! +- Support basic forms of `CREATE SCHEMA` and `DROP SCHEMA` (#173) - thanks @alex-dukhno! +- Support `NULLS FIRST`/`LAST` in `ORDER BY` expressions (#176) - thanks @houqp! +- Support `LISTAGG()` (#174) - thanks @maxcountryman! +- Support the string concatentation operator `||` (#178) - thanks @Dandandan! +- Support bitwise AND (`&`), OR (`|`), XOR (`^`) (#181) - thanks @Dandandan! +- Add serde support to AST structs and enums (#196) - thanks @panarch! +- Support `ALTER TABLE ADD COLUMN`, `RENAME COLUMN`, and `RENAME TO` (#203) - thanks @mashuai! +- Support `ALTER TABLE DROP COLUMN` (#148) - thanks @ivanceras! +- Support `CREATE TABLE ... AS ...` (#206) - thanks @Dandandan! + +### Fixed +- Report an error for unterminated string literals (#165) +- Make file format (`STORED AS`) case insensitive (#200) and don't allow quoting it (#201) - thanks @Dandandan! + +## [0.5.0] - 2019-10-10 + +### Changed +- Replace the `Value::Long(u64)` and `Value::Double(f64)` variants with `Value::Number(String)` to avoid losing precision when parsing decimal literals (#130) - thanks @benesch! +- `--features bigdecimal` can be enabled to work with `Value::Number(BigDecimal)` instead, at the cost of an additional dependency. + +### Added +- Support MySQL `SHOW COLUMNS`, `SET =`, and `SHOW ` statements (#135) - thanks @quodlibetor and @benesch! + +### Fixed +- Don't fail to parse `START TRANSACTION` followed by a semicolon (#139) - thanks @gaffneyk! + + +## [0.4.0] - 2019-07-02 +This release brings us closer to SQL-92 support, mainly thanks to the improvements contributed back from @MaterializeInc's fork and other work by @benesch. + +### Changed +- Remove "SQL" from type and enum variant names, `SQLType` -> `DataType`, remove "sql" prefix from module names (#105, #122) +- Rename `ASTNode` -> `Expr` (#119) +- Improve consistency of binary/unary op nodes (#112): + - `ASTNode::SQLBinaryExpr` is now `Expr::BinaryOp` and `ASTNode::SQLUnary` is `Expr::UnaryOp`; + - The `op: SQLOperator` field is now either a `BinaryOperator` or an `UnaryOperator`. +- Change the representation of JOINs to match the standard (#109): `SQLSelect`'s `relation` and `joins` are replaced with `from: Vec`. Before this change `FROM foo NATURAL JOIN bar, baz` was represented as "foo" as the `relation` followed by two joins (`Inner(Natural)` and `Implicit`); now it's two `TableWithJoins` (`foo NATURAL JOIN bar` and `baz`). +- Extract a `SQLFunction` struct (#89) +- Replace `Option>` with `Vec` in the AST structs (#73) +- Change `Value::Long()` to be unsigned, use u64 consistently (#65) + +### Added +- Infra: + - Implement `fmt::Display` on AST nodes (#124) - thanks @vemoo! + - Implement `Hash` (#88) and `Eq` (#123) on all AST nodes + - Implement `std::error::Error` for `ParserError` (#72) + - Handle Windows line-breaks (#54) +- Expressions: + - Support `INTERVAL` literals (#103) + - Support `DATE` / `TIME` / `TIMESTAMP` literals (#99) + - Support `EXTRACT` (#96) + - Support `X'hex value'` literals (#95) + - Support `EXISTS` subqueries (#90) + - Support nested expressions in `BETWEEN` (#80) + - Support `COUNT(DISTINCT x)` and similar (#77) + - Support `CASE operand WHEN expected_value THEN ..` and table-valued functions (#59) + - Support analytic (window) functions (`OVER` clause) (#50) +- Queries / DML: + - Support nested joins (#100) and derived tables with set operations (#111) + - Support `UPDATE` statements (#97) + - Support `INSERT INTO foo SELECT * FROM bar` and `FROM VALUES (...)` (#91) + - Support `SELECT ALL` (#76) + - Add `FETCH` and `OFFSET` support, and `LATERAL` (#69) - thanks @thomas-jeepe! + - Support `COLLATE`, optional column list in CTEs (#64) +- DDL/TCL: + - Support `START/SET/COMMIT/ROLLBACK TRANSACTION` (#106) - thanks @SamuelMarks! + - Parse column constraints in any order (#93) + - Parse `DECIMAL` and `DEC` aliases for `NUMERIC` type (#92) + - Support `DROP [TABLE|VIEW]` (#75) + - Support arbitrary `WITH` options for `CREATE [TABLE|VIEW]` (#74) + - Support constraints in `CREATE TABLE` (#65) +- Add basic MSSQL dialect (#61) and some MSSQL-specific features: + - `CROSS`/`OUTER APPLY` (#120) + - MSSQL identifier and alias parsing rules (#66) + - `WITH` hints (#59) + +### Fixed +- Report an error for `SELECT * FROM a OUTER JOIN b` instead of parsing `OUTER` as an alias (#118) +- Fix the precedence of `NOT LIKE` (#82) and unary `NOT` (#107) +- Do not panic when `NOT` is not followed by an expected keyword (#71) + successfully instead of returning a parse error - thanks @ivanceras! (#67) - and similar fixes for queries with no `FROM` (#116) +- Fix issues with `ALTER TABLE ADD CONSTRAINT` parsing (#65) +- Serialize the "not equals" operator as `<>` instead of `!=` (#64) +- Remove dependencies on `uuid` (#59) and `chrono` (#61) +- Make `SELECT` query with `LIMIT` clause but no `WHERE` parse - Fix incorrect behavior of `ASTNode::SQLQualifiedWildcard::to_string()` (returned `foo*` instead of `foo.*`) - thanks @thomas-jeepe! (#52) + +## [0.3.1] - 2019-04-20 +### Added +- Extended `SQLStatement::SQLCreateTable` to support Hive's EXTERNAL TABLES (`CREATE EXTERNAL TABLE .. STORED AS .. LOCATION '..'`) - thanks @zhzy0077! (#46) +- Parse `SELECT DISTINCT` to `SQLSelect::distinct` (#49) + +## [0.3.0] - 2019-04-03 +### Changed +This release includes major changes to the AST structs to add a number of features, as described in #37 and #43. In particular: +- `ASTNode` variants that represent statements were extracted from `ASTNode` into a separate `SQLStatement` enum; + - `Parser::parse_sql` now returns a `Vec` of parsed statements. + - `ASTNode` now represents an expression (renamed to `Expr` in 0.4.0) +- The query representation (formerly `ASTNode::SQLSelect`) became more complicated to support: + - `WITH` and `UNION`/`EXCEPT`/`INTERSECT` (via `SQLQuery`, `Cte`, and `SQLSetExpr`), + - aliases and qualified wildcards in `SELECT` (via `SQLSelectItem`), + - and aliases in `FROM`/`JOIN` (via `TableFactor`). +- A new `SQLObjectName` struct is used instead of `String` or `ASTNode::SQLCompoundIdentifier` - for objects like tables, custom types, etc. +- Added support for "delimited identifiers" and made keywords context-specific (thus accepting them as valid identifiers in most contexts) - **this caused a regression in parsing `SELECT .. FROM .. LIMIT ..` (#67), fixed in 0.4.0** + +### Added +Other than the changes listed above, some less intrusive additions include: +- Support `CREATE [MATERIALIZED] VIEW` statement +- Support `IN`, `BETWEEN`, unary +/- in epressions +- Support `CHAR` data type and `NUMERIC` not followed by `(p,s)`. +- Support national string literals (`N'...'`) + +## [0.2.4] - 2019-03-08 +Same as 0.2.2. + +## [0.2.3] - 2019-03-08 [YANKED] + +## [0.2.2] - 2019-03-08 +### Changed +- Removed `Value::String`, `Value::DoubleQuotedString`, and `Token::String`, making + - `'...'` parse as a string literal (`Value::SingleQuotedString`), and + - `"..."` fail to parse until version 0.3.0 (#36) + +## [0.2.1] - 2019-01-13 +We don't have a changelog for the changes made in 2018, but thanks to @crw5996, @cswinter, @fredrikroos, @ivanceras, @nickolay, @virattara for their contributions in the early stages of the project! + +## [0.1.0] - 2018-09-03 +Initial release From d853c35391a3e53a5d1e03b51383de54055afae6 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Thu, 7 Nov 2024 16:59:14 +0100 Subject: [PATCH 588/806] improve support for T-SQL EXECUTE statements (#1490) --- src/ast/mod.rs | 23 ++++++++++++++++------ src/parser/mod.rs | 20 ++++++++++++++----- tests/sqlparser_common.rs | 39 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 9 ++++++--- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a24739a60..31d7af1ba 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3113,10 +3113,14 @@ pub enum Statement { /// EXECUTE name [ ( parameter [, ...] ) ] [USING ] /// ``` /// - /// Note: this is a PostgreSQL-specific statement. + /// Note: this statement is supported by Postgres and MSSQL, with slight differences in syntax. + /// + /// Postgres: + /// MSSQL: Execute { - name: Ident, + name: ObjectName, parameters: Vec, + has_parentheses: bool, using: Vec, }, /// ```sql @@ -4585,12 +4589,19 @@ impl fmt::Display for Statement { Statement::Execute { name, parameters, + has_parentheses, using, } => { - write!(f, "EXECUTE {name}")?; - if !parameters.is_empty() { - write!(f, "({})", display_comma_separated(parameters))?; - } + let (open, close) = if *has_parentheses { + ("(", ")") + } else { + (if parameters.is_empty() { "" } else { " " }, "") + }; + write!( + f, + "EXECUTE {name}{open}{}{close}", + display_comma_separated(parameters), + )?; if !using.is_empty() { write!(f, " USING {}", display_comma_separated(using))?; }; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2bd454369..942ff19fd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -529,7 +529,7 @@ impl<'a> Parser<'a> { // `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific // syntaxes. They are used for Postgres prepared statement. Keyword::DEALLOCATE => self.parse_deallocate(), - Keyword::EXECUTE => self.parse_execute(), + Keyword::EXECUTE | Keyword::EXEC => self.parse_execute(), Keyword::PREPARE => self.parse_prepare(), Keyword::MERGE => self.parse_merge(), // `LISTEN` and `NOTIFY` are Postgres-specific @@ -11807,11 +11807,20 @@ impl<'a> Parser<'a> { } pub fn parse_execute(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_object_name(false)?; - let mut parameters = vec![]; - if self.consume_token(&Token::LParen) { - parameters = self.parse_comma_separated(Parser::parse_expr)?; + let has_parentheses = self.consume_token(&Token::LParen); + + let end_token = match (has_parentheses, self.peek_token().token) { + (true, _) => Token::RParen, + (false, Token::EOF) => Token::EOF, + (false, Token::Word(w)) if w.keyword == Keyword::USING => Token::Word(w), + (false, _) => Token::SemiColon, + }; + + let parameters = self.parse_comma_separated0(Parser::parse_expr, end_token)?; + + if has_parentheses { self.expect_token(&Token::RParen)?; } @@ -11827,6 +11836,7 @@ impl<'a> Parser<'a> { Ok(Statement::Execute { name, parameters, + has_parentheses, using, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 49753a1f4..e37280636 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1396,6 +1396,10 @@ fn pg_and_generic() -> TestedDialects { ]) } +fn ms_and_generic() -> TestedDialects { + TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) +} + #[test] fn parse_json_ops_without_colon() { use self::BinaryOperator::*; @@ -9735,6 +9739,41 @@ fn parse_call() { ); } +#[test] +fn parse_execute_stored_procedure() { + let expected = Statement::Execute { + name: ObjectName(vec![ + Ident { + value: "my_schema".to_string(), + quote_style: None, + }, + Ident { + value: "my_stored_procedure".to_string(), + quote_style: None, + }, + ]), + parameters: vec![ + Expr::Value(Value::NationalStringLiteral("param1".to_string())), + Expr::Value(Value::NationalStringLiteral("param2".to_string())), + ], + has_parentheses: false, + using: vec![], + }; + assert_eq!( + // Microsoft SQL Server does not use parentheses around arguments for EXECUTE + ms_and_generic() + .verified_stmt("EXECUTE my_schema.my_stored_procedure N'param1', N'param2'"), + expected + ); + assert_eq!( + ms_and_generic().one_statement_parses_to( + "EXEC my_schema.my_stored_procedure N'param1', N'param2';", + "EXECUTE my_schema.my_stored_procedure N'param1', N'param2'", + ), + expected + ); +} + #[test] fn parse_create_table_collate() { pg_and_generic().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c30603baa..100c8eeb2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1539,8 +1539,9 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: "a".into(), + name: ObjectName(vec!["a".into()]), parameters: vec![], + has_parentheses: false, using: vec![] } ); @@ -1549,11 +1550,12 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: "a".into(), + name: ObjectName(vec!["a".into()]), parameters: vec![ Expr::Value(number("1")), Expr::Value(Value::SingleQuotedString("t".to_string())) ], + has_parentheses: true, using: vec![] } ); @@ -1563,8 +1565,9 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: "a".into(), + name: ObjectName(vec!["a".into()]), parameters: vec![], + has_parentheses: false, using: vec![ Expr::Cast { kind: CastKind::Cast, From 334a5bf354ac964a14f2e2dc3851ed4d853b28d7 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 7 Nov 2024 11:26:57 -0500 Subject: [PATCH 589/806] Update CHANGELOG.md for `0.52.0` release, add scripts/ instructions for ASF releases (#1479) --- .github/workflows/rust.yml | 35 ----- .gitignore | 1 + CHANGELOG.md | 4 +- Cargo.toml | 14 +- changelog/0.52.0.md | 104 ++++++++++++++ dev/release/README.md | 181 ++++++++++++++++++++++++ dev/release/check-rat-report.py | 59 ++++++++ dev/release/create-tarball.sh | 135 ++++++++++++++++++ dev/release/generate-changelog.py | 164 +++++++++++++++++++++ dev/release/rat_exclude_files.txt | 6 + dev/release/release-tarball.sh | 74 ++++++++++ dev/release/run-rat.sh | 43 ++++++ dev/release/verify-release-candidate.sh | 152 ++++++++++++++++++++ docs/releasing.md | 81 ----------- 14 files changed, 926 insertions(+), 127 deletions(-) create mode 100644 changelog/0.52.0.md create mode 100644 dev/release/README.md create mode 100644 dev/release/check-rat-report.py create mode 100755 dev/release/create-tarball.sh create mode 100755 dev/release/generate-changelog.py create mode 100644 dev/release/rat_exclude_files.txt create mode 100755 dev/release/release-tarball.sh create mode 100755 dev/release/run-rat.sh create mode 100755 dev/release/verify-release-candidate.sh delete mode 100644 docs/releasing.md diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 253d8ab27..2502abe9d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -85,38 +85,3 @@ jobs: use-tool-cache: true - name: Test run: cargo test --all-features - - test-coverage: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Rust Toolchain - uses: ./.github/actions/setup-builder - with: - rust-version: stable - - name: Install Tarpaulin - uses: actions-rs/install@v0.1 - with: - crate: cargo-tarpaulin - version: 0.14.2 - use-tool-cache: true - - name: Coverage - run: cargo tarpaulin -o Lcov --output-dir ./coverage - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - publish-crate: - if: startsWith(github.ref, 'refs/tags/v0') - runs-on: ubuntu-latest - needs: [test] - steps: - - uses: actions/checkout@v4 - - name: Setup Rust Toolchain - uses: ./.github/actions/setup-builder - - name: Publish - shell: bash - run: | - cargo publish --token ${{ secrets.CRATES_TOKEN }} diff --git a/.gitignore b/.gitignore index 4c6821d47..f705d0b0d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /target/ /sqlparser_bench/target/ /derive/target/ +dev/dist # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock diff --git a/CHANGELOG.md b/CHANGELOG.md index e047515ad..ec74bf633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,5 +24,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm Given that the parser produces a typed AST, any changes to the AST will 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.51.0` and earlier: [changelog/0.51.0-pre.md](changelog/0.51.0-pre.md) \ No newline at end of file +- `0.52.0`: [changelog/0.52.0.md](changelog/0.52.0.md) +- `0.51.0` and earlier: [changelog/0.51.0-pre.md](changelog/0.51.0-pre.md) diff --git a/Cargo.toml b/Cargo.toml index 99546465b..18b246e04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,12 +18,12 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.51.0" -authors = ["Andy Grove "] -homepage = "https://github.com/sqlparser-rs/sqlparser-rs" +version = "0.52.0" +authors = ["Apache DataFusion "] +homepage = "https://github.com/apache/datafusion-sqlparser-rs" documentation = "https://docs.rs/sqlparser/" keywords = ["ansi", "sql", "lexer", "parser"] -repository = "https://github.com/sqlparser-rs/sqlparser-rs" +repository = "https://github.com/apache/datafusion-sqlparser-rs" license = "Apache-2.0" include = [ "src/**/*.rs", @@ -58,12 +58,6 @@ simple_logger = "5.0" matches = "0.1" pretty_assertions = "1" -[package.metadata.release] -# Instruct `cargo release` to not run `cargo publish` locally: -# https://github.com/sunng87/cargo-release/blob/master/docs/reference.md#config-fields -# See docs/releasing.md for details. -publish = false - [package.metadata.docs.rs] # Document these features on docs.rs features = ["serde", "visitor"] diff --git a/changelog/0.52.0.md b/changelog/0.52.0.md new file mode 100644 index 000000000..9d5b16c7c --- /dev/null +++ b/changelog/0.52.0.md @@ -0,0 +1,104 @@ + + +# sqlparser-rs 0.52.0 Changelog + +This release consists of 45 commits from 20 contributors. See credits at the end of this changelog for more information. + +**Implemented enhancements:** + +- feat: support explain options [#1426](https://github.com/apache/datafusion-sqlparser-rs/pull/1426) (kysshsy) +- feat: adding Display implementation to DELETE and INSERT [#1427](https://github.com/apache/datafusion-sqlparser-rs/pull/1427) (seve-martinez) + +**Fixed bugs:** + +- fix: `maybe_parse` preventing parser from erroring on recursion limit [#1464](https://github.com/apache/datafusion-sqlparser-rs/pull/1464) (tomershaniii) + +**Other:** + +- Fix parsing of negative values [#1419](https://github.com/apache/datafusion-sqlparser-rs/pull/1419) (agscpp) +- Allow to use ON CLUSTER cluster_name in TRUNCATE syntax [#1428](https://github.com/apache/datafusion-sqlparser-rs/pull/1428) (git-hulk) +- chore: remove redundant punctuation [#1434](https://github.com/apache/datafusion-sqlparser-rs/pull/1434) (Fischer0522) +- MS SQL Server: add support for IDENTITY column option [#1432](https://github.com/apache/datafusion-sqlparser-rs/pull/1432) (7phs) +- Update to ASF header / add when missing [#1437](https://github.com/apache/datafusion-sqlparser-rs/pull/1437) (alamb) +- Some small optimizations [#1424](https://github.com/apache/datafusion-sqlparser-rs/pull/1424) (exrok) +- Fix `codestyle` CI check [#1438](https://github.com/apache/datafusion-sqlparser-rs/pull/1438) (alamb) +- Implements CREATE POLICY syntax for PostgreSQL [#1440](https://github.com/apache/datafusion-sqlparser-rs/pull/1440) (git-hulk) +- make `parse_expr_with_alias` public [#1444](https://github.com/apache/datafusion-sqlparser-rs/pull/1444) (Eason0729) +- Implements DROP POLICY syntax for PostgreSQL [#1445](https://github.com/apache/datafusion-sqlparser-rs/pull/1445) (git-hulk) +- Support `DROP DATABASE` [#1443](https://github.com/apache/datafusion-sqlparser-rs/pull/1443) (linhr) +- Implements ALTER POLICY syntax for PostgreSQL [#1446](https://github.com/apache/datafusion-sqlparser-rs/pull/1446) (git-hulk) +- Add a note discouraging new use of `dialect_of` macro [#1448](https://github.com/apache/datafusion-sqlparser-rs/pull/1448) (alamb) +- Expand handling of `LIMIT 1, 2` handling to include sqlite [#1447](https://github.com/apache/datafusion-sqlparser-rs/pull/1447) (joshuawarner32) +- Fix always uses CommentDef::WithoutEq while parsing the inline comment [#1453](https://github.com/apache/datafusion-sqlparser-rs/pull/1453) (git-hulk) +- Add support for the LIKE ANY and ILIKE ANY pattern-matching condition [#1456](https://github.com/apache/datafusion-sqlparser-rs/pull/1456) (yoavcloud) +- added ability to parse extension to parse_comment inside postgres dialect [#1451](https://github.com/apache/datafusion-sqlparser-rs/pull/1451) (MaxwellKnight) +- Snowflake: support of views column comment [#1441](https://github.com/apache/datafusion-sqlparser-rs/pull/1441) (7phs) +- Add SQLite "ON CONFLICT" column option in CREATE TABLE statements [#1442](https://github.com/apache/datafusion-sqlparser-rs/pull/1442) (nucccc) +- Add support for ASC and DESC in CREATE TABLE column constraints for SQLite. [#1462](https://github.com/apache/datafusion-sqlparser-rs/pull/1462) (caldwell) +- Add support of `EXPLAIN QUERY PLAN` syntax for SQLite dialect [#1458](https://github.com/apache/datafusion-sqlparser-rs/pull/1458) (git-hulk) +- Add "DROP TYPE" support. [#1461](https://github.com/apache/datafusion-sqlparser-rs/pull/1461) (caldwell) +- chore: Add asf.yaml [#1463](https://github.com/apache/datafusion-sqlparser-rs/pull/1463) (Xuanwo) +- Add support for quantified comparison predicates (ALL/ANY/SOME) [#1459](https://github.com/apache/datafusion-sqlparser-rs/pull/1459) (yoavcloud) +- MySQL dialect: Add support for hash comments [#1466](https://github.com/apache/datafusion-sqlparser-rs/pull/1466) (hansott) +- Fix #1469 (SET ROLE regression) [#1474](https://github.com/apache/datafusion-sqlparser-rs/pull/1474) (lovasoa) +- Add support for parsing MsSql alias with equals [#1467](https://github.com/apache/datafusion-sqlparser-rs/pull/1467) (yoavcloud) +- Snowflake: support for extended column options in `CREATE TABLE` [#1454](https://github.com/apache/datafusion-sqlparser-rs/pull/1454) (7phs) +- MsSQL TRY_CONVERT [#1477](https://github.com/apache/datafusion-sqlparser-rs/pull/1477) (yoavcloud) +- Add PostgreSQL specfic "CREATE TYPE t AS ENUM (...)" support. [#1460](https://github.com/apache/datafusion-sqlparser-rs/pull/1460) (caldwell) +- Fix build [#1483](https://github.com/apache/datafusion-sqlparser-rs/pull/1483) (yoavcloud) +- Fix complex blocks warning when running clippy [#1488](https://github.com/apache/datafusion-sqlparser-rs/pull/1488) (git-hulk) +- Add support for SHOW DATABASES/SCHEMAS/TABLES/VIEWS in Hive [#1487](https://github.com/apache/datafusion-sqlparser-rs/pull/1487) (yoavcloud) +- Fix typo in `Dialect::supports_eq_alias_assigment` [#1478](https://github.com/apache/datafusion-sqlparser-rs/pull/1478) (alamb) +- Add support for PostgreSQL `LISTEN/NOTIFY` syntax [#1485](https://github.com/apache/datafusion-sqlparser-rs/pull/1485) (wugeer) +- Add support for TOP before ALL/DISTINCT [#1495](https://github.com/apache/datafusion-sqlparser-rs/pull/1495) (yoavcloud) +- add support for `FOR ORDINALITY` and `NESTED` in JSON_TABLE [#1493](https://github.com/apache/datafusion-sqlparser-rs/pull/1493) (lovasoa) +- Add Apache License to additional files [#1502](https://github.com/apache/datafusion-sqlparser-rs/pull/1502) (alamb) +- Move CHANGELOG content [#1503](https://github.com/apache/datafusion-sqlparser-rs/pull/1503) (alamb) +- improve support for T-SQL EXECUTE statements [#1490](https://github.com/apache/datafusion-sqlparser-rs/pull/1490) (lovasoa) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 8 Andrew Lamb + 7 Yoav Cohen + 7 hulk + 3 Aleksei Piianin + 3 David Caldwell + 3 Ophir LOJKINE + 1 Agaev Guseyn + 1 Eason + 1 Fischer + 1 Hans Ott + 1 Heran Lin + 1 Joshua Warner + 1 Maxwell Knight + 1 Seve Martinez + 1 Siyuan Huang + 1 Thomas Dagenais + 1 Xuanwo + 1 nucccc + 1 tomershaniii + 1 wugeer +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. + diff --git a/dev/release/README.md b/dev/release/README.md new file mode 100644 index 000000000..c440f7387 --- /dev/null +++ b/dev/release/README.md @@ -0,0 +1,181 @@ + + + +## Process Overview + +As part of the Apache governance model, official releases consist of signed +source tarballs approved by the DataFusion PMC. + +We then use the code in the approved artifacts to release to crates.io. + +### Change Log + +We maintain a `CHANGELOG.md` so our users know what has been changed between releases. + +You will need a GitHub Personal Access Token for the following steps. Follow +[these instructions](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) +to generate one if you do not already have one. + +The changelog is generated using a Python script which needs `PyGitHub`, installed using pip: + +```shell +pip3 install PyGitHub +``` + +To generate the changelog, set the `GITHUB_TOKEN` environment variable to a valid token and then run the script +providing two commit ids or tags followed by the version number of the release being created. The following +example generates a change log of all changes between the first commit and the current HEAD revision. + +```shell +export GITHUB_TOKEN= +python ./dev/release/generate-changelog.py v0.51.0 HEAD 0.52.0 > changelog/0.52.0.md +``` + +This script creates a changelog from GitHub PRs based on the labels associated with them as well as looking for +titles starting with `feat:`, `fix:`, or `docs:`. + +Add an entry to CHANGELOG.md for the new version + +## Prepare release commits and PR + +### Update Version + +Checkout the main commit to be released + +```shell +git fetch apache +git checkout apache/main +``` + +Manually update the version in the root `Cargo.toml` to the release version (e.g. `0.52.0`). + +Lastly commit the version change: + +```shell +git commit -a -m 'Update version' +``` + +## Prepare release candidate artifacts + +After the PR gets merged, you are ready to create release artifacts from the +merged commit. + +(Note you need to be a committer to run these scripts as they upload to the apache svn distribution servers) + +### Pick a Release Candidate (RC) number + +Pick numbers in sequential order, with `0` for `rc0`, `1` for `rc1`, etc. + +### Create git tag for the release: + +While the official release artifacts are signed tarballs and zip files, we also +tag the commit it was created for convenience and code archaeology. + +Using a string such as `v0.52.0` as the ``, create and push the tag by running these commands: + +For example to tag version `0.52.0` + +```shell +git fetch apache +git tag v0.52.0-rc1 apache/main +# push tag to Github remote +git push apache v0.52.0-rc1 +``` + +### Create, sign, and upload artifacts + +Run `create-tarball.sh` with the `` tag and `` and you found in previous steps: + +```shell +GITHUB_TOKEN= ./dev/release/create-tarball.sh 0.52.0 1 +``` + +The `create-tarball.sh` script + +1. creates and uploads all release candidate artifacts to the [datafusion + dev](https://dist.apache.org/repos/dist/dev/datafusion) location on the + apache distribution svn server + +2. provide you an email template to + send to dev@datafusion.apache.org for release voting. + +### Vote on Release Candidate artifacts + +Send the email output from the script to dev@datafusion.apache.org. + +For the release to become "official" it needs at least three PMC members to vote +1 on it. + +### Verifying Release Candidates + +The `dev/release/verify-release-candidate.sh` is a script in this repository that can assist in the verification process. Run it like: + +```shell +./dev/release/verify-release-candidate.sh 0.52.0 1 +``` + +#### If the release is not approved + +If the release is not approved, fix whatever the problem is, merge changelog +changes into main if there is any and try again with the next RC number. + +## Finalize the release + +NOTE: steps in this section can only be done by PMC members. + +### After the release is approved + +Move artifacts to the release location in SVN, using the `release-tarball.sh` script: + +```shell +./dev/release/release-tarball.sh 0.52.0 1 +``` + +Congratulations! The release is now official! + +### Publish on Crates.io + +Only approved releases of the tarball should be published to +crates.io, in order to conform to Apache Software Foundation +governance standards. + +A DataFusion committer can publish this crate after an official project release has +been made to crates.io using the following instructions. + +Follow [these +instructions](https://doc.rust-lang.org/cargo/reference/publishing.html) to +create an account and login to crates.io before asking to be added as an owner +to the sqlparser DataFusion crates. + +Download and unpack the official release tarball + +Verify that the Cargo.toml in the tarball contains the correct version +(e.g. `version = "0.52.0"`) and then publish the crates by running the following commands + +```shell +cargo publish +``` + +If necessary, also publish the `sqlparser_derive` crate: + +crates.io homepage: https://crates.io/crates/sqlparser_derive + +```shell +(cd derive && cargo publish +``` diff --git a/dev/release/check-rat-report.py b/dev/release/check-rat-report.py new file mode 100644 index 000000000..e30d72bdd --- /dev/null +++ b/dev/release/check-rat-report.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +############################################################################## +# 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. +############################################################################## +import fnmatch +import re +import sys +import xml.etree.ElementTree as ET + +if len(sys.argv) != 3: + sys.stderr.write("Usage: %s exclude_globs.lst rat_report.xml\n" % + sys.argv[0]) + sys.exit(1) + +exclude_globs_filename = sys.argv[1] +xml_filename = sys.argv[2] + +globs = [line.strip() for line in open(exclude_globs_filename, "r")] + +tree = ET.parse(xml_filename) +root = tree.getroot() +resources = root.findall('resource') + +all_ok = True +for r in resources: + approvals = r.findall('license-approval') + if not approvals or approvals[0].attrib['name'] == 'true': + continue + clean_name = re.sub('^[^/]+/', '', r.attrib['name']) + excluded = False + for g in globs: + if fnmatch.fnmatch(clean_name, g): + excluded = True + break + if not excluded: + sys.stdout.write("NOT APPROVED: %s (%s): %s\n" % ( + clean_name, r.attrib['name'], approvals[0].attrib['name'])) + all_ok = False + +if not all_ok: + sys.exit(1) + +print('OK') +sys.exit(0) diff --git a/dev/release/create-tarball.sh b/dev/release/create-tarball.sh new file mode 100755 index 000000000..4cb17cd36 --- /dev/null +++ b/dev/release/create-tarball.sh @@ -0,0 +1,135 @@ +#!/bin/bash +# +# 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. +# + +# Adapted from https://github.com/apache/datafusion/tree/master/dev/release/create-tarball.sh + +# This script creates a signed tarball in +# dev/dist/apache-datafusion-sqlparser-rs--rc.tar.gz and uploads it to +# the "dev" area of the dist.apache.datafusion repository and prepares an +# email for sending to the dev@datafusion.apache.org list for a formal +# vote. +# +# See release/README.md for full release instructions +# +# Requirements: +# +# 1. gpg setup for signing and have uploaded your public +# signature to https://pgp.mit.edu/ +# +# 2. Logged into the apache svn server with the appropriate +# credentials +# +# 3. Install the requests python package +# +# +# Based in part on 02-source.sh from apache/arrow +# + +set -e + +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOURCE_TOP_DIR="$(cd "${SOURCE_DIR}/../../" && pwd)" + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + echo "ex. $0 0.52.0 2" + exit +fi + +if [[ -z "${GITHUB_TOKEN}" ]]; then + echo "Please set personal github token through GITHUB_TOKEN environment variable" + exit +fi + +version=$1 +rc=$2 +tag="v${version}-rc${rc}" + +echo "Attempting to create ${tarball} from tag ${tag}" +release_hash=$(cd "${SOURCE_TOP_DIR}" && git rev-list --max-count=1 ${tag}) + +release=apache-datafusion-sqlparser-rs-${version} +distdir=${SOURCE_TOP_DIR}/dev/dist/${release}-rc${rc} +tarname=${release}.tar.gz +tarball=${distdir}/${tarname} +url="https://dist.apache.org/repos/dist/dev/datafusion/${release}-rc${rc}" + +if [ -z "$release_hash" ]; then + echo "Cannot continue: unknown git tag: ${tag}" +fi + +echo "Draft email for dev@datafusion.apache.org mailing list" +echo "" +echo "---------------------------------------------------------" +cat < containing the files in git at $release_hash +# the files in the tarball are prefixed with {version} (e.g. 4.0.1) +mkdir -p ${distdir} +(cd "${SOURCE_TOP_DIR}" && git archive ${release_hash} --prefix ${release}/ | gzip > ${tarball}) + +echo "Running rat license checker on ${tarball}" +${SOURCE_DIR}/run-rat.sh ${tarball} + +echo "Signing tarball and creating checksums" +gpg --armor --output ${tarball}.asc --detach-sig ${tarball} +# create signing with relative path of tarball +# so that they can be verified with a command such as +# shasum --check apache-datafusion-sqlparser-rs-0.52.0-rc1.tar.gz.sha512 +(cd ${distdir} && shasum -a 256 ${tarname}) > ${tarball}.sha256 +(cd ${distdir} && shasum -a 512 ${tarname}) > ${tarball}.sha512 + + +echo "Uploading to sqlparser-rs dist/dev to ${url}" +svn co --depth=empty https://dist.apache.org/repos/dist/dev/datafusion ${SOURCE_TOP_DIR}/dev/dist +svn add ${distdir} +svn ci -m "Apache DataFusion ${version} ${rc}" ${distdir} diff --git a/dev/release/generate-changelog.py b/dev/release/generate-changelog.py new file mode 100755 index 000000000..52fd2e548 --- /dev/null +++ b/dev/release/generate-changelog.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python + +# 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. + +import argparse +import sys +from github import Github +import os +import re +import subprocess + +def print_pulls(repo_name, title, pulls): + if len(pulls) > 0: + print("**{}:**".format(title)) + 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)) + print() + + +def generate_changelog(repo, repo_name, tag1, tag2, version): + + # get a list of commits between two tags + print(f"Fetching list of commits between {tag1} and {tag2}", file=sys.stderr) + comparison = repo.compare(tag1, tag2) + + # get the pull requests for these commits + print("Fetching pull requests", file=sys.stderr) + unique_pulls = [] + all_pulls = [] + for commit in comparison.commits: + pulls = commit.get_pulls() + for pull in pulls: + # there can be multiple commits per PR if squash merge is not being used and + # in this case we should get all the author names, but for now just pick one + if pull.number not in unique_pulls: + unique_pulls.append(pull.number) + all_pulls.append((pull, commit)) + + # we split the pulls into categories + breaking = [] + bugs = [] + docs = [] + enhancements = [] + performance = [] + other = [] + + # categorize the pull requests based on GitHub labels + print("Categorizing pull requests", file=sys.stderr) + for (pull, commit) in all_pulls: + + # see if PR title uses Conventional Commits + cc_type = '' + cc_scope = '' + cc_breaking = '' + parts = re.findall(r'^([a-z]+)(\([a-z]+\))?(!)?:', pull.title) + if len(parts) == 1: + parts_tuple = parts[0] + cc_type = parts_tuple[0] # fix, feat, docs, chore + cc_scope = parts_tuple[1] # component within project + cc_breaking = parts_tuple[2] == '!' + + labels = [label.name for label in pull.labels] + if 'api change' in labels or cc_breaking: + breaking.append((pull, commit)) + elif 'bug' in labels or cc_type == 'fix': + bugs.append((pull, commit)) + elif 'performance' in labels or cc_type == 'perf': + performance.append((pull, commit)) + elif 'enhancement' in labels or cc_type == 'feat': + enhancements.append((pull, commit)) + elif 'documentation' in labels or cc_type == 'docs' or cc_type == 'doc': + docs.append((pull, commit)) + else: + other.append((pull, commit)) + + # produce the changelog content + print("Generating changelog content", file=sys.stderr) + + # ASF header + print("""\n""") + + print(f"# sqlparser-rs {version} Changelog\n") + + # get the number of commits + commit_count = subprocess.check_output(f"git log --pretty=oneline {tag1}..{tag2} | wc -l", shell=True, text=True).strip() + + # get number of contributors + contributor_count = subprocess.check_output(f"git shortlog -sn {tag1}..{tag2} | wc -l", shell=True, text=True).strip() + + print(f"This release consists of {commit_count} commits from {contributor_count} contributors. " + f"See credits at the end of this changelog for more information.\n") + + print_pulls(repo_name, "Breaking changes", breaking) + print_pulls(repo_name, "Performance related", performance) + print_pulls(repo_name, "Implemented enhancements", enhancements) + print_pulls(repo_name, "Fixed bugs", bugs) + print_pulls(repo_name, "Documentation updates", docs) + print_pulls(repo_name, "Other", other) + + # show code contributions + credits = subprocess.check_output(f"git shortlog -sn {tag1}..{tag2}", shell=True, text=True).rstrip() + + print("## Credits\n") + print("Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) " + "per contributor.\n") + print("```") + print(credits) + print("```\n") + + print("Thank you also to everyone who contributed in other ways such as filing issues, reviewing " + "PRs, and providing feedback on this release.\n") + +def cli(args=None): + """Process command line arguments.""" + if not args: + args = sys.argv[1:] + + parser = argparse.ArgumentParser() + parser.add_argument("tag1", help="The previous commit or tag (e.g. 0.1.0)") + parser.add_argument("tag2", help="The current commit or tag (e.g. HEAD)") + parser.add_argument("version", help="The version number to include in the changelog") + args = parser.parse_args() + + token = os.getenv("GITHUB_TOKEN") + project = "apache/datafusion-sqlparser-rs" + + g = Github(token) + repo = g.get_repo(project) + generate_changelog(repo, project, args.tag1, args.tag2, args.version) + +if __name__ == "__main__": + cli() \ No newline at end of file diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt new file mode 100644 index 000000000..a567eda9c --- /dev/null +++ b/dev/release/rat_exclude_files.txt @@ -0,0 +1,6 @@ +# Files to exclude from the Apache Rat (license) check +.gitignore +.tool-versions +dev/release/rat_exclude_files.txt +fuzz/.gitignore + diff --git a/dev/release/release-tarball.sh b/dev/release/release-tarball.sh new file mode 100755 index 000000000..e59b2776c --- /dev/null +++ b/dev/release/release-tarball.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# +# 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. +# + +# Adapted from https://github.com/apache/arrow-rs/tree/master/dev/release/release-tarball.sh + +# This script copies a tarball from the "dev" area of the +# dist.apache.datafusion repository to the "release" area +# +# This script should only be run after the release has been approved +# by the Apache DataFusion PMC committee. +# +# See release/README.md for full release instructions +# +# Based in part on post-01-upload.sh from apache/arrow + + +set -e +set -u + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + echo "ex. $0 0.52.0 2" + exit +fi + +version=$1 +rc=$2 + +tmp_dir=tmp-apache-datafusion-dist + +echo "Recreate temporary directory: ${tmp_dir}" +rm -rf ${tmp_dir} +mkdir -p ${tmp_dir} + +echo "Clone dev dist repository" +svn \ + co \ + https://dist.apache.org/repos/dist/dev/datafusion/apache-datafusion-sqlparser-rs-${version}-rc${rc} \ + ${tmp_dir}/dev + +echo "Clone release dist repository" +svn co https://dist.apache.org/repos/dist/release/datafusion ${tmp_dir}/release + +echo "Copy ${version}-rc${rc} to release working copy" +release_version=datafusion-sqlparser-rs-${version} +mkdir -p ${tmp_dir}/release/${release_version} +cp -r ${tmp_dir}/dev/* ${tmp_dir}/release/${release_version}/ +svn add ${tmp_dir}/release/${release_version} + +echo "Commit release" +svn ci -m "Apache DataFusion sqlparser-rs ${version}" ${tmp_dir}/release + +echo "Clean up" +rm -rf ${tmp_dir} + +echo "Success! The release is available here:" +echo " https://dist.apache.org/repos/dist/release/datafusion/${release_version}" diff --git a/dev/release/run-rat.sh b/dev/release/run-rat.sh new file mode 100755 index 000000000..94fa55fbe --- /dev/null +++ b/dev/release/run-rat.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# 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. +# + +RAT_VERSION=0.13 + +# download apache rat +if [ ! -f apache-rat-${RAT_VERSION}.jar ]; then + curl -s https://repo1.maven.org/maven2/org/apache/rat/apache-rat/${RAT_VERSION}/apache-rat-${RAT_VERSION}.jar > apache-rat-${RAT_VERSION}.jar +fi + +RAT="java -jar apache-rat-${RAT_VERSION}.jar -x " + +RELEASE_DIR=$(cd "$(dirname "$BASH_SOURCE")"; pwd) + +# generate the rat report +$RAT $1 > rat.txt +python $RELEASE_DIR/check-rat-report.py $RELEASE_DIR/rat_exclude_files.txt rat.txt > filtered_rat.txt +cat filtered_rat.txt +UNAPPROVED=`cat filtered_rat.txt | grep "NOT APPROVED" | wc -l` + +if [ "0" -eq "${UNAPPROVED}" ]; then + echo "No unapproved licenses" +else + echo "${UNAPPROVED} unapproved licences. Check rat report: rat.txt" + exit 1 +fi diff --git a/dev/release/verify-release-candidate.sh b/dev/release/verify-release-candidate.sh new file mode 100755 index 000000000..9ff7e17b5 --- /dev/null +++ b/dev/release/verify-release-candidate.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# +# 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. +# + +case $# in + 2) VERSION="$1" + RC_NUMBER="$2" + ;; + *) echo "Usage: $0 X.Y.Z RC_NUMBER" + exit 1 + ;; +esac + +set -e +set -x +set -o pipefail + +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +ARROW_DIR="$(dirname $(dirname ${SOURCE_DIR}))" +ARROW_DIST_URL='https://dist.apache.org/repos/dist/dev/datafusion' + +download_dist_file() { + curl \ + --silent \ + --show-error \ + --fail \ + --location \ + --remote-name $ARROW_DIST_URL/$1 +} + +download_rc_file() { + download_dist_file apache-datafusion-sqlparser-rs-${VERSION}-rc${RC_NUMBER}/$1 +} + +import_gpg_keys() { + download_dist_file KEYS + gpg --import KEYS +} + +if type shasum >/dev/null 2>&1; then + sha256_verify="shasum -a 256 -c" + sha512_verify="shasum -a 512 -c" +else + sha256_verify="sha256sum -c" + sha512_verify="sha512sum -c" +fi + +fetch_archive() { + local dist_name=$1 + download_rc_file ${dist_name}.tar.gz + download_rc_file ${dist_name}.tar.gz.asc + download_rc_file ${dist_name}.tar.gz.sha256 + download_rc_file ${dist_name}.tar.gz.sha512 + verify_dir_artifact_signatures +} + +verify_dir_artifact_signatures() { + # verify the signature and the checksums of each artifact + find . -name '*.asc' | while read sigfile; do + artifact=${sigfile/.asc/} + gpg --verify $sigfile $artifact || exit 1 + + # go into the directory because the checksum files contain only the + # basename of the artifact + pushd $(dirname $artifact) + base_artifact=$(basename $artifact) + ${sha256_verify} $base_artifact.sha256 || exit 1 + ${sha512_verify} $base_artifact.sha512 || exit 1 + popd + done +} + +setup_tempdir() { + cleanup() { + if [ "${TEST_SUCCESS}" = "yes" ]; then + rm -fr "${ARROW_TMPDIR}" + else + echo "Failed to verify release candidate. See ${ARROW_TMPDIR} for details." + fi + } + + if [ -z "${ARROW_TMPDIR}" ]; then + # clean up automatically if ARROW_TMPDIR is not defined + ARROW_TMPDIR=$(mktemp -d -t "$1.XXXXX") + trap cleanup EXIT + else + # don't clean up automatically + mkdir -p "${ARROW_TMPDIR}" + fi +} + +test_source_distribution() { + # install rust toolchain in a similar fashion like test-miniconda + export RUSTUP_HOME=$PWD/test-rustup + export CARGO_HOME=$PWD/test-rustup + + curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path + + export PATH=$RUSTUP_HOME/bin:$PATH + source $RUSTUP_HOME/env + + # build and test rust + + # raises on any formatting errors + rustup component add rustfmt --toolchain stable + cargo fmt --all -- --check + + cargo build + cargo test --all-features + + if ( find -iname 'Cargo.toml' | xargs grep SNAPSHOT ); then + echo "Cargo.toml version should not contain SNAPSHOT for releases" + exit 1 + fi + + # Check that publish works + cargo publish --dry-run +} + +TEST_SUCCESS=no + +setup_tempdir "datafusion-sqlparser-rs-${VERSION}" +echo "Working in sandbox ${ARROW_TMPDIR}" +cd ${ARROW_TMPDIR} + +dist_name="apache-datafusion-sqlparser-rs-${VERSION}" +import_gpg_keys +fetch_archive ${dist_name} +tar xf ${dist_name}.tar.gz +pushd ${dist_name} + test_source_distribution +popd + +TEST_SUCCESS=yes +echo 'Release candidate looks good!' +exit 0 diff --git a/docs/releasing.md b/docs/releasing.md deleted file mode 100644 index c1b85a20c..000000000 --- a/docs/releasing.md +++ /dev/null @@ -1,81 +0,0 @@ - - -# Releasing - -## Prerequisites -Publishing to crates.io has been automated via GitHub Actions, so you will only -need push access to the [sqlparser-rs GitHub repository](https://github.com/sqlparser-rs/sqlparser-rs) -in order to publish a release. - -We use the [`cargo release`](https://github.com/sunng87/cargo-release) -subcommand to ensure correct versioning. Install via: - -``` -$ cargo install cargo-release -``` - -## Process - -1. **Before releasing** ensure `CHANGELOG.md` is updated appropriately and that - you have a clean checkout of the `main` branch of the sqlparser repository: - ``` - $ git fetch && git status - On branch main - Your branch is up to date with 'origin/main'. - - nothing to commit, working tree clean - ``` - * If you have the time, check that the examples in the README are up to date. - -2. Using `cargo-release` we can publish a new release like so: - - ``` - $ cargo release minor --push-remote origin - ``` - - After verifying, you can rerun with `--execute` if all looks good. - You can add `--no-push` to stop before actually publishing the release. - - `cargo release` will then: - - * Bump the minor part of the version in `Cargo.toml` (e.g. `0.7.1-alpha.0` - -> `0.8.0`. You can use `patch` instead of `minor`, as appropriate). - * Create a new tag (e.g. `v0.8.0`) locally - * Push the new tag to the specified remote (`origin` in the above - example), which will trigger a publishing process to crates.io as part of - the [corresponding GitHub Action](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/.github/workflows/rust.yml). - - Note that credentials for authoring in this way are securely stored in - the (GitHub) repo secrets as `CRATE_TOKEN`. - -4. Check that the new version of the crate is available on crates.io: - https://crates.io/crates/sqlparser - - -## `sqlparser_derive` crate - -Currently this crate is manually published via `cargo publish`. - -crates.io homepage: https://crates.io/crates/sqlparser_derive - -```shell -cd derive -cargo publish -``` From e857787309e1d03189578b393f20c2e4a850af8d Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Wed, 13 Nov 2024 14:36:33 +0800 Subject: [PATCH 590/806] hive: support for special not expression `!a` and raise error for `a!` factorial operator (#1472) Co-authored-by: Ifeanyi Ubah --- src/ast/operator.rs | 3 ++ src/dialect/hive.rs | 5 ++ src/dialect/mod.rs | 10 ++++ src/dialect/postgresql.rs | 5 ++ src/parser/mod.rs | 12 +++-- tests/sqlparser_common.rs | 110 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 3 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index c3bb379d6..e44ea2bf4 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -51,6 +51,8 @@ pub enum UnaryOperator { PGPrefixFactorial, /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) PGAbs, + /// Unary logical not operator: e.g. `! false` (Hive-specific) + BangNot, } impl fmt::Display for UnaryOperator { @@ -65,6 +67,7 @@ impl fmt::Display for UnaryOperator { UnaryOperator::PGPostfixFactorial => "!", UnaryOperator::PGPrefixFactorial => "!!", UnaryOperator::PGAbs => "@", + UnaryOperator::BangNot => "!", }) } } diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index 63642b33c..b97bf69be 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -51,4 +51,9 @@ impl Dialect for HiveDialect { fn require_interval_qualifier(&self) -> bool { true } + + /// See Hive + fn supports_bang_not_operator(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 453fee3de..7592740ca 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -575,6 +575,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `a!` expressions + fn supports_factorial_operator(&self) -> bool { + false + } + /// Returns true if this dialect supports treating the equals operator `=` within a `SelectItem` /// as an alias assignment operator, rather than a boolean expression. /// For example: the following statements are equivalent for such a dialect: @@ -591,6 +596,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `!a` syntax for boolean `NOT` expressions. + fn supports_bang_not_operator(&self) -> bool { + false + } + /// Returns true if the dialect supports the `LISTEN` statement fn supports_listen(&self) -> bool { false diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index c40c826c4..72841c604 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -201,6 +201,11 @@ impl Dialect for PostgreSqlDialect { fn supports_notify(&self) -> bool { true } + + /// see + fn supports_factorial_operator(&self) -> bool { + true + } } pub fn parse_comment(parser: &mut Parser) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 942ff19fd..e329c0177 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1194,6 +1194,14 @@ impl<'a> Parser<'a> { ), }) } + Token::ExclamationMark if self.dialect.supports_bang_not_operator() => { + Ok(Expr::UnaryOp { + op: UnaryOperator::BangNot, + expr: Box::new( + self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryNot))?, + ), + }) + } tok @ Token::DoubleExclamationMark | tok @ Token::PGSquareRoot | tok @ Token::PGCubeRoot @@ -1287,7 +1295,6 @@ impl<'a> Parser<'a> { } _ => self.expected("an expression", next_token), }?; - if self.parse_keyword(Keyword::COLLATE) { Ok(Expr::Collate { expr: Box::new(expr), @@ -2818,8 +2825,7 @@ impl<'a> Parser<'a> { data_type: self.parse_data_type()?, format: None, }) - } else if Token::ExclamationMark == tok { - // PostgreSQL factorial operation + } else if Token::ExclamationMark == tok && self.dialect.supports_factorial_operator() { Ok(Expr::UnaryOp { op: UnaryOperator::PGPostfixFactorial, expr: Box::new(expr), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e37280636..84f2f718b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11532,3 +11532,113 @@ fn test_select_top() { dialects.verified_stmt("SELECT TOP 3 DISTINCT * FROM tbl"); dialects.verified_stmt("SELECT TOP 3 DISTINCT a, b, c FROM tbl"); } + +#[test] +fn parse_bang_not() { + let dialects = all_dialects_where(|d| d.supports_bang_not_operator()); + let sql = "SELECT !a, !(b > 3)"; + let Select { projection, .. } = dialects.verified_only_select(sql); + + for (i, expr) in [ + Box::new(Expr::Identifier(Ident::new("a"))), + Box::new(Expr::Nested(Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("b"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::Value(Value::Number("3".parse().unwrap(), false))), + }))), + ] + .into_iter() + .enumerate() + { + assert_eq!( + SelectItem::UnnamedExpr(Expr::UnaryOp { + op: UnaryOperator::BangNot, + expr + }), + projection[i] + ) + } + + let sql_statements = ["SELECT a!", "SELECT a ! b", "SELECT a ! as b"]; + + for &sql in &sql_statements { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("No infix parser for token ExclamationMark".to_string()) + ); + } + + let sql_statements = ["SELECT !a", "SELECT !a b", "SELECT !a as b"]; + let dialects = all_dialects_where(|d| !d.supports_bang_not_operator()); + + for &sql in &sql_statements { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected: an expression, found: !".to_string()) + ); + } +} + +#[test] +fn parse_factorial_operator() { + let dialects = all_dialects_where(|d| d.supports_factorial_operator()); + let sql = "SELECT a!, (b + c)!"; + let Select { projection, .. } = dialects.verified_only_select(sql); + + for (i, expr) in [ + Box::new(Expr::Identifier(Ident::new("a"))), + Box::new(Expr::Nested(Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("b"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Identifier(Ident::new("c"))), + }))), + ] + .into_iter() + .enumerate() + { + assert_eq!( + SelectItem::UnnamedExpr(Expr::UnaryOp { + op: UnaryOperator::PGPostfixFactorial, + expr + }), + projection[i] + ) + } + + let sql_statements = ["SELECT !a", "SELECT !a b", "SELECT !a as b"]; + + for &sql in &sql_statements { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected: an expression, found: !".to_string()) + ); + } + + let sql_statements = ["SELECT a!", "SELECT a ! b", "SELECT a ! as b"]; + + // Due to the exclamation mark, which is both part of the `bang not` operator + // and the `factorial` operator, additional filtering not supports + // `bang not` operator is required here. + let dialects = + all_dialects_where(|d| !d.supports_factorial_operator() && !d.supports_bang_not_operator()); + + for &sql in &sql_statements { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("No infix parser for token ExclamationMark".to_string()) + ); + } + + // Due to the exclamation mark, which is both part of the `bang not` operator + // and the `factorial` operator, additional filtering supports + // `bang not` operator is required here. + let dialects = + all_dialects_where(|d| !d.supports_factorial_operator() && d.supports_bang_not_operator()); + + for &sql in &sql_statements { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("No infix parser for token ExclamationMark".to_string()) + ); + } +} From 90824486df096f37b9f9dd8f1d74f8043c7b1f64 Mon Sep 17 00:00:00 2001 From: gaoqiangz <38213294+gaoqiangz@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:41:13 +0800 Subject: [PATCH 591/806] Add support for MSSQL's `OPENJSON WITH` clause (#1498) --- src/ast/mod.rs | 12 +- src/ast/query.rs | 74 +++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 60 +++++++ tests/sqlparser_mssql.rs | 335 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 476 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 31d7af1ba..81bddcd17 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -56,12 +56,12 @@ pub use self::query::{ InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, - NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderBy, OrderByExpr, - PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, - ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, - SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, - TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, Values, - WildcardAdditionalOptions, With, WithFill, + NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, + OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, + SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, + TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, + ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index 7af472430..60ebe3765 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1036,6 +1036,27 @@ pub enum TableFactor { /// The alias for the table. alias: Option, }, + /// The MSSQL's `OPENJSON` table-valued function. + /// + /// ```sql + /// OPENJSON( jsonExpression [ , path ] ) [ ] + /// + /// ::= WITH ( { colName type [ column_path ] [ AS JSON ] } [ ,...n ] ) + /// ```` + /// + /// Reference: + OpenJsonTable { + /// The JSON expression to be evaluated. It must evaluate to a json string + json_expr: Expr, + /// The path to the array or object to be iterated over. + /// It must evaluate to a json array or object. + json_path: Option, + /// The columns to be extracted from each element of the array or object. + /// Each column must have a name and a type. + columns: Vec, + /// The alias for the table. + alias: Option, + }, /// Represents a parenthesized table factor. The SQL spec only allows a /// join expression (`(foo bar [ baz ... ])`) to be nested, /// possibly several times. @@ -1461,6 +1482,25 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::OpenJsonTable { + json_expr, + json_path, + columns, + alias, + } => { + write!(f, "OPENJSON({json_expr}")?; + if let Some(json_path) = json_path { + write!(f, ", {json_path}")?; + } + write!(f, ")")?; + if !columns.is_empty() { + write!(f, " WITH ({})", display_comma_separated(columns))?; + } + if let Some(alias) = alias { + write!(f, " AS {alias}")?; + } + Ok(()) + } TableFactor::NestedJoin { table_with_joins, alias, @@ -2421,6 +2461,40 @@ impl fmt::Display for JsonTableColumnErrorHandling { } } +/// A single column definition in MSSQL's `OPENJSON WITH` clause. +/// +/// ```sql +/// colName type [ column_path ] [ AS JSON ] +/// ``` +/// +/// Reference: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct OpenJsonTableColumn { + /// The name of the column to be extracted. + pub name: Ident, + /// The type of the column to be extracted. + pub r#type: DataType, + /// The path to the column to be extracted. Must be a literal string. + pub path: Option, + /// The `AS JSON` option. + pub as_json: bool, +} + +impl fmt::Display for OpenJsonTableColumn { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.name, self.r#type)?; + if let Some(path) = &self.path { + write!(f, " '{}'", value::escape_single_quote_string(path))?; + } + if self.as_json { + write!(f, " AS JSON")?; + } + Ok(()) + } +} + /// BigQuery supports ValueTables which have 2 modes: /// `SELECT AS STRUCT` /// `SELECT AS VALUE` diff --git a/src/keywords.rs b/src/keywords.rs index d60227c99..982cea81b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -537,6 +537,7 @@ define_keywords!( ONE, ONLY, OPEN, + OPENJSON, OPERATOR, OPTIMIZE, OPTIMIZER_COSTS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e329c0177..a69f1db10 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10049,6 +10049,7 @@ impl<'a> Parser<'a> { | TableFactor::Function { alias, .. } | TableFactor::UNNEST { alias, .. } | TableFactor::JsonTable { alias, .. } + | TableFactor::OpenJsonTable { alias, .. } | TableFactor::TableFunction { alias, .. } | TableFactor::Pivot { alias, .. } | TableFactor::Unpivot { alias, .. } @@ -10162,6 +10163,9 @@ impl<'a> Parser<'a> { columns, alias, }) + } else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[Token::LParen]) { + self.prev_token(); + self.parse_open_json_table_factor() } else { let name = self.parse_object_name(true)?; @@ -10227,6 +10231,34 @@ impl<'a> Parser<'a> { } } + /// Parses `OPENJSON( jsonExpression [ , path ] ) [ ]` clause, + /// assuming the `OPENJSON` keyword was already consumed. + fn parse_open_json_table_factor(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let json_expr = self.parse_expr()?; + let json_path = if self.consume_token(&Token::Comma) { + Some(self.parse_value()?) + } else { + None + }; + self.expect_token(&Token::RParen)?; + let columns = if self.parse_keyword(Keyword::WITH) { + self.expect_token(&Token::LParen)?; + let columns = self.parse_comma_separated(Parser::parse_openjson_table_column_def)?; + self.expect_token(&Token::RParen)?; + columns + } else { + Vec::new() + }; + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + Ok(TableFactor::OpenJsonTable { + json_expr, + json_path, + columns, + alias, + }) + } + fn parse_match_recognize(&mut self, table: TableFactor) -> Result { self.expect_token(&Token::LParen)?; @@ -10513,6 +10545,34 @@ impl<'a> Parser<'a> { })) } + /// Parses MSSQL's `OPENJSON WITH` column definition. + /// + /// ```sql + /// colName type [ column_path ] [ AS JSON ] + /// ``` + /// + /// Reference: + pub fn parse_openjson_table_column_def(&mut self) -> Result { + let name = self.parse_identifier(false)?; + let r#type = self.parse_data_type()?; + let path = if let Token::SingleQuotedString(path) = self.peek_token().token { + self.next_token(); + Some(path) + } else { + None + }; + let as_json = self.parse_keyword(Keyword::AS); + if as_json { + self.expect_keyword(Keyword::JSON)?; + } + Ok(OpenJsonTableColumn { + name, + r#type, + path, + as_json, + }) + } + fn parse_json_table_column_error_handling( &mut self, ) -> Result, ParserError> { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index c5f43b072..a1ec5e24a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -193,6 +193,341 @@ fn parse_mssql_apply_join() { ); } +#[test] +fn parse_mssql_openjson() { + let select = ms().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param, '$.config') WITH (kind VARCHAR(20) '$.kind', [id_list] NVARCHAR(MAX) '$.id_list' AS JSON) AS B", + ); + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: Some(Value::SingleQuotedString("$.config".into())), + columns: vec![ + OpenJsonTableColumn { + name: Ident { + value: "kind".into(), + quote_style: None, + }, + r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 20, + unit: None + })), + path: Some("$.kind".into()), + as_json: false + }, + OpenJsonTableColumn { + name: Ident { + value: "id_list".into(), + quote_style: Some('['), + }, + r#type: DataType::Nvarchar(Some(CharacterLength::Max)), + path: Some("$.id_list".into()), + as_json: true + } + ], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); + let select = ms().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20) '$.kind', [id_list] NVARCHAR(MAX) '$.id_list' AS JSON) AS B", + ); + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: None, + columns: vec![ + OpenJsonTableColumn { + name: Ident { + value: "kind".into(), + quote_style: None, + }, + r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 20, + unit: None + })), + path: Some("$.kind".into()), + as_json: false + }, + OpenJsonTableColumn { + name: Ident { + value: "id_list".into(), + quote_style: Some('['), + }, + r#type: DataType::Nvarchar(Some(CharacterLength::Max)), + path: Some("$.id_list".into()), + as_json: true + } + ], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); + let select = ms().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20), [id_list] NVARCHAR(MAX)) AS B", + ); + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: None, + columns: vec![ + OpenJsonTableColumn { + name: Ident { + value: "kind".into(), + quote_style: None, + }, + r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 20, + unit: None + })), + path: None, + as_json: false + }, + OpenJsonTableColumn { + name: Ident { + value: "id_list".into(), + quote_style: Some('['), + }, + r#type: DataType::Nvarchar(Some(CharacterLength::Max)), + path: None, + as_json: false + } + ], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); + let select = ms_and_generic().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param, '$.config') AS B", + ); + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: Some(Value::SingleQuotedString("$.config".into())), + columns: vec![], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); + let select = ms_and_generic().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param) AS B", + ); + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: None, + columns: vec![], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); +} + #[test] fn parse_mssql_top_paren() { let sql = "SELECT TOP (5) * FROM foo"; From 3a8369aaf5eb0d7daebf906326e80ae08db45c90 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Wed, 13 Nov 2024 11:25:26 +0100 Subject: [PATCH 592/806] Parse true and false as identifiers in mssql (#1510) --- src/dialect/mod.rs | 6 +++ src/dialect/mssql.rs | 5 +++ src/parser/mod.rs | 14 +++++-- tests/sqlparser_common.rs | 81 ++++++++++++--------------------------- tests/sqlparser_mssql.rs | 12 ++++++ tests/sqlparser_mysql.rs | 39 +++++++++++++++++++ 6 files changed, 97 insertions(+), 60 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 7592740ca..c8c11bc95 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -616,6 +616,12 @@ pub trait Dialect: Debug + Any { fn supports_top_before_distinct(&self) -> bool { false } + + /// Returns true if the dialect supports boolean literals (`true` and `false`). + /// For example, in MSSQL these are treated as identifiers rather than boolean literals. + fn supports_boolean_literals(&self) -> bool { + true + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index a5ee0bf75..8aab0bc8a 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -57,4 +57,9 @@ impl Dialect for MsSqlDialect { fn supports_try_convert(&self) -> bool { true } + + /// In MSSQL, there is no boolean type, and `true` and `false` are valid column names + fn supports_boolean_literals(&self) -> bool { + false + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a69f1db10..355456d52 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1014,7 +1014,11 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); let expr = match next_token.token { Token::Word(w) => match w.keyword { - Keyword::TRUE | Keyword::FALSE | Keyword::NULL => { + Keyword::TRUE | Keyword::FALSE if self.dialect.supports_boolean_literals() => { + self.prev_token(); + Ok(Expr::Value(self.parse_value()?)) + } + Keyword::NULL => { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } @@ -7577,8 +7581,12 @@ impl<'a> Parser<'a> { let location = next_token.location; match next_token.token { Token::Word(w) => match w.keyword { - Keyword::TRUE => Ok(Value::Boolean(true)), - Keyword::FALSE => Ok(Value::Boolean(false)), + Keyword::TRUE if self.dialect.supports_boolean_literals() => { + Ok(Value::Boolean(true)) + } + Keyword::FALSE if self.dialect.supports_boolean_literals() => { + Ok(Value::Boolean(false)) + } Keyword::NULL => Ok(Value::Null), Keyword::NoKeyword if w.quote_style.is_some() => match w.quote_style { Some('"') => Ok(Value::DoubleQuotedString(w.value)), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 84f2f718b..bef0f535c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -820,7 +820,7 @@ fn parse_top_level() { verified_stmt("(SELECT 1)"); verified_stmt("((SELECT 1))"); verified_stmt("VALUES (1)"); - verified_stmt("VALUES ROW(1, true, 'a'), ROW(2, false, 'b')"); + verified_stmt("VALUES ROW(1, NULL, 'a'), ROW(2, NULL, 'b')"); } #[test] @@ -1499,7 +1499,7 @@ fn parse_is_not_distinct_from() { #[test] fn parse_not_precedence() { // NOT has higher precedence than OR/AND, so the following must parse as (NOT true) OR true - let sql = "NOT true OR true"; + let sql = "NOT 1 OR 1"; assert_matches!( verified_expr(sql), Expr::BinaryOp { @@ -1919,44 +1919,6 @@ fn parse_binary_all() { ); } -#[test] -fn parse_logical_xor() { - let sql = "SELECT true XOR true, false XOR false, true XOR false, false XOR true"; - let select = verified_only_select(sql); - assert_eq!( - SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(true))), - op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(true))), - }), - select.projection[0] - ); - assert_eq!( - SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(false))), - op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(false))), - }), - select.projection[1] - ); - assert_eq!( - SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(true))), - op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(false))), - }), - select.projection[2] - ); - assert_eq!( - SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(false))), - op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(true))), - }), - select.projection[3] - ); -} - #[test] fn parse_between() { fn chk(negated: bool) { @@ -4113,14 +4075,14 @@ fn parse_alter_table_alter_column() { ); match alter_table_op(verified_stmt(&format!( - "{alter_stmt} ALTER COLUMN is_active SET DEFAULT false" + "{alter_stmt} ALTER COLUMN is_active SET DEFAULT 0" ))) { AlterTableOperation::AlterColumn { column_name, op } => { assert_eq!("is_active", column_name.to_string()); assert_eq!( op, AlterColumnOperation::SetDefault { - value: Expr::Value(Value::Boolean(false)) + value: Expr::Value(test_utils::number("0")) } ); } @@ -6502,7 +6464,7 @@ fn parse_values() { verified_stmt("SELECT * FROM (VALUES (1), (2), (3))"); verified_stmt("SELECT * FROM (VALUES (1), (2), (3)), (VALUES (1, 2, 3))"); verified_stmt("SELECT * FROM (VALUES (1)) UNION VALUES (1)"); - verified_stmt("SELECT * FROM (VALUES ROW(1, true, 'a'), ROW(2, false, 'b')) AS t (a, b, c)"); + verified_stmt("SELECT * FROM (VALUES ROW(1, NULL, 'a'), ROW(2, NULL, 'b')) AS t (a, b, c)"); } #[test] @@ -7321,7 +7283,7 @@ fn lateral_derived() { let lateral_str = if lateral_in { "LATERAL " } else { "" }; let sql = format!( "SELECT * FROM customer LEFT JOIN {lateral_str}\ - (SELECT * FROM order WHERE order.customer = customer.id LIMIT 3) AS order ON true" + (SELECT * FROM orders WHERE orders.customer = customer.id LIMIT 3) AS orders ON 1" ); let select = verified_only_select(&sql); let from = only(select.from); @@ -7329,7 +7291,7 @@ fn lateral_derived() { let join = &from.joins[0]; assert_eq!( join.join_operator, - JoinOperator::LeftOuter(JoinConstraint::On(Expr::Value(Value::Boolean(true)))) + JoinOperator::LeftOuter(JoinConstraint::On(Expr::Value(test_utils::number("1")))) ); if let TableFactor::Derived { lateral, @@ -7338,10 +7300,10 @@ fn lateral_derived() { } = join.relation { assert_eq!(lateral_in, lateral); - assert_eq!(Ident::new("order"), alias.name); + assert_eq!(Ident::new("orders"), alias.name); assert_eq!( subquery.to_string(), - "SELECT * FROM order WHERE order.customer = customer.id LIMIT 3" + "SELECT * FROM orders WHERE orders.customer = customer.id LIMIT 3" ); } else { unreachable!() @@ -8381,7 +8343,7 @@ fn parse_merge() { _ => unreachable!(), }; - let sql = "MERGE INTO s.bar AS dest USING newArrivals AS S ON false WHEN NOT MATCHED THEN INSERT VALUES (stg.A, stg.B, stg.C)"; + let sql = "MERGE INTO s.bar AS dest USING newArrivals AS S ON (1 > 1) WHEN NOT MATCHED THEN INSERT VALUES (stg.A, stg.B, stg.C)"; verified_stmt(sql); } @@ -11160,13 +11122,11 @@ fn parse_explain_with_option_list() { #[test] fn test_create_policy() { - let sql = concat!( - "CREATE POLICY my_policy ON my_table ", - "AS PERMISSIVE FOR SELECT ", - "TO my_role, CURRENT_USER ", - "USING (c0 = 1) ", - "WITH CHECK (true)" - ); + let sql: &str = "CREATE POLICY my_policy ON my_table \ + AS PERMISSIVE FOR SELECT \ + TO my_role, CURRENT_USER \ + USING (c0 = 1) \ + WITH CHECK (1 = 1)"; match all_dialects().verified_stmt(sql) { Statement::CreatePolicy { @@ -11194,7 +11154,14 @@ fn test_create_policy() { right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), }) ); - assert_eq!(with_check, Some(Expr::Value(Value::Boolean(true)))); + assert_eq!( + with_check, + Some(Expr::BinaryOp { + left: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + }) + ); } _ => unreachable!(), } @@ -11205,7 +11172,7 @@ fn test_create_policy() { "AS PERMISSIVE FOR SELECT ", "TO my_role, CURRENT_USER ", "USING (c0 IN (SELECT column FROM t0)) ", - "WITH CHECK (true)" + "WITH CHECK (1 = 1)" )); // omit AS / FOR / TO / USING / WITH CHECK clauses is allowed all_dialects().verified_stmt("CREATE POLICY my_policy ON my_table"); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index a1ec5e24a..4f9f6bb82 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1366,6 +1366,18 @@ fn parse_create_table_with_identity_column() { } } +#[test] +fn parse_true_false_as_identifiers() { + assert_eq!( + ms().verified_expr("true"), + Expr::Identifier(Ident::new("true")) + ); + assert_eq!( + ms().verified_expr("false"), + Expr::Identifier(Ident::new("false")) + ); +} + fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 47f7f5b4b..44b2ac6ba 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2817,3 +2817,42 @@ fn test_group_concat() { mysql_and_generic() .verified_expr("GROUP_CONCAT(DISTINCT test_score ORDER BY test_score DESC SEPARATOR ' ')"); } + +/// The XOR binary operator is only supported in MySQL +#[test] +fn parse_logical_xor() { + let sql = "SELECT true XOR true, false XOR false, true XOR false, false XOR true"; + let select = mysql_and_generic().verified_only_select(sql); + assert_eq!( + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Value(Value::Boolean(true))), + op: BinaryOperator::Xor, + right: Box::new(Expr::Value(Value::Boolean(true))), + }), + select.projection[0] + ); + assert_eq!( + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Value(Value::Boolean(false))), + op: BinaryOperator::Xor, + right: Box::new(Expr::Value(Value::Boolean(false))), + }), + select.projection[1] + ); + assert_eq!( + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Value(Value::Boolean(true))), + op: BinaryOperator::Xor, + right: Box::new(Expr::Value(Value::Boolean(false))), + }), + select.projection[2] + ); + assert_eq!( + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Value(Value::Boolean(false))), + op: BinaryOperator::Xor, + right: Box::new(Expr::Value(Value::Boolean(true))), + }), + select.projection[3] + ); +} From 632ba4cf8e6448e67faf6c3d2dd600642dca207c Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Wed, 13 Nov 2024 19:54:57 +0800 Subject: [PATCH 593/806] Fix the parsing error in MSSQL for multiple statements that include `DECLARE` statements (#1497) --- src/parser/mod.rs | 94 +++++++++++++++++++++------------------- tests/sqlparser_mssql.rs | 71 +++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 46 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 355456d52..d3f432041 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5321,55 +5321,61 @@ impl<'a> Parser<'a> { /// ``` /// [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(&mut self) -> Result { - let mut stmts = vec![]; - - loop { - let name = { - let ident = self.parse_identifier(false)?; - if !ident.value.starts_with('@') { - Err(ParserError::TokenizerError( - "Invalid MsSql variable declaration.".to_string(), - )) - } else { - Ok(ident) - } - }?; + let stmts = self.parse_comma_separated(Parser::parse_mssql_declare_stmt)?; - let (declare_type, data_type) = match self.peek_token().token { - Token::Word(w) => match w.keyword { - Keyword::CURSOR => { - self.next_token(); - (Some(DeclareType::Cursor), None) - } - Keyword::AS => { - self.next_token(); - (None, Some(self.parse_data_type()?)) - } - _ => (None, Some(self.parse_data_type()?)), - }, - _ => (None, Some(self.parse_data_type()?)), - }; + Ok(Statement::Declare { stmts }) + } - let assignment = self.parse_mssql_variable_declaration_expression()?; + /// Parse the body of a [MsSql] `DECLARE`statement. + /// + /// Syntax: + /// ```text + // { + // { @local_variable [AS] data_type [ = value ] } + // | { @cursor_variable_name CURSOR } + // } [ ,...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(false)?; + if !ident.value.starts_with('@') { + Err(ParserError::TokenizerError( + "Invalid MsSql variable declaration.".to_string(), + )) + } else { + Ok(ident) + } + }?; - stmts.push(Declare { - names: vec![name], - data_type, - assignment, - declare_type, - binary: None, - sensitive: None, - scroll: None, - hold: None, - for_query: None, - }); + let (declare_type, data_type) = match self.peek_token().token { + Token::Word(w) => match w.keyword { + Keyword::CURSOR => { + self.next_token(); + (Some(DeclareType::Cursor), None) + } + Keyword::AS => { + self.next_token(); + (None, Some(self.parse_data_type()?)) + } + _ => (None, Some(self.parse_data_type()?)), + }, + _ => (None, Some(self.parse_data_type()?)), + }; - if self.next_token() != Token::Comma { - break; - } - } + let assignment = self.parse_mssql_variable_declaration_expression()?; - Ok(Statement::Declare { stmts }) + Ok(Declare { + names: vec![name], + data_type, + assignment, + declare_type, + binary: None, + sensitive: None, + scroll: None, + hold: None, + for_query: None, + }) } /// Parses the assigned expression in a variable declaration. diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 4f9f6bb82..c28f89e37 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -29,7 +29,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::ParserError; #[test] fn parse_mssql_identifiers() { @@ -910,7 +910,7 @@ fn parse_substring_in_select() { #[test] fn parse_mssql_declare() { let sql = "DECLARE @foo CURSOR, @bar INT, @baz AS TEXT = 'foobar';"; - let ast = Parser::parse_sql(&MsSqlDialect {}, sql).unwrap(); + let ast = ms().parse_sql_statements(sql).unwrap(); assert_eq!( vec![Statement::Declare { @@ -963,6 +963,73 @@ fn parse_mssql_declare() { }], ast ); + + let sql = "DECLARE @bar INT;SET @bar = 2;SELECT @bar * 4"; + let ast = ms().parse_sql_statements(sql).unwrap(); + assert_eq!( + vec![ + Statement::Declare { + stmts: vec![Declare { + names: vec![Ident { + value: "@bar".to_string(), + quote_style: None + }], + data_type: Some(Int(None)), + assignment: None, + declare_type: None, + binary: None, + sensitive: None, + scroll: None, + hold: None, + for_query: None + }] + }, + Statement::SetVariable { + local: false, + hivevar: false, + variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("@bar")])), + value: vec![Expr::Value(Value::Number("2".parse().unwrap(), false))], + }, + Statement::Query(Box::new(Query { + with: None, + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + order_by: None, + settings: None, + format_clause: None, + body: Box::new(SetExpr::Select(Box::new(Select { + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("@bar"))), + op: BinaryOperator::Multiply, + right: Box::new(Expr::Value(Value::Number("4".parse().unwrap(), false))), + })], + 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, + }))) + })) + ], + ast + ); } #[test] From 76322baf2f126faea5ea416be176218fc964699c Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:55:26 +0200 Subject: [PATCH 594/806] Add support for Snowflake SHOW DATABASES/SCHEMAS/TABLES/VIEWS/COLUMNS statements (#1501) --- src/ast/mod.rs | 223 +++++++++++++++++++++++++---------- src/dialect/mod.rs | 6 + src/dialect/snowflake.rs | 6 + src/keywords.rs | 4 + src/parser/mod.rs | 215 ++++++++++++++++++++++++++------- tests/sqlparser_common.rs | 54 ++++++--- tests/sqlparser_mysql.rs | 177 +++++++++++++++++++++------ tests/sqlparser_snowflake.rs | 65 ++++++++++ 8 files changed, 591 insertions(+), 159 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 81bddcd17..848e6bdbd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2773,41 +2773,45 @@ pub enum Statement { /// ```sql /// SHOW COLUMNS /// ``` - /// - /// Note: this is a MySQL-specific statement. ShowColumns { extended: bool, full: bool, - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - filter: Option, + show_options: ShowStatementOptions, }, /// ```sql - /// SHOW DATABASES [LIKE 'pattern'] + /// SHOW DATABASES /// ``` - ShowDatabases { filter: Option }, + ShowDatabases { + terse: bool, + history: bool, + show_options: ShowStatementOptions, + }, /// ```sql - /// SHOW SCHEMAS [LIKE 'pattern'] + /// SHOW SCHEMAS /// ``` - ShowSchemas { filter: Option }, + ShowSchemas { + terse: bool, + history: bool, + show_options: ShowStatementOptions, + }, /// ```sql /// SHOW TABLES /// ``` ShowTables { + terse: bool, + history: bool, extended: bool, full: bool, - clause: Option, - db_name: Option, - filter: Option, + external: bool, + show_options: ShowStatementOptions, }, /// ```sql /// SHOW VIEWS /// ``` ShowViews { + terse: bool, materialized: bool, - clause: Option, - db_name: Option, - filter: Option, + show_options: ShowStatementOptions, }, /// ```sql /// SHOW COLLATION @@ -4387,79 +4391,72 @@ impl fmt::Display for Statement { Statement::ShowColumns { extended, full, - table_name, - filter, + show_options, } => { write!( f, - "SHOW {extended}{full}COLUMNS FROM {table_name}", + "SHOW {extended}{full}COLUMNS{show_options}", extended = if *extended { "EXTENDED " } else { "" }, full = if *full { "FULL " } else { "" }, - table_name = table_name, )?; - if let Some(filter) = filter { - write!(f, " {filter}")?; - } Ok(()) } - Statement::ShowDatabases { filter } => { - write!(f, "SHOW DATABASES")?; - if let Some(filter) = filter { - write!(f, " {filter}")?; - } + Statement::ShowDatabases { + terse, + history, + show_options, + } => { + write!( + f, + "SHOW {terse}DATABASES{history}{show_options}", + terse = if *terse { "TERSE " } else { "" }, + history = if *history { " HISTORY" } else { "" }, + )?; Ok(()) } - Statement::ShowSchemas { filter } => { - write!(f, "SHOW SCHEMAS")?; - if let Some(filter) = filter { - write!(f, " {filter}")?; - } + Statement::ShowSchemas { + terse, + history, + show_options, + } => { + write!( + f, + "SHOW {terse}SCHEMAS{history}{show_options}", + terse = if *terse { "TERSE " } else { "" }, + history = if *history { " HISTORY" } else { "" }, + )?; Ok(()) } Statement::ShowTables { + terse, + history, extended, full, - clause: show_clause, - db_name, - filter, + external, + show_options, } => { write!( f, - "SHOW {extended}{full}TABLES", + "SHOW {terse}{extended}{full}{external}TABLES{history}{show_options}", + terse = if *terse { "TERSE " } else { "" }, extended = if *extended { "EXTENDED " } else { "" }, full = if *full { "FULL " } else { "" }, + external = if *external { "EXTERNAL " } else { "" }, + history = if *history { " HISTORY" } else { "" }, )?; - if let Some(show_clause) = show_clause { - write!(f, " {show_clause}")?; - } - if let Some(db_name) = db_name { - write!(f, " {db_name}")?; - } - if let Some(filter) = filter { - write!(f, " {filter}")?; - } Ok(()) } Statement::ShowViews { + terse, materialized, - clause: show_clause, - db_name, - filter, + show_options, } => { write!( f, - "SHOW {}VIEWS", - if *materialized { "MATERIALIZED " } else { "" } + "SHOW {terse}{materialized}VIEWS{show_options}", + terse = if *terse { "TERSE " } else { "" }, + materialized = if *materialized { "MATERIALIZED " } else { "" } )?; - if let Some(show_clause) = show_clause { - write!(f, " {show_clause}")?; - } - if let Some(db_name) = db_name { - write!(f, " {db_name}")?; - } - if let Some(filter) = filter { - write!(f, " {filter}")?; - } Ok(()) } Statement::ShowFunctions { filter } => { @@ -6172,14 +6169,14 @@ impl fmt::Display for ShowStatementFilter { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum ShowClause { +pub enum ShowStatementInClause { IN, FROM, } -impl fmt::Display for ShowClause { +impl fmt::Display for ShowStatementInClause { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ShowClause::*; + use ShowStatementInClause::*; match self { FROM => write!(f, "FROM"), IN => write!(f, "IN"), @@ -7357,6 +7354,108 @@ impl Display for UtilityOption { } } +/// Represents the different options available for `SHOW` +/// statements to filter the results. Example from Snowflake: +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ShowStatementOptions { + pub show_in: Option, + pub starts_with: Option, + pub limit: Option, + pub limit_from: Option, + pub filter_position: Option, +} + +impl Display for ShowStatementOptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (like_in_infix, like_in_suffix) = match &self.filter_position { + Some(ShowStatementFilterPosition::Infix(filter)) => { + (format!(" {filter}"), "".to_string()) + } + Some(ShowStatementFilterPosition::Suffix(filter)) => { + ("".to_string(), format!(" {filter}")) + } + None => ("".to_string(), "".to_string()), + }; + write!( + f, + "{like_in_infix}{show_in}{starts_with}{limit}{from}{like_in_suffix}", + show_in = match &self.show_in { + Some(i) => format!(" {i}"), + None => String::new(), + }, + starts_with = match &self.starts_with { + Some(s) => format!(" STARTS WITH {s}"), + None => String::new(), + }, + limit = match &self.limit { + Some(l) => format!(" LIMIT {l}"), + None => String::new(), + }, + from = match &self.limit_from { + Some(f) => format!(" FROM {f}"), + None => String::new(), + } + )?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ShowStatementFilterPosition { + Infix(ShowStatementFilter), // For example: SHOW COLUMNS LIKE '%name%' IN TABLE tbl + Suffix(ShowStatementFilter), // For example: SHOW COLUMNS IN tbl LIKE '%name%' +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ShowStatementInParentType { + Account, + Database, + Schema, + Table, + View, +} + +impl fmt::Display for ShowStatementInParentType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ShowStatementInParentType::Account => write!(f, "ACCOUNT"), + ShowStatementInParentType::Database => write!(f, "DATABASE"), + ShowStatementInParentType::Schema => write!(f, "SCHEMA"), + ShowStatementInParentType::Table => write!(f, "TABLE"), + ShowStatementInParentType::View => write!(f, "VIEW"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ShowStatementIn { + pub clause: ShowStatementInClause, + pub parent_type: Option, + pub parent_name: Option, +} + +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)?; + } + if let Some(parent_name) = &self.parent_name { + write!(f, " {}", parent_name)?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c8c11bc95..f37c0d85c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -622,6 +622,12 @@ pub trait Dialect: Debug + Any { fn supports_boolean_literals(&self) -> bool { true } + + /// Returns true if this dialect supports the `LIKE 'pattern'` option in + /// a `SHOW` statement before the `IN` option + fn supports_show_like_before_in(&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 d9331d952..98e8f5e2f 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -203,6 +203,12 @@ impl Dialect for SnowflakeDialect { fn allow_extract_single_quotes(&self) -> bool { true } + + /// Snowflake expects the `LIKE` option before the `IN` option, + /// for example: + fn supports_show_like_before_in(&self) -> bool { + true + } } /// Parse snowflake create table statement. diff --git a/src/keywords.rs b/src/keywords.rs index 982cea81b..9cdc90ce2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -76,6 +76,7 @@ define_keywords!( ABS, ABSOLUTE, ACCESS, + ACCOUNT, ACTION, ADD, ADMIN, @@ -91,6 +92,7 @@ define_keywords!( AND, ANTI, ANY, + APPLICATION, APPLY, ARCHIVE, ARE, @@ -710,6 +712,7 @@ define_keywords!( STABLE, STAGE, START, + STARTS, STATEMENT, STATIC, STATISTICS, @@ -746,6 +749,7 @@ define_keywords!( TEMP, TEMPORARY, TERMINATED, + TERSE, TEXT, TEXTFILE, THEN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d3f432041..7f6961ae8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3205,6 +3205,22 @@ impl<'a> Parser<'a> { }) } + /// Look for all of the expected keywords in sequence, without consuming them + fn peek_keyword(&mut self, expected: Keyword) -> bool { + let index = self.index; + let matched = self.parse_keyword(expected); + self.index = index; + matched + } + + /// Look for all of the expected keywords in sequence, without consuming them + fn peek_keywords(&mut self, expected: &[Keyword]) -> bool { + let index = self.index; + let matched = self.parse_keywords(expected); + self.index = index; + matched + } + /// Return the first non-whitespace token that has not yet been processed /// (or None if reached end-of-file) and mark it as processed. OK to call /// repeatedly after reaching EOF. @@ -9611,21 +9627,23 @@ impl<'a> Parser<'a> { } pub fn parse_show(&mut self) -> Result { + let terse = self.parse_keyword(Keyword::TERSE); let extended = self.parse_keyword(Keyword::EXTENDED); let full = self.parse_keyword(Keyword::FULL); let session = self.parse_keyword(Keyword::SESSION); let global = self.parse_keyword(Keyword::GLOBAL); + let external = self.parse_keyword(Keyword::EXTERNAL); if self .parse_one_of_keywords(&[Keyword::COLUMNS, Keyword::FIELDS]) .is_some() { Ok(self.parse_show_columns(extended, full)?) } else if self.parse_keyword(Keyword::TABLES) { - Ok(self.parse_show_tables(extended, full)?) + Ok(self.parse_show_tables(terse, extended, full, external)?) } else if self.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEWS]) { - Ok(self.parse_show_views(true)?) + Ok(self.parse_show_views(terse, true)?) } else if self.parse_keyword(Keyword::VIEWS) { - Ok(self.parse_show_views(false)?) + Ok(self.parse_show_views(terse, false)?) } else if self.parse_keyword(Keyword::FUNCTIONS) { Ok(self.parse_show_functions()?) } else if extended || full { @@ -9653,9 +9671,9 @@ impl<'a> Parser<'a> { global, }) } else if self.parse_keyword(Keyword::DATABASES) { - self.parse_show_databases() + self.parse_show_databases(terse) } else if self.parse_keyword(Keyword::SCHEMAS) { - self.parse_show_schemas() + self.parse_show_schemas(terse) } else { Ok(Statement::ShowVariable { variable: self.parse_identifiers()?, @@ -9663,15 +9681,23 @@ impl<'a> Parser<'a> { } } - fn parse_show_databases(&mut self) -> Result { + fn parse_show_databases(&mut self, terse: bool) -> Result { + let history = self.parse_keyword(Keyword::HISTORY); + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowDatabases { - filter: self.parse_show_statement_filter()?, + terse, + history, + show_options, }) } - fn parse_show_schemas(&mut self) -> Result { + fn parse_show_schemas(&mut self, terse: bool) -> Result { + let history = self.parse_keyword(Keyword::HISTORY); + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowSchemas { - filter: self.parse_show_statement_filter()?, + terse, + history, + show_options, }) } @@ -9705,58 +9731,43 @@ impl<'a> Parser<'a> { extended: bool, full: bool, ) -> Result { - self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; - let object_name = self.parse_object_name(false)?; - let table_name = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { - Some(_) => { - let db_name = vec![self.parse_identifier(false)?]; - let ObjectName(table_name) = object_name; - let object_name = db_name.into_iter().chain(table_name).collect(); - ObjectName(object_name) - } - None => object_name, - }; - let filter = self.parse_show_statement_filter()?; + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowColumns { extended, full, - table_name, - filter, + show_options, }) } - pub fn parse_show_tables( + fn parse_show_tables( &mut self, + terse: bool, extended: bool, full: bool, + external: bool, ) -> Result { - let (clause, db_name) = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { - Some(Keyword::FROM) => (Some(ShowClause::FROM), Some(self.parse_identifier(false)?)), - Some(Keyword::IN) => (Some(ShowClause::IN), Some(self.parse_identifier(false)?)), - _ => (None, None), - }; - let filter = self.parse_show_statement_filter()?; + let history = !external && self.parse_keyword(Keyword::HISTORY); + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowTables { + terse, + history, extended, full, - clause, - db_name, - filter, + external, + show_options, }) } - fn parse_show_views(&mut self, materialized: bool) -> Result { - let (clause, db_name) = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { - Some(Keyword::FROM) => (Some(ShowClause::FROM), Some(self.parse_identifier(false)?)), - Some(Keyword::IN) => (Some(ShowClause::IN), Some(self.parse_identifier(false)?)), - _ => (None, None), - }; - let filter = self.parse_show_statement_filter()?; + fn parse_show_views( + &mut self, + terse: bool, + materialized: bool, + ) -> Result { + let show_options = self.parse_show_stmt_options()?; Ok(Statement::ShowViews { materialized, - clause, - db_name, - filter, + terse, + show_options, }) } @@ -12395,6 +12406,124 @@ impl<'a> Parser<'a> { } false } + + fn parse_show_stmt_options(&mut self) -> Result { + let show_in; + let mut filter_position = None; + if self.dialect.supports_show_like_before_in() { + if let Some(filter) = self.parse_show_statement_filter()? { + filter_position = Some(ShowStatementFilterPosition::Infix(filter)); + } + show_in = self.maybe_parse_show_stmt_in()?; + } else { + show_in = self.maybe_parse_show_stmt_in()?; + if let Some(filter) = self.parse_show_statement_filter()? { + filter_position = Some(ShowStatementFilterPosition::Suffix(filter)); + } + } + let starts_with = self.maybe_parse_show_stmt_starts_with()?; + let limit = self.maybe_parse_show_stmt_limit()?; + let from = self.maybe_parse_show_stmt_from()?; + Ok(ShowStatementOptions { + filter_position, + show_in, + starts_with, + limit, + limit_from: from, + }) + } + + fn maybe_parse_show_stmt_in(&mut self) -> Result, ParserError> { + let clause = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) { + Some(Keyword::FROM) => ShowStatementInClause::FROM, + Some(Keyword::IN) => ShowStatementInClause::IN, + None => return Ok(None), + _ => return self.expected("FROM or IN", self.peek_token()), + }; + + let (parent_type, parent_name) = match self.parse_one_of_keywords(&[ + Keyword::ACCOUNT, + Keyword::DATABASE, + Keyword::SCHEMA, + Keyword::TABLE, + Keyword::VIEW, + ]) { + // If we see these next keywords it means we don't have a parent name + Some(Keyword::DATABASE) + if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) + | self.peek_keyword(Keyword::LIMIT) => + { + (Some(ShowStatementInParentType::Database), None) + } + Some(Keyword::SCHEMA) + if self.peek_keywords(&[Keyword::STARTS, Keyword::WITH]) + | self.peek_keyword(Keyword::LIMIT) => + { + (Some(ShowStatementInParentType::Schema), None) + } + Some(parent_kw) => { + // The parent name here is still optional, for example: + // SHOW TABLES IN ACCOUNT, so parsing the object name + // may fail because the statement ends. + let parent_name = self.maybe_parse(|p| p.parse_object_name(false))?; + match parent_kw { + Keyword::ACCOUNT => (Some(ShowStatementInParentType::Account), parent_name), + Keyword::DATABASE => (Some(ShowStatementInParentType::Database), parent_name), + Keyword::SCHEMA => (Some(ShowStatementInParentType::Schema), parent_name), + Keyword::TABLE => (Some(ShowStatementInParentType::Table), parent_name), + Keyword::VIEW => (Some(ShowStatementInParentType::View), parent_name), + _ => { + return self.expected( + "one of ACCOUNT, DATABASE, SCHEMA, TABLE or VIEW", + self.peek_token(), + ) + } + } + } + None => { + // Parsing MySQL style FROM tbl_name FROM db_name + // which is equivalent to FROM tbl_name.db_name + let mut parent_name = self.parse_object_name(false)?; + if self + .parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) + .is_some() + { + parent_name.0.insert(0, self.parse_identifier(false)?); + } + (None, Some(parent_name)) + } + }; + + Ok(Some(ShowStatementIn { + clause, + parent_type, + parent_name, + })) + } + + fn maybe_parse_show_stmt_starts_with(&mut self) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::STARTS, Keyword::WITH]) { + Ok(Some(self.parse_value()?)) + } else { + Ok(None) + } + } + + fn maybe_parse_show_stmt_limit(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::LIMIT) { + Ok(self.parse_limit()?) + } else { + Ok(None) + } + } + + fn maybe_parse_show_stmt_from(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::FROM) { + Ok(Some(self.parse_value()?)) + } else { + Ok(None) + } + } } impl Word { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bef0f535c..d08e19d68 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11395,23 +11395,43 @@ fn test_try_convert() { #[test] fn test_show_dbs_schemas_tables_views() { - verified_stmt("SHOW DATABASES"); - verified_stmt("SHOW DATABASES LIKE '%abc'"); - verified_stmt("SHOW SCHEMAS"); - verified_stmt("SHOW SCHEMAS LIKE '%abc'"); - verified_stmt("SHOW TABLES"); - verified_stmt("SHOW TABLES IN db1"); - verified_stmt("SHOW TABLES IN db1 'abc'"); - verified_stmt("SHOW VIEWS"); - verified_stmt("SHOW VIEWS IN db1"); - verified_stmt("SHOW VIEWS IN db1 'abc'"); - verified_stmt("SHOW VIEWS FROM db1"); - verified_stmt("SHOW VIEWS FROM db1 'abc'"); - verified_stmt("SHOW MATERIALIZED VIEWS"); - verified_stmt("SHOW MATERIALIZED VIEWS IN db1"); - verified_stmt("SHOW MATERIALIZED VIEWS IN db1 'abc'"); - verified_stmt("SHOW MATERIALIZED VIEWS FROM db1"); - verified_stmt("SHOW MATERIALIZED VIEWS FROM db1 'abc'"); + // These statements are parsed the same by all dialects + let stmts = vec![ + "SHOW DATABASES", + "SHOW SCHEMAS", + "SHOW TABLES", + "SHOW VIEWS", + "SHOW TABLES IN db1", + "SHOW VIEWS FROM db1", + "SHOW MATERIALIZED VIEWS", + "SHOW MATERIALIZED VIEWS IN db1", + "SHOW MATERIALIZED VIEWS FROM db1", + ]; + for stmt in stmts { + verified_stmt(stmt); + } + + // These statements are parsed the same by all dialects + // except for how the parser interprets the location of + // LIKE option (infix/suffix) + let stmts = vec!["SHOW DATABASES LIKE '%abc'", "SHOW SCHEMAS LIKE '%abc'"]; + for stmt in stmts { + all_dialects_where(|d| d.supports_show_like_before_in()).verified_stmt(stmt); + all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt(stmt); + } + + // These statements are only parsed by dialects that + // support the LIKE option in the suffix + let stmts = vec![ + "SHOW TABLES IN db1 'abc'", + "SHOW VIEWS IN db1 'abc'", + "SHOW VIEWS FROM db1 'abc'", + "SHOW MATERIALIZED VIEWS IN db1 'abc'", + "SHOW MATERIALIZED VIEWS FROM db1 'abc'", + ]; + for stmt in stmts { + all_dialects_where(|d| !d.supports_show_like_before_in()).verified_stmt(stmt); + } } #[test] diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 44b2ac6ba..8269eadc0 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -223,14 +223,22 @@ fn parse_flush() { #[test] fn parse_show_columns() { - let table_name = ObjectName(vec![Ident::new("mytable")]); assert_eq!( mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable"), Statement::ShowColumns { extended: false, full: false, - table_name: table_name.clone(), - filter: None, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: None, + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -238,8 +246,17 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - table_name: ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")]), - filter: None, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")])), + }), + filter_position: None, + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -247,8 +264,17 @@ fn parse_show_columns() { Statement::ShowColumns { extended: true, full: false, - table_name: table_name.clone(), - filter: None, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: None, + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -256,8 +282,17 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: true, - table_name: table_name.clone(), - filter: None, + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: None, + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -265,8 +300,19 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - table_name: table_name.clone(), - filter: Some(ShowStatementFilter::Like("pattern".into())), + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: Some(ShowStatementFilterPosition::Suffix( + ShowStatementFilter::Like("pattern".into()) + )), + limit_from: None, + limit: None, + starts_with: None, + } } ); assert_eq!( @@ -274,18 +320,27 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - table_name, - filter: Some(ShowStatementFilter::Where( - mysql_and_generic().verified_expr("1 = 2") - )), + show_options: ShowStatementOptions { + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + }), + filter_position: Some(ShowStatementFilterPosition::Suffix( + ShowStatementFilter::Where(mysql_and_generic().verified_expr("1 = 2")) + )), + limit_from: None, + limit: None, + starts_with: None, + } } ); mysql_and_generic() .one_statement_parses_to("SHOW FIELDS FROM mytable", "SHOW COLUMNS FROM mytable"); mysql_and_generic() - .one_statement_parses_to("SHOW COLUMNS IN mytable", "SHOW COLUMNS FROM mytable"); + .one_statement_parses_to("SHOW COLUMNS IN mytable", "SHOW COLUMNS IN mytable"); mysql_and_generic() - .one_statement_parses_to("SHOW FIELDS IN mytable", "SHOW COLUMNS FROM mytable"); + .one_statement_parses_to("SHOW FIELDS IN mytable", "SHOW COLUMNS IN mytable"); mysql_and_generic().one_statement_parses_to( "SHOW COLUMNS FROM mytable FROM mydb", "SHOW COLUMNS FROM mydb.mytable", @@ -327,63 +382,111 @@ fn parse_show_tables() { assert_eq!( mysql_and_generic().verified_stmt("SHOW TABLES"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: false, - clause: None, - db_name: None, - filter: None, + external: false, + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: None + } } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW TABLES FROM mydb"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: false, - clause: Some(ShowClause::FROM), - db_name: Some(Ident::new("mydb")), - filter: None, + external: false, + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: Some(ShowStatementIn { + clause: ShowStatementInClause::FROM, + parent_type: None, + parent_name: Some(ObjectName(vec![Ident::new("mydb")])), + }), + filter_position: None + } } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW EXTENDED TABLES"), Statement::ShowTables { + terse: false, + history: false, extended: true, full: false, - clause: None, - db_name: None, - filter: None, + external: false, + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: None + } } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW FULL TABLES"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: true, - clause: None, - db_name: None, - filter: None, + external: false, + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: None + } } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW TABLES LIKE 'pattern'"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: false, - clause: None, - db_name: None, - filter: Some(ShowStatementFilter::Like("pattern".into())), + external: false, + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: Some(ShowStatementFilterPosition::Suffix( + ShowStatementFilter::Like("pattern".into()) + )) + } } ); assert_eq!( mysql_and_generic().verified_stmt("SHOW TABLES WHERE 1 = 2"), Statement::ShowTables { + terse: false, + history: false, extended: false, full: false, - clause: None, - db_name: None, - filter: Some(ShowStatementFilter::Where( - mysql_and_generic().verified_expr("1 = 2") - )), + external: false, + show_options: ShowStatementOptions { + starts_with: None, + limit: None, + limit_from: None, + show_in: None, + filter_position: Some(ShowStatementFilterPosition::Suffix( + ShowStatementFilter::Where(mysql_and_generic().verified_expr("1 = 2")) + )) + } } ); mysql_and_generic().verified_stmt("SHOW TABLES IN mydb"); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c17c7b958..1f1c00e7a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2781,3 +2781,68 @@ fn test_parentheses_overflow() { snowflake_with_recursion_limit(max_nesting_level).parse_sql_statements(sql.as_str()); assert_eq!(parsed.err(), Some(ParserError::RecursionLimitExceeded)); } + +#[test] +fn test_show_databases() { + snowflake().verified_stmt("SHOW DATABASES"); + snowflake().verified_stmt("SHOW DATABASES HISTORY"); + snowflake().verified_stmt("SHOW DATABASES LIKE '%abc%'"); + snowflake().verified_stmt("SHOW DATABASES STARTS WITH 'demo_db'"); + snowflake().verified_stmt("SHOW DATABASES LIMIT 12"); + snowflake() + .verified_stmt("SHOW DATABASES HISTORY LIKE '%aa' STARTS WITH 'demo' LIMIT 20 FROM 'abc'"); + snowflake().verified_stmt("SHOW DATABASES IN ACCOUNT abc"); +} + +#[test] +fn test_parse_show_schemas() { + snowflake().verified_stmt("SHOW SCHEMAS"); + snowflake().verified_stmt("SHOW SCHEMAS IN ACCOUNT"); + snowflake().verified_stmt("SHOW SCHEMAS IN ACCOUNT abc"); + snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE"); + snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE xyz"); + snowflake().verified_stmt("SHOW SCHEMAS HISTORY LIKE '%xa%'"); + snowflake().verified_stmt("SHOW SCHEMAS STARTS WITH 'abc' LIMIT 20"); + snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); +} + +#[test] +fn test_parse_show_tables() { + snowflake().verified_stmt("SHOW TABLES"); + snowflake().verified_stmt("SHOW TABLES IN ACCOUNT"); + snowflake().verified_stmt("SHOW TABLES IN DATABASE"); + snowflake().verified_stmt("SHOW TABLES IN DATABASE xyz"); + snowflake().verified_stmt("SHOW TABLES IN SCHEMA"); + snowflake().verified_stmt("SHOW TABLES IN SCHEMA xyz"); + snowflake().verified_stmt("SHOW TABLES HISTORY LIKE '%xa%'"); + snowflake().verified_stmt("SHOW TABLES STARTS WITH 'abc' LIMIT 20"); + snowflake().verified_stmt("SHOW TABLES IN SCHEMA STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN ACCOUNT"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN DATABASE"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN DATABASE xyz"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN SCHEMA"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES IN SCHEMA xyz"); + snowflake().verified_stmt("SHOW EXTERNAL TABLES STARTS WITH 'abc' LIMIT 20"); + snowflake() + .verified_stmt("SHOW EXTERNAL TABLES IN SCHEMA STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); +} + +#[test] +fn test_show_views() { + snowflake().verified_stmt("SHOW VIEWS"); + snowflake().verified_stmt("SHOW VIEWS IN ACCOUNT"); + snowflake().verified_stmt("SHOW VIEWS IN DATABASE"); + snowflake().verified_stmt("SHOW VIEWS IN DATABASE xyz"); + snowflake().verified_stmt("SHOW VIEWS IN SCHEMA"); + snowflake().verified_stmt("SHOW VIEWS IN SCHEMA xyz"); + snowflake().verified_stmt("SHOW VIEWS STARTS WITH 'abc' LIMIT 20"); + snowflake().verified_stmt("SHOW VIEWS IN SCHEMA STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); +} + +#[test] +fn test_parse_show_columns_sql() { + snowflake().verified_stmt("SHOW COLUMNS IN TABLE"); + snowflake().verified_stmt("SHOW COLUMNS IN TABLE abc"); + snowflake().verified_stmt("SHOW COLUMNS LIKE '%xyz%' IN TABLE abc"); +} From 6d907d3adc36da6ebafd76c9abd5761f19a5ac0b Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 13 Nov 2024 21:23:33 +0800 Subject: [PATCH 595/806] Add support of COMMENT ON syntax for Snowflake (#1516) --- src/ast/mod.rs | 8 ++++ src/dialect/generic.rs | 4 ++ src/dialect/mod.rs | 7 ++- src/dialect/postgresql.rs | 45 +++---------------- src/dialect/snowflake.rs | 5 +++ src/parser/mod.rs | 47 ++++++++++++++++++++ tests/sqlparser_common.rs | 88 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 62 -------------------------- 8 files changed, 164 insertions(+), 102 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 848e6bdbd..505386fbf 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1884,6 +1884,10 @@ pub enum CommentObject { Column, Table, Extension, + Schema, + Database, + User, + Role, } impl fmt::Display for CommentObject { @@ -1892,6 +1896,10 @@ impl fmt::Display for CommentObject { CommentObject::Column => f.write_str("COLUMN"), CommentObject::Table => f.write_str("TABLE"), CommentObject::Extension => f.write_str("EXTENSION"), + CommentObject::Schema => f.write_str("SCHEMA"), + CommentObject::Database => f.write_str("DATABASE"), + CommentObject::User => f.write_str("USER"), + CommentObject::Role => f.write_str("ROLE"), } } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 0a5464c9c..8cfac217b 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -111,4 +111,8 @@ impl Dialect for GenericDialect { fn supports_try_convert(&self) -> bool { true } + + fn supports_comment_on(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f37c0d85c..d95d7c70a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -611,7 +611,7 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if this dialect expects the the `TOP` option + /// Returns true if this dialect expects the `TOP` option /// before the `ALL`/`DISTINCT` options in a `SELECT` statement. fn supports_top_before_distinct(&self) -> bool { false @@ -628,6 +628,11 @@ pub trait Dialect: Debug + Any { fn supports_show_like_before_in(&self) -> bool { false } + + /// Returns true if this dialect supports the `COMMENT` statement + fn supports_comment_on(&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 72841c604..5af1ab853 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -28,7 +28,7 @@ // limitations under the License. use log::debug; -use crate::ast::{CommentObject, ObjectName, Statement, UserDefinedTypeRepresentation}; +use crate::ast::{ObjectName, Statement, UserDefinedTypeRepresentation}; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -136,9 +136,7 @@ impl Dialect for PostgreSqlDialect { } fn parse_statement(&self, parser: &mut Parser) -> Option> { - if parser.parse_keyword(Keyword::COMMENT) { - Some(parse_comment(parser)) - } else if parser.parse_keyword(Keyword::CREATE) { + if parser.parse_keyword(Keyword::CREATE) { parser.prev_token(); // unconsume the CREATE in case we don't end up parsing anything parse_create(parser) } else { @@ -206,42 +204,11 @@ impl Dialect for PostgreSqlDialect { fn supports_factorial_operator(&self) -> bool { true } -} - -pub fn parse_comment(parser: &mut Parser) -> Result { - let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - - parser.expect_keyword(Keyword::ON)?; - let token = parser.next_token(); - - let (object_type, object_name) = match token.token { - Token::Word(w) if w.keyword == Keyword::COLUMN => { - let object_name = parser.parse_object_name(false)?; - (CommentObject::Column, object_name) - } - Token::Word(w) if w.keyword == Keyword::TABLE => { - let object_name = parser.parse_object_name(false)?; - (CommentObject::Table, object_name) - } - Token::Word(w) if w.keyword == Keyword::EXTENSION => { - let object_name = parser.parse_object_name(false)?; - (CommentObject::Extension, object_name) - } - _ => parser.expected("comment object_type", token)?, - }; - parser.expect_keyword(Keyword::IS)?; - let comment = if parser.parse_keyword(Keyword::NULL) { - None - } else { - Some(parser.parse_literal_string()?) - }; - Ok(Statement::Comment { - object_type, - object_name, - comment, - if_exists, - }) + /// see + fn supports_comment_on(&self) -> bool { + true + } } pub fn parse_create(parser: &mut Parser) -> Option> { diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 98e8f5e2f..b584ed9b4 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -96,6 +96,11 @@ impl Dialect for SnowflakeDialect { true } + /// See [doc](https://docs.snowflake.com/en/sql-reference/sql/comment) + fn supports_comment_on(&self) -> bool { + true + } + fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.parse_keyword(Keyword::CREATE) { // possibly CREATE STAGE diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7f6961ae8..756f4d68b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -551,6 +551,8 @@ impl<'a> Parser<'a> { Keyword::OPTIMIZE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { self.parse_optimize_table() } + // `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment + Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(), _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -561,6 +563,51 @@ impl<'a> Parser<'a> { } } + pub fn parse_comment(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + + self.expect_keyword(Keyword::ON)?; + let token = self.next_token(); + + let (object_type, object_name) = match token.token { + Token::Word(w) if w.keyword == Keyword::COLUMN => { + (CommentObject::Column, self.parse_object_name(false)?) + } + Token::Word(w) if w.keyword == Keyword::TABLE => { + (CommentObject::Table, self.parse_object_name(false)?) + } + Token::Word(w) if w.keyword == Keyword::EXTENSION => { + (CommentObject::Extension, self.parse_object_name(false)?) + } + Token::Word(w) if w.keyword == Keyword::SCHEMA => { + (CommentObject::Schema, self.parse_object_name(false)?) + } + Token::Word(w) if w.keyword == Keyword::DATABASE => { + (CommentObject::Database, self.parse_object_name(false)?) + } + Token::Word(w) if w.keyword == Keyword::USER => { + (CommentObject::User, self.parse_object_name(false)?) + } + Token::Word(w) if w.keyword == Keyword::ROLE => { + (CommentObject::Role, self.parse_object_name(false)?) + } + _ => self.expected("comment object_type", token)?, + }; + + self.expect_keyword(Keyword::IS)?; + let comment = if self.parse_keyword(Keyword::NULL) { + None + } else { + Some(self.parse_literal_string()?) + }; + Ok(Statement::Comment { + object_type, + object_name, + comment, + if_exists, + }) + } + pub fn parse_flush(&mut self) -> Result { let mut channel = None; let mut tables: Vec = vec![]; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d08e19d68..25bf306ad 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11629,3 +11629,91 @@ fn parse_factorial_operator() { ); } } + +#[test] +fn parse_comments() { + match all_dialects_where(|d| d.supports_comment_on()) + .verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'") + { + Statement::Comment { + object_type, + object_name, + comment: Some(comment), + if_exists, + } => { + assert_eq!("comment", comment); + assert_eq!("tab.name", object_name.to_string()); + assert_eq!(CommentObject::Column, object_type); + assert!(!if_exists); + } + _ => unreachable!(), + } + + let object_types = [ + ("COLUMN", CommentObject::Column), + ("EXTENSION", CommentObject::Extension), + ("TABLE", CommentObject::Table), + ("SCHEMA", CommentObject::Schema), + ("DATABASE", CommentObject::Database), + ("USER", CommentObject::User), + ("ROLE", CommentObject::Role), + ]; + for (keyword, expected_object_type) in object_types.iter() { + match all_dialects_where(|d| d.supports_comment_on()) + .verified_stmt(format!("COMMENT IF EXISTS ON {keyword} db.t0 IS 'comment'").as_str()) + { + Statement::Comment { + object_type, + object_name, + comment: Some(comment), + if_exists, + } => { + assert_eq!("comment", comment); + assert_eq!("db.t0", object_name.to_string()); + assert_eq!(*expected_object_type, object_type); + assert!(if_exists); + } + _ => unreachable!(), + } + } + + match all_dialects_where(|d| d.supports_comment_on()) + .verified_stmt("COMMENT IF EXISTS ON TABLE public.tab IS NULL") + { + Statement::Comment { + object_type, + object_name, + comment: None, + if_exists, + } => { + assert_eq!("public.tab", object_name.to_string()); + assert_eq!(CommentObject::Table, object_type); + assert!(if_exists); + } + _ => unreachable!(), + } + + // missing IS statement + assert_eq!( + all_dialects_where(|d| d.supports_comment_on()) + .parse_sql_statements("COMMENT ON TABLE t0") + .unwrap_err(), + ParserError::ParserError("Expected: IS, found: EOF".to_string()) + ); + + // missing comment literal + assert_eq!( + all_dialects_where(|d| d.supports_comment_on()) + .parse_sql_statements("COMMENT ON TABLE t0 IS") + .unwrap_err(), + ParserError::ParserError("Expected: literal string, found: EOF".to_string()) + ); + + // unknown object type + assert_eq!( + all_dialects_where(|d| d.supports_comment_on()) + .parse_sql_statements("COMMENT ON UNKNOWN t0 IS 'comment'") + .unwrap_err(), + ParserError::ParserError("Expected: comment object_type, found: UNKNOWN".to_string()) + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 100c8eeb2..a6c480cd7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2891,68 +2891,6 @@ fn test_composite_value() { ); } -#[test] -fn parse_comments() { - match pg().verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'") { - Statement::Comment { - object_type, - object_name, - comment: Some(comment), - if_exists, - } => { - assert_eq!("comment", comment); - assert_eq!("tab.name", object_name.to_string()); - assert_eq!(CommentObject::Column, object_type); - assert!(!if_exists); - } - _ => unreachable!(), - } - - match pg().verified_stmt("COMMENT ON EXTENSION plpgsql IS 'comment'") { - Statement::Comment { - object_type, - object_name, - comment: Some(comment), - if_exists, - } => { - assert_eq!("comment", comment); - assert_eq!("plpgsql", object_name.to_string()); - assert_eq!(CommentObject::Extension, object_type); - assert!(!if_exists); - } - _ => unreachable!(), - } - - match pg().verified_stmt("COMMENT ON TABLE public.tab IS 'comment'") { - Statement::Comment { - object_type, - object_name, - comment: Some(comment), - if_exists, - } => { - assert_eq!("comment", comment); - assert_eq!("public.tab", object_name.to_string()); - assert_eq!(CommentObject::Table, object_type); - assert!(!if_exists); - } - _ => unreachable!(), - } - - match pg().verified_stmt("COMMENT IF EXISTS ON TABLE public.tab IS NULL") { - Statement::Comment { - object_type, - object_name, - comment: None, - if_exists, - } => { - assert_eq!("public.tab", object_name.to_string()); - assert_eq!(CommentObject::Table, object_type); - assert!(if_exists); - } - _ => unreachable!(), - } -} - #[test] fn parse_quoted_identifier() { pg_and_generic().verified_stmt(r#"SELECT "quoted "" ident""#); From 2bb81444bd94d2c02f7336c6b9941bd79769fb7f Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Thu, 14 Nov 2024 01:36:13 +0800 Subject: [PATCH 596/806] Add support for MYSQL's `CREATE TABLE SELECT` expr (#1515) --- src/dialect/mod.rs | 5 +++++ src/dialect/mysql.rs | 5 +++++ src/parser/mod.rs | 5 +++++ tests/sqlparser_common.rs | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index d95d7c70a..a732aa5a0 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -633,6 +633,11 @@ pub trait Dialect: Debug + Any { fn supports_comment_on(&self) -> bool { false } + + /// Returns true if the dialect supports the `CREATE TABLE SELECT` statement + fn supports_create_table_select(&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 d1bf33345..197ce48d4 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -97,6 +97,11 @@ impl Dialect for MySqlDialect { fn supports_limit_comma(&self) -> bool { true } + + /// see + fn supports_create_table_select(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 756f4d68b..4115bbc9a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5990,6 +5990,11 @@ impl<'a> Parser<'a> { // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { Some(self.parse_query()?) + } else if self.dialect.supports_create_table_select() && self.parse_keyword(Keyword::SELECT) + { + // rewind the SELECT keyword + self.prev_token(); + Some(self.parse_query()?) } else { None }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 25bf306ad..daf65edf1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6501,7 +6501,17 @@ fn parse_multiple_statements() { ); test_with("DELETE FROM foo", "SELECT", " bar"); test_with("INSERT INTO foo VALUES (1)", "SELECT", " bar"); - test_with("CREATE TABLE foo (baz INT)", "SELECT", " bar"); + // Since MySQL supports the `CREATE TABLE SELECT` syntax, this needs to be handled separately + let res = parse_sql_statements("CREATE TABLE foo (baz INT); SELECT bar"); + assert_eq!( + vec![ + one_statement_parses_to("CREATE TABLE foo (baz INT)", ""), + one_statement_parses_to("SELECT bar", ""), + ], + res.unwrap() + ); + // Check that extra semicolon at the end is stripped by normalization: + one_statement_parses_to("CREATE TABLE foo (baz INT);", "CREATE TABLE foo (baz INT)"); // Make sure that empty statements do not cause an error: let res = parse_sql_statements(";;"); assert_eq!(0, res.unwrap().len()); @@ -11717,3 +11727,24 @@ fn parse_comments() { ParserError::ParserError("Expected: comment object_type, found: UNKNOWN".to_string()) ); } + +#[test] +fn parse_create_table_select() { + let dialects = all_dialects_where(|d| d.supports_create_table_select()); + let sql_1 = r#"CREATE TABLE foo (baz INT) SELECT bar"#; + let expected = r#"CREATE TABLE foo (baz INT) AS SELECT bar"#; + let _ = dialects.one_statement_parses_to(sql_1, expected); + + let sql_2 = r#"CREATE TABLE foo (baz INT, name STRING) SELECT bar, oth_name FROM test.table_a"#; + let expected = + r#"CREATE TABLE foo (baz INT, name STRING) AS SELECT bar, oth_name FROM test.table_a"#; + let _ = dialects.one_statement_parses_to(sql_2, expected); + + let dialects = all_dialects_where(|d| !d.supports_create_table_select()); + for sql in [sql_1, sql_2] { + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: SELECT".to_string()) + ); + } +} From 62eaee62dc12fe001992650bf5b330f065b92c07 Mon Sep 17 00:00:00 2001 From: gaoqiangz <38213294+gaoqiangz@users.noreply.github.com> Date: Fri, 15 Nov 2024 04:32:57 +0800 Subject: [PATCH 597/806] Add support for MSSQL's `XQuery` methods (#1500) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 39 ++++++++++++++++++++++ src/dialect/mod.rs | 9 +++++ src/dialect/mssql.rs | 4 +++ src/parser/mod.rs | 39 ++++++++++++++++++++++ tests/sqlparser_common.rs | 70 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 505386fbf..b0ac6bc41 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -808,6 +808,23 @@ pub enum Expr { }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), + /// Arbitrary expr method call + /// + /// Syntax: + /// + /// `.....` + /// + /// > `arbitrary-expr` can be any expression including a function call. + /// + /// Example: + /// + /// ```sql + /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') + /// ``` + /// + /// (mssql): + Method(Method), /// `CASE [] WHEN THEN ... [ELSE ] END` /// /// Note we only recognize a complete single expression as ``, @@ -1464,6 +1481,7 @@ impl fmt::Display for Expr { write!(f, " '{}'", &value::escape_single_quote_string(value)) } Expr::Function(fun) => write!(f, "{fun}"), + Expr::Method(method) => write!(f, "{method}"), Expr::Case { operand, conditions, @@ -5609,6 +5627,27 @@ impl fmt::Display for FunctionArgumentClause { } } +/// A method call +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Method { + pub expr: Box, + // always non-empty + pub method_chain: Vec, +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}.{}", + self.expr, + display_separated(&self.method_chain, ".") + ) + } +} + #[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/dialect/mod.rs b/src/dialect/mod.rs index a732aa5a0..ee3fd4714 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -279,6 +279,15 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports method calls, for example: + /// + /// ```sql + /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// ``` + fn supports_methods(&self) -> bool { + false + } + /// Returns true if the dialect supports multiple variable assignment /// using parentheses in a `SET` variable declaration. /// diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 8aab0bc8a..39ce9c125 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -62,4 +62,8 @@ impl Dialect for MsSqlDialect { fn supports_boolean_literals(&self) -> bool { false } + + fn supports_methods(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4115bbc9a..a66a627bc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1317,6 +1317,7 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; + let expr = self.try_parse_method(expr)?; if !self.consume_token(&Token::Period) { Ok(expr) } else { @@ -1346,6 +1347,9 @@ impl<'a> Parser<'a> { } _ => self.expected("an expression", next_token), }?; + + let expr = self.try_parse_method(expr)?; + if self.parse_keyword(Keyword::COLLATE) { Ok(Expr::Collate { expr: Box::new(expr), @@ -1403,6 +1407,41 @@ impl<'a> Parser<'a> { }) } + /// Parses method call expression + fn try_parse_method(&mut self, expr: Expr) -> Result { + if !self.dialect.supports_methods() { + return Ok(expr); + } + let method_chain = self.maybe_parse(|p| { + let mut method_chain = Vec::new(); + while p.consume_token(&Token::Period) { + let tok = p.next_token(); + let name = match tok.token { + Token::Word(word) => word.to_ident(), + _ => return p.expected("identifier", tok), + }; + let func = match p.parse_function(ObjectName(vec![name]))? { + Expr::Function(func) => func, + _ => return p.expected("function", p.peek_token()), + }; + method_chain.push(func); + } + if !method_chain.is_empty() { + Ok(method_chain) + } else { + p.expected("function", p.peek_token()) + } + })?; + if let Some(method_chain) = method_chain { + Ok(Expr::Method(Method { + expr: Box::new(expr), + method_chain, + })) + } else { + Ok(expr) + } + } + pub fn parse_function(&mut self, name: ObjectName) -> Result { self.expect_token(&Token::LParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index daf65edf1..4fdbf7d51 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11403,6 +11403,76 @@ fn test_try_convert() { dialects.verified_expr("TRY_CONVERT('foo', VARCHAR(MAX))"); } +#[test] +fn parse_method_select() { + let dialects = all_dialects_where(|d| d.supports_methods()); + let _ = dialects.verified_only_select( + "SELECT LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T", + ); + let _ = dialects.verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"); + let _ = dialects + .verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"); + + // `CONVERT` support + let dialects = all_dialects_where(|d| { + d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() + }); + let _ = dialects.verified_only_select("SELECT CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T"); +} + +#[test] +fn parse_method_expr() { + let dialects = all_dialects_where(|d| d.supports_methods()); + let expr = dialects + .verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')"); + match expr { + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Function(_))); + assert!(matches!( + method_chain[..], + [Function { .. }, Function { .. }] + )); + } + _ => unreachable!(), + } + let expr = dialects.verified_expr( + "(SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')", + ); + match expr { + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Subquery(_))); + assert!(matches!(method_chain[..], [Function { .. }])); + } + _ => unreachable!(), + } + let expr = dialects.verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')"); + match expr { + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Cast { .. })); + assert!(matches!(method_chain[..], [Function { .. }])); + } + _ => unreachable!(), + } + + // `CONVERT` support + let dialects = all_dialects_where(|d| { + d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() + }); + let expr = dialects.verified_expr( + "CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')", + ); + match expr { + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Convert { .. })); + assert!(matches!( + method_chain[..], + [Function { .. }, Function { .. }] + )); + } + _ => unreachable!(), + } +} + #[test] fn test_show_dbs_schemas_tables_views() { // These statements are parsed the same by all dialects From 724a1d1aba575fb04a2df54ca8425b39ea753938 Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Fri, 15 Nov 2024 22:53:31 +0800 Subject: [PATCH 598/806] Add support for Hive's `LOAD DATA` expr (#1520) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 54 ++++++++++ src/dialect/duckdb.rs | 5 + src/dialect/generic.rs | 4 + src/dialect/hive.rs | 5 + src/dialect/mod.rs | 10 ++ src/keywords.rs | 1 + src/parser/mod.rs | 52 ++++++++-- tests/sqlparser_common.rs | 203 +++++++++++++++++++++++++++++++++++++- 8 files changed, 323 insertions(+), 11 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b0ac6bc41..39c742153 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3347,6 +3347,22 @@ pub enum Statement { channel: Ident, payload: Option, }, + /// ```sql + /// LOAD DATA [LOCAL] INPATH 'filepath' [OVERWRITE] INTO TABLE tablename + /// [PARTITION (partcol1=val1, partcol2=val2 ...)] + /// [INPUTFORMAT 'inputformat' SERDE 'serde'] + /// ``` + /// Loading files into tables + /// + /// See Hive + LoadData { + local: bool, + inpath: String, + overwrite: bool, + table_name: ObjectName, + partitioned: Option>, + table_format: Option, + }, } impl fmt::Display for Statement { @@ -3949,6 +3965,36 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateTable(create_table) => create_table.fmt(f), + Statement::LoadData { + local, + inpath, + overwrite, + table_name, + partitioned, + table_format, + } => { + write!( + f, + "LOAD DATA {local}INPATH '{inpath}' {overwrite}INTO TABLE {table_name}", + local = if *local { "LOCAL " } else { "" }, + inpath = inpath, + overwrite = if *overwrite { "OVERWRITE " } else { "" }, + table_name = table_name, + )?; + if let Some(ref parts) = &partitioned { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + if let Some(HiveLoadDataFormat { + serde, + input_format, + }) = &table_format + { + write!(f, " INPUTFORMAT {input_format} SERDE {serde}")?; + } + Ok(()) + } Statement::CreateVirtualTable { name, if_not_exists, @@ -5855,6 +5901,14 @@ pub enum HiveRowFormat { DELIMITED { delimiters: Vec }, } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct HiveLoadDataFormat { + pub serde: Expr, + pub input_format: Expr, +} + #[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/duckdb.rs b/src/dialect/duckdb.rs index e1b8db118..905b04e36 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -66,4 +66,9 @@ impl Dialect for DuckDbDialect { fn supports_explain_with_utility_options(&self) -> bool { true } + + /// See DuckDB + fn supports_load_extension(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8cfac217b..4998e0f4b 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -115,4 +115,8 @@ impl Dialect for GenericDialect { fn supports_comment_on(&self) -> bool { true } + + fn supports_load_extension(&self) -> bool { + true + } } diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index b97bf69be..571f9b9ba 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -56,4 +56,9 @@ impl Dialect for HiveDialect { fn supports_bang_not_operator(&self) -> bool { true } + + /// See Hive + fn supports_load_data(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index ee3fd4714..956a58986 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -620,6 +620,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports the `LOAD DATA` statement + fn supports_load_data(&self) -> bool { + false + } + + /// Returns true if the dialect supports the `LOAD extension` statement + fn supports_load_extension(&self) -> bool { + false + } + /// Returns true if this dialect expects the `TOP` option /// before the `ALL`/`DISTINCT` options in a `SELECT` statement. fn supports_top_before_distinct(&self) -> bool { diff --git a/src/keywords.rs b/src/keywords.rs index 9cdc90ce2..790268219 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -389,6 +389,7 @@ define_keywords!( INITIALLY, INNER, INOUT, + INPATH, INPUT, INPUTFORMAT, INSENSITIVE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a66a627bc..a583112a7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -543,10 +543,7 @@ impl<'a> Parser<'a> { Keyword::INSTALL if dialect_of!(self is DuckDbDialect | GenericDialect) => { self.parse_install() } - // `LOAD` is duckdb specific https://duckdb.org/docs/extensions/overview - Keyword::LOAD if dialect_of!(self is DuckDbDialect | GenericDialect) => { - self.parse_load() - } + Keyword::LOAD => self.parse_load(), // `OPTIMIZE` is clickhouse specific https://clickhouse.tech/docs/en/sql-reference/statements/optimize/ Keyword::OPTIMIZE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { self.parse_optimize_table() @@ -11222,6 +11219,22 @@ impl<'a> Parser<'a> { } } + pub fn parse_load_data_table_format( + &mut self, + ) -> Result, ParserError> { + if self.parse_keyword(Keyword::INPUTFORMAT) { + let input_format = self.parse_expr()?; + self.expect_keyword(Keyword::SERDE)?; + let serde = self.parse_expr()?; + Ok(Some(HiveLoadDataFormat { + input_format, + serde, + })) + } else { + Ok(None) + } + } + /// Parse an UPDATE statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds @@ -12224,10 +12237,35 @@ impl<'a> Parser<'a> { Ok(Statement::Install { extension_name }) } - /// `LOAD [extension_name]` + /// Parse a SQL LOAD statement pub fn parse_load(&mut self) -> Result { - let extension_name = self.parse_identifier(false)?; - Ok(Statement::Load { extension_name }) + if self.dialect.supports_load_extension() { + let extension_name = self.parse_identifier(false)?; + Ok(Statement::Load { extension_name }) + } else if self.parse_keyword(Keyword::DATA) && self.dialect.supports_load_data() { + let local = self.parse_one_of_keywords(&[Keyword::LOCAL]).is_some(); + self.expect_keyword(Keyword::INPATH)?; + let inpath = self.parse_literal_string()?; + let overwrite = self.parse_one_of_keywords(&[Keyword::OVERWRITE]).is_some(); + self.expect_keyword(Keyword::INTO)?; + self.expect_keyword(Keyword::TABLE)?; + let table_name = self.parse_object_name(false)?; + let partitioned = self.parse_insert_partition()?; + let table_format = self.parse_load_data_table_format()?; + Ok(Statement::LoadData { + local, + inpath, + overwrite, + table_name, + partitioned, + table_format, + }) + } else { + self.expected( + "`DATA` or an extension name after `LOAD`", + self.peek_token(), + ) + } } /// ```sql diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4fdbf7d51..2ffb5f44b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11583,13 +11583,208 @@ fn parse_notify_channel() { dialects.parse_sql_statements(sql).unwrap_err(), ParserError::ParserError("Expected: an SQL statement, found: NOTIFY".to_string()) ); - assert_eq!( - dialects.parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError("Expected: an SQL statement, found: NOTIFY".to_string()) - ); } } +#[test] +fn parse_load_data() { + let dialects = all_dialects_where(|d| d.supports_load_data()); + let only_supports_load_extension_dialects = + all_dialects_where(|d| !d.supports_load_data() && d.supports_load_extension()); + let not_supports_load_dialects = + all_dialects_where(|d| !d.supports_load_data() && !d.supports_load_extension()); + + let sql = "LOAD DATA INPATH '/local/path/to/data.txt' INTO TABLE test.my_table"; + match dialects.verified_stmt(sql) { + Statement::LoadData { + local, + inpath, + overwrite, + table_name, + partitioned, + table_format, + } => { + assert_eq!(false, local); + assert_eq!("/local/path/to/data.txt", inpath); + assert_eq!(false, overwrite); + assert_eq!( + ObjectName(vec![Ident::new("test"), Ident::new("my_table")]), + table_name + ); + assert_eq!(None, partitioned); + assert_eq!(None, table_format); + } + _ => unreachable!(), + }; + + // with OVERWRITE keyword + let sql = "LOAD DATA INPATH '/local/path/to/data.txt' OVERWRITE INTO TABLE my_table"; + match dialects.verified_stmt(sql) { + Statement::LoadData { + local, + inpath, + overwrite, + table_name, + partitioned, + table_format, + } => { + assert_eq!(false, local); + assert_eq!("/local/path/to/data.txt", inpath); + assert_eq!(true, overwrite); + assert_eq!(ObjectName(vec![Ident::new("my_table")]), table_name); + assert_eq!(None, partitioned); + assert_eq!(None, table_format); + } + _ => unreachable!(), + }; + + assert_eq!( + only_supports_load_extension_dialects + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: INPATH".to_string()) + ); + assert_eq!( + not_supports_load_dialects + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError( + "Expected: `DATA` or an extension name after `LOAD`, found: INPATH".to_string() + ) + ); + + // with LOCAL keyword + let sql = "LOAD DATA LOCAL INPATH '/local/path/to/data.txt' INTO TABLE test.my_table"; + match dialects.verified_stmt(sql) { + Statement::LoadData { + local, + inpath, + overwrite, + table_name, + partitioned, + table_format, + } => { + assert_eq!(true, local); + assert_eq!("/local/path/to/data.txt", inpath); + assert_eq!(false, overwrite); + assert_eq!( + ObjectName(vec![Ident::new("test"), Ident::new("my_table")]), + table_name + ); + assert_eq!(None, partitioned); + assert_eq!(None, table_format); + } + _ => unreachable!(), + }; + + assert_eq!( + only_supports_load_extension_dialects + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: LOCAL".to_string()) + ); + assert_eq!( + not_supports_load_dialects + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError( + "Expected: `DATA` or an extension name after `LOAD`, found: LOCAL".to_string() + ) + ); + + // with PARTITION clause + let sql = "LOAD DATA LOCAL INPATH '/local/path/to/data.txt' INTO TABLE my_table PARTITION (year = 2024, month = 11)"; + match dialects.verified_stmt(sql) { + Statement::LoadData { + local, + inpath, + overwrite, + table_name, + partitioned, + table_format, + } => { + assert_eq!(true, local); + assert_eq!("/local/path/to/data.txt", inpath); + assert_eq!(false, overwrite); + assert_eq!(ObjectName(vec![Ident::new("my_table")]), table_name); + assert_eq!( + Some(vec![ + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("year"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("2024".parse().unwrap(), false))), + }, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("month"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("11".parse().unwrap(), false))), + } + ]), + partitioned + ); + assert_eq!(None, table_format); + } + _ => unreachable!(), + }; + + // with PARTITION clause + let sql = "LOAD DATA LOCAL INPATH '/local/path/to/data.txt' OVERWRITE INTO TABLE good.my_table PARTITION (year = 2024, month = 11) INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' SERDE 'org.apache.hadoop.hive.serde2.OpenCSVSerde'"; + match dialects.verified_stmt(sql) { + Statement::LoadData { + local, + inpath, + overwrite, + table_name, + partitioned, + table_format, + } => { + assert_eq!(true, local); + assert_eq!("/local/path/to/data.txt", inpath); + assert_eq!(true, overwrite); + assert_eq!( + ObjectName(vec![Ident::new("good"), Ident::new("my_table")]), + table_name + ); + assert_eq!( + Some(vec![ + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("year"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("2024".parse().unwrap(), false))), + }, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("month"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value(Value::Number("11".parse().unwrap(), false))), + } + ]), + partitioned + ); + assert_eq!( + Some(HiveLoadDataFormat { + serde: Expr::Value(Value::SingleQuotedString( + "org.apache.hadoop.hive.serde2.OpenCSVSerde".to_string() + )), + input_format: Expr::Value(Value::SingleQuotedString( + "org.apache.hadoop.mapred.TextInputFormat".to_string() + )) + }), + table_format + ); + } + _ => unreachable!(), + }; + + // negative test case + let sql = "LOAD DATA2 LOCAL INPATH '/local/path/to/data.txt' INTO TABLE test.my_table"; + assert_eq!( + dialects.parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError( + "Expected: `DATA` or an extension name after `LOAD`, found: DATA2".to_string() + ) + ); +} + #[test] fn test_select_top() { let dialects = all_dialects_where(|d| d.supports_top_before_distinct()); From 4a5f20e9111900eed7480ac92d23402d4c10f1d6 Mon Sep 17 00:00:00 2001 From: hulk Date: Mon, 18 Nov 2024 20:29:28 +0800 Subject: [PATCH 599/806] Fix ClickHouse document link from `Russian` to `English` (#1527) --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 39c742153..fa9e53a5a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3167,7 +3167,7 @@ pub enum Statement { /// KILL [CONNECTION | QUERY | MUTATION] /// ``` /// - /// See + /// See /// See Kill { modifier: Option, From a67a4f3cbe2cb6e266f3914db8520890bfa7e198 Mon Sep 17 00:00:00 2001 From: delamarch3 <68732277+delamarch3@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:30:20 +0000 Subject: [PATCH 600/806] Support ANTI and SEMI joins without LEFT/RIGHT (#1528) --- src/ast/query.rs | 18 ++++++++++++++++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 10 ++++++++++ tests/sqlparser_common.rs | 16 ++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index 60ebe3765..2160da0d0 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1694,6 +1694,13 @@ impl fmt::Display for Join { suffix(constraint) ), JoinOperator::CrossJoin => write!(f, " CROSS JOIN {}", self.relation), + JoinOperator::Semi(constraint) => write!( + f, + " {}SEMI JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + ), JoinOperator::LeftSemi(constraint) => write!( f, " {}LEFT SEMI JOIN {}{}", @@ -1708,6 +1715,13 @@ impl fmt::Display for Join { self.relation, suffix(constraint) ), + JoinOperator::Anti(constraint) => write!( + f, + " {}ANTI JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + ), JoinOperator::LeftAnti(constraint) => write!( f, " {}LEFT ANTI JOIN {}{}", @@ -1746,10 +1760,14 @@ pub enum JoinOperator { RightOuter(JoinConstraint), FullOuter(JoinConstraint), CrossJoin, + /// SEMI (non-standard) + Semi(JoinConstraint), /// LEFT SEMI (non-standard) LeftSemi(JoinConstraint), /// RIGHT SEMI (non-standard) RightSemi(JoinConstraint), + /// ANTI (non-standard) + Anti(JoinConstraint), /// LEFT ANTI (non-standard) LeftAnti(JoinConstraint), /// RIGHT ANTI (non-standard) diff --git a/src/keywords.rs b/src/keywords.rs index 790268219..fdf2bf35c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -892,6 +892,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::CLUSTER, Keyword::DISTRIBUTE, Keyword::GLOBAL, + Keyword::ANTI, + Keyword::SEMI, // for MSSQL-specific OUTER APPLY (seems reserved in most dialects) Keyword::OUTER, Keyword::SET, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a583112a7..1a094c147 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10025,6 +10025,16 @@ impl<'a> Parser<'a> { } } } + Keyword::ANTI => { + let _ = self.next_token(); // consume ANTI + self.expect_keyword(Keyword::JOIN)?; + JoinOperator::Anti + } + Keyword::SEMI => { + let _ = self.next_token(); // consume SEMI + self.expect_keyword(Keyword::JOIN)?; + JoinOperator::Semi + } Keyword::FULL => { let _ = self.next_token(); // consume FULL let _ = self.parse_keyword(Keyword::OUTER); // [ OUTER ] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2ffb5f44b..77496781c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6013,6 +6013,10 @@ fn parse_joins_on() { JoinOperator::RightOuter )] ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 SEMI JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, false, JoinOperator::Semi)] + ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT SEMI JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint( @@ -6031,6 +6035,10 @@ fn parse_joins_on() { JoinOperator::RightSemi )] ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 ANTI JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, false, JoinOperator::Anti)] + ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT ANTI JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint( @@ -6117,6 +6125,10 @@ fn parse_joins_using() { only(&verified_only_select("SELECT * FROM t1 RIGHT JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::RightOuter)] ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 SEMI JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::Semi)] + ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT SEMI JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::LeftSemi)] @@ -6125,6 +6137,10 @@ fn parse_joins_using() { only(&verified_only_select("SELECT * FROM t1 RIGHT SEMI JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::RightSemi)] ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 ANTI JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::Anti)] + ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT ANTI JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::LeftAnti)] From 4c629e8520b68eb289b34882aa326d80a6f8e022 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 18 Nov 2024 13:30:53 +0100 Subject: [PATCH 601/806] support sqlite's OR clauses in update statements (#1530) --- src/ast/dml.rs | 4 ++-- src/ast/mod.rs | 19 +++++++++++++------ src/parser/mod.rs | 39 +++++++++++++++++++++------------------ tests/sqlparser_common.rs | 21 +++++++++++++++++++++ tests/sqlparser_mysql.rs | 1 + tests/sqlparser_sqlite.rs | 1 + 6 files changed, 59 insertions(+), 26 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 2932fafb5..22309c8f8 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -505,8 +505,8 @@ impl Display for Insert { self.table_name.to_string() }; - if let Some(action) = self.or { - write!(f, "INSERT OR {action} INTO {table_name} ")?; + if let Some(on_conflict) = self.or { + write!(f, "INSERT {on_conflict} INTO {table_name} ")?; } else { write!( f, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index fa9e53a5a..ad59f0779 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2396,6 +2396,8 @@ pub enum Statement { selection: Option, /// RETURNING returning: Option>, + /// SQLite-specific conflict resolution clause + or: Option, }, /// ```sql /// DELETE @@ -3691,8 +3693,13 @@ impl fmt::Display for Statement { from, selection, returning, + or, } => { - write!(f, "UPDATE {table}")?; + write!(f, "UPDATE ")?; + if let Some(or) = or { + write!(f, "{or} ")?; + } + write!(f, "{table}")?; if !assignments.is_empty() { write!(f, " SET {}", display_comma_separated(assignments))?; } @@ -6304,11 +6311,11 @@ impl fmt::Display for SqliteOnConflict { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use SqliteOnConflict::*; match self { - Rollback => write!(f, "ROLLBACK"), - Abort => write!(f, "ABORT"), - Fail => write!(f, "FAIL"), - Ignore => write!(f, "IGNORE"), - Replace => write!(f, "REPLACE"), + Rollback => write!(f, "OR ROLLBACK"), + Abort => write!(f, "OR ABORT"), + Fail => write!(f, "OR FAIL"), + Ignore => write!(f, "OR IGNORE"), + Replace => write!(f, "OR REPLACE"), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1a094c147..0c8237889 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11042,24 +11042,7 @@ impl<'a> Parser<'a> { /// Parse an INSERT statement pub fn parse_insert(&mut self) -> Result { - let or = if !dialect_of!(self is SQLiteDialect) { - None - } else if self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]) { - Some(SqliteOnConflict::Replace) - } else if self.parse_keywords(&[Keyword::OR, Keyword::ROLLBACK]) { - Some(SqliteOnConflict::Rollback) - } else if self.parse_keywords(&[Keyword::OR, Keyword::ABORT]) { - Some(SqliteOnConflict::Abort) - } else if self.parse_keywords(&[Keyword::OR, Keyword::FAIL]) { - Some(SqliteOnConflict::Fail) - } else if self.parse_keywords(&[Keyword::OR, Keyword::IGNORE]) { - Some(SqliteOnConflict::Ignore) - } else if self.parse_keyword(Keyword::REPLACE) { - Some(SqliteOnConflict::Replace) - } else { - None - }; - + let or = self.parse_conflict_clause(); let priority = if !dialect_of!(self is MySqlDialect | GenericDialect) { None } else if self.parse_keyword(Keyword::LOW_PRIORITY) { @@ -11218,6 +11201,24 @@ impl<'a> Parser<'a> { } } + fn parse_conflict_clause(&mut self) -> Option { + if self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]) { + Some(SqliteOnConflict::Replace) + } else if self.parse_keywords(&[Keyword::OR, Keyword::ROLLBACK]) { + Some(SqliteOnConflict::Rollback) + } else if self.parse_keywords(&[Keyword::OR, Keyword::ABORT]) { + Some(SqliteOnConflict::Abort) + } else if self.parse_keywords(&[Keyword::OR, Keyword::FAIL]) { + Some(SqliteOnConflict::Fail) + } else if self.parse_keywords(&[Keyword::OR, Keyword::IGNORE]) { + Some(SqliteOnConflict::Ignore) + } else if self.parse_keyword(Keyword::REPLACE) { + Some(SqliteOnConflict::Replace) + } else { + None + } + } + pub fn parse_insert_partition(&mut self) -> Result>, ParserError> { if self.parse_keyword(Keyword::PARTITION) { self.expect_token(&Token::LParen)?; @@ -11253,6 +11254,7 @@ impl<'a> Parser<'a> { } pub fn parse_update(&mut self) -> Result { + let or = self.parse_conflict_clause(); let table = self.parse_table_and_joins()?; self.expect_keyword(Keyword::SET)?; let assignments = self.parse_comma_separated(Parser::parse_assignment)?; @@ -11279,6 +11281,7 @@ impl<'a> Parser<'a> { from, selection, returning, + or, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 77496781c..283071e9b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -443,6 +443,7 @@ fn parse_update_set_from() { ])), }), returning: None, + or: None, } ); } @@ -457,6 +458,7 @@ fn parse_update_with_table_alias() { from: _from, selection, returning, + or: None, } => { assert_eq!( TableWithJoins { @@ -505,6 +507,25 @@ fn parse_update_with_table_alias() { } } +#[test] +fn parse_update_or() { + let expect_or_clause = |sql: &str, expected_action: SqliteOnConflict| match verified_stmt(sql) { + Statement::Update { or, .. } => assert_eq!(or, Some(expected_action)), + other => unreachable!("Expected update with or, got {:?}", other), + }; + expect_or_clause( + "UPDATE OR REPLACE t SET n = n + 1", + SqliteOnConflict::Replace, + ); + expect_or_clause( + "UPDATE OR ROLLBACK t SET n = n + 1", + SqliteOnConflict::Rollback, + ); + expect_or_clause("UPDATE OR ABORT t SET n = n + 1", SqliteOnConflict::Abort); + expect_or_clause("UPDATE OR FAIL t SET n = n + 1", SqliteOnConflict::Fail); + expect_or_clause("UPDATE OR IGNORE t SET n = n + 1", SqliteOnConflict::Ignore); +} + #[test] fn parse_select_with_table_alias_as() { // AS is optional diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8269eadc0..2a876cff2 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1970,6 +1970,7 @@ fn parse_update_with_joins() { from: _from, selection, returning, + or: None, } => { assert_eq!( TableWithJoins { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 6f8bbb2d8..6f8e654dc 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -465,6 +465,7 @@ fn parse_update_tuple_row_values() { assert_eq!( sqlite().verified_stmt("UPDATE x SET (a, b) = (1, 2)"), Statement::Update { + or: None, assignments: vec![Assignment { target: AssignmentTarget::Tuple(vec![ ObjectName(vec![Ident::new("a"),]), From f961efc0c92c271a75663b1b32b1da5b49d86db3 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 18 Nov 2024 15:02:22 +0100 Subject: [PATCH 602/806] support column type definitions in table aliases (#1526) --- src/ast/mod.rs | 4 +-- src/ast/query.rs | 37 +++++++++++++++++++++++- src/parser/mod.rs | 19 +++++++++++-- tests/sqlparser_common.rs | 59 +++++++++++++++++++++++++++++++++------ 4 files changed, 105 insertions(+), 14 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ad59f0779..fc6a1b4f1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -60,8 +60,8 @@ pub use self::query::{ OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, - ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, + TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, + Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index 2160da0d0..078bbc841 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1597,7 +1597,7 @@ impl fmt::Display for TableFactor { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct TableAlias { pub name: Ident, - pub columns: Vec, + pub columns: Vec, } impl fmt::Display for TableAlias { @@ -1610,6 +1610,41 @@ impl fmt::Display for TableAlias { } } +/// SQL column definition in a table expression alias. +/// Most of the time, the data type is not specified. +/// But some table-valued functions do require specifying the data type. +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TableAliasColumnDef { + /// Column name alias + pub name: Ident, + /// Some table-valued functions require specifying the data type in the alias. + pub data_type: Option, +} + +impl TableAliasColumnDef { + /// Create a new table alias column definition with only a name and no type + pub fn from_name>(name: S) -> Self { + TableAliasColumnDef { + name: Ident::new(name), + data_type: None, + } + } +} + +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)?; + } + 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/parser/mod.rs b/src/parser/mod.rs index 0c8237889..82347f58d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8270,7 +8270,7 @@ impl<'a> Parser<'a> { ) -> Result, ParserError> { match self.parse_optional_alias(reserved_kwds)? { Some(name) => { - let columns = self.parse_parenthesized_column_list(Optional, false)?; + let columns = self.parse_table_alias_column_defs()?; Ok(Some(TableAlias { name, columns })) } None => Ok(None), @@ -8607,6 +8607,21 @@ impl<'a> Parser<'a> { } } + /// Parse a parenthesized comma-separated list of table alias column definitions. + fn parse_table_alias_column_defs(&mut self) -> Result, ParserError> { + if self.consume_token(&Token::LParen) { + let cols = self.parse_comma_separated(|p| { + let name = p.parse_identifier(false)?; + let data_type = p.maybe_parse(|p| p.parse_data_type())?; + Ok(TableAliasColumnDef { name, data_type }) + })?; + self.expect_token(&Token::RParen)?; + Ok(cols) + } else { + Ok(vec![]) + } + } + pub fn parse_precision(&mut self) -> Result { self.expect_token(&Token::LParen)?; let n = self.parse_literal_uint()?; @@ -9174,7 +9189,7 @@ impl<'a> Parser<'a> { materialized: is_materialized, } } else { - let columns = self.parse_parenthesized_column_list(Optional, false)?; + let columns = self.parse_table_alias_column_defs()?; self.expect_keyword(Keyword::AS)?; let mut is_materialized = None; if dialect_of!(self is PostgreSqlDialect) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 283071e9b..9fb467102 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -553,7 +553,11 @@ fn parse_select_with_table_alias() { name: ObjectName(vec![Ident::new("lineitem")]), alias: Some(TableAlias { name: Ident::new("l"), - columns: vec![Ident::new("A"), Ident::new("B"), Ident::new("C"),], + columns: vec![ + TableAliasColumnDef::from_name("A"), + TableAliasColumnDef::from_name("B"), + TableAliasColumnDef::from_name("C"), + ], }), args: None, with_hints: vec![], @@ -5597,6 +5601,40 @@ fn parse_table_function() { ); } +#[test] +fn parse_select_with_alias_and_column_defs() { + let sql = r#"SELECT * FROM jsonb_to_record('{"a": "x", "b": 2}'::JSONB) AS x (a TEXT, b INT)"#; + let select = verified_only_select(sql); + + match only(&select.from) { + TableWithJoins { + relation: TableFactor::Table { + alias: Some(alias), .. + }, + .. + } => { + assert_eq!(alias.name.value, "x"); + assert_eq!( + alias.columns, + vec![ + TableAliasColumnDef { + name: Ident::new("a"), + data_type: Some(DataType::Text), + }, + TableAliasColumnDef { + name: Ident::new("b"), + data_type: Some(DataType::Int(None)), + }, + ] + ); + } + _ => unreachable!( + "Expecting only TableWithJoins with TableFactor::Table, got {:#?}", + select.from + ), + } +} + #[test] fn parse_unnest() { let sql = "SELECT UNNEST(make_array(1, 2, 3))"; @@ -6372,7 +6410,10 @@ fn parse_cte_renamed_columns() { let sql = "WITH cte (col1, col2) AS (SELECT foo, bar FROM baz) SELECT * FROM cte"; let query = all_dialects().verified_query(sql); assert_eq!( - vec![Ident::new("col1"), Ident::new("col2")], + vec![ + TableAliasColumnDef::from_name("col1"), + TableAliasColumnDef::from_name("col2") + ], query .with .unwrap() @@ -6401,10 +6442,7 @@ fn parse_recursive_cte() { value: "nums".to_string(), quote_style: None, }, - columns: vec![Ident { - value: "val".to_string(), - quote_style: None, - }], + columns: vec![TableAliasColumnDef::from_name("val")], }, query: Box::new(cte_query), from: None, @@ -9347,7 +9385,10 @@ fn parse_pivot_table() { value: "p".to_string(), quote_style: None }, - columns: vec![Ident::new("c"), Ident::new("d")], + columns: vec![ + TableAliasColumnDef::from_name("c"), + TableAliasColumnDef::from_name("d"), + ], }), } ); @@ -9408,8 +9449,8 @@ fn parse_unpivot_table() { name: Ident::new("u"), columns: ["product", "quarter", "quantity"] .into_iter() - .map(Ident::new) - .collect() + .map(TableAliasColumnDef::from_name) + .collect(), }), } ); From 92be237cfc082ecfd67cd8bbc53b74d8cd6ce46f Mon Sep 17 00:00:00 2001 From: gaoqiangz <38213294+gaoqiangz@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:22:18 +0800 Subject: [PATCH 603/806] Add support for MSSQL's `JSON_ARRAY`/`JSON_OBJECT` expr (#1507) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 56 ++++- src/dialect/duckdb.rs | 4 + src/dialect/generic.rs | 4 + src/dialect/mod.rs | 25 ++- src/dialect/mssql.rs | 12 ++ src/keywords.rs | 1 + src/parser/mod.rs | 115 ++++++---- tests/sqlparser_common.rs | 3 +- tests/sqlparser_mssql.rs | 441 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 617 insertions(+), 44 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index fc6a1b4f1..89e70bdd4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5449,6 +5449,8 @@ pub enum FunctionArgOperator { RightArrow, /// function(arg1 := value1) Assignment, + /// function(arg1 : value1) + Colon, } impl fmt::Display for FunctionArgOperator { @@ -5457,6 +5459,7 @@ impl fmt::Display for FunctionArgOperator { FunctionArgOperator::Equals => f.write_str("="), FunctionArgOperator::RightArrow => f.write_str("=>"), FunctionArgOperator::Assignment => f.write_str(":="), + FunctionArgOperator::Colon => f.write_str(":"), } } } @@ -5465,11 +5468,22 @@ impl fmt::Display for FunctionArgOperator { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum FunctionArg { + /// `name` is identifier + /// + /// Enabled when `Dialect::supports_named_fn_args_with_expr_name` returns 'false' Named { name: Ident, arg: FunctionArgExpr, operator: FunctionArgOperator, }, + /// `name` is arbitrary expression + /// + /// Enabled when `Dialect::supports_named_fn_args_with_expr_name` returns 'true' + ExprNamed { + name: Expr, + arg: FunctionArgExpr, + operator: FunctionArgOperator, + }, Unnamed(FunctionArgExpr), } @@ -5481,6 +5495,11 @@ impl fmt::Display for FunctionArg { arg, operator, } => write!(f, "{name} {operator} {arg}"), + FunctionArg::ExprNamed { + name, + arg, + operator, + } => write!(f, "{name} {operator} {arg}"), FunctionArg::Unnamed(unnamed_arg) => write!(f, "{unnamed_arg}"), } } @@ -5619,7 +5638,10 @@ impl fmt::Display for FunctionArgumentList { } write!(f, "{}", display_comma_separated(&self.args))?; if !self.clauses.is_empty() { - write!(f, " {}", display_separated(&self.clauses, " "))?; + if !self.args.is_empty() { + write!(f, " ")?; + } + write!(f, "{}", display_separated(&self.clauses, " "))?; } Ok(()) } @@ -5661,6 +5683,11 @@ pub enum FunctionArgumentClause { /// /// [`GROUP_CONCAT`]: https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_group-concat Separator(Value), + /// The json-null-clause to the [`JSON_ARRAY`]/[`JSON_OBJECT`] function in MSSQL. + /// + /// [`JSON_ARRAY`]: + /// [`JSON_OBJECT`]: + JsonNullClause(JsonNullClause), } impl fmt::Display for FunctionArgumentClause { @@ -5676,6 +5703,7 @@ impl fmt::Display for FunctionArgumentClause { FunctionArgumentClause::OnOverflow(on_overflow) => write!(f, "{on_overflow}"), FunctionArgumentClause::Having(bound) => write!(f, "{bound}"), FunctionArgumentClause::Separator(sep) => write!(f, "SEPARATOR {sep}"), + FunctionArgumentClause::JsonNullClause(null_clause) => write!(f, "{null_clause}"), } } } @@ -7564,6 +7592,32 @@ impl fmt::Display for ShowStatementIn { } } +/// MSSQL's json null clause +/// +/// ```plaintext +/// ::= +/// NULL ON NULL +/// | ABSENT ON NULL +/// ``` +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum JsonNullClause { + NullOnNull, + AbsentOnNull, +} + +impl Display for JsonNullClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsonNullClause::NullOnNull => write!(f, "NULL ON NULL"), + JsonNullClause::AbsentOnNull => write!(f, "ABSENT ON NULL"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 905b04e36..a2699d850 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -47,6 +47,10 @@ impl Dialect for DuckDbDialect { true } + fn supports_named_fn_args_with_assignment_operator(&self) -> bool { + true + } + // DuckDB uses this syntax for `STRUCT`s. // // https://duckdb.org/docs/sql/data_types/struct.html#creating-structs diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 4998e0f4b..e3beeae7f 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -119,4 +119,8 @@ impl Dialect for GenericDialect { fn supports_load_extension(&self) -> bool { true } + + fn supports_named_fn_args_with_assignment_operator(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 956a58986..39ea98c69 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -231,11 +231,34 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if the dialect supports named arguments of the form FUN(a = '1', b = '2'). + /// Returns true if the dialect supports named arguments of the form `FUN(a = '1', b = '2')`. fn supports_named_fn_args_with_eq_operator(&self) -> bool { false } + /// Returns true if the dialect supports named arguments of the form `FUN(a : '1', b : '2')`. + fn supports_named_fn_args_with_colon_operator(&self) -> bool { + false + } + + /// Returns true if the dialect supports named arguments of the form `FUN(a := '1', b := '2')`. + fn supports_named_fn_args_with_assignment_operator(&self) -> bool { + false + } + + /// Returns true if the dialect supports named arguments of the form `FUN(a => '1', b => '2')`. + fn supports_named_fn_args_with_rarrow_operator(&self) -> bool { + true + } + + /// Returns true if dialect supports argument name as arbitrary expression. + /// e.g. `FUN(LOWER('a'):'1', b:'2')` + /// Such function arguments are represented in the AST by the `FunctionArg::ExprNamed` variant, + /// otherwise use the `FunctionArg::Named` variant (compatible reason). + fn supports_named_fn_args_with_expr_name(&self) -> bool { + false + } + /// Returns true if the dialect supports identifiers starting with a numeric /// prefix such as tables named `59901_user_login` fn supports_numeric_prefix(&self) -> bool { diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 39ce9c125..2d0ef027f 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -66,4 +66,16 @@ impl Dialect for MsSqlDialect { fn supports_methods(&self) -> bool { true } + + fn supports_named_fn_args_with_colon_operator(&self) -> bool { + true + } + + fn supports_named_fn_args_with_expr_name(&self) -> bool { + true + } + + fn supports_named_fn_args_with_rarrow_operator(&self) -> bool { + false + } } diff --git a/src/keywords.rs b/src/keywords.rs index fdf2bf35c..29115a0d2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -74,6 +74,7 @@ macro_rules! define_keywords { define_keywords!( ABORT, ABS, + ABSENT, ABSOLUTE, ACCESS, ACCOUNT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 82347f58d..35ad95803 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11321,45 +11321,58 @@ impl<'a> Parser<'a> { } pub fn parse_function_args(&mut self) -> Result { - if self.peek_nth_token(1) == Token::RArrow { - let name = self.parse_identifier(false)?; - - self.expect_token(&Token::RArrow)?; - let arg = self.parse_wildcard_expr()?.into(); - - Ok(FunctionArg::Named { - name, - arg, - operator: FunctionArgOperator::RightArrow, - }) - } else if self.dialect.supports_named_fn_args_with_eq_operator() - && self.peek_nth_token(1) == Token::Eq - { - let name = self.parse_identifier(false)?; - - self.expect_token(&Token::Eq)?; - let arg = self.parse_wildcard_expr()?.into(); - - Ok(FunctionArg::Named { - name, - arg, - operator: FunctionArgOperator::Equals, - }) - } else if dialect_of!(self is DuckDbDialect | GenericDialect) - && self.peek_nth_token(1) == Token::Assignment - { - let name = self.parse_identifier(false)?; - - self.expect_token(&Token::Assignment)?; - let arg = self.parse_expr()?.into(); - - Ok(FunctionArg::Named { - name, - arg, - operator: FunctionArgOperator::Assignment, - }) + let arg = if self.dialect.supports_named_fn_args_with_expr_name() { + self.maybe_parse(|p| { + let name = p.parse_expr()?; + let operator = p.parse_function_named_arg_operator()?; + let arg = p.parse_wildcard_expr()?.into(); + Ok(FunctionArg::ExprNamed { + name, + arg, + operator, + }) + })? } else { - Ok(FunctionArg::Unnamed(self.parse_wildcard_expr()?.into())) + self.maybe_parse(|p| { + let name = p.parse_identifier(false)?; + let operator = p.parse_function_named_arg_operator()?; + let arg = p.parse_wildcard_expr()?.into(); + Ok(FunctionArg::Named { + name, + arg, + operator, + }) + })? + }; + if let Some(arg) = arg { + return Ok(arg); + } + Ok(FunctionArg::Unnamed(self.parse_wildcard_expr()?.into())) + } + + fn parse_function_named_arg_operator(&mut self) -> Result { + let tok = self.next_token(); + match tok.token { + Token::RArrow if self.dialect.supports_named_fn_args_with_rarrow_operator() => { + Ok(FunctionArgOperator::RightArrow) + } + Token::Eq if self.dialect.supports_named_fn_args_with_eq_operator() => { + Ok(FunctionArgOperator::Equals) + } + Token::Assignment + if self + .dialect + .supports_named_fn_args_with_assignment_operator() => + { + Ok(FunctionArgOperator::Assignment) + } + Token::Colon if self.dialect.supports_named_fn_args_with_colon_operator() => { + Ok(FunctionArgOperator::Colon) + } + _ => { + self.prev_token(); + self.expected("argument operator", tok) + } } } @@ -11403,19 +11416,24 @@ impl<'a> Parser<'a> { /// FIRST_VALUE(x IGNORE NULL); /// ``` fn parse_function_argument_list(&mut self) -> Result { + let mut clauses = vec![]; + + // For MSSQL empty argument list with json-null-clause case, e.g. `JSON_ARRAY(NULL ON NULL)` + if let Some(null_clause) = self.parse_json_null_clause() { + clauses.push(FunctionArgumentClause::JsonNullClause(null_clause)); + } + if self.consume_token(&Token::RParen) { return Ok(FunctionArgumentList { duplicate_treatment: None, args: vec![], - clauses: vec![], + clauses, }); } let duplicate_treatment = self.parse_duplicate_treatment()?; let args = self.parse_comma_separated(Parser::parse_function_args)?; - let mut clauses = vec![]; - if self.dialect.supports_window_function_null_treatment_arg() { if let Some(null_treatment) = self.parse_null_treatment()? { clauses.push(FunctionArgumentClause::IgnoreOrRespectNulls(null_treatment)); @@ -11456,6 +11474,10 @@ impl<'a> Parser<'a> { clauses.push(FunctionArgumentClause::OnOverflow(on_overflow)); } + if let Some(null_clause) = self.parse_json_null_clause() { + clauses.push(FunctionArgumentClause::JsonNullClause(null_clause)); + } + self.expect_token(&Token::RParen)?; Ok(FunctionArgumentList { duplicate_treatment, @@ -11464,6 +11486,17 @@ impl<'a> Parser<'a> { }) } + /// Parses MSSQL's json-null-clause + fn parse_json_null_clause(&mut self) -> Option { + if self.parse_keywords(&[Keyword::ABSENT, Keyword::ON, Keyword::NULL]) { + Some(JsonNullClause::AbsentOnNull) + } else if self.parse_keywords(&[Keyword::NULL, Keyword::ON, Keyword::NULL]) { + Some(JsonNullClause::NullOnNull) + } else { + None + } + } + fn parse_duplicate_treatment(&mut self) -> Result, ParserError> { let loc = self.peek_token().location; match ( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9fb467102..ecdca6b1b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4402,8 +4402,9 @@ fn parse_explain_query_plan() { #[test] fn parse_named_argument_function() { + let dialects = all_dialects_where(|d| d.supports_named_fn_args_with_rarrow_operator()); let sql = "SELECT FUN(a => '1', b => '2') FROM foo"; - let select = verified_only_select(sql); + let select = dialects.verified_only_select(sql); assert_eq!( &Expr::Function(Function { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index c28f89e37..73fd99cf3 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -784,6 +784,447 @@ fn parse_for_json_expect_ast() { ); } +#[test] +fn parse_mssql_json_object() { + let select = ms().verified_only_select("SELECT JSON_OBJECT('name' : 'value', 'type' : 1)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert_eq!( + &[ + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("name".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "value".into() + ))), + operator: FunctionArgOperator::Colon + }, + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("type".into())), + arg: FunctionArgExpr::Expr(Expr::Value(number("1"))), + operator: FunctionArgOperator::Colon + } + ], + &args[..] + ), + _ => unreachable!(), + } + let select = ms() + .verified_only_select("SELECT JSON_OBJECT('name' : 'value', 'type' : NULL ABSENT ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &[ + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("name".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "value".into() + ))), + operator: FunctionArgOperator::Colon + }, + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("type".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::Null)), + operator: FunctionArgOperator::Colon + } + ], + &args[..] + ); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select("SELECT JSON_OBJECT(NULL ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert!(args.is_empty()); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::NullOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select("SELECT JSON_OBJECT(ABSENT ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert!(args.is_empty()); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select( + "SELECT JSON_OBJECT('name' : 'value', 'type' : JSON_ARRAY(1, 2) ABSENT ON NULL)", + ); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("name".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "value".into() + ))), + operator: FunctionArgOperator::Colon + }, + &args[0] + ); + assert!(matches!( + args[1], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::Function(_)), + operator: FunctionArgOperator::Colon + } + )); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select( + "SELECT JSON_OBJECT('name' : 'value', 'type' : JSON_OBJECT('type_id' : 1, 'name' : 'a') NULL ON NULL)", + ); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("name".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "value".into() + ))), + operator: FunctionArgOperator::Colon + }, + &args[0] + ); + assert!(matches!( + args[1], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::Function(_)), + operator: FunctionArgOperator::Colon + } + )); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::NullOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select( + "SELECT JSON_OBJECT('user_name' : USER_NAME(), LOWER(@id_key) : @id_value, 'sid' : (SELECT @@SPID) ABSENT ON NULL)", + ); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert!(matches!( + args[0], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::Function(_)), + operator: FunctionArgOperator::Colon + } + )); + assert!(matches!( + args[1], + FunctionArg::ExprNamed { + name: Expr::Function(_), + arg: FunctionArgExpr::Expr(Expr::Identifier(_)), + operator: FunctionArgOperator::Colon + } + )); + assert!(matches!( + args[2], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::Subquery(_)), + operator: FunctionArgOperator::Colon + } + )); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select( + "SELECT s.session_id, JSON_OBJECT('security_id' : s.security_id, 'login' : s.login_name, 'status' : s.status) AS info \ + FROM sys.dm_exec_sessions AS s \ + WHERE s.is_user_process = 1", + ); + match &select.projection[1] { + SelectItem::ExprWithAlias { + expr: + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }), + .. + } => { + assert!(matches!( + args[0], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), + operator: FunctionArgOperator::Colon + } + )); + assert!(matches!( + args[1], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), + operator: FunctionArgOperator::Colon + } + )); + assert!(matches!( + args[2], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), + operator: FunctionArgOperator::Colon + } + )); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_mssql_json_array() { + let select = ms().verified_only_select("SELECT JSON_ARRAY('a', 1, NULL, 2 NULL ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &[ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("a".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Null))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), + ], + &args[..] + ); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::NullOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select("SELECT JSON_ARRAY('a', 1, NULL, 2 ABSENT ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &[ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("a".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Null))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), + ], + &args[..] + ); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select("SELECT JSON_ARRAY(NULL ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert!(args.is_empty()); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::NullOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select("SELECT JSON_ARRAY(ABSENT ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert!(args.is_empty()); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select( + "SELECT JSON_ARRAY('a', JSON_OBJECT('name' : 'value', 'type' : 1) NULL ON NULL)", + ); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("a".into()) + ))), + &args[0] + ); + assert!(matches!( + args[1], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Function(_))) + )); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::NullOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = ms().verified_only_select( + "SELECT JSON_ARRAY('a', JSON_OBJECT('name' : 'value', 'type' : 1), JSON_ARRAY(1, NULL, 2 NULL ON NULL))", + ); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => { + assert_eq!( + &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("a".into()) + ))), + &args[0] + ); + assert!(matches!( + args[1], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Function(_))) + )); + assert!(matches!( + args[2], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Function(_))) + )); + } + _ => unreachable!(), + } + let select = ms().verified_only_select("SELECT JSON_ARRAY(1, @id_value, (SELECT @@SPID))"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => { + assert_eq!( + &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + &args[0] + ); + assert!(matches!( + args[1], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(_))) + )); + assert!(matches!( + args[2], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Subquery(_))) + )); + } + _ => unreachable!(), + } + let select = ms().verified_only_select( + "SELECT s.session_id, JSON_ARRAY(s.host_name, s.program_name, s.client_interface_name NULL ON NULL) AS info \ + FROM sys.dm_exec_sessions AS s \ + WHERE s.is_user_process = 1", + ); + match &select.projection[1] { + SelectItem::ExprWithAlias { + expr: + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }), + .. + } => { + assert!(matches!( + args[0], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::CompoundIdentifier(_))) + )); + assert!(matches!( + args[1], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::CompoundIdentifier(_))) + )); + assert!(matches!( + args[2], + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::CompoundIdentifier(_))) + )); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::NullOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_ampersand_arobase() { // In SQL Server, a&@b means (a) & (@b), in PostgreSQL it means (a) &@ (b) From 73947a5f021128cfccd47293ca65aa5c4e83f598 Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Wed, 20 Nov 2024 05:14:28 +0800 Subject: [PATCH 604/806] Add support for PostgreSQL `UNLISTEN` syntax and Add support for Postgres `LOAD extension` expr (#1531) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 11 ++++++ src/dialect/mod.rs | 9 +---- src/dialect/postgresql.rs | 12 +++--- src/keywords.rs | 1 + src/parser/mod.rs | 22 +++++++++-- tests/sqlparser_common.rs | 77 +++++++++++++++++++++++++++++++++++++-- tests/sqlparser_duckdb.rs | 14 ------- 7 files changed, 113 insertions(+), 33 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 89e70bdd4..9185c9df4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3340,6 +3340,13 @@ pub enum Statement { /// See Postgres LISTEN { channel: Ident }, /// ```sql + /// UNLISTEN + /// ``` + /// stop listening for a notification + /// + /// See Postgres + UNLISTEN { channel: Ident }, + /// ```sql /// NOTIFY channel [ , payload ] /// ``` /// send a notification event together with an optional “payload” string to channel @@ -4948,6 +4955,10 @@ impl fmt::Display for Statement { write!(f, "LISTEN {channel}")?; Ok(()) } + Statement::UNLISTEN { channel } => { + write!(f, "UNLISTEN {channel}")?; + Ok(()) + } Statement::NOTIFY { channel, payload } => { write!(f, "NOTIFY {channel}")?; if let Some(payload) = payload { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 39ea98c69..985cad749 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -633,13 +633,8 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if the dialect supports the `LISTEN` statement - fn supports_listen(&self) -> bool { - false - } - - /// Returns true if the dialect supports the `NOTIFY` statement - fn supports_notify(&self) -> bool { + /// Returns true if the dialect supports the `LISTEN`, `UNLISTEN` and `NOTIFY` statements + fn supports_listen_notify(&self) -> bool { false } diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 5af1ab853..559586e3f 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -191,12 +191,9 @@ impl Dialect for PostgreSqlDialect { } /// see - fn supports_listen(&self) -> bool { - true - } - + /// see /// see - fn supports_notify(&self) -> bool { + fn supports_listen_notify(&self) -> bool { true } @@ -209,6 +206,11 @@ impl Dialect for PostgreSqlDialect { fn supports_comment_on(&self) -> bool { true } + + /// See + fn supports_load_extension(&self) -> bool { + true + } } pub fn parse_create(parser: &mut Parser) -> Option> { diff --git a/src/keywords.rs b/src/keywords.rs index 29115a0d2..fc2a2927c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -799,6 +799,7 @@ define_keywords!( UNION, UNIQUE, UNKNOWN, + UNLISTEN, UNLOAD, UNLOCK, UNLOGGED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 35ad95803..35c763e93 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -532,10 +532,11 @@ impl<'a> Parser<'a> { Keyword::EXECUTE | Keyword::EXEC => self.parse_execute(), Keyword::PREPARE => self.parse_prepare(), Keyword::MERGE => self.parse_merge(), - // `LISTEN` and `NOTIFY` are Postgres-specific + // `LISTEN`, `UNLISTEN` and `NOTIFY` are Postgres-specific // syntaxes. They are used for Postgres statement. - Keyword::LISTEN if self.dialect.supports_listen() => self.parse_listen(), - Keyword::NOTIFY if self.dialect.supports_notify() => self.parse_notify(), + Keyword::LISTEN if self.dialect.supports_listen_notify() => self.parse_listen(), + Keyword::UNLISTEN if self.dialect.supports_listen_notify() => self.parse_unlisten(), + Keyword::NOTIFY if self.dialect.supports_listen_notify() => self.parse_notify(), // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html Keyword::PRAGMA => self.parse_pragma(), Keyword::UNLOAD => self.parse_unload(), @@ -999,6 +1000,21 @@ impl<'a> Parser<'a> { Ok(Statement::LISTEN { channel }) } + pub fn parse_unlisten(&mut self) -> Result { + let channel = if self.consume_token(&Token::Mul) { + Ident::new(Expr::Wildcard.to_string()) + } else { + match self.parse_identifier(false) { + Ok(expr) => expr, + _ => { + self.prev_token(); + return self.expected("wildcard or identifier", self.peek_token()); + } + } + }; + Ok(Statement::UNLISTEN { channel }) + } + pub fn parse_notify(&mut self) -> Result { let channel = self.parse_identifier(false)?; let payload = if self.consume_token(&Token::Comma) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ecdca6b1b..3d9ba5da2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11595,7 +11595,7 @@ fn test_show_dbs_schemas_tables_views() { #[test] fn parse_listen_channel() { - let dialects = all_dialects_where(|d| d.supports_listen()); + let dialects = all_dialects_where(|d| d.supports_listen_notify()); match dialects.verified_stmt("LISTEN test1") { Statement::LISTEN { channel } => { @@ -11609,7 +11609,7 @@ fn parse_listen_channel() { ParserError::ParserError("Expected: identifier, found: *".to_string()) ); - let dialects = all_dialects_where(|d| !d.supports_listen()); + let dialects = all_dialects_where(|d| !d.supports_listen_notify()); assert_eq!( dialects.parse_sql_statements("LISTEN test1").unwrap_err(), @@ -11617,9 +11617,40 @@ fn parse_listen_channel() { ); } +#[test] +fn parse_unlisten_channel() { + let dialects = all_dialects_where(|d| d.supports_listen_notify()); + + match dialects.verified_stmt("UNLISTEN test1") { + Statement::UNLISTEN { channel } => { + assert_eq!(Ident::new("test1"), channel); + } + _ => unreachable!(), + }; + + match dialects.verified_stmt("UNLISTEN *") { + Statement::UNLISTEN { channel } => { + assert_eq!(Ident::new("*"), channel); + } + _ => unreachable!(), + }; + + assert_eq!( + dialects.parse_sql_statements("UNLISTEN +").unwrap_err(), + ParserError::ParserError("Expected: wildcard or identifier, found: +".to_string()) + ); + + let dialects = all_dialects_where(|d| !d.supports_listen_notify()); + + assert_eq!( + dialects.parse_sql_statements("UNLISTEN test1").unwrap_err(), + ParserError::ParserError("Expected: an SQL statement, found: UNLISTEN".to_string()) + ); +} + #[test] fn parse_notify_channel() { - let dialects = all_dialects_where(|d| d.supports_notify()); + let dialects = all_dialects_where(|d| d.supports_listen_notify()); match dialects.verified_stmt("NOTIFY test1") { Statement::NOTIFY { channel, payload } => { @@ -11655,7 +11686,7 @@ fn parse_notify_channel() { "NOTIFY test1", "NOTIFY test1, 'this is a test notification'", ]; - let dialects = all_dialects_where(|d| !d.supports_notify()); + let dialects = all_dialects_where(|d| !d.supports_listen_notify()); for &sql in &sql_statements { assert_eq!( @@ -11864,6 +11895,44 @@ fn parse_load_data() { ); } +#[test] +fn test_load_extension() { + let dialects = all_dialects_where(|d| d.supports_load_extension()); + let not_supports_load_extension_dialects = all_dialects_where(|d| !d.supports_load_extension()); + let sql = "LOAD my_extension"; + + match dialects.verified_stmt(sql) { + Statement::Load { extension_name } => { + assert_eq!(Ident::new("my_extension"), extension_name); + } + _ => unreachable!(), + }; + + assert_eq!( + not_supports_load_extension_dialects + .parse_sql_statements(sql) + .unwrap_err(), + ParserError::ParserError( + "Expected: `DATA` or an extension name after `LOAD`, found: my_extension".to_string() + ) + ); + + let sql = "LOAD 'filename'"; + + match dialects.verified_stmt(sql) { + Statement::Load { extension_name } => { + assert_eq!( + Ident { + value: "filename".to_string(), + quote_style: Some('\'') + }, + extension_name + ); + } + _ => unreachable!(), + }; +} + #[test] fn test_select_top() { let dialects = all_dialects_where(|d| d.supports_top_before_distinct()); diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index d68f37713..a2db5c282 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -359,20 +359,6 @@ fn test_duckdb_install() { ); } -#[test] -fn test_duckdb_load_extension() { - let stmt = duckdb().verified_stmt("LOAD my_extension"); - assert_eq!( - Statement::Load { - extension_name: Ident { - value: "my_extension".to_string(), - quote_style: None - } - }, - stmt - ); -} - #[test] fn test_duckdb_struct_literal() { //struct literal syntax https://duckdb.org/docs/sql/data_types/struct#creating-structs From fad2ddd6417e2e4d149c298d1977088124a30358 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Tue, 19 Nov 2024 21:55:38 -0800 Subject: [PATCH 605/806] Parse byte/bit string literals in MySQL and Postgres (#1532) --- src/tokenizer.rs | 5 +++-- tests/sqlparser_mysql.rs | 11 +++++++++++ tests/sqlparser_postgres.rs | 11 +++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 4186ec824..05aaf1e28 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -704,8 +704,9 @@ impl<'a> Tokenizer<'a> { } Ok(Some(Token::Whitespace(Whitespace::Newline))) } - // BigQuery uses b or B for byte string literal - b @ 'B' | b @ 'b' if dialect_of!(self is BigQueryDialect | GenericDialect) => { + // BigQuery and MySQL use b or B for byte string literal, Postgres for bit strings + b @ 'B' | b @ 'b' if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | MySqlDialect | GenericDialect) => + { chars.next(); // consume match chars.peek() { Some('\'') => { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 2a876cff2..ce3296737 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2960,3 +2960,14 @@ fn parse_logical_xor() { select.projection[3] ); } + +#[test] +fn parse_bitstring_literal() { + let select = mysql_and_generic().verified_only_select("SELECT B'111'"); + assert_eq!( + select.projection, + vec![SelectItem::UnnamedExpr(Expr::Value( + Value::SingleQuotedByteStringLiteral("111".to_string()) + ))] + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a6c480cd7..2e2c4403c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5098,3 +5098,14 @@ fn parse_create_type_as_enum() { _ => unreachable!(), } } + +#[test] +fn parse_bitstring_literal() { + let select = pg_and_generic().verified_only_select("SELECT B'111'"); + assert_eq!( + select.projection, + vec![SelectItem::UnnamedExpr(Expr::Value( + Value::SingleQuotedByteStringLiteral("111".to_string()) + ))] + ); +} From a1150223af6083ca25c083d0af2ec2fd08507599 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 22 Nov 2024 11:06:42 -0800 Subject: [PATCH 606/806] Allow example CLI to read from stdin (#1536) --- examples/cli.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/examples/cli.rs b/examples/cli.rs index 8a5d6501e..0252fca74 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -17,9 +17,11 @@ #![warn(clippy::all)] -/// A small command-line app to run the parser. -/// Run with `cargo run --example cli` +//! A small command-line app to run the parser. +//! Run with `cargo run --example cli` + use std::fs; +use std::io::{stdin, Read}; use simple_logger::SimpleLogger; use sqlparser::dialect::*; @@ -38,6 +40,9 @@ $ cargo run --example cli FILENAME.sql [--dialectname] To print the parse results as JSON: $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] +To read from stdin instead of a file: +$ cargo run --example cli - [--dialectname] + "#, ); @@ -57,9 +62,18 @@ $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] s => panic!("Unexpected parameter: {s}"), }; - println!("Parsing from file '{}' using {:?}", &filename, dialect); - let contents = fs::read_to_string(&filename) - .unwrap_or_else(|_| panic!("Unable to read the file {}", &filename)); + let contents = if filename == "-" { + println!("Parsing from stdin using {:?}", dialect); + let mut buf = Vec::new(); + stdin() + .read_to_end(&mut buf) + .expect("failed to read from stdin"); + String::from_utf8(buf).expect("stdin content wasn't valid utf8") + } else { + println!("Parsing from file '{}' using {:?}", &filename, dialect); + fs::read_to_string(&filename) + .unwrap_or_else(|_| panic!("Unable to read the file {}", &filename)) + }; let without_bom = if contents.chars().next().unwrap() as u64 != 0xfeff { contents.as_str() } else { From 10519003ed06defa48b1c9ecc734b3d5c92c297d Mon Sep 17 00:00:00 2001 From: tomershaniii <65544633+tomershaniii@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:33:14 +0200 Subject: [PATCH 607/806] recursive select calls are parsed with bad trailing_commas parameter (#1521) --- src/parser/mod.rs | 39 +++++++++++++++++++++++++----------- tests/sqlparser_snowflake.rs | 17 ++++++++++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 35c763e93..c8358767b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3532,16 +3532,11 @@ impl<'a> Parser<'a> { // e.g. `SELECT 1, 2, FROM t` // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#trailing_commas // https://docs.snowflake.com/en/release-notes/2024/8_11#select-supports-trailing-commas - // - // This pattern could be captured better with RAII type semantics, but it's quite a bit of - // code to add for just one case, so we'll just do it manually here. - let old_value = self.options.trailing_commas; - self.options.trailing_commas |= self.dialect.supports_projection_trailing_commas(); - let ret = self.parse_comma_separated(|p| p.parse_select_item()); - self.options.trailing_commas = old_value; + let trailing_commas = + self.options.trailing_commas | self.dialect.supports_projection_trailing_commas(); - ret + self.parse_comma_separated_with_trailing_commas(|p| p.parse_select_item(), trailing_commas) } pub fn parse_actions_list(&mut self) -> Result, ParserError> { @@ -3568,11 +3563,12 @@ impl<'a> Parser<'a> { } /// Parse the comma of a comma-separated syntax element. + /// Allows for control over trailing commas /// Returns true if there is a next element - fn is_parse_comma_separated_end(&mut self) -> bool { + fn is_parse_comma_separated_end_with_trailing_commas(&mut self, trailing_commas: bool) -> bool { if !self.consume_token(&Token::Comma) { true - } else if self.options.trailing_commas { + } else if trailing_commas { let token = self.peek_token().token; match token { Token::Word(ref kw) @@ -3590,15 +3586,34 @@ impl<'a> Parser<'a> { } } + /// Parse the comma of a comma-separated syntax element. + /// Returns true if there is a next element + fn is_parse_comma_separated_end(&mut self) -> bool { + self.is_parse_comma_separated_end_with_trailing_commas(self.options.trailing_commas) + } + /// Parse a comma-separated list of 1+ items accepted by `F` - pub fn parse_comma_separated(&mut self, mut f: F) -> Result, ParserError> + pub fn parse_comma_separated(&mut self, f: F) -> Result, ParserError> + where + F: FnMut(&mut Parser<'a>) -> Result, + { + self.parse_comma_separated_with_trailing_commas(f, self.options.trailing_commas) + } + + /// Parse a comma-separated list of 1+ items accepted by `F` + /// Allows for control over trailing commas + fn parse_comma_separated_with_trailing_commas( + &mut self, + mut f: F, + trailing_commas: bool, + ) -> Result, ParserError> where F: FnMut(&mut Parser<'a>) -> Result, { let mut values = vec![]; loop { values.push(f(self)?); - if self.is_parse_comma_separated_end() { + if self.is_parse_comma_separated_end_with_trailing_commas(trailing_commas) { break; } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 1f1c00e7a..1d053bb0b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2846,3 +2846,20 @@ fn test_parse_show_columns_sql() { snowflake().verified_stmt("SHOW COLUMNS IN TABLE abc"); snowflake().verified_stmt("SHOW COLUMNS LIKE '%xyz%' IN TABLE abc"); } + +#[test] +fn test_projection_with_nested_trailing_commas() { + let sql = "SELECT a, FROM b, LATERAL FLATTEN(input => events)"; + let _ = snowflake().parse_sql_statements(sql).unwrap(); + + //Single nesting + let sql = "SELECT (SELECT a, FROM b, LATERAL FLATTEN(input => events))"; + let _ = snowflake().parse_sql_statements(sql).unwrap(); + + //Double nesting + let sql = "SELECT (SELECT (SELECT a, FROM b, LATERAL FLATTEN(input => events)))"; + let _ = snowflake().parse_sql_statements(sql).unwrap(); + + let sql = "SELECT a, b, FROM c, (SELECT d, e, FROM f, LATERAL FLATTEN(input => events))"; + let _ = snowflake().parse_sql_statements(sql).unwrap(); +} From 62fa8604af11eaae81e3a6276bbb5cc7bc2026e5 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:14:38 +0100 Subject: [PATCH 608/806] PartiQL queries in Redshift (#1534) --- src/ast/query.rs | 6 ++ src/dialect/mod.rs | 6 ++ src/dialect/redshift.rs | 5 ++ src/parser/mod.rs | 21 +++-- src/test_utils.rs | 2 + tests/sqlparser_bigquery.rs | 5 ++ tests/sqlparser_clickhouse.rs | 2 + tests/sqlparser_common.rs | 37 +++++++++ tests/sqlparser_databricks.rs | 1 + tests/sqlparser_duckdb.rs | 2 + tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 18 ++-- tests/sqlparser_mysql.rs | 5 ++ tests/sqlparser_postgres.rs | 1 + tests/sqlparser_redshift.rs | 150 ++++++++++++++++++++++++++++++++++ tests/sqlparser_snowflake.rs | 1 + tests/sqlparser_sqlite.rs | 1 + 17 files changed, 254 insertions(+), 10 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 078bbc841..bf36c626f 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -974,6 +974,8 @@ pub enum TableFactor { with_ordinality: bool, /// [Partition selection](https://dev.mysql.com/doc/refman/8.0/en/partitioning-selection.html), supported by MySQL. partitions: Vec, + /// Optional PartiQL JsonPath: + json_path: Option, }, Derived { lateral: bool, @@ -1375,8 +1377,12 @@ impl fmt::Display for TableFactor { version, partitions, with_ordinality, + json_path, } => { write!(f, "{name}")?; + if let Some(json_path) = json_path { + write!(f, "{json_path}")?; + } if !partitions.is_empty() { write!(f, "PARTITION ({})", display_comma_separated(partitions))?; } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 985cad749..159e14717 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -675,6 +675,12 @@ pub trait Dialect: Debug + Any { fn supports_create_table_select(&self) -> bool { false } + + /// Returns true if the dialect supports PartiQL for querying semi-structured data + /// + fn supports_partiql(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 4d0773843..48eb00ab1 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -74,4 +74,9 @@ impl Dialect for RedshiftSqlDialect { fn supports_top_before_distinct(&self) -> bool { true } + + /// Redshift supports PartiQL: + fn supports_partiql(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c8358767b..1bf173169 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2936,7 +2936,7 @@ impl<'a> Parser<'a> { } else if Token::LBracket == tok { if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) { self.parse_subscript(expr) - } else if dialect_of!(self is SnowflakeDialect) { + } else if dialect_of!(self is SnowflakeDialect) || self.dialect.supports_partiql() { self.prev_token(); self.parse_json_access(expr) } else { @@ -3072,6 +3072,14 @@ impl<'a> Parser<'a> { } fn parse_json_access(&mut self, expr: Expr) -> Result { + let path = self.parse_json_path()?; + Ok(Expr::JsonAccess { + value: Box::new(expr), + path, + }) + } + + fn parse_json_path(&mut self) -> Result { let mut path = Vec::new(); loop { match self.next_token().token { @@ -3095,10 +3103,7 @@ impl<'a> Parser<'a> { } debug_assert!(!path.is_empty()); - Ok(Expr::JsonAccess { - value: Box::new(expr), - path: JsonPath { path }, - }) + Ok(JsonPath { path }) } pub fn parse_map_access(&mut self, expr: Expr) -> Result { @@ -10338,6 +10343,11 @@ impl<'a> Parser<'a> { } else { let name = self.parse_object_name(true)?; + let json_path = match self.peek_token().token { + Token::LBracket if self.dialect.supports_partiql() => Some(self.parse_json_path()?), + _ => None, + }; + let partitions: Vec = if dialect_of!(self is MySqlDialect | GenericDialect) && self.parse_keyword(Keyword::PARTITION) { @@ -10380,6 +10390,7 @@ impl<'a> Parser<'a> { version, partitions, with_ordinality, + json_path, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { diff --git a/src/test_utils.rs b/src/test_utils.rs index b35fc45c2..aaee20c5f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -345,6 +345,7 @@ pub fn table(name: impl Into) -> TableFactor { version: None, partitions: vec![], with_ordinality: false, + json_path: None, } } @@ -360,6 +361,7 @@ pub fn table_with_alias(name: impl Into, alias: impl Into) -> Ta version: None, partitions: vec![], with_ordinality: false, + json_path: None, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 2bf470f71..d4c178bbf 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -229,6 +229,7 @@ fn parse_delete_statement() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, from[0].relation ); @@ -1373,6 +1374,7 @@ fn parse_table_identifiers() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![] },] @@ -1546,6 +1548,7 @@ fn parse_table_time_travel() { ))), partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![] },] @@ -1644,6 +1647,7 @@ fn parse_merge() { version: Default::default(), partitions: Default::default(), with_ordinality: false, + json_path: None, }, table ); @@ -1659,6 +1663,7 @@ fn parse_merge() { version: Default::default(), partitions: Default::default(), with_ordinality: false, + json_path: None, }, source ); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index a71871115..90af12ab7 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -67,6 +67,7 @@ fn parse_map_access_expr() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -172,6 +173,7 @@ fn parse_delimited_identifiers() { version, with_ordinality: _, partitions: _, + json_path: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3d9ba5da2..b41063859 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -364,6 +364,7 @@ fn parse_update_set_from() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }, @@ -394,6 +395,7 @@ fn parse_update_set_from() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -473,6 +475,7 @@ fn parse_update_with_table_alias() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }, @@ -564,6 +567,7 @@ fn parse_select_with_table_alias() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }] @@ -601,6 +605,7 @@ fn parse_delete_statement() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, from[0].relation ); @@ -648,6 +653,7 @@ fn parse_delete_statement_for_multi_tables() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, from[0].relation ); @@ -660,6 +666,7 @@ fn parse_delete_statement_for_multi_tables() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, from[0].joins[0].relation ); @@ -686,6 +693,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, from[0].relation ); @@ -698,6 +706,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, from[1].relation ); @@ -710,6 +719,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, using[0].relation ); @@ -722,6 +732,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, using[0].joins[0].relation ); @@ -753,6 +764,7 @@ fn parse_where_delete_statement() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, from[0].relation, ); @@ -798,6 +810,7 @@ fn parse_where_delete_with_alias_statement() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, from[0].relation, ); @@ -814,6 +827,7 @@ fn parse_where_delete_with_alias_statement() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }]), @@ -4718,6 +4732,7 @@ fn test_parse_named_window() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -5301,6 +5316,7 @@ fn parse_interval_and_or_xor() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -5912,6 +5928,7 @@ fn parse_implicit_join() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }, @@ -5924,6 +5941,7 @@ fn parse_implicit_join() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }, @@ -5944,6 +5962,7 @@ fn parse_implicit_join() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -5954,6 +5973,7 @@ fn parse_implicit_join() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -5968,6 +5988,7 @@ fn parse_implicit_join() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -5978,6 +5999,7 @@ fn parse_implicit_join() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -6002,6 +6024,7 @@ fn parse_cross_join() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, global: false, join_operator: JoinOperator::CrossJoin, @@ -6027,6 +6050,7 @@ fn parse_joins_on() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, global, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -6154,6 +6178,7 @@ fn parse_joins_using() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, global: false, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), @@ -6227,6 +6252,7 @@ fn parse_natural_join() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, global: false, join_operator: f(JoinConstraint::Natural), @@ -6496,6 +6522,7 @@ fn parse_derived_tables() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), @@ -7443,6 +7470,7 @@ fn lateral_function() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![Join { relation: TableFactor::Function { @@ -8258,6 +8286,7 @@ fn parse_merge() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, } ); assert_eq!(table, table_no_into); @@ -8285,6 +8314,7 @@ fn parse_merge() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -9359,6 +9389,7 @@ fn parse_pivot_table() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }), aggregate_functions: vec![ expected_function("a", None), @@ -9432,6 +9463,7 @@ fn parse_unpivot_table() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }), value: Ident { value: "quantity".to_string(), @@ -9499,6 +9531,7 @@ fn parse_pivot_unpivot_table() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }), value: Ident { value: "population".to_string(), @@ -9910,6 +9943,7 @@ fn parse_unload() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -10089,6 +10123,7 @@ fn parse_connect_by() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -10176,6 +10211,7 @@ fn parse_connect_by() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -10337,6 +10373,7 @@ fn test_match_recognize() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }; fn check(options: &str, expect: TableFactor) { diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 7b917bd06..1651d517a 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -193,6 +193,7 @@ fn test_values_clause() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }), query .body diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a2db5c282..73b0f6601 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -282,6 +282,7 @@ fn test_select_union_by_name() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], @@ -323,6 +324,7 @@ fn test_select_union_by_name() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 10bd374c0..8d4f7a680 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -457,6 +457,7 @@ fn parse_delimited_identifiers() { version, with_ordinality: _, partitions: _, + json_path: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 73fd99cf3..74f3c077e 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -70,6 +70,7 @@ fn parse_table_time_travel() { ))), partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![] },] @@ -218,7 +219,8 @@ fn parse_mssql_openjson() { with_hints: vec![], version: None, with_ordinality: false, - partitions: vec![] + partitions: vec![], + json_path: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -293,7 +295,8 @@ fn parse_mssql_openjson() { with_hints: vec![], version: None, with_ordinality: false, - partitions: vec![] + partitions: vec![], + json_path: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -368,7 +371,8 @@ fn parse_mssql_openjson() { with_hints: vec![], version: None, with_ordinality: false, - partitions: vec![] + partitions: vec![], + json_path: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -443,7 +447,8 @@ fn parse_mssql_openjson() { with_hints: vec![], version: None, with_ordinality: false, - partitions: vec![] + partitions: vec![], + json_path: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -496,7 +501,8 @@ fn parse_mssql_openjson() { with_hints: vec![], version: None, with_ordinality: false, - partitions: vec![] + partitions: vec![], + json_path: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -679,6 +685,7 @@ fn parse_delimited_identifiers() { version, with_ordinality: _, partitions: _, + json_path: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); @@ -1314,6 +1321,7 @@ fn parse_substring_in_select() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![] }], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ce3296737..3d8b08630 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1862,6 +1862,7 @@ fn parse_select_with_numeric_prefix_column_name() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![] }], @@ -1918,6 +1919,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![] }], @@ -1985,6 +1987,7 @@ fn parse_update_with_joins() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -1998,6 +2001,7 @@ fn parse_update_with_joins() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, global: false, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { @@ -2428,6 +2432,7 @@ fn parse_substring_in_select() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![] }], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2e2c4403c..098a3464c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3511,6 +3511,7 @@ fn parse_delimited_identifiers() { version, with_ordinality: _, partitions: _, + json_path: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index a25d50605..0a084b340 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -54,6 +54,7 @@ fn test_square_brackets_over_db_schema_table_name() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], } @@ -101,6 +102,7 @@ fn test_double_quotes_over_db_schema_table_name() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], } @@ -123,6 +125,7 @@ fn parse_delimited_identifiers() { version, with_ordinality: _, partitions: _, + json_path: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); @@ -196,3 +199,150 @@ fn test_create_view_with_no_schema_binding() { redshift_and_generic() .verified_stmt("CREATE VIEW myevent AS SELECT eventname FROM event WITH NO SCHEMA BINDING"); } + +#[test] +fn test_redshift_json_path() { + let dialects = all_dialects_where(|d| d.supports_partiql()); + let sql = "SELECT cust.c_orders[0].o_orderkey FROM customer_orders_lineitem"; + let select = dialects.verified_only_select(sql); + + assert_eq!( + &Expr::JsonAccess { + value: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("cust"), + Ident::new("c_orders") + ])), + path: JsonPath { + path: vec![ + JsonPathElem::Bracket { + key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + }, + JsonPathElem::Dot { + key: "o_orderkey".to_string(), + quoted: false + } + ] + } + }, + expr_from_projection(only(&select.projection)) + ); + + let sql = "SELECT cust.c_orders[0]['id'] FROM customer_orders_lineitem"; + let select = dialects.verified_only_select(sql); + assert_eq!( + &Expr::JsonAccess { + value: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("cust"), + Ident::new("c_orders") + ])), + path: JsonPath { + path: vec![ + JsonPathElem::Bracket { + key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + }, + JsonPathElem::Bracket { + key: Expr::Value(Value::SingleQuotedString("id".to_owned())) + } + ] + } + }, + expr_from_projection(only(&select.projection)) + ); + + let sql = "SELECT db1.sc1.tbl1.col1[0]['id'] FROM customer_orders_lineitem"; + let select = dialects.verified_only_select(sql); + assert_eq!( + &Expr::JsonAccess { + value: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("db1"), + Ident::new("sc1"), + Ident::new("tbl1"), + Ident::new("col1") + ])), + path: JsonPath { + path: vec![ + JsonPathElem::Bracket { + key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + }, + JsonPathElem::Bracket { + key: Expr::Value(Value::SingleQuotedString("id".to_owned())) + } + ] + } + }, + expr_from_projection(only(&select.projection)) + ); +} + +#[test] +fn test_parse_json_path_from() { + let dialects = all_dialects_where(|d| d.supports_partiql()); + let select = dialects.verified_only_select("SELECT * FROM src[0].a AS a"); + match &select.from[0].relation { + TableFactor::Table { + name, json_path, .. + } => { + assert_eq!(name, &ObjectName(vec![Ident::new("src")])); + assert_eq!( + json_path, + &Some(JsonPath { + path: vec![ + JsonPathElem::Bracket { + key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + }, + JsonPathElem::Dot { + key: "a".to_string(), + quoted: false + } + ] + }) + ); + } + _ => panic!(), + } + + let select = dialects.verified_only_select("SELECT * FROM src[0].a[1].b AS a"); + match &select.from[0].relation { + TableFactor::Table { + name, json_path, .. + } => { + assert_eq!(name, &ObjectName(vec![Ident::new("src")])); + assert_eq!( + json_path, + &Some(JsonPath { + path: vec![ + JsonPathElem::Bracket { + key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + }, + JsonPathElem::Dot { + key: "a".to_string(), + quoted: false + }, + JsonPathElem::Bracket { + key: Expr::Value(Value::Number("1".parse().unwrap(), false)) + }, + JsonPathElem::Dot { + key: "b".to_string(), + quoted: false + }, + ] + }) + ); + } + _ => panic!(), + } + + let select = dialects.verified_only_select("SELECT * FROM src.a.b"); + match &select.from[0].relation { + TableFactor::Table { + name, json_path, .. + } => { + assert_eq!( + name, + &ObjectName(vec![Ident::new("src"), Ident::new("a"), Ident::new("b")]) + ); + assert_eq!(json_path, &None); + } + _ => panic!(), + } +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 1d053bb0b..f99a00f5b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1190,6 +1190,7 @@ fn parse_delimited_identifiers() { version, with_ordinality: _, partitions: _, + json_path: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 6f8e654dc..c3cfb7a63 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -486,6 +486,7 @@ fn parse_update_tuple_row_values() { version: None, partitions: vec![], with_ordinality: false, + json_path: None, }, joins: vec![], }, From 0fb2ef331ec4acb6e77d73d2aabaee07d8e1944e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 24 Nov 2024 04:06:31 -0800 Subject: [PATCH 609/806] Include license file in sqlparser_derive crate (#1543) --- derive/Cargo.toml | 1 + derive/LICENSE.TXT | 1 + 2 files changed, 2 insertions(+) create mode 120000 derive/LICENSE.TXT diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 0c5852c4c..3b115b950 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -28,6 +28,7 @@ license = "Apache-2.0" include = [ "src/**/*.rs", "Cargo.toml", + "LICENSE.TXT", ] edition = "2021" diff --git a/derive/LICENSE.TXT b/derive/LICENSE.TXT new file mode 120000 index 000000000..14259afe2 --- /dev/null +++ b/derive/LICENSE.TXT @@ -0,0 +1 @@ +../LICENSE.TXT \ No newline at end of file From fd21fae297c7446c3acaf676be1a24556d5bac9a Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:01:02 +0100 Subject: [PATCH 610/806] Fallback to identifier parsing if expression parsing fails (#1513) --- src/dialect/mod.rs | 6 + src/dialect/snowflake.rs | 12 ++ src/keywords.rs | 10 + src/parser/mod.rs | 399 ++++++++++++++++++++---------------- tests/sqlparser_common.rs | 21 +- tests/sqlparser_postgres.rs | 20 +- 6 files changed, 277 insertions(+), 191 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 159e14717..b622c1da3 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -681,6 +681,12 @@ pub trait Dialect: Debug + Any { fn supports_partiql(&self) -> bool { false } + + /// Returns true if the specified keyword is reserved and cannot be + /// used as an identifier without special handling like quoting. + fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { + keywords::RESERVED_FOR_IDENTIFIER.contains(&kw) + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index b584ed9b4..56919fb31 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -38,6 +38,8 @@ use alloc::vec::Vec; #[cfg(not(feature = "std"))] use alloc::{format, vec}; +use super::keywords::RESERVED_FOR_IDENTIFIER; + /// A [`Dialect`] for [Snowflake](https://www.snowflake.com/) #[derive(Debug, Default)] pub struct SnowflakeDialect; @@ -214,6 +216,16 @@ impl Dialect for SnowflakeDialect { fn supports_show_like_before_in(&self) -> bool { true } + + 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 + if matches!(kw, Keyword::INTERVAL) { + false + } else { + RESERVED_FOR_IDENTIFIER.contains(&kw) + } + } } /// Parse snowflake create table statement. diff --git a/src/keywords.rs b/src/keywords.rs index fc2a2927c..8c0ed588f 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -948,3 +948,13 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::INTO, Keyword::END, ]; + +/// Global list of reserved keywords that cannot be parsed as identifiers +/// without special handling like quoting. Parser should call `Dialect::is_reserved_for_identifier` +/// to allow for each dialect to customize the list. +pub const RESERVED_FOR_IDENTIFIER: &[Keyword] = &[ + Keyword::EXISTS, + Keyword::INTERVAL, + Keyword::STRUCT, + Keyword::TRIM, +]; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1bf173169..6767f358a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1025,6 +1025,183 @@ impl<'a> Parser<'a> { Ok(Statement::NOTIFY { channel, payload }) } + // Tries to parse an expression by matching the specified word to known keywords that have a special meaning in the dialect. + // Returns `None if no match is found. + fn parse_expr_prefix_by_reserved_word( + &mut self, + w: &Word, + ) -> Result, ParserError> { + match w.keyword { + Keyword::TRUE | Keyword::FALSE if self.dialect.supports_boolean_literals() => { + self.prev_token(); + Ok(Some(Expr::Value(self.parse_value()?))) + } + Keyword::NULL => { + self.prev_token(); + Ok(Some(Expr::Value(self.parse_value()?))) + } + Keyword::CURRENT_CATALOG + | Keyword::CURRENT_USER + | Keyword::SESSION_USER + | Keyword::USER + if dialect_of!(self is PostgreSqlDialect | GenericDialect) => + { + Ok(Some(Expr::Function(Function { + name: ObjectName(vec![w.to_ident()]), + parameters: FunctionArguments::None, + args: FunctionArguments::None, + null_treatment: None, + filter: None, + over: None, + within_group: vec![], + }))) + } + Keyword::CURRENT_TIMESTAMP + | Keyword::CURRENT_TIME + | Keyword::CURRENT_DATE + | Keyword::LOCALTIME + | Keyword::LOCALTIMESTAMP => { + Ok(Some(self.parse_time_functions(ObjectName(vec![w.to_ident()]))?)) + } + Keyword::CASE => Ok(Some(self.parse_case_expr()?)), + Keyword::CONVERT => Ok(Some(self.parse_convert_expr(false)?)), + Keyword::TRY_CONVERT if self.dialect.supports_try_convert() => Ok(Some(self.parse_convert_expr(true)?)), + Keyword::CAST => Ok(Some(self.parse_cast_expr(CastKind::Cast)?)), + Keyword::TRY_CAST => Ok(Some(self.parse_cast_expr(CastKind::TryCast)?)), + Keyword::SAFE_CAST => Ok(Some(self.parse_cast_expr(CastKind::SafeCast)?)), + Keyword::EXISTS + // Support parsing Databricks has a function named `exists`. + if !dialect_of!(self is DatabricksDialect) + || matches!( + self.peek_nth_token(1).token, + Token::Word(Word { + keyword: Keyword::SELECT | Keyword::WITH, + .. + }) + ) => + { + Ok(Some(self.parse_exists_expr(false)?)) + } + Keyword::EXTRACT => Ok(Some(self.parse_extract_expr()?)), + Keyword::CEIL => Ok(Some(self.parse_ceil_floor_expr(true)?)), + Keyword::FLOOR => Ok(Some(self.parse_ceil_floor_expr(false)?)), + Keyword::POSITION if self.peek_token().token == Token::LParen => { + Ok(Some(self.parse_position_expr(w.to_ident())?)) + } + Keyword::SUBSTRING => Ok(Some(self.parse_substring_expr()?)), + Keyword::OVERLAY => Ok(Some(self.parse_overlay_expr()?)), + Keyword::TRIM => Ok(Some(self.parse_trim_expr()?)), + Keyword::INTERVAL => Ok(Some(self.parse_interval()?)), + // Treat ARRAY[1,2,3] as an array [1,2,3], otherwise try as subquery or a function call + Keyword::ARRAY if self.peek_token() == Token::LBracket => { + self.expect_token(&Token::LBracket)?; + Ok(Some(self.parse_array_expr(true)?)) + } + Keyword::ARRAY + if self.peek_token() == Token::LParen + && !dialect_of!(self is ClickHouseDialect | DatabricksDialect) => + { + self.expect_token(&Token::LParen)?; + let query = self.parse_query()?; + self.expect_token(&Token::RParen)?; + Ok(Some(Expr::Function(Function { + name: ObjectName(vec![w.to_ident()]), + parameters: FunctionArguments::None, + args: FunctionArguments::Subquery(query), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }))) + } + Keyword::NOT => Ok(Some(self.parse_not()?)), + Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => { + Ok(Some(self.parse_match_against()?)) + } + Keyword::STRUCT if dialect_of!(self is BigQueryDialect | GenericDialect) => { + self.prev_token(); + Ok(Some(self.parse_bigquery_struct_literal()?)) + } + Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => { + let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?; + Ok(Some(Expr::Prior(Box::new(expr)))) + } + Keyword::MAP if self.peek_token() == Token::LBrace && self.dialect.support_map_literal_syntax() => { + Ok(Some(self.parse_duckdb_map_literal()?)) + } + _ => Ok(None) + } + } + + // Tries to parse an expression by a word that is not known to have a special meaning in the dialect. + fn parse_expr_prefix_by_unreserved_word(&mut self, w: &Word) -> Result { + match self.peek_token().token { + Token::LParen | Token::Period => { + let mut id_parts: Vec = vec![w.to_ident()]; + let mut ends_with_wildcard = false; + while self.consume_token(&Token::Period) { + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) => id_parts.push(w.to_ident()), + Token::Mul => { + // Postgres explicitly allows funcnm(tablenm.*) and the + // function array_agg traverses this control flow + if dialect_of!(self is PostgreSqlDialect) { + ends_with_wildcard = true; + break; + } else { + return self.expected("an identifier after '.'", next_token); + } + } + Token::SingleQuotedString(s) => id_parts.push(Ident::with_quote('\'', s)), + _ => { + return self.expected("an identifier or a '*' after '.'", next_token); + } + } + } + + if ends_with_wildcard { + Ok(Expr::QualifiedWildcard(ObjectName(id_parts))) + } else if self.consume_token(&Token::LParen) { + if dialect_of!(self is SnowflakeDialect | MsSqlDialect) + && self.consume_tokens(&[Token::Plus, Token::RParen]) + { + Ok(Expr::OuterJoin(Box::new( + match <[Ident; 1]>::try_from(id_parts) { + Ok([ident]) => Expr::Identifier(ident), + Err(parts) => Expr::CompoundIdentifier(parts), + }, + ))) + } else { + self.prev_token(); + self.parse_function(ObjectName(id_parts)) + } + } else { + Ok(Expr::CompoundIdentifier(id_parts)) + } + } + // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html + Token::SingleQuotedString(_) + | Token::DoubleQuotedString(_) + | Token::HexStringLiteral(_) + if w.value.starts_with('_') => + { + Ok(Expr::IntroducedString { + introducer: w.value.clone(), + value: self.parse_introduced_string_value()?, + }) + } + Token::Arrow if self.dialect.supports_lambda_functions() => { + self.expect_token(&Token::Arrow)?; + Ok(Expr::Lambda(LambdaFunction { + params: OneOrManyWithParens::One(w.to_ident()), + body: Box::new(self.parse_expr()?), + })) + } + _ => Ok(Expr::Identifier(w.to_ident())), + } + } + /// Parse an expression prefix. pub fn parse_prefix(&mut self) -> Result { // allow the dialect to override prefix parsing @@ -1073,176 +1250,40 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); let expr = match next_token.token { - Token::Word(w) => match w.keyword { - Keyword::TRUE | Keyword::FALSE if self.dialect.supports_boolean_literals() => { - self.prev_token(); - Ok(Expr::Value(self.parse_value()?)) - } - Keyword::NULL => { - self.prev_token(); - Ok(Expr::Value(self.parse_value()?)) - } - Keyword::CURRENT_CATALOG - | Keyword::CURRENT_USER - | Keyword::SESSION_USER - | Keyword::USER - if dialect_of!(self is PostgreSqlDialect | GenericDialect) => - { - Ok(Expr::Function(Function { - name: ObjectName(vec![w.to_ident()]), - parameters: FunctionArguments::None, - args: FunctionArguments::None, - null_treatment: None, - filter: None, - over: None, - within_group: vec![], - })) - } - Keyword::CURRENT_TIMESTAMP - | Keyword::CURRENT_TIME - | Keyword::CURRENT_DATE - | Keyword::LOCALTIME - | Keyword::LOCALTIMESTAMP => { - self.parse_time_functions(ObjectName(vec![w.to_ident()])) - } - Keyword::CASE => self.parse_case_expr(), - Keyword::CONVERT => self.parse_convert_expr(false), - Keyword::TRY_CONVERT if self.dialect.supports_try_convert() => self.parse_convert_expr(true), - Keyword::CAST => self.parse_cast_expr(CastKind::Cast), - Keyword::TRY_CAST => self.parse_cast_expr(CastKind::TryCast), - Keyword::SAFE_CAST => self.parse_cast_expr(CastKind::SafeCast), - Keyword::EXISTS - // Support parsing Databricks has a function named `exists`. - if !dialect_of!(self is DatabricksDialect) - || matches!( - self.peek_nth_token(1).token, - Token::Word(Word { - keyword: Keyword::SELECT | Keyword::WITH, - .. - }) - ) => - { - self.parse_exists_expr(false) - } - Keyword::EXTRACT => self.parse_extract_expr(), - Keyword::CEIL => self.parse_ceil_floor_expr(true), - Keyword::FLOOR => self.parse_ceil_floor_expr(false), - Keyword::POSITION if self.peek_token().token == Token::LParen => { - self.parse_position_expr(w.to_ident()) - } - Keyword::SUBSTRING => self.parse_substring_expr(), - Keyword::OVERLAY => self.parse_overlay_expr(), - Keyword::TRIM => self.parse_trim_expr(), - Keyword::INTERVAL => self.parse_interval(), - // Treat ARRAY[1,2,3] as an array [1,2,3], otherwise try as subquery or a function call - Keyword::ARRAY if self.peek_token() == Token::LBracket => { - self.expect_token(&Token::LBracket)?; - self.parse_array_expr(true) - } - Keyword::ARRAY - if self.peek_token() == Token::LParen - && !dialect_of!(self is ClickHouseDialect | DatabricksDialect) => - { - self.expect_token(&Token::LParen)?; - let query = self.parse_query()?; - self.expect_token(&Token::RParen)?; - Ok(Expr::Function(Function { - name: ObjectName(vec![w.to_ident()]), - parameters: FunctionArguments::None, - args: FunctionArguments::Subquery(query), - filter: None, - null_treatment: None, - over: None, - within_group: vec![], - })) - } - Keyword::NOT => self.parse_not(), - Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => { - self.parse_match_against() - } - Keyword::STRUCT if dialect_of!(self is BigQueryDialect | GenericDialect) => { - self.prev_token(); - self.parse_bigquery_struct_literal() - } - Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => { - let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?; - Ok(Expr::Prior(Box::new(expr))) - } - Keyword::MAP if self.peek_token() == Token::LBrace && self.dialect.support_map_literal_syntax() => { - self.parse_duckdb_map_literal() - } - // Here `w` is a word, check if it's a part of a multipart - // identifier, a function call, or a simple identifier: - _ => match self.peek_token().token { - Token::LParen | Token::Period => { - let mut id_parts: Vec = vec![w.to_ident()]; - let mut ends_with_wildcard = false; - while self.consume_token(&Token::Period) { - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => id_parts.push(w.to_ident()), - Token::Mul => { - // Postgres explicitly allows funcnm(tablenm.*) and the - // function array_agg traverses this control flow - if dialect_of!(self is PostgreSqlDialect) { - ends_with_wildcard = true; - break; - } else { - return self - .expected("an identifier after '.'", next_token); - } - } - Token::SingleQuotedString(s) => { - id_parts.push(Ident::with_quote('\'', s)) - } - _ => { - return self - .expected("an identifier or a '*' after '.'", next_token); - } - } - } - - if ends_with_wildcard { - Ok(Expr::QualifiedWildcard(ObjectName(id_parts))) - } else if self.consume_token(&Token::LParen) { - if dialect_of!(self is SnowflakeDialect | MsSqlDialect) - && self.consume_tokens(&[Token::Plus, Token::RParen]) - { - Ok(Expr::OuterJoin(Box::new( - match <[Ident; 1]>::try_from(id_parts) { - Ok([ident]) => Expr::Identifier(ident), - Err(parts) => Expr::CompoundIdentifier(parts), - }, - ))) - } else { - self.prev_token(); - self.parse_function(ObjectName(id_parts)) + Token::Word(w) => { + // The word we consumed may fall into one of two cases: it has a special meaning, or not. + // For example, in Snowflake, the word `interval` may have two meanings depending on the context: + // `SELECT CURRENT_DATE() + INTERVAL '1 DAY', MAX(interval) FROM tbl;` + // ^^^^^^^^^^^^^^^^ ^^^^^^^^ + // interval expression identifier + // + // We first try to parse the word and following tokens as a special expression, and if that fails, + // we rollback and try to parse it as an identifier. + match self.try_parse(|parser| parser.parse_expr_prefix_by_reserved_word(&w)) { + // This word indicated an expression prefix and parsing was successful + Ok(Some(expr)) => Ok(expr), + + // No expression prefix associated with this word + Ok(None) => Ok(self.parse_expr_prefix_by_unreserved_word(&w)?), + + // If parsing of the word as a special expression failed, we are facing two options: + // 1. The statement is malformed, e.g. `SELECT INTERVAL '1 DAI` (`DAI` instead of `DAY`) + // 2. The word is used as an identifier, e.g. `SELECT MAX(interval) FROM tbl` + // We first try to parse the word as an identifier and if that fails + // we rollback and return the parsing error we got from trying to parse a + // special expression (to maintain backwards compatibility of parsing errors). + Err(e) => { + if !self.dialect.is_reserved_for_identifier(w.keyword) { + if let Ok(Some(expr)) = self.maybe_parse(|parser| { + parser.parse_expr_prefix_by_unreserved_word(&w) + }) { + return Ok(expr); } - } else { - Ok(Expr::CompoundIdentifier(id_parts)) } + return Err(e); } - // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html - Token::SingleQuotedString(_) - | Token::DoubleQuotedString(_) - | Token::HexStringLiteral(_) - if w.value.starts_with('_') => - { - Ok(Expr::IntroducedString { - introducer: w.value, - value: self.parse_introduced_string_value()?, - }) - } - Token::Arrow if self.dialect.supports_lambda_functions() => { - self.expect_token(&Token::Arrow)?; - return Ok(Expr::Lambda(LambdaFunction { - params: OneOrManyWithParens::One(w.to_ident()), - body: Box::new(self.parse_expr()?), - })); - } - _ => Ok(Expr::Identifier(w.to_ident())), - }, - }, // End of Token::Word + } + } // End of Token::Word // array `[1, 2, 3]` Token::LBracket => self.parse_array_expr(false), tok @ Token::Minus | tok @ Token::Plus => { @@ -3677,18 +3718,30 @@ impl<'a> Parser<'a> { } /// Run a parser method `f`, reverting back to the current position if unsuccessful. - pub fn maybe_parse(&mut self, mut f: F) -> Result, ParserError> + /// Returns `None` if `f` returns an error + pub fn maybe_parse(&mut self, f: F) -> Result, ParserError> where F: FnMut(&mut Parser) -> Result, { - let index = self.index; - match f(self) { + match self.try_parse(f) { Ok(t) => Ok(Some(t)), - // Unwind stack if limit exceeded Err(ParserError::RecursionLimitExceeded) => Err(ParserError::RecursionLimitExceeded), - Err(_) => { + _ => Ok(None), + } + } + + /// Run a parser method `f`, reverting back to the current position if unsuccessful. + pub fn try_parse(&mut self, mut f: F) -> Result + where + F: FnMut(&mut Parser) -> Result, + { + let index = self.index; + match f(self) { + Ok(t) => Ok(t), + Err(e) => { + // Unwind stack if limit exceeded self.index = index; - Ok(None) + Err(e) } } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b41063859..c03370892 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -34,7 +34,7 @@ use sqlparser::dialect::{ GenericDialect, HiveDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, }; -use sqlparser::keywords::ALL_KEYWORDS; +use sqlparser::keywords::{Keyword, ALL_KEYWORDS}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; use sqlparser::tokenizer::Tokenizer; use test_utils::{ @@ -5113,7 +5113,6 @@ fn parse_interval_dont_require_unit() { #[test] fn parse_interval_require_unit() { let dialects = all_dialects_where(|d| d.require_interval_qualifier()); - let sql = "SELECT INTERVAL '1 DAY'"; let err = dialects.parse_sql_statements(sql).unwrap_err(); assert_eq!( @@ -12198,3 +12197,21 @@ fn parse_create_table_select() { ); } } + +#[test] +fn test_reserved_keywords_for_identifiers() { + let dialects = all_dialects_where(|d| d.is_reserved_for_identifier(Keyword::INTERVAL)); + // Dialects that reserve the word INTERVAL will not allow it as an unquoted identifier + let sql = "SELECT MAX(interval) FROM tbl"; + assert_eq!( + dialects.parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: an expression, found: )".to_string() + )) + ); + + // Dialects that do not reserve the word INTERVAL will allow it + let dialects = all_dialects_where(|d| !d.is_reserved_for_identifier(Keyword::INTERVAL)); + let sql = "SELECT MAX(interval) FROM tbl"; + dialects.parse_sql_statements(sql).unwrap(); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 098a3464c..d27569e03 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1352,10 +1352,7 @@ fn parse_set() { local: false, hivevar: false, variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), - value: vec![Expr::Identifier(Ident { - value: "DEFAULT".into(), - quote_style: None - })], + value: vec![Expr::Identifier(Ident::new("DEFAULT"))], } ); @@ -4229,10 +4226,7 @@ fn test_simple_postgres_insert_with_alias() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Identifier(Ident { - value: "DEFAULT".to_string(), - quote_style: None - }), + Expr::Identifier(Ident::new("DEFAULT")), Expr::Value(Value::Number("123".to_string(), false)) ]] })), @@ -4295,10 +4289,7 @@ fn test_simple_postgres_insert_with_alias() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Identifier(Ident { - value: "DEFAULT".to_string(), - quote_style: None - }), + Expr::Identifier(Ident::new("DEFAULT")), Expr::Value(Value::Number( bigdecimal::BigDecimal::new(123.into(), 0), false @@ -4363,10 +4354,7 @@ fn test_simple_insert_with_quoted_alias() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Identifier(Ident { - value: "DEFAULT".to_string(), - quote_style: None - }), + Expr::Identifier(Ident::new("DEFAULT")), Expr::Value(Value::SingleQuotedString("0123".to_string())) ]] })), From 525d1780e8f5c7ba6b7be327eaa788b6c8c47716 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 25 Nov 2024 22:01:23 +0100 Subject: [PATCH 611/806] support `json_object('k':'v')` in postgres (#1546) --- src/dialect/postgresql.rs | 20 +++++ tests/sqlparser_common.rs | 172 +++++++++++++++++++++++++++++++++++++- tests/sqlparser_mssql.rs | 159 ----------------------------------- 3 files changed, 191 insertions(+), 160 deletions(-) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 559586e3f..dcdcc88c1 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -211,6 +211,26 @@ impl Dialect for PostgreSqlDialect { fn supports_load_extension(&self) -> bool { true } + + /// See + /// + /// Required to support the colon in: + /// ```sql + /// SELECT json_object('a': 'b') + /// ``` + fn supports_named_fn_args_with_colon_operator(&self) -> bool { + true + } + + /// See + /// + /// Required to support the label in: + /// ```sql + /// SELECT json_object('label': 'value') + /// ``` + fn supports_named_fn_args_with_expr_name(&self) -> bool { + true + } } pub fn parse_create(parser: &mut Parser) -> Option> { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c03370892..e22877dbe 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1471,6 +1471,173 @@ fn parse_json_ops_without_colon() { } } +#[test] +fn parse_json_object() { + let dialects = TestedDialects::new(vec![ + Box::new(MsSqlDialect {}), + Box::new(PostgreSqlDialect {}), + ]); + let select = dialects.verified_only_select("SELECT JSON_OBJECT('name' : 'value', 'type' : 1)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert_eq!( + &[ + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("name".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "value".into() + ))), + operator: FunctionArgOperator::Colon + }, + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("type".into())), + arg: FunctionArgExpr::Expr(Expr::Value(number("1"))), + operator: FunctionArgOperator::Colon + } + ], + &args[..] + ), + _ => unreachable!(), + } + let select = dialects + .verified_only_select("SELECT JSON_OBJECT('name' : 'value', 'type' : NULL ABSENT ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &[ + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("name".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "value".into() + ))), + operator: FunctionArgOperator::Colon + }, + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("type".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::Null)), + operator: FunctionArgOperator::Colon + } + ], + &args[..] + ); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = dialects.verified_only_select("SELECT JSON_OBJECT(NULL ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert!(args.is_empty()); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::NullOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = dialects.verified_only_select("SELECT JSON_OBJECT(ABSENT ON NULL)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert!(args.is_empty()); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = dialects.verified_only_select( + "SELECT JSON_OBJECT('name' : 'value', 'type' : JSON_ARRAY(1, 2) ABSENT ON NULL)", + ); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("name".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "value".into() + ))), + operator: FunctionArgOperator::Colon + }, + &args[0] + ); + assert!(matches!( + args[1], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::Function(_)), + operator: FunctionArgOperator::Colon + } + )); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::AbsentOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } + let select = dialects.verified_only_select( + "SELECT JSON_OBJECT('name' : 'value', 'type' : JSON_OBJECT('type_id' : 1, 'name' : 'a') NULL ON NULL)", + ); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), + .. + }) => { + assert_eq!( + &FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString("name".into())), + arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( + "value".into() + ))), + operator: FunctionArgOperator::Colon + }, + &args[0] + ); + assert!(matches!( + args[1], + FunctionArg::ExprNamed { + name: Expr::Value(Value::SingleQuotedString(_)), + arg: FunctionArgExpr::Expr(Expr::Function(_)), + operator: FunctionArgOperator::Colon + } + )); + assert_eq!( + &[FunctionArgumentClause::JsonNullClause( + JsonNullClause::NullOnNull + )], + &clauses[..] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_mod_no_spaces() { use self::Expr::*; @@ -4416,7 +4583,10 @@ fn parse_explain_query_plan() { #[test] fn parse_named_argument_function() { - let dialects = all_dialects_where(|d| d.supports_named_fn_args_with_rarrow_operator()); + let dialects = all_dialects_where(|d| { + d.supports_named_fn_args_with_rarrow_operator() + && !d.supports_named_fn_args_with_expr_name() + }); let sql = "SELECT FUN(a => '1', b => '2') FROM foo"; let select = dialects.verified_only_select(sql); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 74f3c077e..d1d8d1248 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -793,165 +793,6 @@ fn parse_for_json_expect_ast() { #[test] fn parse_mssql_json_object() { - let select = ms().verified_only_select("SELECT JSON_OBJECT('name' : 'value', 'type' : 1)"); - match expr_from_projection(&select.projection[0]) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, .. }), - .. - }) => assert_eq!( - &[ - FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), - operator: FunctionArgOperator::Colon - }, - FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("type".into())), - arg: FunctionArgExpr::Expr(Expr::Value(number("1"))), - operator: FunctionArgOperator::Colon - } - ], - &args[..] - ), - _ => unreachable!(), - } - let select = ms() - .verified_only_select("SELECT JSON_OBJECT('name' : 'value', 'type' : NULL ABSENT ON NULL)"); - match expr_from_projection(&select.projection[0]) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), - .. - }) => { - assert_eq!( - &[ - FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), - operator: FunctionArgOperator::Colon - }, - FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("type".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::Null)), - operator: FunctionArgOperator::Colon - } - ], - &args[..] - ); - assert_eq!( - &[FunctionArgumentClause::JsonNullClause( - JsonNullClause::AbsentOnNull - )], - &clauses[..] - ); - } - _ => unreachable!(), - } - let select = ms().verified_only_select("SELECT JSON_OBJECT(NULL ON NULL)"); - match expr_from_projection(&select.projection[0]) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), - .. - }) => { - assert!(args.is_empty()); - assert_eq!( - &[FunctionArgumentClause::JsonNullClause( - JsonNullClause::NullOnNull - )], - &clauses[..] - ); - } - _ => unreachable!(), - } - let select = ms().verified_only_select("SELECT JSON_OBJECT(ABSENT ON NULL)"); - match expr_from_projection(&select.projection[0]) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), - .. - }) => { - assert!(args.is_empty()); - assert_eq!( - &[FunctionArgumentClause::JsonNullClause( - JsonNullClause::AbsentOnNull - )], - &clauses[..] - ); - } - _ => unreachable!(), - } - let select = ms().verified_only_select( - "SELECT JSON_OBJECT('name' : 'value', 'type' : JSON_ARRAY(1, 2) ABSENT ON NULL)", - ); - match expr_from_projection(&select.projection[0]) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), - .. - }) => { - assert_eq!( - &FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), - operator: FunctionArgOperator::Colon - }, - &args[0] - ); - assert!(matches!( - args[1], - FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), - arg: FunctionArgExpr::Expr(Expr::Function(_)), - operator: FunctionArgOperator::Colon - } - )); - assert_eq!( - &[FunctionArgumentClause::JsonNullClause( - JsonNullClause::AbsentOnNull - )], - &clauses[..] - ); - } - _ => unreachable!(), - } - let select = ms().verified_only_select( - "SELECT JSON_OBJECT('name' : 'value', 'type' : JSON_OBJECT('type_id' : 1, 'name' : 'a') NULL ON NULL)", - ); - match expr_from_projection(&select.projection[0]) { - Expr::Function(Function { - args: FunctionArguments::List(FunctionArgumentList { args, clauses, .. }), - .. - }) => { - assert_eq!( - &FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), - operator: FunctionArgOperator::Colon - }, - &args[0] - ); - assert!(matches!( - args[1], - FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), - arg: FunctionArgExpr::Expr(Expr::Function(_)), - operator: FunctionArgOperator::Colon - } - )); - assert_eq!( - &[FunctionArgumentClause::JsonNullClause( - JsonNullClause::NullOnNull - )], - &clauses[..] - ); - } - _ => unreachable!(), - } let select = ms().verified_only_select( "SELECT JSON_OBJECT('user_name' : USER_NAME(), LOWER(@id_key) : @id_value, 'sid' : (SELECT @@SPID) ABSENT ON NULL)", ); From 0adec33b94241f19273c371057bc8ad15e849ef4 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 26 Nov 2024 11:11:56 -0500 Subject: [PATCH 612/806] Document micro benchmarks (#1555) --- README.md | 12 ++++++++++++ sqlparser_bench/Cargo.toml | 1 + sqlparser_bench/README.md | 20 ++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 sqlparser_bench/README.md diff --git a/README.md b/README.md index 934d9d06d..f44300f55 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,18 @@ Our goal as maintainers is to facilitate the integration of various features from various contributors, but not to provide the implementations ourselves, as we simply don't have the resources. +### Benchmarking + +There are several micro benchmarks in the `sqlparser_bench` directory. +You can run them with: + +``` +git checkout main +cd sqlparser_bench +cargo bench +git checkout +cargo bench +``` ## Licensing diff --git a/sqlparser_bench/Cargo.toml b/sqlparser_bench/Cargo.toml index 9c33658a2..2c1f0ae4d 100644 --- a/sqlparser_bench/Cargo.toml +++ b/sqlparser_bench/Cargo.toml @@ -17,6 +17,7 @@ [package] name = "sqlparser_bench" +description = "Benchmarks for sqlparser" version = "0.1.0" authors = ["Dandandan "] edition = "2018" diff --git a/sqlparser_bench/README.md b/sqlparser_bench/README.md new file mode 100644 index 000000000..4cdcfb29c --- /dev/null +++ b/sqlparser_bench/README.md @@ -0,0 +1,20 @@ + + +Benchmarks for sqlparser. See [the main README](../README.md) for more information. \ No newline at end of file From 3c8fd748043188957d2dfcadb4bfcfb0e1f70c82 Mon Sep 17 00:00:00 2001 From: Mark-Oliver Junge Date: Tue, 26 Nov 2024 17:22:30 +0100 Subject: [PATCH 613/806] Implement `Spanned` to retrieve source locations on AST nodes (#1435) Co-authored-by: Ifeanyi Ubah Co-authored-by: Andrew Lamb --- README.md | 17 + docs/source_spans.md | 52 + src/ast/helpers/attached_token.rs | 82 ++ src/ast/helpers/mod.rs | 1 + src/ast/mod.rs | 84 +- src/ast/query.rs | 29 +- src/ast/spans.rs | 2178 +++++++++++++++++++++++++++++ src/parser/mod.rs | 274 ++-- src/tokenizer.rs | 146 +- tests/sqlparser_bigquery.rs | 13 + tests/sqlparser_clickhouse.rs | 10 +- tests/sqlparser_common.rs | 99 +- tests/sqlparser_duckdb.rs | 40 +- tests/sqlparser_mssql.rs | 229 ++- tests/sqlparser_mysql.rs | 64 +- tests/sqlparser_postgres.rs | 150 +- tests/sqlparser_redshift.rs | 19 +- tests/sqlparser_snowflake.rs | 4 +- 18 files changed, 3092 insertions(+), 399 deletions(-) create mode 100644 docs/source_spans.md create mode 100644 src/ast/helpers/attached_token.rs create mode 100644 src/ast/spans.rs diff --git a/README.md b/README.md index f44300f55..9a67abcf8 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,23 @@ similar semantics are represented with the same AST. We welcome PRs to fix such issues and distinguish different syntaxes in the AST. +## WIP: Extracting source locations from AST nodes + +This crate allows recovering source locations from AST nodes via the [Spanned](https://docs.rs/sqlparser/latest/sqlparser/ast/trait.Spanned.html) trait, which can be used for advanced diagnostics tooling. Note that this feature is a work in progress and many nodes report missing or inaccurate spans. Please see [this document](./docs/source_spans.md#source-span-contributing-guidelines) for information on how to contribute missing improvements. + +```rust +use sqlparser::ast::Spanned; + +// Parse SQL +let ast = Parser::parse_sql(&GenericDialect, "SELECT A FROM B").unwrap(); + +// The source span can be retrieved with start and end locations +assert_eq!(ast[0].span(), Span { + start: Location::of(1, 1), + end: Location::of(1, 16), +}); +``` + ## SQL compliance SQL was first standardized in 1987, and revisions of the standard have been diff --git a/docs/source_spans.md b/docs/source_spans.md new file mode 100644 index 000000000..136a4ced2 --- /dev/null +++ b/docs/source_spans.md @@ -0,0 +1,52 @@ + +## Breaking Changes + +These are the current breaking changes introduced by the source spans feature: + +#### Added fields for spans (must be added to any existing pattern matches) +- `Ident` now stores a `Span` +- `Select`, `With`, `Cte`, `WildcardAdditionalOptions` now store a `TokenWithLocation` + +#### Misc. +- `TokenWithLocation` stores a full `Span`, rather than just a source location. Users relying on `token.location` should use `token.location.start` instead. +## Source Span Contributing Guidelines + +For contributing source spans improvement in addition to the general [contribution guidelines](../README.md#contributing), please make sure to pay attention to the following: + + +### Source Span Design Considerations + +- `Ident` always have correct source spans +- Downstream breaking change impact is to be as minimal as possible +- To this end, use recursive merging of spans in favor of storing spans on all nodes +- Any metadata added to compute spans must not change semantics (Eq, Ord, Hash, etc.) + +The primary reason for missing and inaccurate source spans at this time is missing spans of keyword tokens and values in many structures, either due to lack of time or because adding them would break downstream significantly. + +When considering adding support for source spans on a type, consider the impact to consumers of that type and whether your change would require a consumer to do non-trivial changes to their code. + +Example of a trivial change +```rust +match node { + ast::Query { + field1, + field2, + location: _, // add a new line to ignored location +} +``` + +If adding source spans to a type would require a significant change like wrapping that type or similar, please open an issue to discuss. + +### AST Node Equality and Hashes + +When adding tokens to AST nodes, make sure to store them using the [AttachedToken](https://docs.rs/sqlparser/latest/sqlparser/ast/helpers/struct.AttachedToken.html) helper to ensure that semantically equivalent AST nodes always compare as equal and hash to the same value. F.e. `select 5` and `SELECT 5` would compare as different `Select` nodes, if the select token was stored directly. f.e. + +```rust +struct Select { + select_token: AttachedToken, // only used for spans + /// remaining fields + field1, + field2, + ... +} +``` \ No newline at end of file diff --git a/src/ast/helpers/attached_token.rs b/src/ast/helpers/attached_token.rs new file mode 100644 index 000000000..48696c336 --- /dev/null +++ b/src/ast/helpers/attached_token.rs @@ -0,0 +1,82 @@ +// 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 core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; +use core::fmt::{self, Debug, Formatter}; +use core::hash::{Hash, Hasher}; + +use crate::tokenizer::{Token, TokenWithLocation}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +/// A wrapper type for attaching tokens to AST nodes that should be ignored in comparisons and hashing. +/// This should be used when a token is not relevant for semantics, but is still needed for +/// accurate source location tracking. +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AttachedToken(pub TokenWithLocation); + +impl AttachedToken { + pub fn empty() -> Self { + AttachedToken(TokenWithLocation::wrap(Token::EOF)) + } +} + +// Conditional Implementations +impl Debug for AttachedToken { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +// Blanket Implementations +impl PartialEq for AttachedToken { + fn eq(&self, _: &Self) -> bool { + true + } +} + +impl Eq for AttachedToken {} + +impl PartialOrd for AttachedToken { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for AttachedToken { + fn cmp(&self, _: &Self) -> Ordering { + Ordering::Equal + } +} + +impl Hash for AttachedToken { + fn hash(&self, _state: &mut H) { + // Do nothing + } +} + +impl From for AttachedToken { + fn from(value: TokenWithLocation) -> Self { + AttachedToken(value) + } +} diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index d6924ab88..a96bffc51 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -14,5 +14,6 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. +pub mod attached_token; pub mod stmt_create_table; pub mod stmt_data_loading; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9185c9df4..366bf4d25 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -23,9 +23,13 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; +use helpers::attached_token::AttachedToken; -use core::fmt::{self, Display}; use core::ops::Deref; +use core::{ + fmt::{self, Display}, + hash, +}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -33,6 +37,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +use crate::tokenizer::Span; + pub use self::data_type::{ ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, StructBracketKind, TimezoneInfo, @@ -87,6 +93,9 @@ mod dml; pub mod helpers; mod operator; mod query; +mod spans; +pub use spans::Spanned; + mod trigger; mod value; @@ -131,7 +140,7 @@ where } /// An identifier, decomposed into its value or character data and the quote style. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Clone, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Ident { @@ -140,10 +149,41 @@ pub struct Ident { /// The starting quote if any. Valid quote characters are the single quote, /// double quote, backtick, and opening square bracket. pub quote_style: Option, + /// The span of the identifier in the original SQL string. + pub span: Span, +} + +impl PartialEq for Ident { + fn eq(&self, other: &Self) -> bool { + let Ident { + value, + quote_style, + // exhaustiveness check; we ignore spans in comparisons + span: _, + } = self; + + value == &other.value && quote_style == &other.quote_style + } } +impl core::hash::Hash for Ident { + fn hash(&self, state: &mut H) { + let Ident { + value, + quote_style, + // exhaustiveness check; we ignore spans in hashes + span: _, + } = self; + + value.hash(state); + quote_style.hash(state); + } +} + +impl Eq for Ident {} + impl Ident { - /// Create a new identifier with the given value and no quotes. + /// Create a new identifier with the given value and no quotes and an empty span. pub fn new(value: S) -> Self where S: Into, @@ -151,6 +191,7 @@ impl Ident { Ident { value: value.into(), quote_style: None, + span: Span::empty(), } } @@ -164,6 +205,30 @@ impl Ident { Ident { value: value.into(), quote_style: Some(quote), + span: Span::empty(), + } + } + + pub fn with_span(span: Span, value: S) -> Self + where + S: Into, + { + Ident { + value: value.into(), + quote_style: None, + span, + } + } + + pub fn with_quote_and_span(quote: char, span: Span, value: S) -> Self + where + S: Into, + { + assert!(quote == '\'' || quote == '"' || quote == '`' || quote == '['); + Ident { + value: value.into(), + quote_style: Some(quote), + span, } } } @@ -173,6 +238,7 @@ impl From<&str> for Ident { Ident { value: value.to_string(), quote_style: None, + span: Span::empty(), } } } @@ -919,10 +985,10 @@ pub enum Expr { /// `` opt_search_modifier: Option, }, - Wildcard, + Wildcard(AttachedToken), /// Qualified wildcard, e.g. `alias.*` or `schema.table.*`. /// (Same caveats apply to `QualifiedWildcard` as to `Wildcard`.) - QualifiedWildcard(ObjectName), + QualifiedWildcard(ObjectName, AttachedToken), /// Some dialects support an older syntax for outer joins where columns are /// marked with the `(+)` operator in the WHERE clause, for example: /// @@ -1211,8 +1277,8 @@ impl fmt::Display for Expr { Expr::MapAccess { column, keys } => { write!(f, "{column}{}", display_separated(keys, "")) } - Expr::Wildcard => f.write_str("*"), - Expr::QualifiedWildcard(prefix) => write!(f, "{}.*", prefix), + Expr::Wildcard(_) => f.write_str("*"), + Expr::QualifiedWildcard(prefix, _) => write!(f, "{}.*", prefix), Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), Expr::IsTrue(ast) => write!(f, "{ast} IS TRUE"), Expr::IsNotTrue(ast) => write!(f, "{ast} IS NOT TRUE"), @@ -5432,8 +5498,8 @@ pub enum FunctionArgExpr { impl From for FunctionArgExpr { fn from(wildcard_expr: Expr) -> Self { match wildcard_expr { - Expr::QualifiedWildcard(prefix) => Self::QualifiedWildcard(prefix), - Expr::Wildcard => Self::Wildcard, + Expr::QualifiedWildcard(prefix, _) => Self::QualifiedWildcard(prefix), + Expr::Wildcard(_) => Self::Wildcard, expr => Self::Expr(expr), } } diff --git a/src/ast/query.rs b/src/ast/query.rs index bf36c626f..0472026a0 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -18,13 +18,17 @@ #[cfg(not(feature = "std"))] use alloc::{boxed::Box, vec::Vec}; +use helpers::attached_token::AttachedToken; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::ast::*; +use crate::{ + ast::*, + tokenizer::{Token, TokenWithLocation}, +}; /// The most complete variant of a `SELECT` query expression, optionally /// including `WITH`, `UNION` / other set operations, and `ORDER BY`. @@ -276,6 +280,8 @@ impl fmt::Display for Table { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Select { + /// Token for the `SELECT` keyword + pub select_token: AttachedToken, pub distinct: Option, /// MSSQL syntax: `TOP () [ PERCENT ] [ WITH TIES ]` pub top: Option, @@ -505,6 +511,8 @@ impl fmt::Display for NamedWindowDefinition { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct With { + // Token for the "WITH" keyword + pub with_token: AttachedToken, pub recursive: bool, pub cte_tables: Vec, } @@ -556,6 +564,8 @@ pub struct Cte { pub query: Box, pub from: Option, pub materialized: Option, + // Token for the closing parenthesis + pub closing_paren_token: AttachedToken, } impl fmt::Display for Cte { @@ -607,10 +617,12 @@ impl fmt::Display for IdentWithAlias { } /// Additional options for wildcards, e.g. Snowflake `EXCLUDE`/`RENAME` and Bigquery `EXCEPT`. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WildcardAdditionalOptions { + /// The wildcard token `*` + pub wildcard_token: AttachedToken, /// `[ILIKE...]`. /// Snowflake syntax: pub opt_ilike: Option, @@ -628,6 +640,19 @@ pub struct WildcardAdditionalOptions { pub opt_rename: Option, } +impl Default for WildcardAdditionalOptions { + fn default() -> Self { + Self { + wildcard_token: TokenWithLocation::wrap(Token::Mul).into(), + opt_ilike: None, + opt_exclude: None, + opt_except: None, + opt_replace: None, + opt_rename: None, + } + } +} + impl fmt::Display for WildcardAdditionalOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(ilike) = &self.opt_ilike { diff --git a/src/ast/spans.rs b/src/ast/spans.rs new file mode 100644 index 000000000..8e8c7b14a --- /dev/null +++ b/src/ast/spans.rs @@ -0,0 +1,2178 @@ +use core::iter; + +use crate::tokenizer::Span; + +use super::{ + 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, MatchRecognizePattern, Measure, NamedWindowDefinition, + ObjectName, Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, Partition, + PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem, + ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, + Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, + TableFactor, TableOptionsClustered, TableWithJoins, Use, Value, Values, ViewColumnDef, + WildcardAdditionalOptions, With, WithFill, +}; + +/// Given an iterator of spans, return the [Span::union] of all spans. +fn union_spans>(iter: I) -> Span { + iter.reduce(|acc, item| acc.union(&item)) + .unwrap_or(Span::empty()) +} + +/// A trait for AST nodes that have a source span for use in diagnostics. +/// +/// Source spans are not guaranteed to be entirely accurate. They may +/// be missing keywords or other tokens. Some nodes may not have a computable +/// span at all, in which case they return [`Span::empty()`]. +/// +/// Some impl blocks may contain doc comments with information +/// on which nodes are missing spans. +pub trait Spanned { + /// Compute the source span for this AST node, by recursively + /// combining the spans of its children. + fn span(&self) -> Span; +} + +impl Spanned for Query { + fn span(&self) -> Span { + let Query { + with, + body, + order_by, + limit, + limit_by, + offset, + fetch, + locks: _, // todo + for_clause: _, // todo, mssql specific + settings: _, // todo, clickhouse specific + format_clause: _, // todo, clickhouse specific + } = self; + + union_spans( + with.iter() + .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(fetch.as_ref().map(|i| i.span())), + ) + } +} + +impl Spanned for Offset { + fn span(&self) -> Span { + let Offset { + value, + rows: _, // enum + } = self; + + value.span() + } +} + +impl Spanned for Fetch { + fn span(&self) -> Span { + let Fetch { + with_ties: _, // bool + percent: _, // bool + quantity, + } = self; + + quantity.as_ref().map_or(Span::empty(), |i| i.span()) + } +} + +impl Spanned for With { + fn span(&self) -> Span { + let With { + with_token, + recursive: _, // bool + cte_tables, + } = self; + + union_spans( + core::iter::once(with_token.0.span).chain(cte_tables.iter().map(|item| item.span())), + ) + } +} + +impl Spanned for Cte { + fn span(&self) -> Span { + let Cte { + alias, + query, + from, + materialized: _, // enum + closing_paren_token, + } = self; + + union_spans( + core::iter::once(alias.span()) + .chain(core::iter::once(query.span())) + .chain(from.iter().map(|item| item.span)) + .chain(core::iter::once(closing_paren_token.0.span)), + ) + } +} + +/// # partial span +/// +/// [SetExpr::Table] is not implemented. +impl Spanned for SetExpr { + fn span(&self) -> Span { + match self { + SetExpr::Select(select) => select.span(), + SetExpr::Query(query) => query.span(), + SetExpr::SetOperation { + op: _, + set_quantifier: _, + left, + right, + } => left.span().union(&right.span()), + SetExpr::Values(values) => values.span(), + SetExpr::Insert(statement) => statement.span(), + SetExpr::Table(_) => Span::empty(), + SetExpr::Update(statement) => statement.span(), + } + } +} + +impl Spanned for Values { + fn span(&self) -> Span { + let Values { + explicit_row: _, // bool, + rows, + } = self; + + union_spans( + rows.iter() + .map(|row| union_spans(row.iter().map(|expr| expr.span()))), + ) + } +} + +/// # partial span +/// +/// Missing spans: +/// - [Statement::CopyIntoSnowflake] +/// - [Statement::CreateSecret] +/// - [Statement::CreateRole] +/// - [Statement::AlterRole] +/// - [Statement::AttachDatabase] +/// - [Statement::AttachDuckDBDatabase] +/// - [Statement::DetachDuckDBDatabase] +/// - [Statement::Drop] +/// - [Statement::DropFunction] +/// - [Statement::DropProcedure] +/// - [Statement::DropSecret] +/// - [Statement::Declare] +/// - [Statement::CreateExtension] +/// - [Statement::Fetch] +/// - [Statement::Flush] +/// - [Statement::Discard] +/// - [Statement::SetRole] +/// - [Statement::SetVariable] +/// - [Statement::SetTimeZone] +/// - [Statement::SetNames] +/// - [Statement::SetNamesDefault] +/// - [Statement::ShowFunctions] +/// - [Statement::ShowVariable] +/// - [Statement::ShowStatus] +/// - [Statement::ShowVariables] +/// - [Statement::ShowCreate] +/// - [Statement::ShowColumns] +/// - [Statement::ShowTables] +/// - [Statement::ShowCollation] +/// - [Statement::StartTransaction] +/// - [Statement::SetTransaction] +/// - [Statement::Comment] +/// - [Statement::Commit] +/// - [Statement::Rollback] +/// - [Statement::CreateSchema] +/// - [Statement::CreateDatabase] +/// - [Statement::CreateFunction] +/// - [Statement::CreateTrigger] +/// - [Statement::DropTrigger] +/// - [Statement::CreateProcedure] +/// - [Statement::CreateMacro] +/// - [Statement::CreateStage] +/// - [Statement::Assert] +/// - [Statement::Grant] +/// - [Statement::Revoke] +/// - [Statement::Deallocate] +/// - [Statement::Execute] +/// - [Statement::Prepare] +/// - [Statement::Kill] +/// - [Statement::ExplainTable] +/// - [Statement::Explain] +/// - [Statement::Savepoint] +/// - [Statement::ReleaseSavepoint] +/// - [Statement::Merge] +/// - [Statement::Cache] +/// - [Statement::UNCache] +/// - [Statement::CreateSequence] +/// - [Statement::CreateType] +/// - [Statement::Pragma] +/// - [Statement::LockTables] +/// - [Statement::UnlockTables] +/// - [Statement::Unload] +/// - [Statement::OptimizeTable] +impl Spanned for Statement { + fn span(&self) -> Span { + match self { + Statement::Analyze { + table_name, + partitions, + for_columns: _, + columns, + cache_metadata: _, + noscan: _, + compute_statistics: _, + } => union_spans( + core::iter::once(table_name.span()) + .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))) + .chain(columns.iter().map(|i| i.span)), + ), + Statement::Truncate { + table_names, + partitions, + table: _, + only: _, + identity: _, + cascade: _, + on_cluster: _, + } => union_spans( + table_names + .iter() + .map(|i| i.name.span()) + .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))), + ), + Statement::Msck { + table_name, + repair: _, + partition_action: _, + } => table_name.span(), + Statement::Query(query) => query.span(), + Statement::Insert(insert) => insert.span(), + Statement::Install { extension_name } => extension_name.span, + Statement::Load { extension_name } => extension_name.span, + Statement::Directory { + overwrite: _, + local: _, + path: _, + file_format: _, + source, + } => source.span(), + Statement::Call(function) => function.span(), + Statement::Copy { + source, + to: _, + target: _, + options: _, + legacy_options: _, + values: _, + } => source.span(), + Statement::CopyIntoSnowflake { + into: _, + from_stage: _, + from_stage_alias: _, + stage_params: _, + from_transformations: _, + files: _, + pattern: _, + file_format: _, + copy_options: _, + validation_mode: _, + } => Span::empty(), + Statement::Close { cursor } => match cursor { + CloseCursor::All => Span::empty(), + CloseCursor::Specific { name } => name.span, + }, + Statement::Update { + table, + assignments, + from, + selection, + returning, + or: _, + } => union_spans( + core::iter::once(table.span()) + .chain(assignments.iter().map(|i| i.span())) + .chain(from.iter().map(|i| i.span())) + .chain(selection.iter().map(|i| i.span())) + .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))), + ), + Statement::Delete(delete) => delete.span(), + Statement::CreateView { + or_replace: _, + materialized: _, + name, + columns, + query, + options, + cluster_by, + comment: _, + with_no_schema_binding: _, + if_not_exists: _, + temporary: _, + to, + } => union_spans( + core::iter::once(name.span()) + .chain(columns.iter().map(|i| i.span())) + .chain(core::iter::once(query.span())) + .chain(core::iter::once(options.span())) + .chain(cluster_by.iter().map(|i| i.span)) + .chain(to.iter().map(|i| i.span())), + ), + Statement::CreateTable(create_table) => create_table.span(), + Statement::CreateVirtualTable { + name, + if_not_exists: _, + module_name, + module_args, + } => union_spans( + core::iter::once(name.span()) + .chain(core::iter::once(module_name.span)) + .chain(module_args.iter().map(|i| i.span)), + ), + Statement::CreateIndex(create_index) => create_index.span(), + Statement::CreateRole { .. } => Span::empty(), + Statement::CreateSecret { .. } => Span::empty(), + Statement::AlterTable { + name, + if_exists: _, + only: _, + operations, + location: _, + on_cluster, + } => union_spans( + core::iter::once(name.span()) + .chain(operations.iter().map(|i| i.span())) + .chain(on_cluster.iter().map(|i| i.span)), + ), + Statement::AlterIndex { name, operation } => name.span().union(&operation.span()), + Statement::AlterView { + name, + columns, + query, + with_options, + } => union_spans( + core::iter::once(name.span()) + .chain(columns.iter().map(|i| i.span)) + .chain(core::iter::once(query.span())) + .chain(with_options.iter().map(|i| i.span())), + ), + // These statements need to be implemented + Statement::AlterRole { .. } => Span::empty(), + Statement::AttachDatabase { .. } => Span::empty(), + Statement::AttachDuckDBDatabase { .. } => Span::empty(), + Statement::DetachDuckDBDatabase { .. } => Span::empty(), + Statement::Drop { .. } => Span::empty(), + Statement::DropFunction { .. } => Span::empty(), + Statement::DropProcedure { .. } => Span::empty(), + Statement::DropSecret { .. } => Span::empty(), + Statement::Declare { .. } => Span::empty(), + Statement::CreateExtension { .. } => Span::empty(), + 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::ShowFunctions { .. } => Span::empty(), + Statement::ShowVariable { .. } => Span::empty(), + Statement::ShowStatus { .. } => Span::empty(), + Statement::ShowVariables { .. } => Span::empty(), + Statement::ShowCreate { .. } => Span::empty(), + Statement::ShowColumns { .. } => Span::empty(), + Statement::ShowTables { .. } => Span::empty(), + 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(), + Statement::CreateSchema { .. } => Span::empty(), + Statement::CreateDatabase { .. } => Span::empty(), + Statement::CreateFunction { .. } => Span::empty(), + Statement::CreateTrigger { .. } => Span::empty(), + Statement::DropTrigger { .. } => Span::empty(), + Statement::CreateProcedure { .. } => Span::empty(), + Statement::CreateMacro { .. } => Span::empty(), + Statement::CreateStage { .. } => Span::empty(), + Statement::Assert { .. } => Span::empty(), + Statement::Grant { .. } => Span::empty(), + Statement::Revoke { .. } => Span::empty(), + Statement::Deallocate { .. } => Span::empty(), + Statement::Execute { .. } => Span::empty(), + Statement::Prepare { .. } => Span::empty(), + Statement::Kill { .. } => Span::empty(), + Statement::ExplainTable { .. } => Span::empty(), + Statement::Explain { .. } => Span::empty(), + Statement::Savepoint { .. } => Span::empty(), + Statement::ReleaseSavepoint { .. } => Span::empty(), + Statement::Merge { .. } => Span::empty(), + Statement::Cache { .. } => Span::empty(), + Statement::UNCache { .. } => Span::empty(), + Statement::CreateSequence { .. } => Span::empty(), + Statement::CreateType { .. } => Span::empty(), + Statement::Pragma { .. } => Span::empty(), + Statement::LockTables { .. } => Span::empty(), + Statement::UnlockTables => Span::empty(), + Statement::Unload { .. } => Span::empty(), + Statement::OptimizeTable { .. } => Span::empty(), + Statement::CreatePolicy { .. } => Span::empty(), + Statement::AlterPolicy { .. } => Span::empty(), + Statement::DropPolicy { .. } => Span::empty(), + Statement::ShowDatabases { .. } => Span::empty(), + Statement::ShowSchemas { .. } => Span::empty(), + Statement::ShowViews { .. } => Span::empty(), + Statement::LISTEN { .. } => Span::empty(), + Statement::NOTIFY { .. } => Span::empty(), + Statement::LoadData { .. } => Span::empty(), + Statement::UNLISTEN { .. } => Span::empty(), + } + } +} + +impl Spanned for Use { + fn span(&self) -> Span { + match self { + Use::Catalog(object_name) => object_name.span(), + Use::Schema(object_name) => object_name.span(), + Use::Database(object_name) => object_name.span(), + Use::Warehouse(object_name) => object_name.span(), + Use::Object(object_name) => object_name.span(), + Use::Default => Span::empty(), + } + } +} + +impl Spanned for CreateTable { + fn span(&self) -> Span { + let CreateTable { + or_replace: _, // bool + temporary: _, // bool + external: _, // bool + global: _, // bool + if_not_exists: _, // bool + transient: _, // bool + volatile: _, // bool + name, + columns, + constraints, + hive_distribution: _, // hive specific + hive_formats: _, // hive specific + table_properties, + with_options, + 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 + 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 + strict: _, // bool + copy_grants: _, // bool + enable_schema_evolution: _, // bool + change_tracking: _, // bool + data_retention_time_in_days: _, // u64, no span + max_data_extension_time_in_days: _, // u64, no span + default_ddl_collation: _, // string, no span + with_aggregation_policy: _, // todo, Snowflake specific + with_row_access_policy: _, // todo, Snowflake specific + with_tags: _, // todo, Snowflake specific + } = self; + + union_spans( + core::iter::once(name.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())), + ) + } +} + +impl Spanned for ColumnDef { + fn span(&self) -> Span { + let ColumnDef { + name, + data_type: _, // enum + collation, + options, + } = self; + + union_spans( + core::iter::once(name.span) + .chain(collation.iter().map(|i| i.span())) + .chain(options.iter().map(|i| i.span())), + ) + } +} + +impl Spanned for ColumnOptionDef { + fn span(&self) -> Span { + let ColumnOptionDef { name, option } = self; + + option.span().union_opt(&name.as_ref().map(|i| i.span)) + } +} + +impl Spanned for TableConstraint { + fn span(&self) -> Span { + match self { + TableConstraint::Unique { + name, + index_name, + index_type_display: _, + index_type: _, + columns, + index_options: _, + characteristics, + } => union_spans( + name.iter() + .map(|i| i.span) + .chain(index_name.iter().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span)) + .chain(characteristics.iter().map(|i| i.span())), + ), + TableConstraint::PrimaryKey { + name, + index_name, + index_type: _, + columns, + index_options: _, + characteristics, + } => union_spans( + name.iter() + .map(|i| i.span) + .chain(index_name.iter().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span)) + .chain(characteristics.iter().map(|i| i.span())), + ), + TableConstraint::ForeignKey { + name, + columns, + foreign_table, + referred_columns, + on_delete, + on_update, + characteristics, + } => union_spans( + 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)) + .chain(on_delete.iter().map(|i| i.span())) + .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::Index { + display_as_key: _, + name, + index_type: _, + columns, + } => union_spans( + name.iter() + .map(|i| i.span) + .chain(columns.iter().map(|i| i.span)), + ), + TableConstraint::FulltextOrSpatial { + fulltext: _, + index_type_display: _, + opt_index_name, + columns, + } => union_spans( + opt_index_name + .iter() + .map(|i| i.span) + .chain(columns.iter().map(|i| i.span)), + ), + } + } +} + +impl Spanned for CreateIndex { + fn span(&self) -> Span { + let CreateIndex { + name, + table_name, + using, + columns, + unique: _, // bool + concurrently: _, // bool + if_not_exists: _, // bool + include, + nulls_distinct: _, // bool + with, + predicate, + } = self; + + union_spans( + 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(include.iter().map(|i| i.span)) + .chain(with.iter().map(|i| i.span())) + .chain(predicate.iter().map(|i| i.span())), + ) + } +} + +/// # partial span +/// +/// Missing spans: +/// - [ColumnOption::Null] +/// - [ColumnOption::NotNull] +/// - [ColumnOption::Comment] +/// - [ColumnOption::Unique]¨ +/// - [ColumnOption::DialectSpecific] +/// - [ColumnOption::Generated] +impl Spanned for ColumnOption { + fn span(&self) -> Span { + match self { + ColumnOption::Null => Span::empty(), + ColumnOption::NotNull => Span::empty(), + ColumnOption::Default(expr) => expr.span(), + ColumnOption::Materialized(expr) => expr.span(), + ColumnOption::Ephemeral(expr) => expr.as_ref().map_or(Span::empty(), |e| e.span()), + ColumnOption::Alias(expr) => expr.span(), + ColumnOption::Unique { .. } => Span::empty(), + ColumnOption::ForeignKey { + foreign_table, + referred_columns, + on_delete, + on_update, + characteristics, + } => union_spans( + core::iter::once(foreign_table.span()) + .chain(referred_columns.iter().map(|i| i.span)) + .chain(on_delete.iter().map(|i| i.span())) + .chain(on_update.iter().map(|i| i.span())) + .chain(characteristics.iter().map(|i| i.span())), + ), + ColumnOption::Check(expr) => expr.span(), + ColumnOption::DialectSpecific(_) => Span::empty(), + ColumnOption::CharacterSet(object_name) => object_name.span(), + ColumnOption::Comment(_) => Span::empty(), + ColumnOption::OnUpdate(expr) => expr.span(), + ColumnOption::Generated { .. } => Span::empty(), + ColumnOption::Options(vec) => union_spans(vec.iter().map(|i| i.span())), + ColumnOption::Identity(..) => Span::empty(), + ColumnOption::OnConflict(..) => Span::empty(), + ColumnOption::Policy(..) => Span::empty(), + ColumnOption::Tags(..) => Span::empty(), + } + } +} + +/// # missing span +impl Spanned for ReferentialAction { + fn span(&self) -> Span { + Span::empty() + } +} + +/// # missing span +impl Spanned for ConstraintCharacteristics { + fn span(&self) -> Span { + let ConstraintCharacteristics { + deferrable: _, // bool + initially: _, // enum + enforced: _, // bool + } = self; + + Span::empty() + } +} + +/// # partial span +/// +/// Missing spans: +/// - [AlterColumnOperation::SetNotNull] +/// - [AlterColumnOperation::DropNotNull] +/// - [AlterColumnOperation::DropDefault] +/// - [AlterColumnOperation::AddGenerated] +impl Spanned for AlterColumnOperation { + fn span(&self) -> Span { + match self { + AlterColumnOperation::SetNotNull => Span::empty(), + AlterColumnOperation::DropNotNull => Span::empty(), + AlterColumnOperation::SetDefault { value } => value.span(), + AlterColumnOperation::DropDefault => Span::empty(), + AlterColumnOperation::SetDataType { + data_type: _, + using, + } => using.as_ref().map_or(Span::empty(), |u| u.span()), + AlterColumnOperation::AddGenerated { .. } => Span::empty(), + } + } +} + +impl Spanned for CopySource { + fn span(&self) -> Span { + match self { + CopySource::Table { + table_name, + columns, + } => union_spans( + core::iter::once(table_name.span()).chain(columns.iter().map(|i| i.span)), + ), + CopySource::Query(query) => query.span(), + } + } +} + +impl Spanned for Delete { + fn span(&self) -> Span { + let Delete { + tables, + from, + using, + selection, + returning, + order_by, + limit, + } = self; + + union_spans( + tables + .iter() + .map(|i| i.span()) + .chain(core::iter::once(from.span())) + .chain( + using + .iter() + .map(|u| union_spans(u.iter().map(|i| i.span()))), + ) + .chain(selection.iter().map(|i| i.span())) + .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) + .chain(order_by.iter().map(|i| i.span())) + .chain(limit.iter().map(|i| i.span())), + ) + } +} + +impl Spanned for FromTable { + fn span(&self) -> Span { + match self { + FromTable::WithFromKeyword(vec) => union_spans(vec.iter().map(|i| i.span())), + FromTable::WithoutKeyword(vec) => union_spans(vec.iter().map(|i| i.span())), + } + } +} + +impl Spanned for ViewColumnDef { + fn span(&self) -> Span { + let ViewColumnDef { + name, + data_type: _, // todo, DataType + options, + } = self; + + union_spans( + core::iter::once(name.span) + .chain(options.iter().flat_map(|i| i.iter().map(|k| k.span()))), + ) + } +} + +impl Spanned for SqlOption { + fn span(&self) -> Span { + match self { + SqlOption::Clustered(table_options_clustered) => table_options_clustered.span(), + SqlOption::Ident(ident) => ident.span, + SqlOption::KeyValue { key, value } => key.span.union(&value.span()), + SqlOption::Partition { + column_name, + range_direction: _, + for_values, + } => union_spans( + core::iter::once(column_name.span).chain(for_values.iter().map(|i| i.span())), + ), + } + } +} + +/// # partial span +/// +/// Missing spans: +/// - [TableOptionsClustered::ColumnstoreIndex] +impl Spanned for TableOptionsClustered { + fn span(&self) -> Span { + match self { + TableOptionsClustered::ColumnstoreIndex => Span::empty(), + TableOptionsClustered::ColumnstoreIndexOrder(vec) => { + union_spans(vec.iter().map(|i| i.span)) + } + TableOptionsClustered::Index(vec) => union_spans(vec.iter().map(|i| i.span())), + } + } +} + +impl Spanned for ClusteredIndex { + fn span(&self) -> Span { + let ClusteredIndex { + name, + asc: _, // bool + } = self; + + name.span + } +} + +impl Spanned for CreateTableOptions { + fn span(&self) -> Span { + 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())), + } + } +} + +/// # partial span +/// +/// Missing spans: +/// - [AlterTableOperation::OwnerTo] +impl Spanned for AlterTableOperation { + fn span(&self) -> Span { + match self { + AlterTableOperation::AddConstraint(table_constraint) => table_constraint.span(), + AlterTableOperation::AddColumn { + column_keyword: _, + if_not_exists: _, + column_def, + column_position: _, + } => column_def.span(), + AlterTableOperation::AddProjection { + if_not_exists: _, + name, + select, + } => name.span.union(&select.span()), + AlterTableOperation::DropProjection { if_exists: _, name } => name.span, + AlterTableOperation::MaterializeProjection { + if_exists: _, + name, + partition, + } => name.span.union_opt(&partition.as_ref().map(|i| i.span)), + AlterTableOperation::ClearProjection { + if_exists: _, + name, + partition, + } => name.span.union_opt(&partition.as_ref().map(|i| i.span)), + AlterTableOperation::DisableRowLevelSecurity => Span::empty(), + AlterTableOperation::DisableRule { name } => name.span, + AlterTableOperation::DisableTrigger { name } => name.span, + AlterTableOperation::DropConstraint { + if_exists: _, + name, + cascade: _, + } => name.span, + AlterTableOperation::DropColumn { + column_name, + if_exists: _, + cascade: _, + } => column_name.span, + AlterTableOperation::AttachPartition { partition } => partition.span(), + AlterTableOperation::DetachPartition { partition } => partition.span(), + AlterTableOperation::FreezePartition { + partition, + with_name, + } => partition + .span() + .union_opt(&with_name.as_ref().map(|n| n.span)), + AlterTableOperation::UnfreezePartition { + partition, + with_name, + } => partition + .span() + .union_opt(&with_name.as_ref().map(|n| n.span)), + AlterTableOperation::DropPrimaryKey => Span::empty(), + AlterTableOperation::EnableAlwaysRule { name } => name.span, + AlterTableOperation::EnableAlwaysTrigger { name } => name.span, + AlterTableOperation::EnableReplicaRule { name } => name.span, + AlterTableOperation::EnableReplicaTrigger { name } => name.span, + AlterTableOperation::EnableRowLevelSecurity => Span::empty(), + AlterTableOperation::EnableRule { name } => name.span, + AlterTableOperation::EnableTrigger { name } => name.span, + AlterTableOperation::RenamePartitions { + old_partitions, + new_partitions, + } => union_spans( + old_partitions + .iter() + .map(|i| i.span()) + .chain(new_partitions.iter().map(|i| i.span())), + ), + AlterTableOperation::AddPartitions { + if_not_exists: _, + new_partitions, + } => union_spans(new_partitions.iter().map(|i| i.span())), + AlterTableOperation::DropPartitions { + partitions, + if_exists: _, + } => union_spans(partitions.iter().map(|i| i.span())), + AlterTableOperation::RenameColumn { + old_column_name, + new_column_name, + } => old_column_name.span.union(&new_column_name.span), + AlterTableOperation::RenameTable { table_name } => table_name.span(), + AlterTableOperation::ChangeColumn { + old_name, + new_name, + data_type: _, + options, + column_position: _, + } => union_spans( + core::iter::once(old_name.span) + .chain(core::iter::once(new_name.span)) + .chain(options.iter().map(|i| i.span())), + ), + AlterTableOperation::ModifyColumn { + col_name, + data_type: _, + options, + column_position: _, + } => { + union_spans(core::iter::once(col_name.span).chain(options.iter().map(|i| i.span()))) + } + AlterTableOperation::RenameConstraint { old_name, new_name } => { + old_name.span.union(&new_name.span) + } + AlterTableOperation::AlterColumn { column_name, op } => { + column_name.span.union(&op.span()) + } + AlterTableOperation::SwapWith { table_name } => table_name.span(), + AlterTableOperation::SetTblProperties { table_properties } => { + union_spans(table_properties.iter().map(|i| i.span())) + } + AlterTableOperation::OwnerTo { .. } => Span::empty(), + } + } +} + +impl Spanned for Partition { + fn span(&self) -> Span { + match self { + Partition::Identifier(ident) => ident.span, + Partition::Expr(expr) => expr.span(), + Partition::Part(expr) => expr.span(), + Partition::Partitions(vec) => union_spans(vec.iter().map(|i| i.span())), + } + } +} + +impl Spanned for ProjectionSelect { + fn span(&self) -> Span { + let ProjectionSelect { + projection, + order_by, + group_by, + } = self; + + union_spans( + projection + .iter() + .map(|i| i.span()) + .chain(order_by.iter().map(|i| i.span())) + .chain(group_by.iter().map(|i| i.span())), + ) + } +} + +impl Spanned for OrderBy { + fn span(&self) -> Span { + let OrderBy { exprs, interpolate } = self; + + union_spans( + exprs + .iter() + .map(|i| i.span()) + .chain(interpolate.iter().map(|i| i.span())), + ) + } +} + +/// # partial span +/// +/// Missing spans: +/// - [GroupByExpr::All] +impl Spanned for GroupByExpr { + fn span(&self) -> Span { + match self { + GroupByExpr::All(_) => Span::empty(), + GroupByExpr::Expressions(exprs, _modifiers) => { + union_spans(exprs.iter().map(|i| i.span())) + } + } + } +} + +impl Spanned for Interpolate { + fn span(&self) -> Span { + let Interpolate { exprs } = self; + + union_spans(exprs.iter().flat_map(|i| i.iter().map(|e| e.span()))) + } +} + +impl Spanned for InterpolateExpr { + fn span(&self) -> Span { + let InterpolateExpr { column, expr } = self; + + column.span.union_opt(&expr.as_ref().map(|e| e.span())) + } +} + +impl Spanned for AlterIndexOperation { + fn span(&self) -> Span { + match self { + AlterIndexOperation::RenameIndex { index_name } => index_name.span(), + } + } +} + +/// # partial span +/// +/// Missing spans:ever +/// - [Insert::insert_alias] +impl Spanned for Insert { + fn span(&self) -> Span { + let Insert { + or: _, // enum, sqlite specific + ignore: _, // bool + into: _, // bool + table_name, + table_alias, + columns, + overwrite: _, // bool + source, + partitioned, + after_columns, + table: _, // bool + on, + returning, + replace_into: _, // bool + priority: _, // todo, mysql specific + insert_alias: _, // todo, mysql specific + } = self; + + union_spans( + core::iter::once(table_name.span()) + .chain(table_alias.as_ref().map(|i| i.span)) + .chain(columns.iter().map(|i| i.span)) + .chain(source.as_ref().map(|q| q.span())) + .chain(partitioned.iter().flat_map(|i| i.iter().map(|k| k.span()))) + .chain(after_columns.iter().map(|i| i.span)) + .chain(on.as_ref().map(|i| i.span())) + .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))), + ) + } +} + +impl Spanned for OnInsert { + fn span(&self) -> Span { + match self { + OnInsert::DuplicateKeyUpdate(vec) => union_spans(vec.iter().map(|i| i.span())), + OnInsert::OnConflict(on_conflict) => on_conflict.span(), + } + } +} + +impl Spanned for OnConflict { + fn span(&self) -> Span { + let OnConflict { + conflict_target, + action, + } = self; + + action + .span() + .union_opt(&conflict_target.as_ref().map(|i| i.span())) + } +} + +impl Spanned for ConflictTarget { + fn span(&self) -> Span { + match self { + ConflictTarget::Columns(vec) => union_spans(vec.iter().map(|i| i.span)), + ConflictTarget::OnConstraint(object_name) => object_name.span(), + } + } +} + +/// # partial span +/// +/// Missing spans: +/// - [OnConflictAction::DoNothing] +impl Spanned for OnConflictAction { + fn span(&self) -> Span { + match self { + OnConflictAction::DoNothing => Span::empty(), + OnConflictAction::DoUpdate(do_update) => do_update.span(), + } + } +} + +impl Spanned for DoUpdate { + fn span(&self) -> Span { + let DoUpdate { + assignments, + selection, + } = self; + + union_spans( + assignments + .iter() + .map(|i| i.span()) + .chain(selection.iter().map(|i| i.span())), + ) + } +} + +impl Spanned for Assignment { + fn span(&self) -> Span { + let Assignment { target, value } = self; + + target.span().union(&value.span()) + } +} + +impl Spanned for AssignmentTarget { + fn span(&self) -> Span { + match self { + AssignmentTarget::ColumnName(object_name) => object_name.span(), + AssignmentTarget::Tuple(vec) => union_spans(vec.iter().map(|i| i.span())), + } + } +} + +/// # partial span +/// +/// Most expressions are missing keywords in their spans. +/// f.e. `IS NULL ` reports as `::span`. +/// +/// Missing spans: +/// - [Expr::TypedString] +/// - [Expr::MatchAgainst] # MySQL specific +/// - [Expr::RLike] # MySQL specific +/// - [Expr::Struct] # BigQuery specific +/// - [Expr::Named] # BigQuery specific +/// - [Expr::Dictionary] # DuckDB specific +/// - [Expr::Map] # DuckDB specific +/// - [Expr::Lambda] +impl Spanned for Expr { + fn span(&self) -> Span { + match self { + Expr::Identifier(ident) => ident.span, + Expr::CompoundIdentifier(vec) => union_spans(vec.iter().map(|i| i.span)), + Expr::CompositeAccess { expr, key } => expr.span().union(&key.span), + Expr::IsFalse(expr) => expr.span(), + Expr::IsNotFalse(expr) => expr.span(), + Expr::IsTrue(expr) => expr.span(), + Expr::IsNotTrue(expr) => expr.span(), + Expr::IsNull(expr) => expr.span(), + Expr::IsNotNull(expr) => expr.span(), + Expr::IsUnknown(expr) => expr.span(), + Expr::IsNotUnknown(expr) => expr.span(), + Expr::IsDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()), + Expr::IsNotDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()), + Expr::InList { + expr, + list, + negated: _, + } => union_spans( + core::iter::once(expr.span()).chain(list.iter().map(|item| item.span())), + ), + Expr::InSubquery { + expr, + subquery, + negated: _, + } => expr.span().union(&subquery.span()), + Expr::InUnnest { + expr, + array_expr, + negated: _, + } => expr.span().union(&array_expr.span()), + Expr::Between { + expr, + negated: _, + low, + high, + } => expr.span().union(&low.span()).union(&high.span()), + + Expr::BinaryOp { left, op: _, right } => left.span().union(&right.span()), + Expr::Like { + negated: _, + expr, + pattern, + escape_char: _, + any: _, + } => expr.span().union(&pattern.span()), + Expr::ILike { + negated: _, + expr, + pattern, + escape_char: _, + any: _, + } => expr.span().union(&pattern.span()), + Expr::SimilarTo { + negated: _, + expr, + pattern, + escape_char: _, + } => expr.span().union(&pattern.span()), + Expr::Ceil { expr, field: _ } => expr.span(), + Expr::Floor { expr, field: _ } => expr.span(), + Expr::Position { expr, r#in } => expr.span().union(&r#in.span()), + Expr::Overlay { + expr, + overlay_what, + overlay_from, + overlay_for, + } => expr + .span() + .union(&overlay_what.span()) + .union(&overlay_from.span()) + .union_opt(&overlay_for.as_ref().map(|i| i.span())), + Expr::Collate { expr, collation } => expr + .span() + .union(&union_spans(collation.0.iter().map(|i| i.span))), + Expr::Nested(expr) => expr.span(), + Expr::Value(value) => value.span(), + Expr::TypedString { .. } => Span::empty(), + Expr::MapAccess { column, keys } => column + .span() + .union(&union_spans(keys.iter().map(|i| i.key.span()))), + Expr::Function(function) => function.span(), + Expr::GroupingSets(vec) => { + union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span()))) + } + Expr::Cube(vec) => union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span()))), + Expr::Rollup(vec) => union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span()))), + Expr::Tuple(vec) => union_spans(vec.iter().map(|i| i.span())), + Expr::Array(array) => array.span(), + Expr::MatchAgainst { .. } => Span::empty(), + Expr::JsonAccess { value, path } => value.span().union(&path.span()), + Expr::RLike { .. } => Span::empty(), + Expr::AnyOp { + left, + compare_op: _, + right, + is_some: _, + } => left.span().union(&right.span()), + Expr::AllOp { + left, + compare_op: _, + right, + } => left.span().union(&right.span()), + Expr::UnaryOp { op: _, expr } => expr.span(), + Expr::Convert { + expr, + data_type: _, + charset, + target_before_value: _, + styles, + is_try: _, + } => union_spans( + core::iter::once(expr.span()) + .chain(charset.as_ref().map(|i| i.span())) + .chain(styles.iter().map(|i| i.span())), + ), + Expr::Cast { + kind: _, + expr, + data_type: _, + format: _, + } => expr.span(), + Expr::AtTimeZone { + timestamp, + time_zone, + } => timestamp.span().union(&time_zone.span()), + Expr::Extract { + field: _, + syntax: _, + expr, + } => expr.span(), + Expr::Substring { + expr, + substring_from, + substring_for, + special: _, + } => union_spans( + core::iter::once(expr.span()) + .chain(substring_from.as_ref().map(|i| i.span())) + .chain(substring_for.as_ref().map(|i| i.span())), + ), + Expr::Trim { + expr, + trim_where: _, + trim_what, + trim_characters, + } => union_spans( + core::iter::once(expr.span()) + .chain(trim_what.as_ref().map(|i| i.span())) + .chain( + trim_characters + .as_ref() + .map(|items| union_spans(items.iter().map(|i| i.span()))), + ), + ), + Expr::IntroducedString { value, .. } => value.span(), + Expr::Case { + operand, + conditions, + results, + else_result, + } => union_spans( + operand + .as_ref() + .map(|i| i.span()) + .into_iter() + .chain(conditions.iter().map(|i| i.span())) + .chain(results.iter().map(|i| i.span())) + .chain(else_result.as_ref().map(|i| i.span())), + ), + Expr::Exists { subquery, .. } => subquery.span(), + Expr::Subquery(query) => query.span(), + Expr::Struct { .. } => Span::empty(), + Expr::Named { .. } => Span::empty(), + Expr::Dictionary(_) => Span::empty(), + Expr::Map(_) => Span::empty(), + Expr::Subscript { expr, subscript } => expr.span().union(&subscript.span()), + Expr::Interval(interval) => interval.value.span(), + Expr::Wildcard(token) => token.0.span, + Expr::QualifiedWildcard(object_name, token) => union_spans( + object_name + .0 + .iter() + .map(|i| i.span) + .chain(iter::once(token.0.span)), + ), + Expr::OuterJoin(expr) => expr.span(), + Expr::Prior(expr) => expr.span(), + Expr::Lambda(_) => Span::empty(), + Expr::Method(_) => Span::empty(), + } + } +} + +impl Spanned for Subscript { + fn span(&self) -> Span { + match self { + Subscript::Index { index } => index.span(), + Subscript::Slice { + lower_bound, + upper_bound, + stride, + } => union_spans( + [ + lower_bound.as_ref().map(|i| i.span()), + upper_bound.as_ref().map(|i| i.span()), + stride.as_ref().map(|i| i.span()), + ] + .into_iter() + .flatten(), + ), + } + } +} + +impl Spanned for ObjectName { + fn span(&self) -> Span { + let ObjectName(segments) = self; + + union_spans(segments.iter().map(|i| i.span)) + } +} + +impl Spanned for Array { + fn span(&self) -> Span { + let Array { + elem, + named: _, // bool + } = self; + + union_spans(elem.iter().map(|i| i.span())) + } +} + +impl Spanned for Function { + fn span(&self) -> Span { + let Function { + name, + parameters, + args, + filter, + null_treatment: _, // enum + over: _, // todo + within_group, + } = self; + + union_spans( + name.0 + .iter() + .map(|i| i.span) + .chain(iter::once(args.span())) + .chain(iter::once(parameters.span())) + .chain(filter.iter().map(|i| i.span())) + .chain(within_group.iter().map(|i| i.span())), + ) + } +} + +/// # partial span +/// +/// The span of [FunctionArguments::None] is empty. +impl Spanned for FunctionArguments { + fn span(&self) -> Span { + match self { + FunctionArguments::None => Span::empty(), + FunctionArguments::Subquery(query) => query.span(), + FunctionArguments::List(list) => list.span(), + } + } +} + +impl Spanned for FunctionArgumentList { + fn span(&self) -> Span { + let FunctionArgumentList { + duplicate_treatment: _, // enum + args, + clauses, + } = self; + + union_spans( + // # todo: duplicate-treatment span + args.iter() + .map(|i| i.span()) + .chain(clauses.iter().map(|i| i.span())), + ) + } +} + +impl Spanned for FunctionArgumentClause { + fn span(&self) -> Span { + match self { + FunctionArgumentClause::IgnoreOrRespectNulls(_) => Span::empty(), + FunctionArgumentClause::OrderBy(vec) => union_spans(vec.iter().map(|i| i.expr.span())), + FunctionArgumentClause::Limit(expr) => expr.span(), + FunctionArgumentClause::OnOverflow(_) => Span::empty(), + FunctionArgumentClause::Having(HavingBound(_kind, expr)) => expr.span(), + FunctionArgumentClause::Separator(value) => value.span(), + FunctionArgumentClause::JsonNullClause(_) => Span::empty(), + } + } +} + +/// # partial span +/// +/// see Spanned impl for JsonPathElem for more information +impl Spanned for JsonPath { + fn span(&self) -> Span { + let JsonPath { path } = self; + + union_spans(path.iter().map(|i| i.span())) + } +} + +/// # partial span +/// +/// Missing spans: +/// - [JsonPathElem::Dot] +impl Spanned for JsonPathElem { + fn span(&self) -> Span { + match self { + JsonPathElem::Dot { .. } => Span::empty(), + JsonPathElem::Bracket { key } => key.span(), + } + } +} + +impl Spanned for SelectItem { + fn span(&self) -> Span { + match self { + SelectItem::UnnamedExpr(expr) => expr.span(), + SelectItem::ExprWithAlias { expr, alias } => expr.span().union(&alias.span), + SelectItem::QualifiedWildcard(object_name, wildcard_additional_options) => union_spans( + object_name + .0 + .iter() + .map(|i| i.span) + .chain(iter::once(wildcard_additional_options.span())), + ), + SelectItem::Wildcard(wildcard_additional_options) => wildcard_additional_options.span(), + } + } +} + +impl Spanned for WildcardAdditionalOptions { + fn span(&self) -> Span { + let WildcardAdditionalOptions { + wildcard_token, + opt_ilike, + opt_exclude, + opt_except, + opt_replace, + opt_rename, + } = self; + + union_spans( + core::iter::once(wildcard_token.0.span) + .chain(opt_ilike.as_ref().map(|i| i.span())) + .chain(opt_exclude.as_ref().map(|i| i.span())) + .chain(opt_rename.as_ref().map(|i| i.span())) + .chain(opt_replace.as_ref().map(|i| i.span())) + .chain(opt_except.as_ref().map(|i| i.span())), + ) + } +} + +/// # missing span +impl Spanned for IlikeSelectItem { + fn span(&self) -> Span { + Span::empty() + } +} + +impl Spanned for ExcludeSelectItem { + fn span(&self) -> Span { + match self { + ExcludeSelectItem::Single(ident) => ident.span, + ExcludeSelectItem::Multiple(vec) => union_spans(vec.iter().map(|i| i.span)), + } + } +} + +impl Spanned for RenameSelectItem { + fn span(&self) -> Span { + match self { + RenameSelectItem::Single(ident) => ident.ident.span.union(&ident.alias.span), + RenameSelectItem::Multiple(vec) => { + union_spans(vec.iter().map(|i| i.ident.span.union(&i.alias.span))) + } + } + } +} + +impl Spanned for ExceptSelectItem { + fn span(&self) -> Span { + let ExceptSelectItem { + first_element, + additional_elements, + } = self; + + union_spans( + iter::once(first_element.span).chain(additional_elements.iter().map(|i| i.span)), + ) + } +} + +impl Spanned for ReplaceSelectItem { + fn span(&self) -> Span { + let ReplaceSelectItem { items } = self; + + union_spans(items.iter().map(|i| i.span())) + } +} + +impl Spanned for ReplaceSelectElement { + fn span(&self) -> Span { + let ReplaceSelectElement { + expr, + column_name, + as_keyword: _, // bool + } = self; + + expr.span().union(&column_name.span) + } +} + +/// # partial span +/// +/// Missing spans: +/// - [TableFactor::JsonTable] +impl Spanned for TableFactor { + fn span(&self) -> Span { + match self { + TableFactor::Table { + name, + alias, + args: _, + with_hints: _, + version: _, + with_ordinality: _, + partitions: _, + json_path: _, + } => union_spans( + name.0 + .iter() + .map(|i| i.span) + .chain(alias.as_ref().map(|alias| { + union_spans( + iter::once(alias.name.span) + .chain(alias.columns.iter().map(|i| i.span())), + ) + })), + ), + TableFactor::Derived { + lateral: _, + subquery, + alias, + } => subquery + .span() + .union_opt(&alias.as_ref().map(|alias| alias.span())), + TableFactor::TableFunction { expr, alias } => expr + .span() + .union_opt(&alias.as_ref().map(|alias| alias.span())), + TableFactor::UNNEST { + alias, + with_offset: _, + with_offset_alias, + array_exprs, + with_ordinality: _, + } => union_spans( + alias + .iter() + .map(|i| i.span()) + .chain(array_exprs.iter().map(|i| i.span())) + .chain(with_offset_alias.as_ref().map(|i| i.span)), + ), + TableFactor::NestedJoin { + table_with_joins, + alias, + } => table_with_joins + .span() + .union_opt(&alias.as_ref().map(|alias| alias.span())), + TableFactor::Function { + lateral: _, + name, + args, + alias, + } => union_spans( + name.0 + .iter() + .map(|i| i.span) + .chain(args.iter().map(|i| i.span())) + .chain(alias.as_ref().map(|alias| alias.span())), + ), + TableFactor::JsonTable { .. } => Span::empty(), + TableFactor::Pivot { + table, + aggregate_functions, + value_column, + value_source, + default_on_null, + alias, + } => union_spans( + core::iter::once(table.span()) + .chain(aggregate_functions.iter().map(|i| i.span())) + .chain(value_column.iter().map(|i| i.span)) + .chain(core::iter::once(value_source.span())) + .chain(default_on_null.as_ref().map(|i| i.span())) + .chain(alias.as_ref().map(|i| i.span())), + ), + TableFactor::Unpivot { + table, + value, + name, + columns, + alias, + } => union_spans( + core::iter::once(table.span()) + .chain(core::iter::once(value.span)) + .chain(core::iter::once(name.span)) + .chain(columns.iter().map(|i| i.span)) + .chain(alias.as_ref().map(|alias| alias.span())), + ), + TableFactor::MatchRecognize { + table, + partition_by, + order_by, + measures, + rows_per_match: _, + after_match_skip: _, + pattern, + symbols, + alias, + } => union_spans( + core::iter::once(table.span()) + .chain(partition_by.iter().map(|i| i.span())) + .chain(order_by.iter().map(|i| i.span())) + .chain(measures.iter().map(|i| i.span())) + .chain(core::iter::once(pattern.span())) + .chain(symbols.iter().map(|i| i.span())) + .chain(alias.as_ref().map(|i| i.span())), + ), + TableFactor::OpenJsonTable { .. } => Span::empty(), + } + } +} + +impl Spanned for PivotValueSource { + fn span(&self) -> Span { + match self { + PivotValueSource::List(vec) => union_spans(vec.iter().map(|i| i.span())), + PivotValueSource::Any(vec) => union_spans(vec.iter().map(|i| i.span())), + PivotValueSource::Subquery(query) => query.span(), + } + } +} + +impl Spanned for ExprWithAlias { + fn span(&self) -> Span { + let ExprWithAlias { expr, alias } = self; + + expr.span().union_opt(&alias.as_ref().map(|i| i.span)) + } +} + +/// # missing span +impl Spanned for MatchRecognizePattern { + fn span(&self) -> Span { + Span::empty() + } +} + +impl Spanned for SymbolDefinition { + fn span(&self) -> Span { + let SymbolDefinition { symbol, definition } = self; + + symbol.span.union(&definition.span()) + } +} + +impl Spanned for Measure { + fn span(&self) -> Span { + let Measure { expr, alias } = self; + + expr.span().union(&alias.span) + } +} + +impl Spanned for OrderByExpr { + fn span(&self) -> Span { + let OrderByExpr { + expr, + asc: _, // bool + nulls_first: _, // bool + with_fill, + } = self; + + expr.span().union_opt(&with_fill.as_ref().map(|f| f.span())) + } +} + +impl Spanned for WithFill { + fn span(&self) -> Span { + let WithFill { from, to, step } = self; + + union_spans( + from.iter() + .map(|f| f.span()) + .chain(to.iter().map(|t| t.span())) + .chain(step.iter().map(|s| s.span())), + ) + } +} + +impl Spanned for FunctionArg { + fn span(&self) -> Span { + match self { + FunctionArg::Named { + name, + arg, + operator: _, + } => name.span.union(&arg.span()), + FunctionArg::Unnamed(arg) => arg.span(), + FunctionArg::ExprNamed { + name, + arg, + operator: _, + } => name.span().union(&arg.span()), + } + } +} + +/// # partial span +/// +/// Missing spans: +/// - [FunctionArgExpr::Wildcard] +impl Spanned for FunctionArgExpr { + fn span(&self) -> Span { + match self { + FunctionArgExpr::Expr(expr) => expr.span(), + FunctionArgExpr::QualifiedWildcard(object_name) => { + union_spans(object_name.0.iter().map(|i| i.span)) + } + FunctionArgExpr::Wildcard => Span::empty(), + } + } +} + +impl Spanned for TableAlias { + fn span(&self) -> Span { + let TableAlias { name, columns } = self; + + union_spans(iter::once(name.span).chain(columns.iter().map(|i| i.span()))) + } +} + +impl Spanned for TableAliasColumnDef { + fn span(&self) -> Span { + let TableAliasColumnDef { name, data_type: _ } = self; + + name.span + } +} + +/// # missing span +/// +/// The span of a `Value` is currently not implemented, as doing so +/// requires a breaking changes, which may be done in a future release. +impl Spanned for Value { + fn span(&self) -> Span { + Span::empty() // # todo: Value needs to store spans before this is possible + } +} + +impl Spanned for Join { + fn span(&self) -> Span { + let Join { + relation, + global: _, // bool + join_operator, + } = self; + + relation.span().union(&join_operator.span()) + } +} + +/// # partial span +/// +/// Missing spans: +/// - [JoinOperator::CrossJoin] +/// - [JoinOperator::CrossApply] +/// - [JoinOperator::OuterApply] +impl Spanned for JoinOperator { + fn span(&self) -> Span { + match self { + JoinOperator::Inner(join_constraint) => join_constraint.span(), + JoinOperator::LeftOuter(join_constraint) => join_constraint.span(), + JoinOperator::RightOuter(join_constraint) => join_constraint.span(), + JoinOperator::FullOuter(join_constraint) => join_constraint.span(), + JoinOperator::CrossJoin => Span::empty(), + JoinOperator::LeftSemi(join_constraint) => join_constraint.span(), + JoinOperator::RightSemi(join_constraint) => join_constraint.span(), + JoinOperator::LeftAnti(join_constraint) => join_constraint.span(), + JoinOperator::RightAnti(join_constraint) => join_constraint.span(), + JoinOperator::CrossApply => Span::empty(), + JoinOperator::OuterApply => Span::empty(), + JoinOperator::AsOf { + match_condition, + constraint, + } => match_condition.span().union(&constraint.span()), + JoinOperator::Anti(join_constraint) => join_constraint.span(), + JoinOperator::Semi(join_constraint) => join_constraint.span(), + } + } +} + +/// # partial span +/// +/// Missing spans: +/// - [JoinConstraint::Natural] +/// - [JoinConstraint::None] +impl Spanned for JoinConstraint { + fn span(&self) -> Span { + match self { + JoinConstraint::On(expr) => expr.span(), + JoinConstraint::Using(vec) => union_spans(vec.iter().map(|i| i.span)), + JoinConstraint::Natural => Span::empty(), + JoinConstraint::None => Span::empty(), + } + } +} + +impl Spanned for TableWithJoins { + fn span(&self) -> Span { + let TableWithJoins { relation, joins } = self; + + union_spans(core::iter::once(relation.span()).chain(joins.iter().map(|item| item.span()))) + } +} + +impl Spanned for Select { + fn span(&self) -> Span { + let Select { + select_token, + distinct: _, // todo + top: _, // todo, mysql specific + projection, + into, + from, + lateral_views, + prewhere, + selection, + group_by, + cluster_by, + distribute_by, + sort_by, + having, + named_window, + qualify, + window_before_qualify: _, // bool + value_table_mode: _, // todo, BigQuery specific + connect_by, + top_before_distinct: _, + } = self; + + union_spans( + core::iter::once(select_token.0.span) + .chain(projection.iter().map(|item| item.span())) + .chain(into.iter().map(|item| item.span())) + .chain(from.iter().map(|item| item.span())) + .chain(lateral_views.iter().map(|item| item.span())) + .chain(prewhere.iter().map(|item| item.span())) + .chain(selection.iter().map(|item| item.span())) + .chain(core::iter::once(group_by.span())) + .chain(cluster_by.iter().map(|item| item.span())) + .chain(distribute_by.iter().map(|item| item.span())) + .chain(sort_by.iter().map(|item| item.span())) + .chain(having.iter().map(|item| item.span())) + .chain(named_window.iter().map(|item| item.span())) + .chain(qualify.iter().map(|item| item.span())) + .chain(connect_by.iter().map(|item| item.span())), + ) + } +} + +impl Spanned for ConnectBy { + fn span(&self) -> Span { + let ConnectBy { + condition, + relationships, + } = self; + + union_spans( + core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())), + ) + } +} + +impl Spanned for NamedWindowDefinition { + fn span(&self) -> Span { + let NamedWindowDefinition( + ident, + _, // todo: NamedWindowExpr + ) = self; + + ident.span + } +} + +impl Spanned for LateralView { + fn span(&self) -> Span { + let LateralView { + lateral_view, + lateral_view_name, + lateral_col_alias, + outer: _, // bool + } = self; + + union_spans( + core::iter::once(lateral_view.span()) + .chain(core::iter::once(lateral_view_name.span())) + .chain(lateral_col_alias.iter().map(|i| i.span)), + ) + } +} + +impl Spanned for SelectInto { + fn span(&self) -> Span { + let SelectInto { + temporary: _, // bool + unlogged: _, // bool + table: _, // bool + name, + } = self; + + name.span() + } +} + +#[cfg(test)] +pub mod tests { + use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; + use crate::parser::Parser; + use crate::tokenizer::Span; + + use super::*; + + struct SpanTest<'a>(Parser<'a>, &'a str); + + impl<'a> SpanTest<'a> { + fn new(dialect: &'a dyn Dialect, sql: &'a str) -> Self { + Self(Parser::new(dialect).try_with_sql(sql).unwrap(), sql) + } + + // get the subsection of the source string that corresponds to the span + // only works on single-line strings + fn get_source(&self, span: Span) -> &'a str { + // lines in spans are 1-indexed + &self.1[(span.start.column as usize - 1)..(span.end.column - 1) as usize] + } + } + + #[test] + fn test_join() { + let dialect = &GenericDialect; + let mut test = SpanTest::new( + dialect, + "SELECT id, name FROM users LEFT JOIN companies ON users.company_id = companies.id", + ); + + let query = test.0.parse_select().unwrap(); + let select_span = query.span(); + + assert_eq!( + test.get_source(select_span), + "SELECT id, name FROM users LEFT JOIN companies ON users.company_id = companies.id" + ); + + let join_span = query.from[0].joins[0].span(); + + // 'LEFT JOIN' missing + assert_eq!( + test.get_source(join_span), + "companies ON users.company_id = companies.id" + ); + } + + #[test] + pub fn test_union() { + let dialect = &GenericDialect; + let mut test = SpanTest::new( + dialect, + "SELECT a FROM postgres.public.source UNION SELECT a FROM postgres.public.source", + ); + + let query = test.0.parse_query().unwrap(); + let select_span = query.span(); + + assert_eq!( + test.get_source(select_span), + "SELECT a FROM postgres.public.source UNION SELECT a FROM postgres.public.source" + ); + } + + #[test] + pub fn test_subquery() { + let dialect = &GenericDialect; + let mut test = SpanTest::new( + dialect, + "SELECT a FROM (SELECT a FROM postgres.public.source) AS b", + ); + + let query = test.0.parse_select().unwrap(); + let select_span = query.span(); + + assert_eq!( + test.get_source(select_span), + "SELECT a FROM (SELECT a FROM postgres.public.source) AS b" + ); + + let subquery_span = query.from[0].span(); + + // left paren missing + assert_eq!( + test.get_source(subquery_span), + "SELECT a FROM postgres.public.source) AS b" + ); + } + + #[test] + pub fn test_cte() { + let dialect = &GenericDialect; + let mut test = SpanTest::new(dialect, "WITH cte_outer AS (SELECT a FROM postgres.public.source), cte_ignored AS (SELECT a FROM cte_outer), cte_inner AS (SELECT a FROM cte_outer) SELECT a FROM cte_inner"); + + let query = test.0.parse_query().unwrap(); + + let select_span = query.span(); + + assert_eq!(test.get_source(select_span), "WITH cte_outer AS (SELECT a FROM postgres.public.source), cte_ignored AS (SELECT a FROM cte_outer), cte_inner AS (SELECT a FROM cte_outer) SELECT a FROM cte_inner"); + } + + #[test] + pub fn test_snowflake_lateral_flatten() { + let dialect = &SnowflakeDialect; + let mut test = SpanTest::new(dialect, "SELECT FLATTENED.VALUE:field::TEXT AS FIELD FROM SNOWFLAKE.SCHEMA.SOURCE AS S, LATERAL FLATTEN(INPUT => S.JSON_ARRAY) AS FLATTENED"); + + let query = test.0.parse_select().unwrap(); + + let select_span = query.span(); + + assert_eq!(test.get_source(select_span), "SELECT FLATTENED.VALUE:field::TEXT AS FIELD FROM SNOWFLAKE.SCHEMA.SOURCE AS S, LATERAL FLATTEN(INPUT => S.JSON_ARRAY) AS FLATTENED"); + } + + #[test] + pub fn test_wildcard_from_cte() { + let dialect = &GenericDialect; + let mut test = SpanTest::new( + dialect, + "WITH cte AS (SELECT a FROM postgres.public.source) SELECT cte.* FROM cte", + ); + + let query = test.0.parse_query().unwrap(); + let cte_span = query.clone().with.unwrap().cte_tables[0].span(); + let cte_query_span = query.clone().with.unwrap().cte_tables[0].query.span(); + let body_span = query.body.span(); + + // the WITH keyboard is part of the query + assert_eq!( + test.get_source(cte_span), + "cte AS (SELECT a FROM postgres.public.source)" + ); + assert_eq!( + test.get_source(cte_query_span), + "SELECT a FROM postgres.public.source" + ); + + assert_eq!(test.get_source(body_span), "SELECT cte.* FROM cte"); + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6767f358a..b7f5cb866 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -24,6 +24,7 @@ use core::{ fmt::{self, Display}, str::FromStr, }; +use helpers::attached_token::AttachedToken; use log::debug; @@ -371,7 +372,7 @@ impl<'a> Parser<'a> { .into_iter() .map(|token| TokenWithLocation { token, - location: Location { line: 0, column: 0 }, + span: Span::empty(), }) .collect(); self.with_tokens_with_locations(tokens_with_locations) @@ -613,7 +614,7 @@ impl<'a> Parser<'a> { let mut export = false; if !dialect_of!(self is MySqlDialect | GenericDialect) { - return parser_err!("Unsupported statement FLUSH", self.peek_token().location); + return parser_err!("Unsupported statement FLUSH", self.peek_token().span.start); } let location = if self.parse_keyword(Keyword::NO_WRITE_TO_BINLOG) { @@ -914,7 +915,7 @@ impl<'a> Parser<'a> { t @ (Token::Word(_) | Token::SingleQuotedString(_)) => { if self.peek_token().token == Token::Period { let mut id_parts: Vec = vec![match t { - Token::Word(w) => w.to_ident(), + Token::Word(w) => w.to_ident(next_token.span), Token::SingleQuotedString(s) => Ident::with_quote('\'', s), _ => unreachable!(), // We matched above }]; @@ -922,13 +923,16 @@ impl<'a> Parser<'a> { while self.consume_token(&Token::Period) { let next_token = self.next_token(); match next_token.token { - Token::Word(w) => id_parts.push(w.to_ident()), + Token::Word(w) => id_parts.push(w.to_ident(next_token.span)), Token::SingleQuotedString(s) => { // SQLite has single-quoted identifiers id_parts.push(Ident::with_quote('\'', s)) } Token::Mul => { - return Ok(Expr::QualifiedWildcard(ObjectName(id_parts))); + return Ok(Expr::QualifiedWildcard( + ObjectName(id_parts), + AttachedToken(next_token), + )); } _ => { return self @@ -939,7 +943,7 @@ impl<'a> Parser<'a> { } } Token::Mul => { - return Ok(Expr::Wildcard); + return Ok(Expr::Wildcard(AttachedToken(next_token))); } _ => (), }; @@ -1002,7 +1006,7 @@ impl<'a> Parser<'a> { pub fn parse_unlisten(&mut self) -> Result { let channel = if self.consume_token(&Token::Mul) { - Ident::new(Expr::Wildcard.to_string()) + Ident::new(Expr::Wildcard(AttachedToken::empty()).to_string()) } else { match self.parse_identifier(false) { Ok(expr) => expr, @@ -1030,6 +1034,7 @@ impl<'a> Parser<'a> { fn parse_expr_prefix_by_reserved_word( &mut self, w: &Word, + w_span: Span, ) -> Result, ParserError> { match w.keyword { Keyword::TRUE | Keyword::FALSE if self.dialect.supports_boolean_literals() => { @@ -1047,7 +1052,7 @@ impl<'a> Parser<'a> { if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.to_ident()]), + name: ObjectName(vec![w.to_ident(w_span)]), parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, @@ -1061,7 +1066,7 @@ impl<'a> Parser<'a> { | Keyword::CURRENT_DATE | Keyword::LOCALTIME | Keyword::LOCALTIMESTAMP => { - Ok(Some(self.parse_time_functions(ObjectName(vec![w.to_ident()]))?)) + Ok(Some(self.parse_time_functions(ObjectName(vec![w.to_ident(w_span)]))?)) } Keyword::CASE => Ok(Some(self.parse_case_expr()?)), Keyword::CONVERT => Ok(Some(self.parse_convert_expr(false)?)), @@ -1086,7 +1091,7 @@ impl<'a> Parser<'a> { Keyword::CEIL => Ok(Some(self.parse_ceil_floor_expr(true)?)), Keyword::FLOOR => Ok(Some(self.parse_ceil_floor_expr(false)?)), Keyword::POSITION if self.peek_token().token == Token::LParen => { - Ok(Some(self.parse_position_expr(w.to_ident())?)) + Ok(Some(self.parse_position_expr(w.to_ident(w_span))?)) } Keyword::SUBSTRING => Ok(Some(self.parse_substring_expr()?)), Keyword::OVERLAY => Ok(Some(self.parse_overlay_expr()?)), @@ -1105,7 +1110,7 @@ impl<'a> Parser<'a> { let query = self.parse_query()?; self.expect_token(&Token::RParen)?; Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.to_ident()]), + name: ObjectName(vec![w.to_ident(w_span)]), parameters: FunctionArguments::None, args: FunctionArguments::Subquery(query), filter: None, @@ -1134,20 +1139,24 @@ impl<'a> Parser<'a> { } // Tries to parse an expression by a word that is not known to have a special meaning in the dialect. - fn parse_expr_prefix_by_unreserved_word(&mut self, w: &Word) -> Result { + fn parse_expr_prefix_by_unreserved_word( + &mut self, + w: &Word, + w_span: Span, + ) -> Result { match self.peek_token().token { Token::LParen | Token::Period => { - let mut id_parts: Vec = vec![w.to_ident()]; - let mut ends_with_wildcard = false; + let mut id_parts: Vec = vec![w.to_ident(w_span)]; + let mut ending_wildcard: Option = None; while self.consume_token(&Token::Period) { let next_token = self.next_token(); match next_token.token { - Token::Word(w) => id_parts.push(w.to_ident()), + Token::Word(w) => id_parts.push(w.to_ident(next_token.span)), Token::Mul => { // Postgres explicitly allows funcnm(tablenm.*) and the // function array_agg traverses this control flow if dialect_of!(self is PostgreSqlDialect) { - ends_with_wildcard = true; + ending_wildcard = Some(next_token); break; } else { return self.expected("an identifier after '.'", next_token); @@ -1160,8 +1169,11 @@ impl<'a> Parser<'a> { } } - if ends_with_wildcard { - Ok(Expr::QualifiedWildcard(ObjectName(id_parts))) + if let Some(wildcard_token) = ending_wildcard { + Ok(Expr::QualifiedWildcard( + ObjectName(id_parts), + AttachedToken(wildcard_token), + )) } else if self.consume_token(&Token::LParen) { if dialect_of!(self is SnowflakeDialect | MsSqlDialect) && self.consume_tokens(&[Token::Plus, Token::RParen]) @@ -1194,11 +1206,11 @@ impl<'a> Parser<'a> { Token::Arrow if self.dialect.supports_lambda_functions() => { self.expect_token(&Token::Arrow)?; Ok(Expr::Lambda(LambdaFunction { - params: OneOrManyWithParens::One(w.to_ident()), + params: OneOrManyWithParens::One(w.to_ident(w_span)), body: Box::new(self.parse_expr()?), })) } - _ => Ok(Expr::Identifier(w.to_ident())), + _ => Ok(Expr::Identifier(w.to_ident(w_span))), } } @@ -1225,7 +1237,7 @@ impl<'a> Parser<'a> { // Note also that naively `SELECT date` looks like a syntax error because the `date` type // name is not followed by a string literal, but in fact in PostgreSQL it is a valid // expression that should parse as the column name "date". - let loc = self.peek_token().location; + let loc = self.peek_token().span.start; let opt_expr = self.maybe_parse(|parser| { match parser.parse_data_type()? { DataType::Interval => parser.parse_interval(), @@ -1259,12 +1271,14 @@ impl<'a> Parser<'a> { // // We first try to parse the word and following tokens as a special expression, and if that fails, // we rollback and try to parse it as an identifier. - match self.try_parse(|parser| parser.parse_expr_prefix_by_reserved_word(&w)) { + match self.try_parse(|parser| { + parser.parse_expr_prefix_by_reserved_word(&w, next_token.span) + }) { // This word indicated an expression prefix and parsing was successful Ok(Some(expr)) => Ok(expr), // No expression prefix associated with this word - Ok(None) => Ok(self.parse_expr_prefix_by_unreserved_word(&w)?), + Ok(None) => Ok(self.parse_expr_prefix_by_unreserved_word(&w, next_token.span)?), // If parsing of the word as a special expression failed, we are facing two options: // 1. The statement is malformed, e.g. `SELECT INTERVAL '1 DAI` (`DAI` instead of `DAY`) @@ -1275,7 +1289,7 @@ impl<'a> Parser<'a> { Err(e) => { if !self.dialect.is_reserved_for_identifier(w.keyword) { if let Ok(Some(expr)) = self.maybe_parse(|parser| { - parser.parse_expr_prefix_by_unreserved_word(&w) + parser.parse_expr_prefix_by_unreserved_word(&w, next_token.span) }) { return Ok(expr); } @@ -1377,11 +1391,11 @@ impl<'a> Parser<'a> { } else { let tok = self.next_token(); let key = match tok.token { - Token::Word(word) => word.to_ident(), + Token::Word(word) => word.to_ident(tok.span), _ => { return parser_err!( format!("Expected identifier, found: {tok}"), - tok.location + tok.span.start ) } }; @@ -1471,7 +1485,7 @@ impl<'a> Parser<'a> { while p.consume_token(&Token::Period) { let tok = p.next_token(); let name = match tok.token { - Token::Word(word) => word.to_ident(), + Token::Word(word) => word.to_ident(tok.span), _ => return p.expected("identifier", tok), }; let func = match p.parse_function(ObjectName(vec![name]))? { @@ -2290,7 +2304,7 @@ impl<'a> Parser<'a> { } else if self.dialect.require_interval_qualifier() { return parser_err!( "INTERVAL requires a unit after the literal value", - self.peek_token().location + self.peek_token().span.start ); } else { None @@ -2381,7 +2395,10 @@ impl<'a> Parser<'a> { let (fields, trailing_bracket) = self.parse_struct_type_def(Self::parse_struct_field_def)?; if trailing_bracket.0 { - return parser_err!("unmatched > in STRUCT literal", self.peek_token().location); + return parser_err!( + "unmatched > in STRUCT literal", + self.peek_token().span.start + ); } self.expect_token(&Token::LParen)?; @@ -2411,7 +2428,7 @@ impl<'a> Parser<'a> { if typed_syntax { return parser_err!("Typed syntax does not allow AS", { self.prev_token(); - self.peek_token().location + self.peek_token().span.start }); } let field_name = self.parse_identifier(false)?; @@ -2464,7 +2481,7 @@ impl<'a> Parser<'a> { // 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.location); + return parser_err!("unmatched > in STRUCT definition", start_token.span.start); } }; @@ -2833,7 +2850,7 @@ impl<'a> Parser<'a> { format!( "Expected one of [=, >, <, =>, =<, !=] as comparison operator, found: {op}" ), - tok.location + tok.span.start ); }; @@ -2959,7 +2976,7 @@ impl<'a> Parser<'a> { // Can only happen if `get_next_precedence` got out of sync with this function _ => parser_err!( format!("No infix parser for token {:?}", tok.token), - tok.location + tok.span.start ), } } else if Token::DoubleColon == tok { @@ -2990,7 +3007,7 @@ impl<'a> Parser<'a> { // Can only happen if `get_next_precedence` got out of sync with this function parser_err!( format!("No infix parser for token {:?}", tok.token), - tok.location + tok.span.start ) } } @@ -3298,14 +3315,14 @@ impl<'a> Parser<'a> { index += 1; if let Some(TokenWithLocation { token: Token::Whitespace(_), - location: _, + span: _, }) = token { continue; } break token.cloned().unwrap_or(TokenWithLocation { token: Token::EOF, - location: Location { line: 0, column: 0 }, + span: Span::empty(), }); }) } @@ -3318,13 +3335,13 @@ impl<'a> Parser<'a> { match self.tokens.get(index - 1) { Some(TokenWithLocation { token: Token::Whitespace(_), - location: _, + span: _, }) => continue, non_whitespace => { if n == 0 { return non_whitespace.cloned().unwrap_or(TokenWithLocation { token: Token::EOF, - location: Location { line: 0, column: 0 }, + span: Span::empty(), }); } n -= 1; @@ -3346,18 +3363,10 @@ impl<'a> Parser<'a> { .cloned() .unwrap_or(TokenWithLocation { token: Token::EOF, - location: Location { line: 0, column: 0 }, + span: Span::empty(), }) } - /// Look for all of the expected keywords in sequence, without consuming them - fn peek_keyword(&mut self, expected: Keyword) -> bool { - let index = self.index; - let matched = self.parse_keyword(expected); - self.index = index; - matched - } - /// Look for all of the expected keywords in sequence, without consuming them fn peek_keywords(&mut self, expected: &[Keyword]) -> bool { let index = self.index; @@ -3375,7 +3384,7 @@ impl<'a> Parser<'a> { match self.tokens.get(self.index - 1) { Some(TokenWithLocation { token: Token::Whitespace(_), - location: _, + span: _, }) => continue, token => { return token @@ -3401,7 +3410,7 @@ impl<'a> Parser<'a> { self.index -= 1; if let Some(TokenWithLocation { token: Token::Whitespace(_), - location: _, + span: _, }) = self.tokens.get(self.index) { continue; @@ -3414,7 +3423,7 @@ impl<'a> Parser<'a> { pub fn expected(&self, expected: &str, found: TokenWithLocation) -> Result { parser_err!( format!("Expected: {expected}, found: {found}"), - found.location + found.span.start ) } @@ -3422,15 +3431,22 @@ impl<'a> Parser<'a> { /// true. Otherwise, no tokens are consumed and returns false. #[must_use] pub fn parse_keyword(&mut self, expected: Keyword) -> bool { + self.parse_keyword_token(expected).is_some() + } + + #[must_use] + pub fn parse_keyword_token(&mut self, expected: Keyword) -> Option { match self.peek_token().token { - Token::Word(w) if expected == w.keyword => { - self.next_token(); - true - } - _ => false, + Token::Word(w) if expected == w.keyword => Some(self.next_token()), + _ => None, } } + #[must_use] + pub fn peek_keyword(&mut self, expected: Keyword) -> bool { + matches!(self.peek_token().token, Token::Word(w) if expected == w.keyword) + } + /// If the current token is the `expected` keyword followed by /// specified tokens, consume them and returns true. /// Otherwise, no tokens are consumed and returns false. @@ -3508,9 +3524,9 @@ impl<'a> Parser<'a> { /// If the current token is the `expected` keyword, consume the token. /// Otherwise, return an error. - pub fn expect_keyword(&mut self, expected: Keyword) -> Result<(), ParserError> { - if self.parse_keyword(expected) { - Ok(()) + pub fn expect_keyword(&mut self, expected: Keyword) -> Result { + if let Some(token) = self.parse_keyword_token(expected) { + Ok(token) } else { self.expected(format!("{:?}", &expected).as_str(), self.peek_token()) } @@ -3552,9 +3568,9 @@ impl<'a> Parser<'a> { } /// Bail out if the current token is not an expected keyword, or consume it if it is - pub fn expect_token(&mut self, expected: &Token) -> Result<(), ParserError> { - if self.consume_token(expected) { - Ok(()) + pub fn expect_token(&mut self, expected: &Token) -> Result { + if self.peek_token() == *expected { + Ok(self.next_token()) } else { self.expected(&expected.to_string(), self.peek_token()) } @@ -3749,7 +3765,7 @@ impl<'a> Parser<'a> { /// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns [`None`] if `ALL` is parsed /// and results in a [`ParserError`] if both `ALL` and `DISTINCT` are found. pub fn parse_all_or_distinct(&mut self) -> Result, ParserError> { - let loc = self.peek_token().location; + let loc = self.peek_token().span.start; let all = self.parse_keyword(Keyword::ALL); let distinct = self.parse_keyword(Keyword::DISTINCT); if !distinct { @@ -4828,7 +4844,7 @@ impl<'a> Parser<'a> { let loc = self .tokens .get(self.index - 1) - .map_or(Location { line: 0, column: 0 }, |t| t.location); + .map_or(Location { line: 0, column: 0 }, |t| t.span.start); match keyword { Keyword::AUTHORIZATION => { if authorization_owner.is_some() { @@ -5138,7 +5154,7 @@ impl<'a> Parser<'a> { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let names = self.parse_comma_separated(|p| p.parse_object_name(false))?; - let loc = self.peek_token().location; + let loc = self.peek_token().span.start; let cascade = self.parse_keyword(Keyword::CASCADE); let restrict = self.parse_keyword(Keyword::RESTRICT); let purge = self.parse_keyword(Keyword::PURGE); @@ -6029,7 +6045,7 @@ impl<'a> Parser<'a> { 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.location)?), + Token::Number(s, _) => Some(Self::parse::(s, next_token.span.start)?), _ => self.expected("literal int", next_token)?, } } else { @@ -6818,7 +6834,7 @@ impl<'a> Parser<'a> { "FULLTEXT or SPATIAL option without constraint name", TokenWithLocation { token: Token::make_keyword(&name.to_string()), - location: next_token.location, + span: next_token.span, }, ); } @@ -7527,7 +7543,7 @@ impl<'a> Parser<'a> { Expr::Function(f) => Ok(Statement::Call(f)), other => parser_err!( format!("Expected a simple procedure call but found: {other}"), - self.peek_token().location + self.peek_token().span.start ), } } else { @@ -7731,7 +7747,7 @@ impl<'a> Parser<'a> { let loc = self .tokens .get(self.index - 1) - .map_or(Location { line: 0, column: 0 }, |t| t.location); + .map_or(Location { line: 0, column: 0 }, |t| t.span.start); return parser_err!(format!("Expect a char, found {s:?}"), loc); } Ok(s.chars().next().unwrap()) @@ -7777,7 +7793,7 @@ impl<'a> Parser<'a> { /// Parse a literal value (numbers, strings, date/time, booleans) pub fn parse_value(&mut self) -> Result { let next_token = self.next_token(); - let location = next_token.location; + let span = next_token.span; match next_token.token { Token::Word(w) => match w.keyword { Keyword::TRUE if self.dialect.supports_boolean_literals() => { @@ -7794,7 +7810,7 @@ impl<'a> Parser<'a> { "A value?", TokenWithLocation { token: Token::Word(w), - location, + span, }, )?, }, @@ -7802,14 +7818,14 @@ impl<'a> Parser<'a> { "a concrete value", TokenWithLocation { token: Token::Word(w), - location, + span, }, ), }, // The call to n.parse() returns a bigdecimal when the // bigdecimal feature is enabled, and is otherwise a no-op // (i.e., it returns the input string). - Token::Number(n, l) => Ok(Value::Number(Self::parse(n, location)?, l)), + Token::Number(n, l) => Ok(Value::Number(Self::parse(n, span.start)?, l)), Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), Token::TripleSingleQuotedString(ref s) => { @@ -7853,7 +7869,7 @@ impl<'a> Parser<'a> { // This because snowflake allows numbers as placeholders let next_token = self.next_token(); let ident = match next_token.token { - Token::Word(w) => Ok(w.to_ident()), + Token::Word(w) => Ok(w.to_ident(next_token.span)), Token::Number(w, false) => Ok(Ident::new(w)), _ => self.expected("placeholder", next_token), }?; @@ -7864,7 +7880,7 @@ impl<'a> Parser<'a> { "a value", TokenWithLocation { token: unexpected, - location, + span, }, ), } @@ -7904,7 +7920,7 @@ impl<'a> Parser<'a> { fn parse_introduced_string_value(&mut self) -> Result { let next_token = self.next_token(); - let location = next_token.location; + 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())), @@ -7913,7 +7929,7 @@ impl<'a> Parser<'a> { "a string value", TokenWithLocation { token: unexpected, - location, + span, }, ), } @@ -7923,7 +7939,7 @@ impl<'a> Parser<'a> { pub fn parse_literal_uint(&mut self) -> Result { let next_token = self.next_token(); match next_token.token { - Token::Number(s, _) => Self::parse::(s, next_token.location), + Token::Number(s, _) => Self::parse::(s, next_token.span.start), _ => self.expected("literal int", next_token), } } @@ -8322,7 +8338,7 @@ impl<'a> Parser<'a> { // (For example, in `FROM t1 JOIN` the `JOIN` will always be parsed as a keyword, // not an alias.) Token::Word(w) if after_as || !reserved_kwds.contains(&w.keyword) => { - Ok(Some(w.to_ident())) + Ok(Some(w.to_ident(next_token.span))) } // MSSQL supports single-quoted strings as aliases for columns // We accept them as table aliases too, although MSSQL does not. @@ -8392,7 +8408,7 @@ impl<'a> Parser<'a> { _ => { return parser_err!( "BUG: expected to match GroupBy modifier keyword", - self.peek_token().location + self.peek_token().span.start ) } }); @@ -8455,6 +8471,7 @@ impl<'a> Parser<'a> { .map(|value| Ident { value: value.into(), quote_style: ident.quote_style, + span: ident.span, }) .collect::>() }) @@ -8470,7 +8487,7 @@ impl<'a> Parser<'a> { loop { match self.peek_token().token { Token::Word(w) => { - idents.push(w.to_ident()); + idents.push(w.to_ident(self.peek_token().span)); } Token::EOF | Token::Eq => break, _ => {} @@ -8523,8 +8540,9 @@ impl<'a> Parser<'a> { let mut idents = vec![]; // expecting at least one word for identifier - match self.next_token().token { - Token::Word(w) => idents.push(w.to_ident()), + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) => idents.push(w.to_ident(next_token.span)), Token::EOF => { return Err(ParserError::ParserError( "Empty input when parsing identifier".to_string(), @@ -8541,19 +8559,22 @@ impl<'a> Parser<'a> { loop { match self.next_token().token { // ensure that optional period is succeeded by another identifier - Token::Period => match self.next_token().token { - Token::Word(w) => idents.push(w.to_ident()), - Token::EOF => { - return Err(ParserError::ParserError( - "Trailing period in identifier".to_string(), - ))? - } - token => { - return Err(ParserError::ParserError(format!( - "Unexpected token following period in identifier: {token}" - )))? + Token::Period => { + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) => idents.push(w.to_ident(next_token.span)), + Token::EOF => { + return Err(ParserError::ParserError( + "Trailing period in identifier".to_string(), + ))? + } + token => { + return Err(ParserError::ParserError(format!( + "Unexpected token following period in identifier: {token}" + )))? + } } - }, + } Token::EOF => break, token => { return Err(ParserError::ParserError(format!( @@ -8575,7 +8596,7 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { Token::Word(w) => { - let mut ident = w.to_ident(); + let mut ident = w.to_ident(next_token.span); // On BigQuery, hyphens are permitted in unquoted identifiers inside of a FROM or // TABLE clause [0]. @@ -9006,8 +9027,9 @@ impl<'a> Parser<'a> { /// expect the initial keyword to be already consumed pub fn parse_query(&mut self) -> Result, ParserError> { let _guard = self.recursion_counter.try_decrease()?; - let with = if self.parse_keyword(Keyword::WITH) { + let with = if let Some(with_token) = self.parse_keyword_token(Keyword::WITH) { Some(With { + with_token: with_token.into(), recursive: self.parse_keyword(Keyword::RECURSIVE), cte_tables: self.parse_comma_separated(Parser::parse_cte)?, }) @@ -9265,8 +9287,10 @@ impl<'a> Parser<'a> { } } self.expect_token(&Token::LParen)?; + let query = self.parse_query()?; - self.expect_token(&Token::RParen)?; + let closing_paren_token = self.expect_token(&Token::RParen)?; + let alias = TableAlias { name, columns: vec![], @@ -9276,6 +9300,7 @@ impl<'a> Parser<'a> { query, from: None, materialized: is_materialized, + closing_paren_token: closing_paren_token.into(), } } else { let columns = self.parse_table_alias_column_defs()?; @@ -9289,14 +9314,17 @@ impl<'a> Parser<'a> { } } self.expect_token(&Token::LParen)?; + let query = self.parse_query()?; - self.expect_token(&Token::RParen)?; + let closing_paren_token = self.expect_token(&Token::RParen)?; + let alias = TableAlias { name, columns }; Cte { alias, query, from: None, materialized: is_materialized, + closing_paren_token: closing_paren_token.into(), } }; if self.parse_keyword(Keyword::FROM) { @@ -9316,7 +9344,7 @@ impl<'a> Parser<'a> { pub fn parse_query_body(&mut self, precedence: u8) -> Result, ParserError> { // We parse the expression using a Pratt parser, as in `parse_expr()`. // Start by parsing a restricted SELECT or a `(subquery)`: - let expr = if self.parse_keyword(Keyword::SELECT) { + let expr = if self.peek_keyword(Keyword::SELECT) { SetExpr::Select(self.parse_select().map(Box::new)?) } else if self.consume_token(&Token::LParen) { // CTEs are not allowed here, but the parser currently accepts them @@ -9405,9 +9433,9 @@ impl<'a> Parser<'a> { } } - /// Parse a restricted `SELECT` statement (no CTEs / `UNION` / `ORDER BY`), - /// assuming the initial `SELECT` was already consumed + /// Parse a restricted `SELECT` statement (no CTEs / `UNION` / `ORDER BY`) pub fn parse_select(&mut self) -> Result { + 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) { @@ -9571,6 +9599,7 @@ impl<'a> Parser<'a> { }; Ok(Select { + select_token: AttachedToken(select_token), distinct, top, top_before_distinct, @@ -10656,7 +10685,7 @@ impl<'a> Parser<'a> { return self.expected("literal number", next_token); }; self.expect_token(&Token::RBrace)?; - RepetitionQuantifier::AtMost(Self::parse(n, token.location)?) + RepetitionQuantifier::AtMost(Self::parse(n, token.span.start)?) } Token::Number(n, _) if self.consume_token(&Token::Comma) => { let next_token = self.next_token(); @@ -10664,12 +10693,12 @@ impl<'a> Parser<'a> { Token::Number(m, _) => { self.expect_token(&Token::RBrace)?; RepetitionQuantifier::Range( - Self::parse(n, token.location)?, - Self::parse(m, token.location)?, + Self::parse(n, token.span.start)?, + Self::parse(m, token.span.start)?, ) } Token::RBrace => { - RepetitionQuantifier::AtLeast(Self::parse(n, token.location)?) + RepetitionQuantifier::AtLeast(Self::parse(n, token.span.start)?) } _ => { return self.expected("} or upper bound", next_token); @@ -10678,7 +10707,7 @@ impl<'a> Parser<'a> { } Token::Number(n, _) => { self.expect_token(&Token::RBrace)?; - RepetitionQuantifier::Exactly(Self::parse(n, token.location)?) + RepetitionQuantifier::Exactly(Self::parse(n, token.span.start)?) } _ => return self.expected("quantifier range", token), } @@ -11113,7 +11142,7 @@ impl<'a> Parser<'a> { .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) .then(|| self.parse_identifier(false).unwrap()); - let loc = self.peek_token().location; + let loc = self.peek_token().span.start; let cascade = self.parse_keyword(Keyword::CASCADE); let restrict = self.parse_keyword(Keyword::RESTRICT); if cascade && restrict { @@ -11132,7 +11161,10 @@ impl<'a> Parser<'a> { /// Parse an REPLACE statement pub fn parse_replace(&mut self) -> Result { if !dialect_of!(self is MySqlDialect | GenericDialect) { - return parser_err!("Unsupported statement REPLACE", self.peek_token().location); + return parser_err!( + "Unsupported statement REPLACE", + self.peek_token().span.start + ); } let mut insert = self.parse_insert()?; @@ -11593,7 +11625,7 @@ impl<'a> Parser<'a> { } fn parse_duplicate_treatment(&mut self) -> Result, ParserError> { - let loc = self.peek_token().location; + let loc = self.peek_token().span.start; match ( self.parse_keyword(Keyword::ALL), self.parse_keyword(Keyword::DISTINCT), @@ -11608,17 +11640,17 @@ impl<'a> Parser<'a> { /// Parse a comma-delimited list of projections after SELECT pub fn parse_select_item(&mut self) -> Result { match self.parse_wildcard_expr()? { - Expr::QualifiedWildcard(prefix) => Ok(SelectItem::QualifiedWildcard( + Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard( prefix, - self.parse_wildcard_additional_options()?, + self.parse_wildcard_additional_options(token.0)?, )), - Expr::Wildcard => Ok(SelectItem::Wildcard( - self.parse_wildcard_additional_options()?, + Expr::Wildcard(token) => Ok(SelectItem::Wildcard( + self.parse_wildcard_additional_options(token.0)?, )), Expr::Identifier(v) if v.value.to_lowercase() == "from" && v.quote_style.is_none() => { parser_err!( format!("Expected an expression, found: {}", v), - self.peek_token().location + self.peek_token().span.start ) } Expr::BinaryOp { @@ -11631,7 +11663,7 @@ impl<'a> Parser<'a> { let Expr::Identifier(alias) = *left else { return parser_err!( "BUG: expected identifier expression as alias", - self.peek_token().location + self.peek_token().span.start ); }; Ok(SelectItem::ExprWithAlias { @@ -11653,6 +11685,7 @@ impl<'a> Parser<'a> { /// If it is not possible to parse it, will return an option. pub fn parse_wildcard_additional_options( &mut self, + wildcard_token: TokenWithLocation, ) -> Result { let opt_ilike = if dialect_of!(self is GenericDialect | SnowflakeDialect) { self.parse_optional_select_item_ilike()? @@ -11684,6 +11717,7 @@ impl<'a> Parser<'a> { }; Ok(WildcardAdditionalOptions { + wildcard_token: wildcard_token.into(), opt_ilike, opt_exclude, opt_except, @@ -11931,7 +11965,7 @@ impl<'a> Parser<'a> { } else { let next_token = self.next_token(); let quantity = match next_token.token { - Token::Number(s, _) => Self::parse::(s, next_token.location)?, + Token::Number(s, _) => Self::parse::(s, next_token.span.start)?, _ => self.expected("literal int", next_token)?, }; Some(TopQuantity::Constant(quantity)) @@ -12812,10 +12846,11 @@ impl<'a> Parser<'a> { } impl Word { - pub fn to_ident(&self) -> Ident { + pub fn to_ident(&self, span: Span) -> Ident { Ident { value: self.value.clone(), quote_style: self.quote_style, + span, } } } @@ -13389,14 +13424,17 @@ mod tests { Ident { value: "CATALOG".to_string(), quote_style: None, + span: Span::empty(), }, Ident { value: "F(o)o. \"bar".to_string(), quote_style: Some('"'), + span: Span::empty(), }, Ident { value: "table".to_string(), quote_style: None, + span: Span::empty(), }, ]; dialect.run_parser_method(r#"CATALOG."F(o)o. ""bar".table"#, |parser| { @@ -13409,10 +13447,12 @@ mod tests { Ident { value: "CATALOG".to_string(), quote_style: None, + span: Span::empty(), }, Ident { value: "table".to_string(), quote_style: None, + span: Span::empty(), }, ]; dialect.run_parser_method("CATALOG . table", |parser| { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 05aaf1e28..a57ba2ec8 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -29,10 +29,10 @@ use alloc::{ vec, vec::Vec, }; -use core::fmt; use core::iter::Peekable; use core::num::NonZeroU8; use core::str::Chars; +use core::{cmp, fmt}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -422,7 +422,9 @@ impl fmt::Display for Whitespace { } /// Location in input string -#[derive(Debug, Eq, PartialEq, Clone, Copy)] +#[derive(Eq, PartialEq, Hash, Clone, Copy, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Location { /// Line number, starting from 1 pub line: u64, @@ -431,36 +433,114 @@ pub struct Location { } impl fmt::Display for Location { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.line == 0 { return Ok(()); } - write!( - f, - // TODO: use standard compiler location syntax (::) - " at Line: {}, Column: {}", - self.line, self.column, - ) + write!(f, " at Line: {}, Column: {}", self.line, self.column) + } +} + +impl fmt::Debug for Location { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Location({},{})", self.line, self.column) + } +} + +impl Location { + pub fn of(line: u64, column: u64) -> Self { + Self { line, column } + } + + pub fn span_to(self, end: Self) -> Span { + Span { start: self, end } + } +} + +impl From<(u64, u64)> for Location { + fn from((line, column): (u64, u64)) -> Self { + Self { line, column } + } +} + +/// A span of source code locations (start, end) +#[derive(Eq, PartialEq, Hash, Clone, PartialOrd, Ord, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Span { + pub start: Location, + pub end: Location, +} + +impl fmt::Debug for Span { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Span({:?}..{:?})", self.start, self.end) + } +} + +impl Span { + // An empty span (0, 0) -> (0, 0) + // We need a const instance for pattern matching + const EMPTY: Span = Self::empty(); + + pub fn new(start: Location, end: Location) -> Span { + Span { start, end } + } + + /// Returns an empty span (0, 0) -> (0, 0) + /// Empty spans represent no knowledge of source location + pub const fn empty() -> Span { + Span { + start: Location { line: 0, column: 0 }, + end: Location { line: 0, column: 0 }, + } + } + + /// Returns the smallest Span that contains both `self` and `other` + /// If either span is [Span::empty], the other span is returned + pub fn union(&self, other: &Span) -> Span { + // If either span is empty, return the other + // this prevents propagating (0, 0) through the tree + match (self, other) { + (&Span::EMPTY, _) => *other, + (_, &Span::EMPTY) => *self, + _ => Span { + start: cmp::min(self.start, other.start), + end: cmp::max(self.end, other.end), + }, + } + } + + /// Same as [Span::union] for `Option` + /// If `other` is `None`, `self` is returned + pub fn union_opt(&self, other: &Option) -> Span { + match other { + Some(other) => self.union(other), + None => *self, + } } } /// A [Token] with [Location] attached to it -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct TokenWithLocation { pub token: Token, - pub location: Location, + pub span: Span, } impl TokenWithLocation { - pub fn new(token: Token, line: u64, column: u64) -> TokenWithLocation { - TokenWithLocation { - token, - location: Location { line, column }, - } + pub fn new(token: Token, span: Span) -> TokenWithLocation { + TokenWithLocation { token, span } } pub fn wrap(token: Token) -> TokenWithLocation { - TokenWithLocation::new(token, 0, 0) + TokenWithLocation::new(token, Span::empty()) + } + + pub fn at(token: Token, start: Location, end: Location) -> TokenWithLocation { + TokenWithLocation::new(token, Span::new(start, end)) } } @@ -656,7 +736,9 @@ impl<'a> Tokenizer<'a> { let mut location = state.location(); while let Some(token) = self.next_token(&mut state)? { - buf.push(TokenWithLocation { token, location }); + let span = location.span_to(state.location()); + + buf.push(TokenWithLocation { token, span }); location = state.location(); } @@ -2669,18 +2751,30 @@ mod tests { .tokenize_with_location() .unwrap(); let expected = vec![ - TokenWithLocation::new(Token::make_keyword("SELECT"), 1, 1), - TokenWithLocation::new(Token::Whitespace(Whitespace::Space), 1, 7), - TokenWithLocation::new(Token::make_word("a", None), 1, 8), - TokenWithLocation::new(Token::Comma, 1, 9), - TokenWithLocation::new(Token::Whitespace(Whitespace::Newline), 1, 10), - TokenWithLocation::new(Token::Whitespace(Whitespace::Space), 2, 1), - TokenWithLocation::new(Token::make_word("b", None), 2, 2), + TokenWithLocation::at(Token::make_keyword("SELECT"), (1, 1).into(), (1, 7).into()), + TokenWithLocation::at( + Token::Whitespace(Whitespace::Space), + (1, 7).into(), + (1, 8).into(), + ), + TokenWithLocation::at(Token::make_word("a", None), (1, 8).into(), (1, 9).into()), + TokenWithLocation::at(Token::Comma, (1, 9).into(), (1, 10).into()), + TokenWithLocation::at( + Token::Whitespace(Whitespace::Newline), + (1, 10).into(), + (2, 1).into(), + ), + TokenWithLocation::at( + Token::Whitespace(Whitespace::Space), + (2, 1).into(), + (2, 2).into(), + ), + TokenWithLocation::at(Token::make_word("b", None), (2, 2).into(), (2, 3).into()), ]; compare(expected, tokens); } - fn compare(expected: Vec, actual: Vec) { + fn compare(expected: Vec, actual: Vec) { //println!("------------------------------"); //println!("tokens = {:?}", actual); //println!("expected = {:?}", expected); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index d4c178bbf..00d12ed83 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -23,6 +23,7 @@ use std::ops::Deref; use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; use sqlparser::parser::{ParserError, ParserOptions}; +use sqlparser::tokenizer::Span; use test_utils::*; #[test] @@ -678,10 +679,12 @@ fn parse_typed_struct_syntax_bigquery() { Ident { value: "t".into(), quote_style: None, + span: Span::empty(), }, Ident { value: "str_col".into(), quote_style: None, + span: Span::empty(), }, ]), ], @@ -690,6 +693,7 @@ fn parse_typed_struct_syntax_bigquery() { field_name: Some(Ident { value: "x".into(), quote_style: None, + span: Span::empty(), }), field_type: DataType::Int64 }, @@ -697,6 +701,7 @@ fn parse_typed_struct_syntax_bigquery() { field_name: Some(Ident { value: "y".into(), quote_style: None, + span: Span::empty(), }), field_type: DataType::String(None) }, @@ -709,6 +714,7 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::Identifier(Ident { value: "nested_col".into(), quote_style: None, + span: Span::empty(), }),], fields: vec![ StructField { @@ -740,6 +746,7 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::Identifier(Ident { value: "nested_col".into(), quote_style: None, + span: Span::empty(), }),], fields: vec![ StructField { @@ -987,10 +994,12 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { Ident { value: "t".into(), quote_style: None, + span: Span::empty(), }, Ident { value: "str_col".into(), quote_style: None, + span: Span::empty(), }, ]), ], @@ -999,6 +1008,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { field_name: Some(Ident { value: "x".into(), quote_style: None, + span: Span::empty(), }), field_type: DataType::Int64 }, @@ -1006,6 +1016,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { field_name: Some(Ident { value: "y".into(), quote_style: None, + span: Span::empty(), }), field_type: DataType::String(None) }, @@ -1018,6 +1029,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::Identifier(Ident { value: "nested_col".into(), quote_style: None, + span: Span::empty(), }),], fields: vec![ StructField { @@ -1049,6 +1061,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::Identifier(Ident { value: "nested_col".into(), quote_style: None, + span: Span::empty(), }),], fields: vec![ StructField { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 90af12ab7..ed0c74021 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -21,6 +21,8 @@ #[macro_use] mod test_utils; +use helpers::attached_token::AttachedToken; +use sqlparser::tokenizer::Span; use test_utils::*; use sqlparser::ast::Expr::{BinaryOp, Identifier, MapAccess}; @@ -39,12 +41,14 @@ fn parse_map_access_expr() { assert_eq!( Select { distinct: None, + select_token: AttachedToken::empty(), top: None, top_before_distinct: false, projection: vec![UnnamedExpr(MapAccess { column: Box::new(Identifier(Ident { value: "string_values".to_string(), quote_style: None, + span: Span::empty(), })), keys: vec![MapAccessKey { key: call( @@ -903,7 +907,8 @@ fn parse_create_view_with_fields_data_types() { data_type: Some(DataType::Custom( ObjectName(vec![Ident { value: "int".into(), - quote_style: Some('"') + quote_style: Some('"'), + span: Span::empty(), }]), vec![] )), @@ -914,7 +919,8 @@ fn parse_create_view_with_fields_data_types() { data_type: Some(DataType::Custom( ObjectName(vec![Ident { value: "String".into(), - quote_style: Some('"') + quote_style: Some('"'), + span: Span::empty(), }]), vec![] )), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e22877dbe..4e0cac45b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -25,6 +25,7 @@ extern crate core; +use helpers::attached_token::AttachedToken; use matches::assert_matches; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::{Pivot, Unpivot}; @@ -36,6 +37,7 @@ use sqlparser::dialect::{ }; use sqlparser::keywords::{Keyword, ALL_KEYWORDS}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; +use sqlparser::tokenizer::Span; use sqlparser::tokenizer::Tokenizer; use test_utils::{ all_dialects, all_dialects_where, alter_table_op, assert_eq_vec, call, expr_from_projection, @@ -378,6 +380,7 @@ fn parse_update_set_from() { subquery: Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -1271,6 +1274,7 @@ fn parse_select_with_date_column_name() { &Expr::Identifier(Ident { value: "date".into(), quote_style: None, + span: Span::empty(), }), expr_from_projection(only(&select.projection)), ); @@ -1789,6 +1793,7 @@ fn parse_null_like() { alias: Ident { value: "col_null".to_owned(), quote_style: None, + span: Span::empty(), }, }, select.projection[0] @@ -1805,6 +1810,7 @@ fn parse_null_like() { alias: Ident { value: "null_col".to_owned(), quote_style: None, + span: Span::empty(), }, }, select.projection[1] @@ -2823,6 +2829,7 @@ fn parse_listagg() { expr: Expr::Identifier(Ident { value: "id".to_string(), quote_style: None, + span: Span::empty(), }), asc: None, nulls_first: None, @@ -2832,6 +2839,7 @@ fn parse_listagg() { expr: Expr::Identifier(Ident { value: "username".to_string(), quote_style: None, + span: Span::empty(), }), asc: None, nulls_first: None, @@ -4038,7 +4046,8 @@ fn parse_alter_table() { [SqlOption::KeyValue { key: Ident { value: "classification".to_string(), - quote_style: Some('\'') + quote_style: Some('\''), + span: Span::empty(), }, value: Expr::Value(Value::SingleQuotedString("parquet".to_string())), }], @@ -4824,6 +4833,7 @@ fn test_parse_named_window() { ORDER BY C3"; let actual_select_only = verified_only_select(sql); let expected = Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -4833,6 +4843,7 @@ fn test_parse_named_window() { name: ObjectName(vec![Ident { value: "MIN".to_string(), quote_style: None, + span: Span::empty(), }]), parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -4841,6 +4852,7 @@ fn test_parse_named_window() { Expr::Identifier(Ident { value: "c12".to_string(), quote_style: None, + span: Span::empty(), }), ))], clauses: vec![], @@ -4850,12 +4862,14 @@ fn test_parse_named_window() { over: Some(WindowType::NamedWindow(Ident { value: "window1".to_string(), quote_style: None, + span: Span::empty(), })), within_group: vec![], }), alias: Ident { value: "min1".to_string(), quote_style: None, + span: Span::empty(), }, }, SelectItem::ExprWithAlias { @@ -4863,6 +4877,7 @@ fn test_parse_named_window() { name: ObjectName(vec![Ident { value: "MAX".to_string(), quote_style: None, + span: Span::empty(), }]), parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -4871,6 +4886,7 @@ fn test_parse_named_window() { Expr::Identifier(Ident { value: "c12".to_string(), quote_style: None, + span: Span::empty(), }), ))], clauses: vec![], @@ -4880,12 +4896,14 @@ fn test_parse_named_window() { over: Some(WindowType::NamedWindow(Ident { value: "window2".to_string(), quote_style: None, + span: Span::empty(), })), within_group: vec![], }), alias: Ident { value: "max1".to_string(), quote_style: None, + span: Span::empty(), }, }, ], @@ -4895,6 +4913,7 @@ fn test_parse_named_window() { name: ObjectName(vec![Ident { value: "aggregate_test_100".to_string(), quote_style: None, + span: Span::empty(), }]), alias: None, args: None, @@ -4919,6 +4938,7 @@ fn test_parse_named_window() { Ident { value: "window1".to_string(), quote_style: None, + span: Span::empty(), }, NamedWindowExpr::WindowSpec(WindowSpec { window_name: None, @@ -4927,6 +4947,7 @@ fn test_parse_named_window() { expr: Expr::Identifier(Ident { value: "C12".to_string(), quote_style: None, + span: Span::empty(), }), asc: None, nulls_first: None, @@ -4939,12 +4960,14 @@ fn test_parse_named_window() { Ident { value: "window2".to_string(), quote_style: None, + span: Span::empty(), }, NamedWindowExpr::WindowSpec(WindowSpec { window_name: None, partition_by: vec![Expr::Identifier(Ident { value: "C11".to_string(), quote_style: None, + span: Span::empty(), })], order_by: vec![], window_frame: None, @@ -5425,6 +5448,7 @@ fn interval_disallow_interval_expr_gt() { right: Box::new(Expr::Identifier(Ident { value: "x".to_string(), quote_style: None, + span: Span::empty(), })), } ) @@ -5465,12 +5489,14 @@ fn parse_interval_and_or_xor() { let expected_ast = vec![Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, projection: vec![UnnamedExpr(Expr::Identifier(Ident { value: "col".to_string(), quote_style: None, + span: Span::empty(), }))], into: None, from: vec![TableWithJoins { @@ -5478,6 +5504,7 @@ fn parse_interval_and_or_xor() { name: ObjectName(vec![Ident { value: "test".to_string(), quote_style: None, + span: Span::empty(), }]), alias: None, args: None, @@ -5496,12 +5523,14 @@ fn parse_interval_and_or_xor() { left: Box::new(Expr::Identifier(Ident { value: "d3_date".to_string(), quote_style: None, + span: Span::empty(), })), op: BinaryOperator::Gt, right: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { value: "d1_date".to_string(), quote_style: None, + span: Span::empty(), })), op: BinaryOperator::Plus, right: Box::new(Expr::Interval(Interval { @@ -5520,12 +5549,14 @@ fn parse_interval_and_or_xor() { left: Box::new(Expr::Identifier(Ident { value: "d2_date".to_string(), quote_style: None, + span: Span::empty(), })), op: BinaryOperator::Gt, right: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { value: "d1_date".to_string(), quote_style: None, + span: Span::empty(), })), op: BinaryOperator::Plus, right: Box::new(Expr::Interval(Interval { @@ -5617,6 +5648,7 @@ fn parse_at_timezone() { alias: Ident { value: "hour".to_string(), quote_style: Some('"'), + span: Span::empty(), }, }, only(&select.projection), @@ -6637,12 +6669,14 @@ fn parse_recursive_cte() { name: Ident { value: "nums".to_string(), quote_style: None, + span: Span::empty(), }, columns: vec![TableAliasColumnDef::from_name("val")], }, query: Box::new(cte_query), from: None, materialized: None, + closing_paren_token: AttachedToken::empty(), }; assert_eq!(with.cte_tables.first().unwrap(), &expected); } @@ -7616,22 +7650,18 @@ fn lateral_function() { let sql = "SELECT * FROM customer LEFT JOIN LATERAL generate_series(1, customer.id)"; let actual_select_only = verified_only_select(sql); let expected = Select { + select_token: AttachedToken::empty(), distinct: None, top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], top_before_distinct: false, - projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { - opt_ilike: None, - opt_exclude: None, - opt_except: None, - opt_rename: None, - opt_replace: None, - })], into: None, from: vec![TableWithJoins { relation: TableFactor::Table { name: ObjectName(vec![Ident { value: "customer".to_string(), quote_style: None, + span: Span::empty(), }]), alias: None, args: None, @@ -8270,10 +8300,12 @@ fn parse_grant() { Ident { value: "shape".into(), quote_style: None, + span: Span::empty(), }, Ident { value: "size".into(), quote_style: None, + span: Span::empty(), }, ]) }, @@ -8467,6 +8499,7 @@ fn parse_merge() { subquery: Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -8515,6 +8548,7 @@ fn parse_merge() { name: Ident { value: "stg".to_string(), quote_style: None, + span: Span::empty(), }, columns: vec![], }), @@ -8714,7 +8748,8 @@ fn test_lock_table() { lock.of.unwrap().0, vec![Ident { value: "school".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }] ); assert!(lock.nonblock.is_none()); @@ -8728,7 +8763,8 @@ fn test_lock_table() { lock.of.unwrap().0, vec![Ident { value: "school".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }] ); assert!(lock.nonblock.is_none()); @@ -8742,7 +8778,8 @@ fn test_lock_table() { lock.of.unwrap().0, vec![Ident { value: "school".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }] ); assert!(lock.nonblock.is_none()); @@ -8752,7 +8789,8 @@ fn test_lock_table() { lock.of.unwrap().0, vec![Ident { value: "student".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }] ); assert!(lock.nonblock.is_none()); @@ -8769,7 +8807,8 @@ fn test_lock_nonblock() { lock.of.unwrap().0, vec![Ident { value: "school".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }] ); assert_eq!(lock.nonblock.unwrap(), NonBlock::SkipLocked); @@ -8783,7 +8822,8 @@ fn test_lock_nonblock() { lock.of.unwrap().0, vec![Ident { value: "school".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }] ); assert_eq!(lock.nonblock.unwrap(), NonBlock::Nowait); @@ -9584,7 +9624,8 @@ fn parse_pivot_table() { alias: Some(TableAlias { name: Ident { value: "p".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }, columns: vec![ TableAliasColumnDef::from_name("c"), @@ -9636,12 +9677,14 @@ fn parse_unpivot_table() { }), value: Ident { value: "quantity".to_string(), - quote_style: None + quote_style: None, + span: Span::empty() }, name: Ident { value: "quarter".to_string(), - quote_style: None + quote_style: None, + span: Span::empty() }, columns: ["Q1", "Q2", "Q3", "Q4"] .into_iter() @@ -9704,12 +9747,14 @@ fn parse_pivot_unpivot_table() { }), value: Ident { value: "population".to_string(), - quote_style: None + quote_style: None, + span: Span::empty() }, name: Ident { value: "year".to_string(), - quote_style: None + quote_style: None, + span: Span::empty() }, columns: ["population_2000", "population_2010"] .into_iter() @@ -9999,10 +10044,12 @@ fn parse_execute_stored_procedure() { Ident { value: "my_schema".to_string(), quote_style: None, + span: Span::empty(), }, Ident { value: "my_stored_procedure".to_string(), quote_style: None, + span: Span::empty(), }, ]), parameters: vec![ @@ -10098,6 +10145,7 @@ fn parse_unload() { Statement::Unload { query: Box::new(Query { body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -10143,12 +10191,14 @@ fn parse_unload() { }), to: Ident { value: "s3://...".to_string(), - quote_style: Some('\'') + quote_style: Some('\''), + span: Span::empty(), }, with: vec![SqlOption::KeyValue { key: Ident { value: "format".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }, value: Expr::Value(Value::SingleQuotedString("AVRO".to_string())) }] @@ -10275,6 +10325,7 @@ fn parse_map_access_expr() { #[test] fn parse_connect_by() { let expect_query = Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -10363,6 +10414,7 @@ fn parse_connect_by() { assert_eq!( all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_3), Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -11206,6 +11258,7 @@ fn test_extract_seconds_ok() { field: DateTimeField::Custom(Ident { value: "seconds".to_string(), quote_style: None, + span: Span::empty(), }), syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { @@ -11231,6 +11284,7 @@ fn test_extract_seconds_single_quote_ok() { field: DateTimeField::Custom(Ident { value: "seconds".to_string(), quote_style: Some('\''), + span: Span::empty(), }), syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { @@ -12130,7 +12184,8 @@ fn test_load_extension() { assert_eq!( Ident { value: "filename".to_string(), - quote_style: Some('\'') + quote_style: Some('\''), + span: Span::empty(), }, extension_name ); diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 73b0f6601..01ac0649a 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -18,6 +18,8 @@ #[macro_use] mod test_utils; +use helpers::attached_token::AttachedToken; +use sqlparser::tokenizer::Span; use test_utils::*; use sqlparser::ast::*; @@ -259,22 +261,18 @@ fn test_select_union_by_name() { op: SetOperator::Union, set_quantifier: *expected_quantifier, left: Box::::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], top_before_distinct: false, - projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { - opt_ilike: None, - opt_exclude: None, - opt_except: None, - opt_rename: None, - opt_replace: None, - })], into: None, from: vec![TableWithJoins { relation: TableFactor::Table { name: ObjectName(vec![Ident { value: "capitals".to_string(), quote_style: None, + span: Span::empty(), }]), alias: None, args: None, @@ -301,22 +299,18 @@ fn test_select_union_by_name() { connect_by: None, }))), right: Box::::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, + projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], top_before_distinct: false, - projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { - opt_ilike: None, - opt_exclude: None, - opt_except: None, - opt_rename: None, - opt_replace: None, - })], into: None, from: vec![TableWithJoins { relation: TableFactor::Table { name: ObjectName(vec![Ident { value: "weather".to_string(), quote_style: None, + span: Span::empty(), }]), alias: None, args: None, @@ -355,12 +349,28 @@ fn test_duckdb_install() { Statement::Install { extension_name: Ident { value: "tpch".to_string(), - quote_style: None + quote_style: None, + span: Span::empty() } } ); } +#[test] +fn test_duckdb_load_extension() { + let stmt = duckdb().verified_stmt("LOAD my_extension"); + assert_eq!( + Statement::Load { + extension_name: Ident { + value: "my_extension".to_string(), + quote_style: None, + span: Span::empty() + } + }, + stmt + ); +} + #[test] fn test_duckdb_struct_literal() { //struct literal syntax https://duckdb.org/docs/sql/data_types/struct#creating-structs diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index d1d8d1248..31668c86a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -22,6 +22,8 @@ #[macro_use] mod test_utils; +use helpers::attached_token::AttachedToken; +use sqlparser::tokenizer::Span; use test_utils::*; use sqlparser::ast::DataType::{Int, Text}; @@ -113,6 +115,7 @@ fn parse_create_procedure() { settings: None, format_clause: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -138,14 +141,16 @@ fn parse_create_procedure() { ProcedureParam { name: Ident { value: "@foo".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, data_type: DataType::Int(None) }, ProcedureParam { name: Ident { value: "@bar".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, data_type: DataType::Varchar(Some(CharacterLength::IntegerLength { length: 256, @@ -155,7 +160,8 @@ fn parse_create_procedure() { ]), name: ObjectName(vec![Ident { value: "test".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }]) } ) @@ -204,15 +210,9 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "t_test_table".into(), - quote_style: None, - },]), + name: ObjectName(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { - name: Ident { - value: "A".into(), - quote_style: None - }, + name: Ident::new("A"), columns: vec![] }), args: None, @@ -224,23 +224,13 @@ fn parse_mssql_openjson() { }, joins: vec![Join { relation: TableFactor::OpenJsonTable { - json_expr: Expr::CompoundIdentifier(vec![ - Ident { - value: "A".into(), - quote_style: None, - }, - Ident { - value: "param".into(), - quote_style: None, - } - ]), + json_expr: Expr::CompoundIdentifier( + vec![Ident::new("A"), Ident::new("param"),] + ), json_path: Some(Value::SingleQuotedString("$.config".into())), columns: vec![ OpenJsonTableColumn { - name: Ident { - value: "kind".into(), - quote_style: None, - }, + name: Ident::new("kind"), r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { length: 20, unit: None @@ -252,6 +242,7 @@ fn parse_mssql_openjson() { name: Ident { value: "id_list".into(), quote_style: Some('['), + span: Span::empty(), }, r#type: DataType::Nvarchar(Some(CharacterLength::Max)), path: Some("$.id_list".into()), @@ -259,10 +250,7 @@ fn parse_mssql_openjson() { } ], alias: Some(TableAlias { - name: Ident { - value: "B".into(), - quote_style: None - }, + name: Ident::new("B"), columns: vec![] }) }, @@ -280,15 +268,9 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "t_test_table".into(), - quote_style: None, - },]), + name: ObjectName(vec![Ident::new("t_test_table"),]), alias: Some(TableAlias { - name: Ident { - value: "A".into(), - quote_style: None - }, + name: Ident::new("A"), columns: vec![] }), args: None, @@ -300,23 +282,13 @@ fn parse_mssql_openjson() { }, joins: vec![Join { relation: TableFactor::OpenJsonTable { - json_expr: Expr::CompoundIdentifier(vec![ - Ident { - value: "A".into(), - quote_style: None, - }, - Ident { - value: "param".into(), - quote_style: None, - } - ]), + json_expr: Expr::CompoundIdentifier( + vec![Ident::new("A"), Ident::new("param"),] + ), json_path: None, columns: vec![ OpenJsonTableColumn { - name: Ident { - value: "kind".into(), - quote_style: None, - }, + name: Ident::new("kind"), r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { length: 20, unit: None @@ -328,6 +300,7 @@ fn parse_mssql_openjson() { name: Ident { value: "id_list".into(), quote_style: Some('['), + span: Span::empty(), }, r#type: DataType::Nvarchar(Some(CharacterLength::Max)), path: Some("$.id_list".into()), @@ -335,10 +308,7 @@ fn parse_mssql_openjson() { } ], alias: Some(TableAlias { - name: Ident { - value: "B".into(), - quote_style: None - }, + name: Ident::new("B"), columns: vec![] }) }, @@ -356,15 +326,10 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "t_test_table".into(), - quote_style: None, - },]), + name: ObjectName(vec![Ident::new("t_test_table")]), + alias: Some(TableAlias { - name: Ident { - value: "A".into(), - quote_style: None - }, + name: Ident::new("A"), columns: vec![] }), args: None, @@ -376,23 +341,13 @@ fn parse_mssql_openjson() { }, joins: vec![Join { relation: TableFactor::OpenJsonTable { - json_expr: Expr::CompoundIdentifier(vec![ - Ident { - value: "A".into(), - quote_style: None, - }, - Ident { - value: "param".into(), - quote_style: None, - } - ]), + json_expr: Expr::CompoundIdentifier( + vec![Ident::new("A"), Ident::new("param"),] + ), json_path: None, columns: vec![ OpenJsonTableColumn { - name: Ident { - value: "kind".into(), - quote_style: None, - }, + name: Ident::new("kind"), r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { length: 20, unit: None @@ -404,6 +359,7 @@ fn parse_mssql_openjson() { name: Ident { value: "id_list".into(), quote_style: Some('['), + span: Span::empty(), }, r#type: DataType::Nvarchar(Some(CharacterLength::Max)), path: None, @@ -411,10 +367,7 @@ fn parse_mssql_openjson() { } ], alias: Some(TableAlias { - name: Ident { - value: "B".into(), - quote_style: None - }, + name: Ident::new("B"), columns: vec![] }) }, @@ -432,15 +385,9 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "t_test_table".into(), - quote_style: None, - },]), + name: ObjectName(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { - name: Ident { - value: "A".into(), - quote_style: None - }, + name: Ident::new("A"), columns: vec![] }), args: None, @@ -452,23 +399,13 @@ fn parse_mssql_openjson() { }, joins: vec![Join { relation: TableFactor::OpenJsonTable { - json_expr: Expr::CompoundIdentifier(vec![ - Ident { - value: "A".into(), - quote_style: None, - }, - Ident { - value: "param".into(), - quote_style: None, - } - ]), + json_expr: Expr::CompoundIdentifier( + vec![Ident::new("A"), Ident::new("param"),] + ), json_path: Some(Value::SingleQuotedString("$.config".into())), columns: vec![], alias: Some(TableAlias { - name: Ident { - value: "B".into(), - quote_style: None - }, + name: Ident::new("B"), columns: vec![] }) }, @@ -486,15 +423,9 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "t_test_table".into(), - quote_style: None, - },]), + name: ObjectName(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { - name: Ident { - value: "A".into(), - quote_style: None - }, + name: Ident::new("A"), columns: vec![] }), args: None, @@ -506,23 +437,13 @@ fn parse_mssql_openjson() { }, joins: vec![Join { relation: TableFactor::OpenJsonTable { - json_expr: Expr::CompoundIdentifier(vec![ - Ident { - value: "A".into(), - quote_style: None, - }, - Ident { - value: "param".into(), - quote_style: None, - } - ]), + json_expr: Expr::CompoundIdentifier( + vec![Ident::new("A"), Ident::new("param"),] + ), json_path: None, columns: vec![], alias: Some(TableAlias { - name: Ident { - value: "B".into(), - quote_style: None - }, + name: Ident::new("B"), columns: vec![] }) }, @@ -607,7 +528,8 @@ fn parse_mssql_create_role() { authorization_owner, Some(ObjectName(vec![Ident { value: "helena".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }])) ); } @@ -623,12 +545,14 @@ fn parse_alter_role() { [Statement::AlterRole { name: Ident { value: "old_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::RenameRole { role_name: Ident { value: "new_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), } }, }] @@ -640,12 +564,14 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::AddMember { member_name: Ident { value: "new_member".into(), - quote_style: None + quote_style: None, + span: Span::empty(), } }, } @@ -657,12 +583,14 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::DropMember { member_name: Ident { value: "old_member".into(), - quote_style: None + quote_style: None, + span: Span::empty(), } }, } @@ -1137,13 +1065,15 @@ fn parse_substring_in_select() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: Some(Distinct::Distinct), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Substring { expr: Box::new(Expr::Identifier(Ident { value: "description".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), })), substring_from: Some(Box::new(Expr::Value(number("0")))), substring_for: Some(Box::new(Expr::Value(number("1")))), @@ -1154,7 +1084,8 @@ fn parse_substring_in_select() { relation: TableFactor::Table { name: ObjectName(vec![Ident { value: "test".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), alias: None, args: None, @@ -1208,7 +1139,8 @@ fn parse_mssql_declare() { Declare { names: vec![Ident { value: "@foo".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }], data_type: None, assignment: None, @@ -1222,7 +1154,8 @@ fn parse_mssql_declare() { Declare { names: vec![Ident { value: "@bar".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }], data_type: Some(Int(None)), assignment: None, @@ -1236,7 +1169,8 @@ fn parse_mssql_declare() { Declare { names: vec![Ident { value: "@baz".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }], data_type: Some(Text), assignment: Some(MsSqlAssignment(Box::new(Expr::Value(SingleQuotedString( @@ -1260,10 +1194,7 @@ fn parse_mssql_declare() { vec![ Statement::Declare { stmts: vec![Declare { - names: vec![Ident { - value: "@bar".to_string(), - quote_style: None - }], + names: vec![Ident::new("@bar"),], data_type: Some(Int(None)), assignment: None, declare_type: None, @@ -1292,6 +1223,7 @@ fn parse_mssql_declare() { settings: None, format_clause: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -1364,10 +1296,12 @@ fn parse_create_table_with_valid_options() { key: Ident { value: "DISTRIBUTION".to_string(), quote_style: None, + span: Span::empty(), }, value: Expr::Identifier(Ident { value: "ROUND_ROBIN".to_string(), quote_style: None, + span: Span::empty(), }) }, SqlOption::Partition { @@ -1411,6 +1345,7 @@ fn parse_create_table_with_valid_options() { name: Ident { value: "column_a".to_string(), quote_style: None, + span: Span::empty(), }, asc: Some(true), }, @@ -1418,6 +1353,7 @@ fn parse_create_table_with_valid_options() { name: Ident { value: "column_b".to_string(), quote_style: None, + span: Span::empty(), }, asc: Some(false), }, @@ -1425,6 +1361,7 @@ fn parse_create_table_with_valid_options() { name: Ident { value: "column_c".to_string(), quote_style: None, + span: Span::empty(), }, asc: None, }, @@ -1438,6 +1375,7 @@ fn parse_create_table_with_valid_options() { key: Ident { value: "DISTRIBUTION".to_string(), quote_style: None, + span: Span::empty(), }, value: Expr::Function( Function { @@ -1446,6 +1384,7 @@ fn parse_create_table_with_valid_options() { Ident { value: "HASH".to_string(), quote_style: None, + span: Span::empty(), }, ], ), @@ -1460,6 +1399,7 @@ fn parse_create_table_with_valid_options() { Ident { value: "column_a".to_string(), quote_style: None, + span: Span::empty(), }, ), ), @@ -1470,6 +1410,7 @@ fn parse_create_table_with_valid_options() { Ident { value: "column_b".to_string(), quote_style: None, + span: Span::empty(), }, ), ), @@ -1504,12 +1445,14 @@ fn parse_create_table_with_valid_options() { name: ObjectName(vec![Ident { value: "mytable".to_string(), quote_style: None, + span: Span::empty(), },],), columns: vec![ ColumnDef { name: Ident { value: "column_a".to_string(), quote_style: None, + span: Span::empty(), }, data_type: Int(None,), collation: None, @@ -1519,6 +1462,7 @@ fn parse_create_table_with_valid_options() { name: Ident { value: "column_b".to_string(), quote_style: None, + span: Span::empty(), }, data_type: Int(None,), collation: None, @@ -1528,6 +1472,7 @@ fn parse_create_table_with_valid_options() { name: Ident { value: "column_c".to_string(), quote_style: None, + span: Span::empty(), }, data_type: Int(None,), collation: None, @@ -1669,11 +1614,13 @@ fn parse_create_table_with_identity_column() { name: ObjectName(vec![Ident { value: "mytable".to_string(), quote_style: None, + span: Span::empty(), },],), columns: vec![ColumnDef { name: Ident { value: "columnA".to_string(), quote_style: None, + span: Span::empty(), }, data_type: Int(None,), collation: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 3d8b08630..943a61718 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -19,12 +19,14 @@ //! Test SQL syntax specific to MySQL. The parser based on the generic dialect //! is also tested (on the inputs it can handle). +use helpers::attached_token::AttachedToken; use matches::assert_matches; use sqlparser::ast::MysqlInsertPriority::{Delayed, HighPriority, LowPriority}; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; use sqlparser::parser::{ParserError, ParserOptions}; +use sqlparser::tokenizer::Span; use sqlparser::tokenizer::Token; use test_utils::*; @@ -142,16 +144,19 @@ fn parse_flush() { ObjectName(vec![ Ident { value: "mek".to_string(), - quote_style: Some('`') + quote_style: Some('`'), + span: Span::empty(), }, Ident { value: "table1".to_string(), - quote_style: Some('`') + quote_style: Some('`'), + span: Span::empty(), } ]), ObjectName(vec![Ident { value: "table2".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]) ] } @@ -179,16 +184,19 @@ fn parse_flush() { ObjectName(vec![ Ident { value: "mek".to_string(), - quote_style: Some('`') + quote_style: Some('`'), + span: Span::empty(), }, Ident { value: "table1".to_string(), - quote_style: Some('`') + quote_style: Some('`'), + span: Span::empty(), } ]), ObjectName(vec![Ident { value: "table2".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]) ] } @@ -205,16 +213,19 @@ fn parse_flush() { ObjectName(vec![ Ident { value: "mek".to_string(), - quote_style: Some('`') + quote_style: Some('`'), + span: Span::empty(), }, Ident { value: "table1".to_string(), - quote_style: Some('`') + quote_style: Some('`'), + span: Span::empty(), } ]), ObjectName(vec![Ident { value: "table2".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]) ] } @@ -1058,12 +1069,14 @@ fn parse_escaped_quote_identifiers_with_escape() { Statement::Query(Box::new(Query { with: None, 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::Identifier(Ident { value: "quoted ` identifier".into(), quote_style: Some('`'), + span: Span::empty(), }))], into: None, from: vec![], @@ -1109,12 +1122,14 @@ fn parse_escaped_quote_identifiers_with_no_escape() { Statement::Query(Box::new(Query { with: None, 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::Identifier(Ident { value: "quoted `` identifier".into(), quote_style: Some('`'), + span: Span::empty(), }))], into: None, from: vec![], @@ -1153,12 +1168,15 @@ fn parse_escaped_backticks_with_escape() { Statement::Query(Box::new(Query { with: None, 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::Identifier(Ident { value: "`quoted identifier`".into(), quote_style: Some('`'), + span: Span::empty(), }))], into: None, from: vec![], @@ -1201,12 +1219,15 @@ fn parse_escaped_backticks_with_no_escape() { Statement::Query(Box::new(Query { with: None, 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::Identifier(Ident { value: "``quoted identifier``".into(), quote_style: Some('`'), + span: Span::empty(), }))], into: None, from: vec![], @@ -1846,6 +1867,8 @@ fn parse_select_with_numeric_prefix_column_name() { assert_eq!( q.body, Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), + distinct: None, top: None, top_before_distinct: false, @@ -1902,6 +1925,8 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { assert_eq!( q.body, Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), + distinct: None, top: None, top_before_distinct: false, @@ -2055,7 +2080,8 @@ fn parse_delete_with_order_by() { vec![OrderByExpr { expr: Expr::Identifier(Ident { value: "id".to_owned(), - quote_style: None + quote_style: None, + span: Span::empty(), }), asc: Some(false), nulls_first: None, @@ -2136,7 +2162,8 @@ fn parse_alter_table_add_column() { }, column_position: Some(MySQLColumnPosition::After(Ident { value: String::from("foo"), - quote_style: None + quote_style: None, + span: Span::empty(), })), },] ); @@ -2187,6 +2214,7 @@ fn parse_alter_table_add_columns() { column_position: Some(MySQLColumnPosition::After(Ident { value: String::from("foo"), quote_style: None, + span: Span::empty(), })), }, ] @@ -2247,6 +2275,7 @@ fn parse_alter_table_change_column() { column_position: Some(MySQLColumnPosition::After(Ident { value: String::from("foo"), quote_style: None, + span: Span::empty(), })), }; let sql4 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL AFTER foo"; @@ -2286,6 +2315,7 @@ fn parse_alter_table_change_column_with_column_position() { column_position: Some(MySQLColumnPosition::After(Ident { value: String::from("total_count"), quote_style: None, + span: Span::empty(), })), }; @@ -2342,6 +2372,7 @@ fn parse_alter_table_modify_column() { column_position: Some(MySQLColumnPosition::After(Ident { value: String::from("foo"), quote_style: None, + span: Span::empty(), })), }; let sql4 = "ALTER TABLE orders MODIFY COLUMN description TEXT NOT NULL AFTER foo"; @@ -2379,6 +2410,7 @@ fn parse_alter_table_modify_column_with_column_position() { column_position: Some(MySQLColumnPosition::After(Ident { value: String::from("total_count"), quote_style: None, + span: Span::empty(), })), }; @@ -2397,6 +2429,8 @@ fn parse_alter_table_modify_column_with_column_position() { #[test] fn parse_substring_in_select() { + use sqlparser::tokenizer::Span; + let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; match mysql().one_statement_parses_to( sql, @@ -2407,13 +2441,15 @@ fn parse_substring_in_select() { Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: Some(Distinct::Distinct), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Substring { expr: Box::new(Expr::Identifier(Ident { value: "description".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), })), substring_from: Some(Box::new(Expr::Value(number("0")))), substring_for: Some(Box::new(Expr::Value(number("1")))), @@ -2424,7 +2460,8 @@ fn parse_substring_in_select() { relation: TableFactor::Table { name: ObjectName(vec![Ident { value: "test".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), alias: None, args: None, @@ -2730,6 +2767,7 @@ fn parse_hex_string_introducer() { Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d27569e03..54f77b7be 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -21,6 +21,8 @@ #[macro_use] mod test_utils; +use helpers::attached_token::AttachedToken; +use sqlparser::tokenizer::Span; use test_utils::*; use sqlparser::ast::*; @@ -1163,6 +1165,7 @@ fn parse_copy_to() { source: CopySource::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -1172,6 +1175,7 @@ fn parse_copy_to() { alias: Ident { value: "a".into(), quote_style: None, + span: Span::empty(), }, }, SelectItem::ExprWithAlias { @@ -1179,6 +1183,7 @@ fn parse_copy_to() { alias: Ident { value: "b".into(), quote_style: None, + span: Span::empty(), }, } ], @@ -1318,7 +1323,8 @@ fn parse_set() { variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), value: vec![Expr::Identifier(Ident { value: "b".into(), - quote_style: None + quote_style: None, + span: Span::empty(), })], } ); @@ -1380,7 +1386,8 @@ fn parse_set() { ])), value: vec![Expr::Identifier(Ident { value: "b".into(), - quote_style: None + quote_style: None, + span: Span::empty(), })], } ); @@ -1452,6 +1459,7 @@ fn parse_set_role() { role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\"'), + span: Span::empty(), }), } ); @@ -1466,6 +1474,7 @@ fn parse_set_role() { role_name: Some(Ident { value: "rolename".to_string(), quote_style: Some('\''), + span: Span::empty(), }), } ); @@ -1765,7 +1774,8 @@ fn parse_pg_on_conflict() { selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { value: "dsize".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), })), op: BinaryOperator::Gt, right: Box::new(Expr::Value(Value::Placeholder("$2".to_string()))) @@ -1802,7 +1812,8 @@ fn parse_pg_on_conflict() { selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { value: "dsize".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), })), op: BinaryOperator::Gt, right: Box::new(Expr::Value(Value::Placeholder("$2".to_string()))) @@ -2105,14 +2116,16 @@ fn parse_array_index_expr() { subscript: Box::new(Subscript::Index { index: Expr::Identifier(Ident { value: "baz".to_string(), - quote_style: Some('"') + quote_style: Some('"'), + span: Span::empty(), }) }) }), subscript: Box::new(Subscript::Index { index: Expr::Identifier(Ident { value: "fooz".to_string(), - quote_style: Some('"') + quote_style: Some('"'), + span: Span::empty(), }) }) }, @@ -2504,6 +2517,7 @@ fn parse_array_subquery_expr() { op: SetOperator::Union, set_quantifier: SetQuantifier::None, left: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -2525,6 +2539,7 @@ fn parse_array_subquery_expr() { connect_by: None, }))), right: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), distinct: None, top: None, top_before_distinct: false, @@ -3123,6 +3138,7 @@ fn parse_custom_operator() { left: Box::new(Expr::Identifier(Ident { value: "relname".into(), quote_style: None, + span: Span::empty(), })), op: BinaryOperator::PGCustomBinaryOperator(vec![ "database".into(), @@ -3142,6 +3158,7 @@ fn parse_custom_operator() { left: Box::new(Expr::Identifier(Ident { value: "relname".into(), quote_style: None, + span: Span::empty(), })), op: BinaryOperator::PGCustomBinaryOperator(vec!["pg_catalog".into(), "~".into()]), right: Box::new(Expr::Value(Value::SingleQuotedString("^(table)$".into()))) @@ -3157,6 +3174,7 @@ fn parse_custom_operator() { left: Box::new(Expr::Identifier(Ident { value: "relname".into(), quote_style: None, + span: Span::empty(), })), op: BinaryOperator::PGCustomBinaryOperator(vec!["~".into()]), right: Box::new(Expr::Value(Value::SingleQuotedString("^(table)$".into()))) @@ -3307,12 +3325,14 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "old_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::RenameRole { role_name: Ident { value: "new_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), } }, } @@ -3324,7 +3344,8 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::WithOptions { options: vec![ @@ -3353,7 +3374,8 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::WithOptions { options: vec![ @@ -3376,12 +3398,14 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::Set { config_name: ObjectName(vec![Ident { value: "maintenance_work_mem".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), config_value: SetConfigValue::FromCurrent, in_database: None @@ -3395,17 +3419,20 @@ fn parse_alter_role() { [Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::Set { config_name: ObjectName(vec![Ident { value: "maintenance_work_mem".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), config_value: SetConfigValue::Value(Expr::Value(number("100000"))), in_database: Some(ObjectName(vec![Ident { value: "database_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }])) }, }] @@ -3417,17 +3444,20 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::Set { config_name: ObjectName(vec![Ident { value: "maintenance_work_mem".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), config_value: SetConfigValue::Value(Expr::Value(number("100000"))), in_database: Some(ObjectName(vec![Ident { value: "database_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }])) }, } @@ -3439,17 +3469,20 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::Set { config_name: ObjectName(vec![Ident { value: "maintenance_work_mem".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), config_value: SetConfigValue::Default, in_database: Some(ObjectName(vec![Ident { value: "database_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }])) }, } @@ -3461,7 +3494,8 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::Reset { config_name: ResetConfig::ALL, @@ -3476,16 +3510,19 @@ fn parse_alter_role() { Statement::AlterRole { name: Ident { value: "role_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }, operation: AlterRoleOperation::Reset { config_name: ResetConfig::ConfigName(ObjectName(vec![Ident { value: "maintenance_work_mem".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }])), in_database: Some(ObjectName(vec![Ident { value: "database_name".into(), - quote_style: None + quote_style: None, + span: Span::empty(), }])) }, } @@ -3630,7 +3667,8 @@ fn parse_drop_function() { func_desc: vec![FunctionDesc { name: ObjectName(vec![Ident { value: "test_func".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), args: None }], @@ -3646,7 +3684,8 @@ fn parse_drop_function() { func_desc: vec![FunctionDesc { name: ObjectName(vec![Ident { value: "test_func".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), args: Some(vec![ OperateFunctionArg::with_name("a", DataType::Integer(None)), @@ -3671,7 +3710,8 @@ fn parse_drop_function() { FunctionDesc { name: ObjectName(vec![Ident { value: "test_func1".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), args: Some(vec![ OperateFunctionArg::with_name("a", DataType::Integer(None)), @@ -3689,7 +3729,8 @@ fn parse_drop_function() { FunctionDesc { name: ObjectName(vec![Ident { value: "test_func2".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), args: Some(vec![ OperateFunctionArg::with_name("a", DataType::Varchar(None)), @@ -3720,7 +3761,8 @@ fn parse_drop_procedure() { proc_desc: vec![FunctionDesc { name: ObjectName(vec![Ident { value: "test_proc".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), args: None }], @@ -3736,7 +3778,8 @@ fn parse_drop_procedure() { proc_desc: vec![FunctionDesc { name: ObjectName(vec![Ident { value: "test_proc".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), args: Some(vec![ OperateFunctionArg::with_name("a", DataType::Integer(None)), @@ -3761,7 +3804,8 @@ fn parse_drop_procedure() { FunctionDesc { name: ObjectName(vec![Ident { value: "test_proc1".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), args: Some(vec![ OperateFunctionArg::with_name("a", DataType::Integer(None)), @@ -3779,7 +3823,8 @@ fn parse_drop_procedure() { FunctionDesc { name: ObjectName(vec![Ident { value: "test_proc2".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), args: Some(vec![ OperateFunctionArg::with_name("a", DataType::Varchar(None)), @@ -3860,6 +3905,7 @@ fn parse_dollar_quoted_string() { alias: Ident { value: "col_name".into(), quote_style: None, + span: Span::empty(), }, } ); @@ -4204,20 +4250,24 @@ fn test_simple_postgres_insert_with_alias() { into: true, table_name: ObjectName(vec![Ident { value: "test_tables".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), table_alias: Some(Ident { value: "test_table".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }), columns: vec![ Ident { value: "id".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }, Ident { value: "a".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), } ], overwrite: false, @@ -4267,20 +4317,24 @@ fn test_simple_postgres_insert_with_alias() { into: true, table_name: ObjectName(vec![Ident { value: "test_tables".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), table_alias: Some(Ident { value: "test_table".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }), columns: vec![ Ident { value: "id".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }, Ident { value: "a".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), } ], overwrite: false, @@ -4332,20 +4386,24 @@ fn test_simple_insert_with_quoted_alias() { into: true, table_name: ObjectName(vec![Ident { value: "test_tables".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }]), table_alias: Some(Ident { value: "Test_Table".to_string(), - quote_style: Some('"') + quote_style: Some('"'), + span: Span::empty(), }), columns: vec![ Ident { value: "id".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), }, Ident { value: "a".to_string(), - quote_style: None + quote_style: None, + span: Span::empty(), } ], overwrite: false, @@ -5017,6 +5075,7 @@ fn check_arrow_precedence(sql: &str, arrow_operator: BinaryOperator) { left: Box::new(Expr::Identifier(Ident { value: "foo".to_string(), quote_style: None, + span: Span::empty(), })), op: arrow_operator, right: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), @@ -5047,6 +5106,7 @@ fn arrow_cast_precedence() { left: Box::new(Expr::Identifier(Ident { value: "foo".to_string(), quote_style: None, + span: Span::empty(), })), op: BinaryOperator::Arrow, right: Box::new(Expr::Cast { diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 0a084b340..f0c1f0c74 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -18,6 +18,7 @@ #[macro_use] mod test_utils; +use sqlparser::tokenizer::Span; use test_utils::*; use sqlparser::ast::*; @@ -31,7 +32,8 @@ fn test_square_brackets_over_db_schema_table_name() { select.projection[0], SelectItem::UnnamedExpr(Expr::Identifier(Ident { value: "col1".to_string(), - quote_style: Some('[') + quote_style: Some('['), + span: Span::empty(), })), ); assert_eq!( @@ -41,11 +43,13 @@ fn test_square_brackets_over_db_schema_table_name() { name: ObjectName(vec![ Ident { value: "test_schema".to_string(), - quote_style: Some('[') + quote_style: Some('['), + span: Span::empty(), }, Ident { value: "test_table".to_string(), - quote_style: Some('[') + quote_style: Some('['), + span: Span::empty(), } ]), alias: None, @@ -79,7 +83,8 @@ fn test_double_quotes_over_db_schema_table_name() { select.projection[0], SelectItem::UnnamedExpr(Expr::Identifier(Ident { value: "col1".to_string(), - quote_style: Some('"') + quote_style: Some('"'), + span: Span::empty(), })), ); assert_eq!( @@ -89,11 +94,13 @@ fn test_double_quotes_over_db_schema_table_name() { name: ObjectName(vec![ Ident { value: "test_schema".to_string(), - quote_style: Some('"') + quote_style: Some('"'), + span: Span::empty(), }, Ident { value: "test_table".to_string(), - quote_style: Some('"') + quote_style: Some('"'), + span: Span::empty(), } ]), alias: None, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index f99a00f5b..08792380d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2762,7 +2762,9 @@ fn parse_view_column_descriptions() { #[test] fn test_parentheses_overflow() { - let max_nesting_level: usize = 30; + // TODO: increase / improve after we fix the recursion limit + // for real (see https://github.com/apache/datafusion-sqlparser-rs/issues/984) + let max_nesting_level: usize = 25; // Verify the recursion check is not too wasteful... (num of parentheses - 2 is acceptable) let slack = 2; From 5a510ac4d9715528ad5c518bf1ce0719cc813b8c Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 27 Nov 2024 11:33:31 -0500 Subject: [PATCH 614/806] Fix error in benchmark queries (#1560) --- sqlparser_bench/benches/sqlparser_bench.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs index 27c58b450..32a6da1bc 100644 --- a/sqlparser_bench/benches/sqlparser_bench.rs +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -23,9 +23,9 @@ fn basic_queries(c: &mut Criterion) { let mut group = c.benchmark_group("sqlparser-rs parsing benchmark"); let dialect = GenericDialect {}; - let string = "SELECT * FROM table WHERE 1 = 1"; + let string = "SELECT * FROM my_table WHERE 1 = 1"; group.bench_function("sqlparser::select", |b| { - b.iter(|| Parser::parse_sql(&dialect, string)); + b.iter(|| Parser::parse_sql(&dialect, string).unwrap()); }); let with_query = " @@ -33,14 +33,14 @@ fn basic_queries(c: &mut Criterion) { SELECT MAX(a) AS max_a, COUNT(b) AS b_num, user_id - FROM TABLE + FROM MY_TABLE GROUP BY user_id ) - SELECT * FROM table + SELECT * FROM my_table LEFT JOIN derived USING (user_id) "; group.bench_function("sqlparser::with_select", |b| { - b.iter(|| Parser::parse_sql(&dialect, with_query)); + b.iter(|| Parser::parse_sql(&dialect, with_query).unwrap()); }); } From 6291afb2c75871edf34b3d2c01ef9249a5369c81 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Fri, 29 Nov 2024 12:37:06 +0100 Subject: [PATCH 615/806] Fix clippy warnings on rust 1.83 (#1570) --- src/ast/ddl.rs | 13 ++++++++----- src/ast/mod.rs | 2 +- src/ast/query.rs | 2 +- src/ast/value.rs | 6 +++--- src/parser/alter.rs | 2 +- src/parser/mod.rs | 6 ++---- src/tokenizer.rs | 2 +- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 21a716d25..3ced478ca 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1327,15 +1327,18 @@ pub enum ColumnOption { /// `DEFAULT ` Default(Expr), - /// ClickHouse supports `MATERIALIZE`, `EPHEMERAL` and `ALIAS` expr to generate default values. + /// `MATERIALIZE ` /// Syntax: `b INT MATERIALIZE (a + 1)` + /// /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values) - - /// `MATERIALIZE ` Materialized(Expr), /// `EPHEMERAL []` + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values) Ephemeral(Option), /// `ALIAS ` + /// + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values) Alias(Expr), /// `{ PRIMARY KEY | UNIQUE } []` @@ -1552,7 +1555,7 @@ pub enum GeneratedExpressionMode { #[must_use] fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); - impl<'a> fmt::Display for ConstraintName<'a> { + impl fmt::Display for ConstraintName<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(name) = self.0 { write!(f, "CONSTRAINT {name} ")?; @@ -1573,7 +1576,7 @@ fn display_option<'a, T: fmt::Display>( option: &'a Option, ) -> impl fmt::Display + 'a { struct OptionDisplay<'a, T>(&'a str, &'a str, &'a Option); - impl<'a, T: fmt::Display> fmt::Display for OptionDisplay<'a, T> { + impl fmt::Display for OptionDisplay<'_, T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(inner) = self.2 { let (prefix, postfix) = (self.0, self.1); diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 366bf4d25..386e42fb3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -110,7 +110,7 @@ where sep: &'static str, } -impl<'a, T> fmt::Display for DisplaySeparated<'a, T> +impl fmt::Display for DisplaySeparated<'_, T> where T: fmt::Display, { diff --git a/src/ast/query.rs b/src/ast/query.rs index 0472026a0..716ffe98c 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1713,7 +1713,7 @@ impl fmt::Display for Join { } fn suffix(constraint: &'_ JoinConstraint) -> impl fmt::Display + '_ { struct Suffix<'a>(&'a JoinConstraint); - impl<'a> fmt::Display for Suffix<'a> { + impl fmt::Display for Suffix<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.0 { JoinConstraint::On(expr) => write!(f, " ON {expr}"), diff --git a/src/ast/value.rs b/src/ast/value.rs index 30d956a07..28bf89ba8 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -261,7 +261,7 @@ pub struct EscapeQuotedString<'a> { quote: char, } -impl<'a> fmt::Display for EscapeQuotedString<'a> { +impl fmt::Display for EscapeQuotedString<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // EscapeQuotedString doesn't know which mode of escape was // chosen by the user. So this code must to correctly display @@ -325,7 +325,7 @@ pub fn escape_double_quote_string(s: &str) -> EscapeQuotedString<'_> { pub struct EscapeEscapedStringLiteral<'a>(&'a str); -impl<'a> fmt::Display for EscapeEscapedStringLiteral<'a> { +impl fmt::Display for EscapeEscapedStringLiteral<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for c in self.0.chars() { match c { @@ -359,7 +359,7 @@ pub fn escape_escaped_string(s: &str) -> EscapeEscapedStringLiteral<'_> { pub struct EscapeUnicodeStringLiteral<'a>(&'a str); -impl<'a> fmt::Display for EscapeUnicodeStringLiteral<'a> { +impl fmt::Display for EscapeUnicodeStringLiteral<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for c in self.0.chars() { match c { diff --git a/src/parser/alter.rs b/src/parser/alter.rs index 534105790..3ac4ab0c7 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -26,7 +26,7 @@ use crate::{ tokenizer::Token, }; -impl<'a> Parser<'a> { +impl Parser<'_> { pub fn parse_alter_role(&mut self) -> Result { if dialect_of!(self is PostgreSqlDialect) { return self.parse_pg_alter_role(); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b7f5cb866..fe6fae8bf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10883,13 +10883,12 @@ impl<'a> Parser<'a> { Ok(ExprWithAlias { expr, alias }) } /// Parses an expression with an optional alias - + /// /// Examples: - + /// /// ```sql /// SUM(price) AS total_price /// ``` - /// ```sql /// SUM(price) /// ``` @@ -10905,7 +10904,6 @@ impl<'a> Parser<'a> { /// assert_eq!(Some("b".to_string()), expr_with_alias.alias.map(|x|x.value)); /// # Ok(()) /// # } - pub fn parse_expr_with_alias(&mut self) -> Result { let expr = self.parse_expr()?; let alias = if self.parse_keyword(Keyword::AS) { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index a57ba2ec8..bed2d9b52 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -584,7 +584,7 @@ struct State<'a> { pub col: u64, } -impl<'a> State<'a> { +impl State<'_> { /// return the next character and advance the stream pub fn next(&mut self) -> Option { match self.peekable.next() { From 92c6e7f79b2a9b54a17566e338c915565c8267bb Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Fri, 29 Nov 2024 20:08:52 +0800 Subject: [PATCH 616/806] Support relation visitor to visit the `Option` field (#1556) --- derive/README.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++ derive/src/lib.rs | 36 +++++++++++++++++++++++++++------- src/ast/mod.rs | 1 + src/ast/visitor.rs | 11 ++++++++++- 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/derive/README.md b/derive/README.md index aa70e7c71..b5ccc69e0 100644 --- a/derive/README.md +++ b/derive/README.md @@ -151,6 +151,55 @@ visitor.post_visit_expr() visitor.post_visit_expr() ``` +If the field is a `Option` and add `#[with = "visit_xxx"]` to the field, the generated code +will try to access the field only if it is `Some`: + +```rust +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ShowStatementIn { + pub clause: ShowStatementInClause, + pub parent_type: Option, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub parent_name: Option, +} +``` + +This will generate + +```rust +impl sqlparser::ast::Visit for ShowStatementIn { + fn visit( + &self, + visitor: &mut V, + ) -> ::std::ops::ControlFlow { + sqlparser::ast::Visit::visit(&self.clause, visitor)?; + sqlparser::ast::Visit::visit(&self.parent_type, visitor)?; + if let Some(value) = &self.parent_name { + visitor.pre_visit_relation(value)?; + sqlparser::ast::Visit::visit(value, visitor)?; + visitor.post_visit_relation(value)?; + } + ::std::ops::ControlFlow::Continue(()) + } +} + +impl sqlparser::ast::VisitMut for ShowStatementIn { + fn visit( + &mut self, + visitor: &mut V, + ) -> ::std::ops::ControlFlow { + sqlparser::ast::VisitMut::visit(&mut self.clause, visitor)?; + sqlparser::ast::VisitMut::visit(&mut self.parent_type, visitor)?; + if let Some(value) = &mut self.parent_name { + visitor.pre_visit_relation(value)?; + sqlparser::ast::VisitMut::visit(value, visitor)?; + visitor.post_visit_relation(value)?; + } + ::std::ops::ControlFlow::Continue(()) + } +} +``` + ## Releasing This crate's release is not automated. Instead it is released manually as needed diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5ad1607f9..dd4d37b41 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -18,11 +18,8 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::spanned::Spanned; -use syn::{ - parse::{Parse, ParseStream}, - parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Fields, GenericParam, Generics, - Ident, Index, LitStr, Meta, Token, -}; +use syn::{parse::{Parse, ParseStream}, parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Fields, GenericParam, Generics, Ident, Index, LitStr, Meta, Token, Type, TypePath}; +use syn::{Path, PathArguments}; /// Implementation of `[#derive(Visit)]` #[proc_macro_derive(VisitMut, attributes(visit))] @@ -182,9 +179,21 @@ fn visit_children( Fields::Named(fields) => { let recurse = fields.named.iter().map(|f| { let name = &f.ident; + let is_option = is_option(&f.ty); let attributes = Attributes::parse(&f.attrs); - let (pre_visit, post_visit) = attributes.visit(quote!(&#modifier self.#name)); - quote_spanned!(f.span() => #pre_visit sqlparser::ast::#visit_trait::visit(&#modifier self.#name, visitor)?; #post_visit) + if is_option && attributes.with.is_some() { + let (pre_visit, post_visit) = attributes.visit(quote!(value)); + quote_spanned!(f.span() => + if let Some(value) = &#modifier self.#name { + #pre_visit sqlparser::ast::#visit_trait::visit(value, visitor)?; #post_visit + } + ) + } else { + let (pre_visit, post_visit) = attributes.visit(quote!(&#modifier self.#name)); + quote_spanned!(f.span() => + #pre_visit sqlparser::ast::#visit_trait::visit(&#modifier self.#name, visitor)?; #post_visit + ) + } }); quote! { #(#recurse)* @@ -256,3 +265,16 @@ fn visit_children( Data::Union(_) => unimplemented!(), } } + +fn is_option(ty: &Type) -> bool { + if let Type::Path(TypePath { path: Path { segments, .. }, .. }) = ty { + if let Some(segment) = segments.last() { + if segment.ident == "Option" { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + return args.args.len() == 1; + } + } + } + } + false +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 386e42fb3..19da04c62 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7653,6 +7653,7 @@ impl fmt::Display for ShowStatementInParentType { pub struct ShowStatementIn { pub clause: ShowStatementInClause, pub parent_type: Option, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub parent_name: Option, } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 418e0a299..eacd268a4 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -876,7 +876,16 @@ mod tests { "POST: QUERY: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID", "POST: STATEMENT: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID", ] - ) + ), + ( + "SHOW COLUMNS FROM t1", + vec![ + "PRE: STATEMENT: SHOW COLUMNS FROM t1", + "PRE: RELATION: t1", + "POST: RELATION: t1", + "POST: STATEMENT: SHOW COLUMNS FROM t1", + ], + ), ]; for (sql, expected) in tests { let actual = do_visit(sql); From a134910a362d12acb668ddf63239525073e7340f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 30 Nov 2024 07:55:21 -0500 Subject: [PATCH 617/806] Rename `TokenWithLocation` to `TokenWithSpan`, in backwards compatible way (#1562) --- src/ast/helpers/attached_token.rs | 10 ++--- src/ast/query.rs | 4 +- src/parser/mod.rs | 66 +++++++++++++++---------------- src/tokenizer.rs | 50 ++++++++++++----------- 4 files changed, 67 insertions(+), 63 deletions(-) diff --git a/src/ast/helpers/attached_token.rs b/src/ast/helpers/attached_token.rs index 48696c336..ed340359d 100644 --- a/src/ast/helpers/attached_token.rs +++ b/src/ast/helpers/attached_token.rs @@ -19,7 +19,7 @@ use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt::{self, Debug, Formatter}; use core::hash::{Hash, Hasher}; -use crate::tokenizer::{Token, TokenWithLocation}; +use crate::tokenizer::{Token, TokenWithSpan}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -33,11 +33,11 @@ use sqlparser_derive::{Visit, VisitMut}; #[derive(Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct AttachedToken(pub TokenWithLocation); +pub struct AttachedToken(pub TokenWithSpan); impl AttachedToken { pub fn empty() -> Self { - AttachedToken(TokenWithLocation::wrap(Token::EOF)) + AttachedToken(TokenWithSpan::wrap(Token::EOF)) } } @@ -75,8 +75,8 @@ impl Hash for AttachedToken { } } -impl From for AttachedToken { - fn from(value: TokenWithLocation) -> Self { +impl From for AttachedToken { + fn from(value: TokenWithSpan) -> Self { AttachedToken(value) } } diff --git a/src/ast/query.rs b/src/ast/query.rs index 716ffe98c..f3a76d893 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -27,7 +27,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::{ ast::*, - tokenizer::{Token, TokenWithLocation}, + tokenizer::{Token, TokenWithSpan}, }; /// The most complete variant of a `SELECT` query expression, optionally @@ -643,7 +643,7 @@ pub struct WildcardAdditionalOptions { impl Default for WildcardAdditionalOptions { fn default() -> Self { Self { - wildcard_token: TokenWithLocation::wrap(Token::Mul).into(), + wildcard_token: TokenWithSpan::wrap(Token::Mul).into(), opt_ilike: None, opt_exclude: None, opt_except: None, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fe6fae8bf..1f8dc8ba9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -265,7 +265,7 @@ enum ParserState { } pub struct Parser<'a> { - tokens: Vec, + tokens: Vec, /// The index of the first unprocessed token in [`Parser::tokens`]. index: usize, /// The current state of the parser. @@ -359,7 +359,7 @@ impl<'a> Parser<'a> { } /// Reset this parser to parse the specified token stream - pub fn with_tokens_with_locations(mut self, tokens: Vec) -> Self { + pub fn with_tokens_with_locations(mut self, tokens: Vec) -> Self { self.tokens = tokens; self.index = 0; self @@ -368,9 +368,9 @@ impl<'a> Parser<'a> { /// Reset this parser state to parse the specified tokens pub fn with_tokens(self, tokens: Vec) -> Self { // Put in dummy locations - let tokens_with_locations: Vec = tokens + let tokens_with_locations: Vec = tokens .into_iter() - .map(|token| TokenWithLocation { + .map(|token| TokenWithSpan { token, span: Span::empty(), }) @@ -1147,7 +1147,7 @@ impl<'a> Parser<'a> { match self.peek_token().token { Token::LParen | Token::Period => { let mut id_parts: Vec = vec![w.to_ident(w_span)]; - let mut ending_wildcard: Option = None; + let mut ending_wildcard: Option = None; while self.consume_token(&Token::Period) { let next_token = self.next_token(); match next_token.token { @@ -3273,7 +3273,7 @@ impl<'a> Parser<'a> { /// Return the first non-whitespace token that has not yet been processed /// (or None if reached end-of-file) - pub fn peek_token(&self) -> TokenWithLocation { + pub fn peek_token(&self) -> TokenWithSpan { self.peek_nth_token(0) } @@ -3308,19 +3308,19 @@ impl<'a> Parser<'a> { /// yet been processed. /// /// See [`Self::peek_token`] for an example. - pub fn peek_tokens_with_location(&self) -> [TokenWithLocation; N] { + pub fn peek_tokens_with_location(&self) -> [TokenWithSpan; N] { let mut index = self.index; core::array::from_fn(|_| loop { let token = self.tokens.get(index); index += 1; - if let Some(TokenWithLocation { + if let Some(TokenWithSpan { token: Token::Whitespace(_), span: _, }) = token { continue; } - break token.cloned().unwrap_or(TokenWithLocation { + break token.cloned().unwrap_or(TokenWithSpan { token: Token::EOF, span: Span::empty(), }); @@ -3328,18 +3328,18 @@ impl<'a> Parser<'a> { } /// Return nth non-whitespace token that has not yet been processed - pub fn peek_nth_token(&self, mut n: usize) -> TokenWithLocation { + pub fn peek_nth_token(&self, mut n: usize) -> TokenWithSpan { let mut index = self.index; loop { index += 1; match self.tokens.get(index - 1) { - Some(TokenWithLocation { + Some(TokenWithSpan { token: Token::Whitespace(_), span: _, }) => continue, non_whitespace => { if n == 0 { - return non_whitespace.cloned().unwrap_or(TokenWithLocation { + return non_whitespace.cloned().unwrap_or(TokenWithSpan { token: Token::EOF, span: Span::empty(), }); @@ -3352,16 +3352,16 @@ impl<'a> Parser<'a> { /// Return the first token, possibly whitespace, that has not yet been processed /// (or None if reached end-of-file). - pub fn peek_token_no_skip(&self) -> TokenWithLocation { + pub fn peek_token_no_skip(&self) -> TokenWithSpan { self.peek_nth_token_no_skip(0) } /// Return nth token, possibly whitespace, that has not yet been processed. - pub fn peek_nth_token_no_skip(&self, n: usize) -> TokenWithLocation { + pub fn peek_nth_token_no_skip(&self, n: usize) -> TokenWithSpan { self.tokens .get(self.index + n) .cloned() - .unwrap_or(TokenWithLocation { + .unwrap_or(TokenWithSpan { token: Token::EOF, span: Span::empty(), }) @@ -3378,25 +3378,25 @@ impl<'a> Parser<'a> { /// Return the first non-whitespace token that has not yet been processed /// (or None if reached end-of-file) and mark it as processed. OK to call /// repeatedly after reaching EOF. - pub fn next_token(&mut self) -> TokenWithLocation { + pub fn next_token(&mut self) -> TokenWithSpan { loop { self.index += 1; match self.tokens.get(self.index - 1) { - Some(TokenWithLocation { + Some(TokenWithSpan { token: Token::Whitespace(_), span: _, }) => continue, token => { return token .cloned() - .unwrap_or_else(|| TokenWithLocation::wrap(Token::EOF)) + .unwrap_or_else(|| TokenWithSpan::wrap(Token::EOF)) } } } } /// Return the first unprocessed token, possibly whitespace. - pub fn next_token_no_skip(&mut self) -> Option<&TokenWithLocation> { + pub fn next_token_no_skip(&mut self) -> Option<&TokenWithSpan> { self.index += 1; self.tokens.get(self.index - 1) } @@ -3408,7 +3408,7 @@ impl<'a> Parser<'a> { loop { assert!(self.index > 0); self.index -= 1; - if let Some(TokenWithLocation { + if let Some(TokenWithSpan { token: Token::Whitespace(_), span: _, }) = self.tokens.get(self.index) @@ -3420,7 +3420,7 @@ impl<'a> Parser<'a> { } /// Report `found` was encountered instead of `expected` - pub fn expected(&self, expected: &str, found: TokenWithLocation) -> Result { + pub fn expected(&self, expected: &str, found: TokenWithSpan) -> Result { parser_err!( format!("Expected: {expected}, found: {found}"), found.span.start @@ -3435,7 +3435,7 @@ impl<'a> Parser<'a> { } #[must_use] - pub fn parse_keyword_token(&mut self, expected: Keyword) -> Option { + pub fn parse_keyword_token(&mut self, expected: Keyword) -> Option { match self.peek_token().token { Token::Word(w) if expected == w.keyword => Some(self.next_token()), _ => None, @@ -3524,7 +3524,7 @@ impl<'a> Parser<'a> { /// If the current token is the `expected` keyword, consume the token. /// Otherwise, return an error. - pub fn expect_keyword(&mut self, expected: Keyword) -> Result { + pub fn expect_keyword(&mut self, expected: Keyword) -> Result { if let Some(token) = self.parse_keyword_token(expected) { Ok(token) } else { @@ -3568,7 +3568,7 @@ impl<'a> Parser<'a> { } /// Bail out if the current token is not an expected keyword, or consume it if it is - pub fn expect_token(&mut self, expected: &Token) -> Result { + pub fn expect_token(&mut self, expected: &Token) -> Result { if self.peek_token() == *expected { Ok(self.next_token()) } else { @@ -4107,7 +4107,7 @@ impl<'a> Parser<'a> { Keyword::ARCHIVE => Ok(Some(CreateFunctionUsing::Archive(uri))), _ => self.expected( "JAR, FILE or ARCHIVE, got {:?}", - TokenWithLocation::wrap(Token::make_keyword(format!("{keyword:?}").as_str())), + TokenWithSpan::wrap(Token::make_keyword(format!("{keyword:?}").as_str())), ), } } @@ -6832,7 +6832,7 @@ impl<'a> Parser<'a> { if let Some(name) = name { return self.expected( "FULLTEXT or SPATIAL option without constraint name", - TokenWithLocation { + TokenWithSpan { token: Token::make_keyword(&name.to_string()), span: next_token.span, }, @@ -7808,7 +7808,7 @@ impl<'a> Parser<'a> { Some('\'') => Ok(Value::SingleQuotedString(w.value)), _ => self.expected( "A value?", - TokenWithLocation { + TokenWithSpan { token: Token::Word(w), span, }, @@ -7816,7 +7816,7 @@ impl<'a> Parser<'a> { }, _ => self.expected( "a concrete value", - TokenWithLocation { + TokenWithSpan { token: Token::Word(w), span, }, @@ -7878,7 +7878,7 @@ impl<'a> Parser<'a> { } unexpected => self.expected( "a value", - TokenWithLocation { + TokenWithSpan { token: unexpected, span, }, @@ -7927,7 +7927,7 @@ impl<'a> Parser<'a> { Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), unexpected => self.expected( "a string value", - TokenWithLocation { + TokenWithSpan { token: unexpected, span, }, @@ -8618,7 +8618,7 @@ impl<'a> Parser<'a> { let token = self .next_token_no_skip() .cloned() - .unwrap_or(TokenWithLocation::wrap(Token::EOF)); + .unwrap_or(TokenWithSpan::wrap(Token::EOF)); requires_whitespace = match token.token { Token::Word(next_word) if next_word.quote_style.is_none() => { ident.value.push_str(&next_word.value); @@ -11683,7 +11683,7 @@ impl<'a> Parser<'a> { /// If it is not possible to parse it, will return an option. pub fn parse_wildcard_additional_options( &mut self, - wildcard_token: TokenWithLocation, + wildcard_token: TokenWithSpan, ) -> Result { let opt_ilike = if dialect_of!(self is GenericDialect | SnowflakeDialect) { self.parse_optional_select_item_ilike()? @@ -12708,7 +12708,7 @@ impl<'a> Parser<'a> { } /// Consume the parser and return its underlying token buffer - pub fn into_tokens(self) -> Vec { + pub fn into_tokens(self) -> Vec { self.tokens } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index bed2d9b52..7a79445e0 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -521,42 +521,46 @@ impl Span { } } +/// Backwards compatibility struct for [`TokenWithSpan`] +#[deprecated(since = "0.53.0", note = "please use `TokenWithSpan` instead")] +pub type TokenWithLocation = TokenWithSpan; + /// A [Token] with [Location] attached to it #[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct TokenWithLocation { +pub struct TokenWithSpan { pub token: Token, pub span: Span, } -impl TokenWithLocation { - pub fn new(token: Token, span: Span) -> TokenWithLocation { - TokenWithLocation { token, span } +impl TokenWithSpan { + pub fn new(token: Token, span: Span) -> TokenWithSpan { + TokenWithSpan { token, span } } - pub fn wrap(token: Token) -> TokenWithLocation { - TokenWithLocation::new(token, Span::empty()) + pub fn wrap(token: Token) -> TokenWithSpan { + TokenWithSpan::new(token, Span::empty()) } - pub fn at(token: Token, start: Location, end: Location) -> TokenWithLocation { - TokenWithLocation::new(token, Span::new(start, end)) + pub fn at(token: Token, start: Location, end: Location) -> TokenWithSpan { + TokenWithSpan::new(token, Span::new(start, end)) } } -impl PartialEq for TokenWithLocation { +impl PartialEq for TokenWithSpan { fn eq(&self, other: &Token) -> bool { &self.token == other } } -impl PartialEq for Token { - fn eq(&self, other: &TokenWithLocation) -> bool { +impl PartialEq for Token { + fn eq(&self, other: &TokenWithSpan) -> bool { self == &other.token } } -impl fmt::Display for TokenWithLocation { +impl fmt::Display for TokenWithSpan { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.token.fmt(f) } @@ -716,8 +720,8 @@ impl<'a> Tokenizer<'a> { } /// Tokenize the statement and produce a vector of tokens with location information - pub fn tokenize_with_location(&mut self) -> Result, TokenizerError> { - let mut tokens: Vec = vec![]; + pub fn tokenize_with_location(&mut self) -> Result, TokenizerError> { + let mut tokens: Vec = vec![]; self.tokenize_with_location_into_buf(&mut tokens) .map(|_| tokens) } @@ -726,7 +730,7 @@ impl<'a> Tokenizer<'a> { /// If an error is thrown, the buffer will contain all tokens that were successfully parsed before the error. pub fn tokenize_with_location_into_buf( &mut self, - buf: &mut Vec, + buf: &mut Vec, ) -> Result<(), TokenizerError> { let mut state = State { peekable: self.query.chars().peekable(), @@ -738,7 +742,7 @@ impl<'a> Tokenizer<'a> { while let Some(token) = self.next_token(&mut state)? { let span = location.span_to(state.location()); - buf.push(TokenWithLocation { token, span }); + buf.push(TokenWithSpan { token, span }); location = state.location(); } @@ -2751,25 +2755,25 @@ mod tests { .tokenize_with_location() .unwrap(); let expected = vec![ - TokenWithLocation::at(Token::make_keyword("SELECT"), (1, 1).into(), (1, 7).into()), - TokenWithLocation::at( + TokenWithSpan::at(Token::make_keyword("SELECT"), (1, 1).into(), (1, 7).into()), + TokenWithSpan::at( Token::Whitespace(Whitespace::Space), (1, 7).into(), (1, 8).into(), ), - TokenWithLocation::at(Token::make_word("a", None), (1, 8).into(), (1, 9).into()), - TokenWithLocation::at(Token::Comma, (1, 9).into(), (1, 10).into()), - TokenWithLocation::at( + TokenWithSpan::at(Token::make_word("a", None), (1, 8).into(), (1, 9).into()), + TokenWithSpan::at(Token::Comma, (1, 9).into(), (1, 10).into()), + TokenWithSpan::at( Token::Whitespace(Whitespace::Newline), (1, 10).into(), (2, 1).into(), ), - TokenWithLocation::at( + TokenWithSpan::at( Token::Whitespace(Whitespace::Space), (2, 1).into(), (2, 2).into(), ), - TokenWithLocation::at(Token::make_word("b", None), (2, 2).into(), (2, 3).into()), + TokenWithSpan::at(Token::make_word("b", None), (2, 2).into(), (2, 3).into()), ]; compare(expected, tokens); } From 48b0e4db4e07c6f9552e2a646cfbc699add41ae1 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Sat, 30 Nov 2024 04:55:54 -0800 Subject: [PATCH 618/806] Support MySQL size variants for BLOB and TEXT columns (#1564) --- src/ast/data_type.rs | 30 ++++++++++++++++++++++++++++++ src/keywords.rs | 6 ++++++ src/parser/mod.rs | 6 ++++++ tests/sqlparser_mysql.rs | 17 +++++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index bc48341c4..fbfdc2dcf 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -76,6 +76,18 @@ pub enum DataType { /// [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]: https://dev.mysql.com/doc/refman/9.1/en/blob.html + TinyBlob, + /// [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]: 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 @@ -275,6 +287,18 @@ pub enum DataType { Regclass, /// Text Text, + /// [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]: https://dev.mysql.com/doc/refman/9.1/en/blob.html + MediumText, + /// [MySQL] text with up to 2**32 bytes + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/blob.html + LongText, /// String with optional length. String(Option), /// A fixed-length string e.g [ClickHouse][1]. @@ -355,6 +379,9 @@ impl fmt::Display for DataType { format_type_with_optional_length(f, "VARBINARY", size, false) } DataType::Blob(size) => format_type_with_optional_length(f, "BLOB", size, false), + DataType::TinyBlob => write!(f, "TINYBLOB"), + DataType::MediumBlob => write!(f, "MEDIUMBLOB"), + DataType::LongBlob => write!(f, "LONGBLOB"), DataType::Bytes(size) => format_type_with_optional_length(f, "BYTES", size, false), DataType::Numeric(info) => { write!(f, "NUMERIC{info}") @@ -486,6 +513,9 @@ impl fmt::Display for DataType { DataType::JSONB => write!(f, "JSONB"), DataType::Regclass => write!(f, "REGCLASS"), DataType::Text => write!(f, "TEXT"), + DataType::TinyText => write!(f, "TINYTEXT"), + DataType::MediumText => write!(f, "MEDIUMTEXT"), + DataType::LongText => write!(f, "LONGTEXT"), DataType::String(size) => format_type_with_optional_length(f, "STRING", size, false), DataType::Bytea => write!(f, "BYTEA"), DataType::Array(ty) => match ty { diff --git a/src/keywords.rs b/src/keywords.rs index 8c0ed588f..4ec088941 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -453,6 +453,8 @@ define_keywords!( LOCKED, LOGIN, LOGS, + LONGBLOB, + LONGTEXT, LOWCARDINALITY, LOWER, LOW_PRIORITY, @@ -471,7 +473,9 @@ define_keywords!( MAXVALUE, MAX_DATA_EXTENSION_TIME_IN_DAYS, MEASURES, + MEDIUMBLOB, MEDIUMINT, + MEDIUMTEXT, MEMBER, MERGE, METADATA, @@ -765,7 +769,9 @@ define_keywords!( TIMEZONE_HOUR, TIMEZONE_MINUTE, TIMEZONE_REGION, + TINYBLOB, TINYINT, + TINYTEXT, TO, TOP, TOTALS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1f8dc8ba9..afce1f713 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8129,6 +8129,9 @@ impl<'a> Parser<'a> { Keyword::BINARY => Ok(DataType::Binary(self.parse_optional_precision()?)), Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_optional_precision()?)), Keyword::BLOB => Ok(DataType::Blob(self.parse_optional_precision()?)), + Keyword::TINYBLOB => Ok(DataType::TinyBlob), + Keyword::MEDIUMBLOB => Ok(DataType::MediumBlob), + Keyword::LONGBLOB => Ok(DataType::LongBlob), Keyword::BYTES => Ok(DataType::Bytes(self.parse_optional_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), @@ -8188,6 +8191,9 @@ impl<'a> Parser<'a> { Ok(DataType::FixedString(character_length)) } Keyword::TEXT => Ok(DataType::Text), + Keyword::TINYTEXT => Ok(DataType::TinyText), + Keyword::MEDIUMTEXT => Ok(DataType::MediumText), + Keyword::LONGTEXT => Ok(DataType::LongText), Keyword::BYTEA => Ok(DataType::Bytea), Keyword::NUMERIC => Ok(DataType::Numeric( self.parse_exact_number_optional_precision_scale()?, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 943a61718..2b132331e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3014,3 +3014,20 @@ fn parse_bitstring_literal() { ))] ); } + +#[test] +fn parse_longblob_type() { + let sql = "CREATE TABLE foo (bar LONGBLOB)"; + let stmt = mysql_and_generic().verified_stmt(sql); + if let Statement::CreateTable(CreateTable { columns, .. }) = stmt { + assert_eq!(columns.len(), 1); + assert_eq!(columns[0].data_type, DataType::LongBlob); + } else { + unreachable!() + } + mysql_and_generic().verified_stmt("CREATE TABLE foo (bar TINYBLOB)"); + mysql_and_generic().verified_stmt("CREATE TABLE foo (bar MEDIUMBLOB)"); + mysql_and_generic().verified_stmt("CREATE TABLE foo (bar TINYTEXT)"); + mysql_and_generic().verified_stmt("CREATE TABLE foo (bar MEDIUMTEXT)"); + mysql_and_generic().verified_stmt("CREATE TABLE foo (bar LONGTEXT)"); +} From b0007389dc769783fd050a2f4e9f1a45e5f07778 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 30 Nov 2024 08:00:34 -0500 Subject: [PATCH 619/806] Increase version of sqlparser_derive from 0.2.2 to 0.3.0 (#1571) --- Cargo.toml | 2 +- derive/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 18b246e04..c4d0094f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } # of dev-dependencies because of # https://github.com/rust-lang/cargo/issues/1596 serde_json = { version = "1.0", optional = true } -sqlparser_derive = { version = "0.2.0", path = "derive", optional = true } +sqlparser_derive = { version = "0.3.0", path = "derive", optional = true } [dev-dependencies] simple_logger = "5.0" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 3b115b950..7b6477300 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -17,8 +17,8 @@ [package] name = "sqlparser_derive" -description = "proc macro for sqlparser" -version = "0.2.2" +description = "Procedural (proc) macros for sqlparser" +version = "0.3.0" authors = ["sqlparser-rs authors"] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser_derive/" From 96f7c0277a20d0c953f2e1026347795191370caf Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Sat, 30 Nov 2024 14:01:13 +0100 Subject: [PATCH 620/806] `json_object('k' VALUE 'v')` in postgres (#1547) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 3 +++ src/parser/mod.rs | 3 +++ tests/sqlparser_postgres.rs | 13 +++++++++++++ 3 files changed, 19 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 19da04c62..6d35badf9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5528,6 +5528,8 @@ pub enum FunctionArgOperator { Assignment, /// function(arg1 : value1) Colon, + /// function(arg1 VALUE value1) + Value, } impl fmt::Display for FunctionArgOperator { @@ -5537,6 +5539,7 @@ impl fmt::Display for FunctionArgOperator { FunctionArgOperator::RightArrow => f.write_str("=>"), FunctionArgOperator::Assignment => f.write_str(":="), FunctionArgOperator::Colon => f.write_str(":"), + FunctionArgOperator::Value => f.write_str("VALUE"), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index afce1f713..7148ae48a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11482,6 +11482,9 @@ impl<'a> Parser<'a> { } fn parse_function_named_arg_operator(&mut self) -> Result { + if self.parse_keyword(Keyword::VALUE) { + return Ok(FunctionArgOperator::Value); + } let tok = self.next_token(); match tok.token { Token::RArrow if self.dialect.supports_named_fn_args_with_rarrow_operator() => { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 54f77b7be..f94e2f540 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2824,6 +2824,19 @@ fn test_json() { ); } +#[test] +fn test_fn_arg_with_value_operator() { + match pg().verified_expr("JSON_OBJECT('name' VALUE 'value')") { + Expr::Function(Function { args: FunctionArguments::List(FunctionArgumentList { args, .. }), .. }) => { + assert!(matches!( + &args[..], + &[FunctionArg::ExprNamed { operator: FunctionArgOperator::Value, .. }] + ), "Invalid function argument: {:?}", args); + } + other => panic!("Expected: JSON_OBJECT('name' VALUE 'value') to be parsed as a function, but got {other:?}"), + } +} + #[test] fn parse_json_table_is_not_reserved() { // JSON_TABLE is not a reserved keyword in PostgreSQL, even though it is in SQL:2023 From f4f112d7d6ffc1a9de30fc66128030b78415c67c Mon Sep 17 00:00:00 2001 From: Ayman Elkfrawy <120422207+ayman-sigma@users.noreply.github.com> Date: Sat, 30 Nov 2024 05:02:08 -0800 Subject: [PATCH 621/806] Support snowflake double dot notation for object name (#1540) --- src/dialect/mod.rs | 10 ++++++++++ src/dialect/snowflake.rs | 8 ++++++++ src/parser/mod.rs | 7 +++++++ tests/sqlparser_snowflake.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b622c1da3..a8993e685 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -365,6 +365,16 @@ pub trait Dialect: Debug + Any { self.supports_trailing_commas() } + /// Returns true if the dialect supports double dot notation for object names + /// + /// Example + /// ```sql + /// SELECT * FROM db_name..table_name + /// ``` + fn supports_object_name_double_dot_notation(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 56919fb31..77d2ccff1 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -54,6 +54,14 @@ impl Dialect for SnowflakeDialect { true } + // Snowflake supports double-dot notation when the schema name is not specified + // In this case the default PUBLIC schema is used + // + // see https://docs.snowflake.com/en/sql-reference/name-resolution#resolution-when-schema-omitted-double-dot-notation + fn supports_object_name_double_dot_notation(&self) -> bool { + true + } + fn is_identifier_part(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7148ae48a..16362ebba 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8457,6 +8457,13 @@ impl<'a> Parser<'a> { pub fn parse_object_name(&mut self, in_table_clause: bool) -> Result { let mut idents = vec![]; loop { + 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("")); + } idents.push(self.parse_identifier(in_table_clause)?); if !self.consume_token(&Token::Period) { break; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 08792380d..e31811c2b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2866,3 +2866,35 @@ fn test_projection_with_nested_trailing_commas() { let sql = "SELECT a, b, FROM c, (SELECT d, e, FROM f, LATERAL FLATTEN(input => events))"; let _ = snowflake().parse_sql_statements(sql).unwrap(); } + +#[test] +fn test_sf_double_dot_notation() { + snowflake().verified_stmt("SELECT * FROM db_name..table_name"); + snowflake().verified_stmt("SELECT * FROM x, y..z JOIN a..b AS b ON x.id = b.id"); + + assert_eq!( + snowflake() + .parse_sql_statements("SELECT * FROM X.Y..") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: ." + ); + assert_eq!( + snowflake() + .parse_sql_statements("SELECT * FROM X..Y..Z") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: ." + ); + assert_eq!( + // Ensure we don't parse leading token + snowflake() + .parse_sql_statements("SELECT * FROM .X.Y") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: ." + ); +} + +#[test] +fn test_parse_double_dot_notation_wrong_position() {} From 4ab3ab91473d152c652e6582b63abb13535703f9 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 30 Nov 2024 08:08:55 -0500 Subject: [PATCH 622/806] Update comments / docs for `Spanned` (#1549) Co-authored-by: Ifeanyi Ubah --- README.md | 19 +++-- docs/source_spans.md | 52 ------------ src/ast/helpers/attached_token.rs | 64 +++++++++++++-- src/ast/mod.rs | 16 +++- src/ast/query.rs | 5 +- src/ast/spans.rs | 50 ++++++++--- src/lib.rs | 59 ++++++++++++- src/tokenizer.rs | 132 +++++++++++++++++++++++++++--- 8 files changed, 306 insertions(+), 91 deletions(-) delete mode 100644 docs/source_spans.md diff --git a/README.md b/README.md index 9a67abcf8..fd676d115 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,18 @@ similar semantics are represented with the same AST. We welcome PRs to fix such issues and distinguish different syntaxes in the AST. -## WIP: Extracting source locations from AST nodes +## Source Locations (Work in Progress) -This crate allows recovering source locations from AST nodes via the [Spanned](https://docs.rs/sqlparser/latest/sqlparser/ast/trait.Spanned.html) trait, which can be used for advanced diagnostics tooling. Note that this feature is a work in progress and many nodes report missing or inaccurate spans. Please see [this document](./docs/source_spans.md#source-span-contributing-guidelines) for information on how to contribute missing improvements. +This crate allows recovering source locations from AST nodes via the [Spanned] +trait, which can be used for advanced diagnostics tooling. Note that this +feature is a work in progress and many nodes report missing or inaccurate spans. +Please see [this ticket] for information on how to contribute missing +improvements. -```rust -use sqlparser::ast::Spanned; +[Spanned]: https://docs.rs/sqlparser/latest/sqlparser/ast/trait.Spanned.html +[this ticket]: https://github.com/apache/datafusion-sqlparser-rs/issues/1548 +```rust // Parse SQL let ast = Parser::parse_sql(&GenericDialect, "SELECT A FROM B").unwrap(); @@ -123,9 +128,9 @@ SQL was first standardized in 1987, and revisions of the standard have been published regularly since. Most revisions have added significant new features to the language, and as a result no database claims to support the full breadth of features. This parser currently supports most of the SQL-92 syntax, plus some -syntax from newer versions that have been explicitly requested, plus some MSSQL, -PostgreSQL, and other dialect-specific syntax. Whenever possible, the [online -SQL:2016 grammar][sql-2016-grammar] is used to guide what syntax to accept. +syntax from newer versions that have been explicitly requested, plus various +other dialect-specific syntax. Whenever possible, the [online SQL:2016 +grammar][sql-2016-grammar] is used to guide what syntax to accept. Unfortunately, stating anything more specific about compliance is difficult. There is no publicly available test suite that can assess compliance diff --git a/docs/source_spans.md b/docs/source_spans.md deleted file mode 100644 index 136a4ced2..000000000 --- a/docs/source_spans.md +++ /dev/null @@ -1,52 +0,0 @@ - -## Breaking Changes - -These are the current breaking changes introduced by the source spans feature: - -#### Added fields for spans (must be added to any existing pattern matches) -- `Ident` now stores a `Span` -- `Select`, `With`, `Cte`, `WildcardAdditionalOptions` now store a `TokenWithLocation` - -#### Misc. -- `TokenWithLocation` stores a full `Span`, rather than just a source location. Users relying on `token.location` should use `token.location.start` instead. -## Source Span Contributing Guidelines - -For contributing source spans improvement in addition to the general [contribution guidelines](../README.md#contributing), please make sure to pay attention to the following: - - -### Source Span Design Considerations - -- `Ident` always have correct source spans -- Downstream breaking change impact is to be as minimal as possible -- To this end, use recursive merging of spans in favor of storing spans on all nodes -- Any metadata added to compute spans must not change semantics (Eq, Ord, Hash, etc.) - -The primary reason for missing and inaccurate source spans at this time is missing spans of keyword tokens and values in many structures, either due to lack of time or because adding them would break downstream significantly. - -When considering adding support for source spans on a type, consider the impact to consumers of that type and whether your change would require a consumer to do non-trivial changes to their code. - -Example of a trivial change -```rust -match node { - ast::Query { - field1, - field2, - location: _, // add a new line to ignored location -} -``` - -If adding source spans to a type would require a significant change like wrapping that type or similar, please open an issue to discuss. - -### AST Node Equality and Hashes - -When adding tokens to AST nodes, make sure to store them using the [AttachedToken](https://docs.rs/sqlparser/latest/sqlparser/ast/helpers/struct.AttachedToken.html) helper to ensure that semantically equivalent AST nodes always compare as equal and hash to the same value. F.e. `select 5` and `SELECT 5` would compare as different `Select` nodes, if the select token was stored directly. f.e. - -```rust -struct Select { - select_token: AttachedToken, // only used for spans - /// remaining fields - field1, - field2, - ... -} -``` \ No newline at end of file diff --git a/src/ast/helpers/attached_token.rs b/src/ast/helpers/attached_token.rs index ed340359d..6b930b513 100644 --- a/src/ast/helpers/attached_token.rs +++ b/src/ast/helpers/attached_token.rs @@ -19,7 +19,7 @@ use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt::{self, Debug, Formatter}; use core::hash::{Hash, Hasher}; -use crate::tokenizer::{Token, TokenWithSpan}; +use crate::tokenizer::TokenWithSpan; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -27,17 +27,65 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -/// A wrapper type for attaching tokens to AST nodes that should be ignored in comparisons and hashing. -/// This should be used when a token is not relevant for semantics, but is still needed for -/// accurate source location tracking. +/// A wrapper over [`TokenWithSpan`]s that ignores the token and source +/// location in comparisons and hashing. +/// +/// This type is used when the token and location is not relevant for semantics, +/// but is still needed for accurate source location tracking, for example, in +/// the nodes in the [ast](crate::ast) module. +/// +/// Note: **All** `AttachedTokens` are equal. +/// +/// # Examples +/// +/// Same token, different location are equal +/// ``` +/// # use sqlparser::ast::helpers::attached_token::AttachedToken; +/// # use sqlparser::tokenizer::{Location, Span, Token, TokenWithLocation}; +/// // commas @ line 1, column 10 +/// let tok1 = TokenWithLocation::new( +/// Token::Comma, +/// Span::new(Location::new(1, 10), Location::new(1, 11)), +/// ); +/// // commas @ line 2, column 20 +/// let tok2 = TokenWithLocation::new( +/// Token::Comma, +/// Span::new(Location::new(2, 20), Location::new(2, 21)), +/// ); +/// +/// assert_ne!(tok1, tok2); // token with locations are *not* equal +/// assert_eq!(AttachedToken(tok1), AttachedToken(tok2)); // attached tokens are +/// ``` +/// +/// Different token, different location are equal 🤯 +/// +/// ``` +/// # use sqlparser::ast::helpers::attached_token::AttachedToken; +/// # use sqlparser::tokenizer::{Location, Span, Token, TokenWithLocation}; +/// // commas @ line 1, column 10 +/// let tok1 = TokenWithLocation::new( +/// Token::Comma, +/// Span::new(Location::new(1, 10), Location::new(1, 11)), +/// ); +/// // period @ line 2, column 20 +/// let tok2 = TokenWithLocation::new( +/// Token::Period, +/// Span::new(Location::new(2, 10), Location::new(2, 21)), +/// ); +/// +/// assert_ne!(tok1, tok2); // token with locations are *not* equal +/// assert_eq!(AttachedToken(tok1), AttachedToken(tok2)); // attached tokens are +/// ``` +/// // period @ line 2, column 20 #[derive(Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct AttachedToken(pub TokenWithSpan); impl AttachedToken { + /// Return a new Empty AttachedToken pub fn empty() -> Self { - AttachedToken(TokenWithSpan::wrap(Token::EOF)) + AttachedToken(TokenWithSpan::new_eof()) } } @@ -80,3 +128,9 @@ impl From for AttachedToken { AttachedToken(value) } } + +impl From for TokenWithSpan { + fn from(value: AttachedToken) -> Self { + value.0 + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6d35badf9..e52251d52 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -596,9 +596,21 @@ pub enum CeilFloorKind { /// An SQL expression of any type. /// +/// # Semantics / Type Checking +/// /// The parser does not distinguish between expressions of different types -/// (e.g. boolean vs string), so the caller must handle expressions of -/// inappropriate type, like `WHERE 1` or `SELECT 1=1`, as necessary. +/// (e.g. boolean vs string). The caller is responsible for detecting and +/// validating types as necessary (for example `WHERE 1` vs `SELECT 1=1`) +/// See the [README.md] for more details. +/// +/// [README.md]: https://github.com/apache/datafusion-sqlparser-rs/blob/main/README.md#syntax-vs-semantics +/// +/// # Equality and Hashing Does not Include Source Locations +/// +/// The `Expr` type implements `PartialEq` and `Eq` based on the semantic value +/// of the expression (not bitwise comparison). This means that `Expr` instances +/// that are semantically equivalent but have different spans (locations in the +/// source tree) will compare as equal. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr( diff --git a/src/ast/query.rs b/src/ast/query.rs index f3a76d893..ad7fd261e 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -282,6 +282,7 @@ impl fmt::Display for Table { pub struct Select { /// Token for the `SELECT` keyword pub select_token: AttachedToken, + /// `SELECT [DISTINCT] ...` pub distinct: Option, /// MSSQL syntax: `TOP () [ PERCENT ] [ WITH TIES ]` pub top: Option, @@ -511,7 +512,7 @@ impl fmt::Display for NamedWindowDefinition { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct With { - // Token for the "WITH" keyword + /// Token for the "WITH" keyword pub with_token: AttachedToken, pub recursive: bool, pub cte_tables: Vec, @@ -564,7 +565,7 @@ pub struct Cte { pub query: Box, pub from: Option, pub materialized: Option, - // Token for the closing parenthesis + /// Token for the closing parenthesis pub closing_paren_token: AttachedToken, } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8e8c7b14a..1e0f1bf09 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -21,21 +21,51 @@ use super::{ /// Given an iterator of spans, return the [Span::union] of all spans. fn union_spans>(iter: I) -> Span { - iter.reduce(|acc, item| acc.union(&item)) - .unwrap_or(Span::empty()) + Span::union_iter(iter) } -/// A trait for AST nodes that have a source span for use in diagnostics. +/// Trait for AST nodes that have a source location information. /// -/// Source spans are not guaranteed to be entirely accurate. They may -/// be missing keywords or other tokens. Some nodes may not have a computable -/// span at all, in which case they return [`Span::empty()`]. +/// # Notes: +/// +/// Source [`Span`] are not yet complete. They may be missing: +/// +/// 1. keywords or other tokens +/// 2. span information entirely, in which case they return [`Span::empty()`]. +/// +/// Note Some impl blocks (rendered below) are annotated with which nodes are +/// missing spans. See [this ticket] for additional information and status. +/// +/// [this ticket]: https://github.com/apache/datafusion-sqlparser-rs/issues/1548 +/// +/// # Example +/// ``` +/// # use sqlparser::parser::{Parser, ParserError}; +/// # use sqlparser::ast::Spanned; +/// # use sqlparser::dialect::GenericDialect; +/// # use sqlparser::tokenizer::Location; +/// # fn main() -> Result<(), ParserError> { +/// let dialect = GenericDialect {}; +/// let sql = r#"SELECT * +/// FROM table_1"#; +/// let statements = Parser::new(&dialect) +/// .try_with_sql(sql)? +/// .parse_statements()?; +/// // Get the span of the first statement (SELECT) +/// let span = statements[0].span(); +/// // statement starts at line 1, column 1 (1 based, not 0 based) +/// assert_eq!(span.start, Location::new(1, 1)); +/// // statement ends on line 2, column 15 +/// assert_eq!(span.end, Location::new(2, 15)); +/// # Ok(()) +/// # } +/// ``` /// -/// Some impl blocks may contain doc comments with information -/// on which nodes are missing spans. pub trait Spanned { - /// Compute the source span for this AST node, by recursively - /// combining the spans of its children. + /// Return the [`Span`] (the minimum and maximum [`Location`]) for this AST + /// node, by recursively combining the spans of its children. + /// + /// [`Location`]: crate::tokenizer::Location fn span(&self) -> Span; } diff --git a/src/lib.rs b/src/lib.rs index 6c8987b63..5d72f9f0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ //! 1. [`Parser::parse_sql`] and [`Parser::new`] for the Parsing API //! 2. [`ast`] for the AST structure //! 3. [`Dialect`] for supported SQL dialects +//! 4. [`Spanned`] for source text locations (see "Source Spans" below for details) +//! +//! [`Spanned`]: ast::Spanned //! //! # Example parsing SQL text //! @@ -61,13 +64,67 @@ //! // The original SQL text can be generated from the AST //! assert_eq!(ast[0].to_string(), sql); //! ``` -//! //! [sqlparser crates.io page]: https://crates.io/crates/sqlparser //! [`Parser::parse_sql`]: crate::parser::Parser::parse_sql //! [`Parser::new`]: crate::parser::Parser::new //! [`AST`]: crate::ast //! [`ast`]: crate::ast //! [`Dialect`]: crate::dialect::Dialect +//! +//! # Source Spans +//! +//! Starting with version `0.53.0` sqlparser introduced source spans to the +//! AST. This feature provides source information for syntax errors, enabling +//! better error messages. See [issue #1548] for more information and the +//! [`Spanned`] trait to access the spans. +//! +//! [issue #1548]: https://github.com/apache/datafusion-sqlparser-rs/issues/1548 +//! [`Spanned`]: ast::Spanned +//! +//! ## Migration Guide +//! +//! For the next few releases, we will be incrementally adding source spans to the +//! AST nodes, trying to minimize the impact on existing users. Some breaking +//! changes are inevitable, and the following is a summary of the changes: +//! +//! #### New fields for spans (must be added to any existing pattern matches) +//! +//! The primary change is that new fields will be added to AST nodes to store the source `Span` or `TokenWithLocation`. +//! +//! This will require +//! 1. Adding new fields to existing pattern matches. +//! 2. Filling in the proper span information when constructing AST nodes. +//! +//! For example, since `Ident` now stores a `Span`, to construct an `Ident` you +//! must provide now provide one: +//! +//! Previously: +//! ```text +//! # use sqlparser::ast::Ident; +//! Ident { +//! value: "name".into(), +//! quote_style: None, +//! } +//! ``` +//! Now +//! ```rust +//! # use sqlparser::ast::Ident; +//! # use sqlparser::tokenizer::Span; +//! Ident { +//! value: "name".into(), +//! quote_style: None, +//! span: Span::empty(), +//! }; +//! ``` +//! +//! Similarly, when pattern matching on `Ident`, you must now account for the +//! `span` field. +//! +//! #### Misc. +//! - [`TokenWithLocation`] stores a full `Span`, rather than just a source location. +//! Users relying on `token.location` should use `token.location.start` instead. +//! +//![`TokenWithLocation`]: tokenizer::TokenWithLocation #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::upper_case_acronyms)] diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 7a79445e0..aacfc16fa 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -422,13 +422,35 @@ impl fmt::Display for Whitespace { } /// Location in input string +/// +/// # Create an "empty" (unknown) `Location` +/// ``` +/// # use sqlparser::tokenizer::Location; +/// let location = Location::empty(); +/// ``` +/// +/// # Create a `Location` from a line and column +/// ``` +/// # use sqlparser::tokenizer::Location; +/// let location = Location::new(1, 1); +/// ``` +/// +/// # Create a `Location` from a pair +/// ``` +/// # use sqlparser::tokenizer::Location; +/// let location = Location::from((1, 1)); +/// ``` #[derive(Eq, PartialEq, Hash, Clone, Copy, Ord, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Location { - /// Line number, starting from 1 + /// Line number, starting from 1. + /// + /// Note: Line 0 is used for empty spans pub line: u64, - /// Line column, starting from 1 + /// Line column, starting from 1. + /// + /// Note: Column 0 is used for empty spans pub column: u64, } @@ -448,10 +470,25 @@ impl fmt::Debug for Location { } impl Location { - pub fn of(line: u64, column: u64) -> Self { + /// Return an "empty" / unknown location + pub fn empty() -> Self { + Self { line: 0, column: 0 } + } + + /// Create a new `Location` for a given line and column + pub fn new(line: u64, column: u64) -> Self { Self { line, column } } + /// Create a new location for a given line and column + /// + /// Alias for [`Self::new`] + // TODO: remove / deprecate in favor of` `new` for consistency? + pub fn of(line: u64, column: u64) -> Self { + Self::new(line, column) + } + + /// Combine self and `end` into a new `Span` pub fn span_to(self, end: Self) -> Span { Span { start: self, end } } @@ -463,7 +500,9 @@ impl From<(u64, u64)> for Location { } } -/// A span of source code locations (start, end) +/// A span represents a linear portion of the input string (start, end) +/// +/// See [Spanned](crate::ast::Spanned) for more information. #[derive(Eq, PartialEq, Hash, Clone, PartialOrd, Ord, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -483,12 +522,15 @@ impl Span { // We need a const instance for pattern matching const EMPTY: Span = Self::empty(); + /// Create a new span from a start and end [`Location`] pub fn new(start: Location, end: Location) -> Span { Span { start, end } } - /// Returns an empty span (0, 0) -> (0, 0) + /// Returns an empty span `(0, 0) -> (0, 0)` + /// /// Empty spans represent no knowledge of source location + /// See [Spanned](crate::ast::Spanned) for more information. pub const fn empty() -> Span { Span { start: Location { line: 0, column: 0 }, @@ -498,6 +540,19 @@ impl Span { /// Returns the smallest Span that contains both `self` and `other` /// If either span is [Span::empty], the other span is returned + /// + /// # Examples + /// ``` + /// # use sqlparser::tokenizer::{Span, Location}; + /// // line 1, column1 -> line 2, column 5 + /// let span1 = Span::new(Location::new(1, 1), Location::new(2, 5)); + /// // line 2, column 3 -> line 3, column 7 + /// let span2 = Span::new(Location::new(2, 3), Location::new(3, 7)); + /// // Union of the two is the min/max of the two spans + /// // line 1, column 1 -> line 3, column 7 + /// let union = span1.union(&span2); + /// assert_eq!(union, Span::new(Location::new(1, 1), Location::new(3, 7))); + /// ``` pub fn union(&self, other: &Span) -> Span { // If either span is empty, return the other // this prevents propagating (0, 0) through the tree @@ -512,6 +567,7 @@ impl Span { } /// Same as [Span::union] for `Option` + /// /// If `other` is `None`, `self` is returned pub fn union_opt(&self, other: &Option) -> Span { match other { @@ -519,13 +575,57 @@ impl Span { None => *self, } } + + /// Return the [Span::union] of all spans in the iterator + /// + /// If the iterator is empty, an empty span is returned + /// + /// # Example + /// ``` + /// # use sqlparser::tokenizer::{Span, Location}; + /// let spans = vec![ + /// Span::new(Location::new(1, 1), Location::new(2, 5)), + /// Span::new(Location::new(2, 3), Location::new(3, 7)), + /// Span::new(Location::new(3, 1), Location::new(4, 2)), + /// ]; + /// // line 1, column 1 -> line 4, column 2 + /// assert_eq!( + /// Span::union_iter(spans), + /// Span::new(Location::new(1, 1), Location::new(4, 2)) + /// ); + pub fn union_iter>(iter: I) -> Span { + iter.into_iter() + .reduce(|acc, item| acc.union(&item)) + .unwrap_or(Span::empty()) + } } /// Backwards compatibility struct for [`TokenWithSpan`] #[deprecated(since = "0.53.0", note = "please use `TokenWithSpan` instead")] pub type TokenWithLocation = TokenWithSpan; -/// A [Token] with [Location] attached to it +/// A [Token] with [Span] attached to it +/// +/// This is used to track the location of a token in the input string +/// +/// # Examples +/// ``` +/// # use sqlparser::tokenizer::{Location, Span, Token, TokenWithSpan}; +/// // commas @ line 1, column 10 +/// let tok1 = TokenWithSpan::new( +/// Token::Comma, +/// Span::new(Location::new(1, 10), Location::new(1, 11)), +/// ); +/// assert_eq!(tok1, Token::Comma); // can compare the token +/// +/// // commas @ line 2, column 20 +/// let tok2 = TokenWithSpan::new( +/// Token::Comma, +/// Span::new(Location::new(2, 20), Location::new(2, 21)), +/// ); +/// // same token but different locations are not equal +/// assert_ne!(tok1, tok2); +/// ``` #[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -535,16 +635,24 @@ pub struct TokenWithSpan { } impl TokenWithSpan { - pub fn new(token: Token, span: Span) -> TokenWithSpan { - TokenWithSpan { token, span } + /// Create a new [`TokenWithSpan`] from a [`Token`] and a [`Span`] + pub fn new(token: Token, span: Span) -> Self { + Self { token, span } + } + + /// Wrap a token with an empty span + pub fn wrap(token: Token) -> Self { + Self::new(token, Span::empty()) } - pub fn wrap(token: Token) -> TokenWithSpan { - TokenWithSpan::new(token, Span::empty()) + /// Wrap a token with a location from `start` to `end` + pub fn at(token: Token, start: Location, end: Location) -> Self { + Self::new(token, Span::new(start, end)) } - pub fn at(token: Token, start: Location, end: Location) -> TokenWithSpan { - TokenWithSpan::new(token, Span::new(start, end)) + /// Return an EOF token with no location + pub fn new_eof() -> Self { + Self::wrap(Token::EOF) } } From bd750dfadadf7eda90d1c7c69ad0a4208b0fd05a Mon Sep 17 00:00:00 2001 From: Ayman Elkfrawy <120422207+ayman-sigma@users.noreply.github.com> Date: Mon, 2 Dec 2024 07:23:48 -0800 Subject: [PATCH 623/806] Support Databricks struct literal (#1542) --- src/ast/mod.rs | 6 ++++-- src/dialect/bigquery.rs | 5 +++++ src/dialect/databricks.rs | 5 +++++ src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 10 ++++++++++ src/parser/mod.rs | 15 ++++++++------- tests/sqlparser_databricks.rs | 35 +++++++++++++++++++++++++++++++++++ 7 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e52251d52..d928370a0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -931,12 +931,14 @@ pub enum Expr { Rollup(Vec>), /// ROW / TUPLE a single value, such as `SELECT (1, 2)` Tuple(Vec), - /// `BigQuery` specific `Struct` literal expression [1] + /// `Struct` literal expression /// Syntax: /// ```sql /// STRUCT<[field_name] field_type, ...>( expr1 [, ... ]) + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type) + /// [Databricks](https://docs.databricks.com/en/sql/language-manual/functions/struct.html) /// ``` - /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type Struct { /// Struct values. values: Vec, diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 96633552b..66d7d2061 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -72,4 +72,9 @@ impl Dialect for BigQueryDialect { fn require_interval_qualifier(&self) -> bool { true } + + // See https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#constructing_a_struct + fn supports_struct_literal(&self) -> bool { + true + } } diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index 4924e8077..a3476b1b8 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -59,4 +59,9 @@ impl Dialect for DatabricksDialect { fn require_interval_qualifier(&self) -> bool { true } + + // See https://docs.databricks.com/en/sql/language-manual/functions/struct.html + fn supports_struct_literal(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index e3beeae7f..61e5070fb 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -123,4 +123,8 @@ impl Dialect for GenericDialect { fn supports_named_fn_args_with_assignment_operator(&self) -> bool { true } + + fn supports_struct_literal(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a8993e685..f40cba719 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -375,6 +375,16 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports the STRUCT literal + /// + /// Example + /// ```sql + /// SELECT STRUCT(1 as one, 'foo' as foo, false) + /// ``` + fn supports_struct_literal(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 16362ebba..831098ba1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1123,9 +1123,8 @@ impl<'a> Parser<'a> { Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => { Ok(Some(self.parse_match_against()?)) } - Keyword::STRUCT if dialect_of!(self is BigQueryDialect | GenericDialect) => { - self.prev_token(); - Ok(Some(self.parse_bigquery_struct_literal()?)) + Keyword::STRUCT if self.dialect.supports_struct_literal() => { + Ok(Some(self.parse_struct_literal()?)) } Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => { let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?; @@ -2383,7 +2382,6 @@ impl<'a> Parser<'a> { } } - /// Bigquery specific: Parse a struct literal /// Syntax /// ```sql /// -- typed @@ -2391,7 +2389,9 @@ impl<'a> Parser<'a> { /// -- typeless /// STRUCT( expr1 [AS field_name] [, ... ]) /// ``` - fn parse_bigquery_struct_literal(&mut self) -> Result { + fn parse_struct_literal(&mut self) -> Result { + // Parse the fields definition if exist `<[field_name] field_type, ...>` + self.prev_token(); let (fields, trailing_bracket) = self.parse_struct_type_def(Self::parse_struct_field_def)?; if trailing_bracket.0 { @@ -2401,6 +2401,7 @@ impl<'a> Parser<'a> { ); } + // Parse the struct values `(expr1 [, ... ])` self.expect_token(&Token::LParen)?; let values = self .parse_comma_separated(|parser| parser.parse_struct_field_expr(!fields.is_empty()))?; @@ -2409,13 +2410,13 @@ impl<'a> Parser<'a> { Ok(Expr::Struct { values, fields }) } - /// Parse an expression value for a bigquery struct [1] + /// Parse an expression value for a struct literal /// Syntax /// ```sql /// expr [AS name] /// ``` /// - /// Parameter typed_syntax is set to true if the expression + /// For biquery [1], Parameter typed_syntax is set to true if the expression /// is to be parsed as a field expression declared using typed /// struct syntax [2], and false if using typeless struct syntax [3]. /// diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 1651d517a..d73c088a7 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -278,3 +278,38 @@ fn parse_use() { ); } } + +#[test] +fn parse_databricks_struct_function() { + assert_eq!( + databricks_and_generic() + .verified_only_select("SELECT STRUCT(1, 'foo')") + .projection[0], + SelectItem::UnnamedExpr(Expr::Struct { + values: vec![ + Expr::Value(number("1")), + Expr::Value(Value::SingleQuotedString("foo".to_string())) + ], + fields: vec![] + }) + ); + assert_eq!( + databricks_and_generic() + .verified_only_select("SELECT STRUCT(1 AS one, 'foo' AS foo, false)") + .projection[0], + SelectItem::UnnamedExpr(Expr::Struct { + values: vec![ + Expr::Named { + expr: Expr::Value(number("1")).into(), + name: Ident::new("one") + }, + Expr::Named { + expr: Expr::Value(Value::SingleQuotedString("foo".to_string())).into(), + name: Ident::new("foo") + }, + Expr::Value(Value::Boolean(false)) + ], + fields: vec![] + }) + ); +} From e16b24679a1e87dd54ce9565e87e818dce4d4a0a Mon Sep 17 00:00:00 2001 From: Philip Cristiano Date: Mon, 2 Dec 2024 12:45:14 -0500 Subject: [PATCH 624/806] Encapsulate CreateFunction (#1573) --- src/ast/ddl.rs | 129 +++++++++++++++++++++++++++++++++- src/ast/mod.rs | 133 ++---------------------------------- src/parser/mod.rs | 12 ++-- tests/sqlparser_bigquery.rs | 4 +- tests/sqlparser_hive.rs | 11 +-- tests/sqlparser_postgres.rs | 8 +-- 6 files changed, 149 insertions(+), 148 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3ced478ca..9a7d297bc 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,8 +30,10 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, - ObjectName, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, + display_comma_separated, display_separated, CreateFunctionBody, CreateFunctionUsing, DataType, + Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, + Ident, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, + SequenceOptions, SqlOption, Tag, Value, }; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -1819,3 +1821,126 @@ impl fmt::Display for ClusteredBy { write!(f, " INTO {} BUCKETS", self.num_buckets) } } + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateFunction { + pub or_replace: bool, + pub temporary: bool, + pub if_not_exists: bool, + pub name: ObjectName, + pub args: Option>, + pub return_type: Option, + /// The expression that defines the function. + /// + /// Examples: + /// ```sql + /// AS ((SELECT 1)) + /// AS "console.log();" + /// ``` + pub function_body: Option, + /// Behavior attribute for the function + /// + /// IMMUTABLE | STABLE | VOLATILE + /// + /// [Postgres](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) + pub called_on_null: Option, + /// PARALLEL { UNSAFE | RESTRICTED | SAFE } + /// + /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) + pub parallel: Option, + /// USING ... (Hive only) + pub using: Option, + /// Language used in a UDF definition. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION foo() LANGUAGE js AS "console.log();" + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_javascript_udf) + pub language: Option, + /// Determinism keyword used for non-sql UDF definitions. + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) + pub determinism_specifier: Option, + /// List of options for creating the function. + /// + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) + pub options: Option>, + /// Connection resource for a remote function. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION foo() + /// RETURNS FLOAT64 + /// REMOTE WITH CONNECTION us.myconnection + /// ``` + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_remote_function) + pub remote_connection: Option, +} + +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}", + name = self.name, + temp = if self.temporary { "TEMPORARY " } else { "" }, + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + )?; + if let Some(args) = &self.args { + write!(f, "({})", display_comma_separated(args))?; + } + if let Some(return_type) = &self.return_type { + write!(f, " RETURNS {return_type}")?; + } + if let Some(determinism_specifier) = &self.determinism_specifier { + write!(f, " {determinism_specifier}")?; + } + if let Some(language) = &self.language { + write!(f, " LANGUAGE {language}")?; + } + if let Some(behavior) = &self.behavior { + write!(f, " {behavior}")?; + } + if let Some(called_on_null) = &self.called_on_null { + write!(f, " {called_on_null}")?; + } + if let Some(parallel) = &self.parallel { + write!(f, " {parallel}")?; + } + if let Some(remote_connection) = &self.remote_connection { + write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; + } + if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = &self.function_body { + write!(f, " AS {function_body}")?; + } + if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { + write!(f, " RETURN {function_body}")?; + } + if let Some(using) = &self.using { + write!(f, " {using}")?; + } + if let Some(options) = &self.options { + write!( + f, + " OPTIONS({})", + display_comma_separated(options.as_slice()) + )?; + } + if let Some(CreateFunctionBody::AsAfterOptions(function_body)) = &self.function_body { + write!(f, " AS {function_body}")?; + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d928370a0..ef4ccff4b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -47,7 +47,7 @@ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue, pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterPolicyOperation, AlterTableOperation, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, Deduplicate, DeferrableInitial, GeneratedAs, + ConstraintCharacteristics, CreateFunction, Deduplicate, DeferrableInitial, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, Owner, Partition, ProcedureParam, ReferentialAction, TableConstraint, TagsColumnOption, @@ -897,7 +897,7 @@ pub enum Expr { /// Example: /// /// ```sql - /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') /// ``` /// @@ -3003,64 +3003,7 @@ pub enum Statement { /// 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) /// 3. [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement) - CreateFunction { - or_replace: bool, - temporary: bool, - if_not_exists: bool, - name: ObjectName, - args: Option>, - return_type: Option, - /// The expression that defines the function. - /// - /// Examples: - /// ```sql - /// AS ((SELECT 1)) - /// AS "console.log();" - /// ``` - function_body: Option, - /// Behavior attribute for the function - /// - /// IMMUTABLE | STABLE | VOLATILE - /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) - behavior: Option, - /// CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT - /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) - called_on_null: Option, - /// PARALLEL { UNSAFE | RESTRICTED | SAFE } - /// - /// [Postgres](https://www.postgresql.org/docs/current/sql-createfunction.html) - parallel: Option, - /// USING ... (Hive only) - using: Option, - /// Language used in a UDF definition. - /// - /// Example: - /// ```sql - /// CREATE FUNCTION foo() LANGUAGE js AS "console.log();" - /// ``` - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_javascript_udf) - language: Option, - /// Determinism keyword used for non-sql UDF definitions. - /// - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) - determinism_specifier: Option, - /// List of options for creating the function. - /// - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11) - options: Option>, - /// Connection resource for a remote function. - /// - /// Example: - /// ```sql - /// CREATE FUNCTION foo() - /// RETURNS FLOAT64 - /// REMOTE WITH CONNECTION us.myconnection - /// ``` - /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_a_remote_function) - remote_connection: Option, - }, + CreateFunction(CreateFunction), /// CREATE TRIGGER /// /// Examples: @@ -3826,75 +3769,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::CreateFunction { - or_replace, - temporary, - if_not_exists, - name, - args, - return_type, - function_body, - language, - behavior, - called_on_null, - parallel, - using, - determinism_specifier, - options, - remote_connection, - } => { - write!( - f, - "CREATE {or_replace}{temp}FUNCTION {if_not_exists}{name}", - temp = if *temporary { "TEMPORARY " } else { "" }, - or_replace = if *or_replace { "OR REPLACE " } else { "" }, - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - )?; - if let Some(args) = args { - write!(f, "({})", display_comma_separated(args))?; - } - if let Some(return_type) = return_type { - write!(f, " RETURNS {return_type}")?; - } - if let Some(determinism_specifier) = determinism_specifier { - write!(f, " {determinism_specifier}")?; - } - if let Some(language) = language { - write!(f, " LANGUAGE {language}")?; - } - if let Some(behavior) = behavior { - write!(f, " {behavior}")?; - } - if let Some(called_on_null) = called_on_null { - write!(f, " {called_on_null}")?; - } - if let Some(parallel) = parallel { - write!(f, " {parallel}")?; - } - if let Some(remote_connection) = remote_connection { - write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; - } - if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = function_body { - write!(f, " AS {function_body}")?; - } - if let Some(CreateFunctionBody::Return(function_body)) = function_body { - write!(f, " RETURN {function_body}")?; - } - if let Some(using) = using { - write!(f, " {using}")?; - } - if let Some(options) = options { - write!( - f, - " OPTIONS({})", - display_comma_separated(options.as_slice()) - )?; - } - if let Some(CreateFunctionBody::AsAfterOptions(function_body)) = function_body { - write!(f, " AS {function_body}")?; - } - Ok(()) - } + Statement::CreateFunction(create_function) => create_function.fmt(f), Statement::CreateTrigger { or_replace, is_constraint, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 831098ba1..90665e9f9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4240,7 +4240,7 @@ impl<'a> Parser<'a> { } } - Ok(Statement::CreateFunction { + Ok(Statement::CreateFunction(CreateFunction { or_replace, temporary, name, @@ -4256,7 +4256,7 @@ impl<'a> Parser<'a> { determinism_specifier: None, options: None, remote_connection: None, - }) + })) } /// Parse `CREATE FUNCTION` for [Hive] @@ -4273,7 +4273,7 @@ impl<'a> Parser<'a> { let as_ = self.parse_create_function_body_string()?; let using = self.parse_optional_create_function_using()?; - Ok(Statement::CreateFunction { + Ok(Statement::CreateFunction(CreateFunction { or_replace, temporary, name, @@ -4289,7 +4289,7 @@ impl<'a> Parser<'a> { determinism_specifier: None, options: None, remote_connection: None, - }) + })) } /// Parse `CREATE FUNCTION` for [BigQuery] @@ -4362,7 +4362,7 @@ impl<'a> Parser<'a> { None }; - Ok(Statement::CreateFunction { + Ok(Statement::CreateFunction(CreateFunction { or_replace, temporary, if_not_exists, @@ -4378,7 +4378,7 @@ impl<'a> Parser<'a> { behavior: None, called_on_null: None, parallel: None, - }) + })) } fn parse_function_arg(&mut self) -> Result { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 00d12ed83..2be128a8c 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2011,7 +2011,7 @@ fn test_bigquery_create_function() { let stmt = bigquery().verified_stmt(sql); assert_eq!( stmt, - Statement::CreateFunction { + Statement::CreateFunction(CreateFunction { or_replace: true, temporary: true, if_not_exists: false, @@ -2036,7 +2036,7 @@ fn test_bigquery_create_function() { remote_connection: None, called_on_null: None, parallel: None, - } + }) ); let sqls = [ diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 8d4f7a680..546b289ac 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, CreateFunctionBody, CreateFunctionUsing, CreateTable, Expr, Function, - FunctionArgumentList, FunctionArguments, Ident, ObjectName, OneOrManyWithParens, OrderByExpr, - SelectItem, Statement, TableFactor, UnaryOperator, Use, Value, + ClusteredBy, CommentDef, CreateFunction, CreateFunctionBody, CreateFunctionUsing, CreateTable, + Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, + OneOrManyWithParens, OrderByExpr, SelectItem, Statement, TableFactor, UnaryOperator, Use, + Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::ParserError; @@ -392,13 +393,13 @@ fn set_statement_with_minus() { fn parse_create_function() { let sql = "CREATE TEMPORARY FUNCTION mydb.myfunc AS 'org.random.class.Name' USING JAR 'hdfs://somewhere.com:8020/very/far'"; match hive().verified_stmt(sql) { - Statement::CreateFunction { + Statement::CreateFunction(CreateFunction { temporary, name, function_body, using, .. - } => { + }) => { assert!(temporary); assert_eq!(name.to_string(), "mydb.myfunc"); assert_eq!( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index f94e2f540..52fe6c403 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3631,7 +3631,7 @@ fn parse_create_function() { let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'"; assert_eq!( pg_and_generic().verified_stmt(sql), - Statement::CreateFunction { + Statement::CreateFunction(CreateFunction { or_replace: false, temporary: false, name: ObjectName(vec![Ident::new("add")]), @@ -3652,7 +3652,7 @@ fn parse_create_function() { determinism_specifier: None, options: None, remote_connection: None, - } + }) ); } @@ -4987,7 +4987,7 @@ fn parse_trigger_related_functions() { assert_eq!( create_function, - Statement::CreateFunction { + Statement::CreateFunction(CreateFunction { or_replace: false, temporary: false, if_not_exists: false, @@ -5017,7 +5017,7 @@ fn parse_trigger_related_functions() { options: None, remote_connection: None } - ); + )); // Check the third statement From 6d4188de53bd60e1d0cfae9c11c3491c214af633 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Tue, 3 Dec 2024 16:47:12 -0800 Subject: [PATCH 625/806] Support BIT column types (#1577) --- src/ast/data_type.rs | 14 ++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 7 +++++++ tests/sqlparser_common.rs | 19 +++++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index fbfdc2dcf..ccca7f4cb 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -307,6 +307,16 @@ pub enum DataType { FixedString(u64), /// Bytea Bytea, + /// Bit string, e.g. [Postgres], [MySQL], or [MSSQL] + /// + /// [Postgres]: 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), + /// Variable-length bit string e.g. [Postgres] + /// + /// [Postgres]: https://www.postgresql.org/docs/current/datatype-bit.html + BitVarying(Option), /// Custom type such as enums Custom(ObjectName, Vec), /// Arrays @@ -518,6 +528,10 @@ impl fmt::Display for DataType { DataType::LongText => write!(f, "LONGTEXT"), DataType::String(size) => format_type_with_optional_length(f, "STRING", size, false), DataType::Bytea => write!(f, "BYTEA"), + DataType::Bit(size) => format_type_with_optional_length(f, "BIT", size, false), + DataType::BitVarying(size) => { + format_type_with_optional_length(f, "BIT VARYING", size, false) + } DataType::Array(ty) => match ty { ArrayElemTypeDef::None => write!(f, "ARRAY"), ArrayElemTypeDef::SquareBracket(t, None) => write!(f, "{t}[]"), diff --git a/src/keywords.rs b/src/keywords.rs index 4ec088941..e00e26a62 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -126,6 +126,7 @@ define_keywords!( BIGNUMERIC, BINARY, BINDING, + BIT, BLOB, BLOOMFILTER, BOOL, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 90665e9f9..efdf0d6d0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8134,6 +8134,13 @@ impl<'a> Parser<'a> { Keyword::MEDIUMBLOB => Ok(DataType::MediumBlob), Keyword::LONGBLOB => Ok(DataType::LongBlob), Keyword::BYTES => Ok(DataType::Bytes(self.parse_optional_precision()?)), + Keyword::BIT => { + if self.parse_keyword(Keyword::VARYING) { + Ok(DataType::BitVarying(self.parse_optional_precision()?)) + } else { + Ok(DataType::Bit(self.parse_optional_precision()?)) + } + } Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), Keyword::DATE32 => Ok(DataType::Date32), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4e0cac45b..f146b298e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12440,3 +12440,22 @@ fn test_reserved_keywords_for_identifiers() { let sql = "SELECT MAX(interval) FROM tbl"; dialects.parse_sql_statements(sql).unwrap(); } + +#[test] +fn parse_create_table_with_bit_types() { + let sql = "CREATE TABLE t (a BIT, b BIT VARYING, c BIT(42), d BIT VARYING(43))"; + match verified_stmt(sql) { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!(columns.len(), 4); + assert_eq!(columns[0].data_type, DataType::Bit(None)); + assert_eq!(columns[0].to_string(), "a BIT"); + assert_eq!(columns[1].data_type, DataType::BitVarying(None)); + assert_eq!(columns[1].to_string(), "b BIT VARYING"); + assert_eq!(columns[2].data_type, DataType::Bit(Some(42))); + assert_eq!(columns[2].to_string(), "c BIT(42)"); + assert_eq!(columns[3].data_type, DataType::BitVarying(Some(43))); + assert_eq!(columns[3].to_string(), "d BIT VARYING(43)"); + } + _ => unreachable!(), + } +} From 6517da6b7db4176fa340add848ba518392f1f934 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Tue, 3 Dec 2024 17:09:00 -0800 Subject: [PATCH 626/806] Support parsing optional nulls handling for unique constraint (#1567) --- src/ast/ddl.rs | 30 +++++++++++++++++++++++++++++- src/ast/mod.rs | 7 ++++--- src/ast/spans.rs | 1 + src/parser/mod.rs | 17 +++++++++++++++++ tests/sqlparser_mysql.rs | 1 + tests/sqlparser_postgres.rs | 19 +++++++++++++++++++ 6 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9a7d297bc..6c930a422 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -669,6 +669,8 @@ pub enum TableConstraint { columns: Vec, index_options: Vec, characteristics: Option, + /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` + nulls_distinct: NullsDistinctOption, }, /// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\ /// * `[CONSTRAINT []] PRIMARY KEY [index_name] [index_type] () ` @@ -777,10 +779,11 @@ impl fmt::Display for TableConstraint { columns, index_options, characteristics, + nulls_distinct, } => { write!( f, - "{}UNIQUE{index_type_display:>}{}{} ({})", + "{}UNIQUE{nulls_distinct}{index_type_display:>}{}{} ({})", display_constraint_name(name), display_option_spaced(index_name), display_option(" USING ", "", index_type), @@ -988,6 +991,31 @@ impl fmt::Display for IndexOption { } } +/// [Postgres] unique index nulls handling option: `[ NULLS [ NOT ] DISTINCT ]` +/// +/// [Postgres]: 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))] +pub enum NullsDistinctOption { + /// Not specified + None, + /// NULLS DISTINCT + Distinct, + /// NULLS NOT DISTINCT + NotDistinct, +} + +impl fmt::Display for NullsDistinctOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::None => Ok(()), + Self::Distinct => write!(f, " NULLS DISTINCT"), + Self::NotDistinct => write!(f, " NULLS NOT DISTINCT"), + } + } +} + #[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 ef4ccff4b..d4278e4f9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -49,9 +49,10 @@ pub use self::ddl::{ ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateFunction, Deduplicate, DeferrableInitial, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, - IdentityPropertyKind, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, Owner, - Partition, ProcedureParam, ReferentialAction, TableConstraint, TagsColumnOption, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + IdentityPropertyKind, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, + NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, TableConstraint, + TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1e0f1bf09..a54394174 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -587,6 +587,7 @@ impl Spanned for TableConstraint { columns, index_options: _, characteristics, + nulls_distinct: _, } => union_spans( name.iter() .map(|i| i.span) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index efdf0d6d0..32e7e3743 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6729,6 +6729,8 @@ impl<'a> Parser<'a> { .expected("`index_name` or `(column_name [, ...])`", self.peek_token()); } + let nulls_distinct = self.parse_optional_nulls_distinct()?; + // optional index name let index_name = self.parse_optional_indent()?; let index_type = self.parse_optional_using_then_index_type()?; @@ -6744,6 +6746,7 @@ impl<'a> Parser<'a> { columns, index_options, characteristics, + nulls_distinct, })) } Token::Word(w) if w.keyword == Keyword::PRIMARY => { @@ -6866,6 +6869,20 @@ impl<'a> Parser<'a> { } } + fn parse_optional_nulls_distinct(&mut self) -> Result { + Ok(if self.parse_keyword(Keyword::NULLS) { + let not = self.parse_keyword(Keyword::NOT); + self.expect_keyword(Keyword::DISTINCT)?; + if not { + NullsDistinctOption::NotDistinct + } else { + NullsDistinctOption::Distinct + } + } else { + NullsDistinctOption::None + }) + } + pub fn maybe_parse_options( &mut self, keyword: Keyword, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 2b132331e..f20a759af 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -669,6 +669,7 @@ fn table_constraint_unique_primary_ctor( columns, index_options, characteristics, + nulls_distinct: NullsDistinctOption::None, }, None => TableConstraint::PrimaryKey { name, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 52fe6c403..92368e9ee 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -594,6 +594,25 @@ fn parse_alter_table_constraints_rename() { } } +#[test] +fn parse_alter_table_constraints_unique_nulls_distinct() { + match pg_and_generic() + .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, .. + }) => { + assert_eq!(nulls_distinct, &NullsDistinctOption::NotDistinct) + } + _ => unreachable!(), + }, + _ => unreachable!(), + } + pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS DISTINCT (c)"); + pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE (c)"); +} + #[test] fn parse_alter_table_disable() { pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE ROW LEVEL SECURITY"); From c761f0babbeefdc7b2e8fff5bf0e7bb02988ad03 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Tue, 3 Dec 2024 17:10:28 -0800 Subject: [PATCH 627/806] Fix displaying WORK or TRANSACTION after BEGIN (#1565) --- src/ast/mod.rs | 29 ++++++++++++++++++++++++++--- src/parser/mod.rs | 8 +++++++- tests/sqlparser_common.rs | 6 +++--- tests/sqlparser_mysql.rs | 5 +++++ tests/sqlparser_sqlite.rs | 6 +++--- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d4278e4f9..326375b5f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2944,6 +2944,7 @@ pub enum Statement { StartTransaction { modes: Vec, begin: bool, + transaction: Option, /// Only for SQLite modifier: Option, }, @@ -4519,16 +4520,20 @@ impl fmt::Display for Statement { Statement::StartTransaction { modes, begin: syntax_begin, + transaction, modifier, } => { if *syntax_begin { if let Some(modifier) = *modifier { - write!(f, "BEGIN {} TRANSACTION", modifier)?; + write!(f, "BEGIN {}", modifier)?; } else { - write!(f, "BEGIN TRANSACTION")?; + write!(f, "BEGIN")?; } } else { - write!(f, "START TRANSACTION")?; + write!(f, "START")?; + } + if let Some(transaction) = transaction { + write!(f, " {transaction}")?; } if !modes.is_empty() { write!(f, " {}", display_comma_separated(modes))?; @@ -5023,6 +5028,24 @@ pub enum TruncateCascadeOption { Restrict, } +/// Transaction started with [ TRANSACTION | WORK ] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum BeginTransactionKind { + Transaction, + Work, +} + +impl Display for BeginTransactionKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + BeginTransactionKind::Transaction => write!(f, "TRANSACTION"), + BeginTransactionKind::Work => write!(f, "WORK"), + } + } +} + /// Can use to describe options in create sequence or table column type identity /// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 32e7e3743..7b175f1d9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12123,6 +12123,7 @@ impl<'a> Parser<'a> { Ok(Statement::StartTransaction { modes: self.parse_transaction_modes()?, begin: false, + transaction: Some(BeginTransactionKind::Transaction), modifier: None, }) } @@ -12139,10 +12140,15 @@ impl<'a> Parser<'a> { } else { None }; - let _ = self.parse_one_of_keywords(&[Keyword::TRANSACTION, Keyword::WORK]); + let transaction = match self.parse_one_of_keywords(&[Keyword::TRANSACTION, Keyword::WORK]) { + Some(Keyword::TRANSACTION) => Some(BeginTransactionKind::Transaction), + Some(Keyword::WORK) => Some(BeginTransactionKind::Work), + _ => None, + }; Ok(Statement::StartTransaction { modes: self.parse_transaction_modes()?, begin: true, + transaction, modifier, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f146b298e..e80223807 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7736,9 +7736,9 @@ fn parse_start_transaction() { } verified_stmt("START TRANSACTION"); - one_statement_parses_to("BEGIN", "BEGIN TRANSACTION"); - one_statement_parses_to("BEGIN WORK", "BEGIN TRANSACTION"); - one_statement_parses_to("BEGIN TRANSACTION", "BEGIN TRANSACTION"); + verified_stmt("BEGIN"); + verified_stmt("BEGIN WORK"); + verified_stmt("BEGIN TRANSACTION"); verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED"); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f20a759af..f7a21f99b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3032,3 +3032,8 @@ fn parse_longblob_type() { mysql_and_generic().verified_stmt("CREATE TABLE foo (bar MEDIUMTEXT)"); mysql_and_generic().verified_stmt("CREATE TABLE foo (bar LONGTEXT)"); } + +#[test] +fn parse_begin_without_transaction() { + mysql().verified_stmt("BEGIN"); +} diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index c3cfb7a63..4f23979c5 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -527,9 +527,9 @@ fn parse_start_transaction_with_modifier() { sqlite_and_generic().verified_stmt("BEGIN DEFERRED TRANSACTION"); sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE TRANSACTION"); sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE TRANSACTION"); - sqlite_and_generic().one_statement_parses_to("BEGIN DEFERRED", "BEGIN DEFERRED TRANSACTION"); - sqlite_and_generic().one_statement_parses_to("BEGIN IMMEDIATE", "BEGIN IMMEDIATE TRANSACTION"); - sqlite_and_generic().one_statement_parses_to("BEGIN EXCLUSIVE", "BEGIN EXCLUSIVE TRANSACTION"); + sqlite_and_generic().verified_stmt("BEGIN DEFERRED"); + sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE"); + sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE"); let unsupported_dialects = TestedDialects::new( all_dialects() From dd7ba72a0b2cd24e352b6078bed8edf1ad1253c4 Mon Sep 17 00:00:00 2001 From: hulk Date: Thu, 5 Dec 2024 22:59:07 +0800 Subject: [PATCH 628/806] Add support of the ENUM8|ENUM16 for ClickHouse dialect (#1574) --- src/ast/data_type.rs | 32 +++++++++++--- src/ast/mod.rs | 2 +- src/keywords.rs | 2 + src/parser/mod.rs | 91 +++++++++++++++++++++++---------------- tests/sqlparser_common.rs | 87 +++++++++++++++++++++++++++++++++++-- tests/sqlparser_mysql.rs | 14 ++++-- 6 files changed, 179 insertions(+), 49 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index ccca7f4cb..5b0239e17 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -25,10 +25,21 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::ast::{display_comma_separated, ObjectName, StructField, UnionField}; +use crate::ast::{display_comma_separated, Expr, ObjectName, StructField, UnionField}; use super::{value::escape_single_quote_string, ColumnDef}; +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +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) + NamedValue(String, Expr), +} + /// SQL data types #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -334,7 +345,7 @@ pub enum DataType { /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nested-data-structures/nested Nested(Vec), /// Enums - Enum(Vec), + Enum(Vec, Option), /// Set Set(Vec), /// Struct @@ -546,13 +557,24 @@ impl fmt::Display for DataType { write!(f, "{}({})", ty, modifiers.join(", ")) } } - DataType::Enum(vals) => { - write!(f, "ENUM(")?; + DataType::Enum(vals, bits) => { + match bits { + Some(bits) => write!(f, "ENUM{}", bits), + None => write!(f, "ENUM"), + }?; + write!(f, "(")?; for (i, v) in vals.iter().enumerate() { if i != 0 { write!(f, ", ")?; } - write!(f, "'{}'", escape_single_quote_string(v))?; + match v { + EnumMember::Name(name) => { + write!(f, "'{}'", escape_single_quote_string(name))? + } + EnumMember::NamedValue(name, value) => { + write!(f, "'{}' = {}", escape_single_quote_string(name), value)? + } + } } write!(f, ")") } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 326375b5f..f782b3634 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,7 +40,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::tokenizer::Span; pub use self::data_type::{ - ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, + ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, EnumMember, ExactNumberInfo, StructBracketKind, TimezoneInfo, }; pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue, Use}; diff --git a/src/keywords.rs b/src/keywords.rs index e00e26a62..be3910f8f 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -286,6 +286,8 @@ define_keywords!( ENFORCED, ENGINE, ENUM, + ENUM16, + ENUM8, EPHEMERAL, EPOCH, EQUALS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7b175f1d9..04a103c61 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1049,18 +1049,18 @@ impl<'a> Parser<'a> { | Keyword::CURRENT_USER | Keyword::SESSION_USER | Keyword::USER - if dialect_of!(self is PostgreSqlDialect | GenericDialect) => - { - Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.to_ident(w_span)]), - parameters: FunctionArguments::None, - args: FunctionArguments::None, - null_treatment: None, - filter: None, - over: None, - within_group: vec![], - }))) - } + if dialect_of!(self is PostgreSqlDialect | GenericDialect) => + { + Ok(Some(Expr::Function(Function { + name: ObjectName(vec![w.to_ident(w_span)]), + parameters: FunctionArguments::None, + args: FunctionArguments::None, + null_treatment: None, + filter: None, + over: None, + within_group: vec![], + }))) + } Keyword::CURRENT_TIMESTAMP | Keyword::CURRENT_TIME | Keyword::CURRENT_DATE @@ -1075,18 +1075,18 @@ impl<'a> Parser<'a> { Keyword::TRY_CAST => Ok(Some(self.parse_cast_expr(CastKind::TryCast)?)), Keyword::SAFE_CAST => Ok(Some(self.parse_cast_expr(CastKind::SafeCast)?)), Keyword::EXISTS - // Support parsing Databricks has a function named `exists`. - if !dialect_of!(self is DatabricksDialect) - || matches!( + // Support parsing Databricks has a function named `exists`. + if !dialect_of!(self is DatabricksDialect) + || matches!( self.peek_nth_token(1).token, Token::Word(Word { keyword: Keyword::SELECT | Keyword::WITH, .. }) ) => - { - Ok(Some(self.parse_exists_expr(false)?)) - } + { + Ok(Some(self.parse_exists_expr(false)?)) + } Keyword::EXTRACT => Ok(Some(self.parse_extract_expr()?)), Keyword::CEIL => Ok(Some(self.parse_ceil_floor_expr(true)?)), Keyword::FLOOR => Ok(Some(self.parse_ceil_floor_expr(false)?)), @@ -1103,22 +1103,22 @@ impl<'a> Parser<'a> { Ok(Some(self.parse_array_expr(true)?)) } Keyword::ARRAY - if self.peek_token() == Token::LParen - && !dialect_of!(self is ClickHouseDialect | DatabricksDialect) => - { - self.expect_token(&Token::LParen)?; - let query = self.parse_query()?; - self.expect_token(&Token::RParen)?; - Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.to_ident(w_span)]), - parameters: FunctionArguments::None, - args: FunctionArguments::Subquery(query), - filter: None, - null_treatment: None, - over: None, - within_group: vec![], - }))) - } + if self.peek_token() == Token::LParen + && !dialect_of!(self is ClickHouseDialect | DatabricksDialect) => + { + self.expect_token(&Token::LParen)?; + let query = self.parse_query()?; + self.expect_token(&Token::RParen)?; + Ok(Some(Expr::Function(Function { + name: ObjectName(vec![w.to_ident(w_span)]), + parameters: FunctionArguments::None, + args: FunctionArguments::Subquery(query), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }))) + } Keyword::NOT => Ok(Some(self.parse_not()?)), Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => { Ok(Some(self.parse_match_against()?)) @@ -5023,7 +5023,7 @@ impl<'a> Parser<'a> { return Err(ParserError::ParserError(format!("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. {e}"))) } } - }, + } }; Ok(owner) } @@ -7997,6 +7997,23 @@ impl<'a> Parser<'a> { } } + pub fn parse_enum_values(&mut self) -> Result, ParserError> { + self.expect_token(&Token::LParen)?; + let values = self.parse_comma_separated(|parser| { + let name = parser.parse_literal_string()?; + let e = if parser.consume_token(&Token::Eq) { + let value = parser.parse_number()?; + EnumMember::NamedValue(name, value) + } else { + EnumMember::Name(name) + }; + Ok(e) + })?; + self.expect_token(&Token::RParen)?; + + Ok(values) + } + /// Parse a SQL datatype (in the context of a CREATE TABLE statement for example) pub fn parse_data_type(&mut self) -> Result { let (ty, trailing_bracket) = self.parse_data_type_helper()?; @@ -8235,7 +8252,9 @@ impl<'a> Parser<'a> { Keyword::BIGDECIMAL => Ok(DataType::BigDecimal( self.parse_exact_number_optional_precision_scale()?, )), - Keyword::ENUM => Ok(DataType::Enum(self.parse_string_values()?)), + Keyword::ENUM => Ok(DataType::Enum(self.parse_enum_values()?, None)), + Keyword::ENUM8 => Ok(DataType::Enum(self.parse_enum_values()?, Some(8))), + Keyword::ENUM16 => Ok(DataType::Enum(self.parse_enum_values()?, Some(16))), Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)), Keyword::ARRAY => { if dialect_of!(self is SnowflakeDialect) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e80223807..61c742dac 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -51,6 +51,7 @@ mod test_utils; use pretty_assertions::assert_eq; use sqlparser::ast::ColumnOption::Comment; use sqlparser::ast::Expr::{Identifier, UnaryOp}; +use sqlparser::ast::Value::Number; use sqlparser::test_utils::all_dialects_except; #[test] @@ -9250,7 +9251,7 @@ fn parse_cache_table() { format!( "CACHE {table_flag} TABLE '{cache_table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) {sql}", ) - .as_str() + .as_str() ), Statement::Cache { table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), @@ -9275,7 +9276,7 @@ fn parse_cache_table() { format!( "CACHE {table_flag} TABLE '{cache_table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) AS {sql}", ) - .as_str() + .as_str() ), Statement::Cache { table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), @@ -11459,7 +11460,7 @@ fn parse_explain_with_option_list() { }), }, ]; - run_explain_analyze ( + run_explain_analyze( all_dialects_where(|d| d.supports_explain_with_utility_options()), "EXPLAIN (ANALYZE, VERBOSE true, WAL OFF, FORMAT YAML, USER_DEF_NUM -100.1) SELECT sqrt(id) FROM foo", false, @@ -12459,3 +12460,83 @@ fn parse_create_table_with_bit_types() { _ => unreachable!(), } } + +#[test] +fn parse_create_table_with_enum_types() { + let sql = "CREATE TABLE t0 (foo ENUM8('a' = 1, 'b' = 2), bar ENUM16('a' = 1, 'b' = 2), baz ENUM('a', 'b'))"; + match all_dialects().verified_stmt(sql) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!(name.to_string(), "t0"); + assert_eq!( + vec![ + ColumnDef { + name: Ident::new("foo"), + data_type: DataType::Enum( + vec![ + EnumMember::NamedValue( + "a".to_string(), + Expr::Value(Number("1".parse().unwrap(), false)) + ), + EnumMember::NamedValue( + "b".to_string(), + Expr::Value(Number("2".parse().unwrap(), false)) + ) + ], + Some(8) + ), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident::new("bar"), + data_type: DataType::Enum( + vec![ + EnumMember::NamedValue( + "a".to_string(), + Expr::Value(Number("1".parse().unwrap(), false)) + ), + EnumMember::NamedValue( + "b".to_string(), + Expr::Value(Number("2".parse().unwrap(), false)) + ) + ], + Some(16) + ), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident::new("baz"), + data_type: DataType::Enum( + vec![ + EnumMember::Name("a".to_string()), + EnumMember::Name("b".to_string()) + ], + None + ), + collation: None, + options: vec![], + } + ], + columns + ); + } + _ => unreachable!(), + } + + // invalid case missing value for enum pair + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE TABLE t0 (foo ENUM8('a' = 1, 'b' = ))") + .unwrap_err(), + ParserError::ParserError("Expected: a value, found: )".to_string()) + ); + + // invalid case that name is not a string + assert_eq!( + all_dialects() + .parse_sql_statements("CREATE TABLE t0 (foo ENUM8('a' = 1, 2))") + .unwrap_err(), + ParserError::ParserError("Expected: literal string, found: 2".to_string()) + ); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f7a21f99b..cac1af852 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -685,7 +685,7 @@ fn table_constraint_unique_primary_ctor( #[test] fn parse_create_table_primary_and_unique_key() { let sqls = ["UNIQUE KEY", "PRIMARY KEY"] - .map(|key_ty|format!("CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, CONSTRAINT bar_key {key_ty} (bar))")); + .map(|key_ty| format!("CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, CONSTRAINT bar_key {key_ty} (bar))")); let index_type_display = [Some(KeyOrIndexDisplay::Key), None]; @@ -753,7 +753,7 @@ fn parse_create_table_primary_and_unique_key() { #[test] fn parse_create_table_primary_and_unique_key_with_index_options() { let sqls = ["UNIQUE INDEX", "PRIMARY KEY"] - .map(|key_ty|format!("CREATE TABLE foo (bar INT, var INT, CONSTRAINT constr {key_ty} index_name (bar, var) USING HASH COMMENT 'yes, ' USING BTREE COMMENT 'MySQL allows')")); + .map(|key_ty| format!("CREATE TABLE foo (bar INT, var INT, CONSTRAINT constr {key_ty} index_name (bar, var) USING HASH COMMENT 'yes, ' USING BTREE COMMENT 'MySQL allows')")); let index_type_display = [Some(KeyOrIndexDisplay::Index), None]; @@ -827,7 +827,7 @@ fn parse_create_table_primary_and_unique_key_with_index_type() { #[test] fn parse_create_table_primary_and_unique_key_characteristic_test() { let sqls = ["UNIQUE INDEX", "PRIMARY KEY"] - .map(|key_ty|format!("CREATE TABLE x (y INT, CONSTRAINT constr {key_ty} (y) NOT DEFERRABLE INITIALLY IMMEDIATE)")); + .map(|key_ty| format!("CREATE TABLE x (y INT, CONSTRAINT constr {key_ty} (y) NOT DEFERRABLE INITIALLY IMMEDIATE)")); for sql in &sqls { mysql_and_generic().verified_stmt(sql); } @@ -890,7 +890,13 @@ fn parse_create_table_set_enum() { }, ColumnDef { name: Ident::new("baz"), - data_type: DataType::Enum(vec!["a".to_string(), "b".to_string()]), + data_type: DataType::Enum( + vec![ + EnumMember::Name("a".to_string()), + EnumMember::Name("b".to_string()) + ], + None + ), collation: None, options: vec![], } From 7b50ac31c342258a11a744a3f83ac0e99dda3978 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:17:52 +0100 Subject: [PATCH 629/806] Parse Snowflake USE ROLE and USE SECONDARY ROLES (#1578) --- src/ast/dcl.rs | 41 ++++++++++++++++++++++++++++++------ src/ast/mod.rs | 4 +++- src/ast/spans.rs | 17 ++++++++++----- src/keywords.rs | 2 ++ src/parser/mod.rs | 39 +++++++++++++++++++++++++++------- tests/sqlparser_common.rs | 14 ++++++------ tests/sqlparser_snowflake.rs | 34 +++++++++++++++++++++++------- 7 files changed, 115 insertions(+), 36 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index d47476ffa..735ab0cce 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -28,7 +28,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use super::{Expr, Ident, Password}; +use super::{display_comma_separated, Expr, Ident, Password}; use crate::ast::{display_separated, ObjectName}; /// An option in `ROLE` statement. @@ -204,12 +204,14 @@ impl fmt::Display for AlterRoleOperation { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum Use { - Catalog(ObjectName), // e.g. `USE CATALOG foo.bar` - Schema(ObjectName), // e.g. `USE SCHEMA foo.bar` - Database(ObjectName), // e.g. `USE DATABASE foo.bar` - Warehouse(ObjectName), // e.g. `USE WAREHOUSE foo.bar` - Object(ObjectName), // e.g. `USE foo.bar` - Default, // e.g. `USE DEFAULT` + Catalog(ObjectName), // e.g. `USE CATALOG foo.bar` + Schema(ObjectName), // e.g. `USE SCHEMA foo.bar` + Database(ObjectName), // e.g. `USE DATABASE foo.bar` + Warehouse(ObjectName), // e.g. `USE WAREHOUSE foo.bar` + Role(ObjectName), // e.g. `USE ROLE PUBLIC` + SecondaryRoles(SecondaryRoles), // e.g. `USE SECONDARY ROLES ALL` + Object(ObjectName), // e.g. `USE foo.bar` + Default, // e.g. `USE DEFAULT` } impl fmt::Display for Use { @@ -220,8 +222,33 @@ impl fmt::Display for Use { 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) + } Use::Object(name) => write!(f, "{}", name), Use::Default => write!(f, "DEFAULT"), } } } + +/// Snowflake `SECONDARY ROLES` USE variant +/// 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 SecondaryRoles { + All, + None, + List(Vec), +} + +impl fmt::Display for SecondaryRoles { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SecondaryRoles::All => write!(f, "ALL"), + SecondaryRoles::None => write!(f, "NONE"), + SecondaryRoles::List(roles) => write!(f, "{}", display_comma_separated(roles)), + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f782b3634..bc4dda349 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -43,7 +43,9 @@ pub use self::data_type::{ ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, EnumMember, ExactNumberInfo, StructBracketKind, TimezoneInfo, }; -pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue, Use}; +pub use self::dcl::{ + AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, +}; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterPolicyOperation, AlterTableOperation, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a54394174..cd3bda1c2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -3,11 +3,11 @@ use core::iter; use crate::tokenizer::Span; use super::{ - 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, + dcl::SecondaryRoles, 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, MatchRecognizePattern, Measure, NamedWindowDefinition, @@ -484,6 +484,13 @@ impl Spanned for Use { Use::Schema(object_name) => object_name.span(), Use::Database(object_name) => object_name.span(), Use::Warehouse(object_name) => object_name.span(), + Use::Role(object_name) => object_name.span(), + Use::SecondaryRoles(secondary_roles) => { + if let SecondaryRoles::List(roles) = secondary_roles { + return union_spans(roles.iter().map(|i| i.span)); + } + Span::empty() + } Use::Object(object_name) => object_name.span(), Use::Default => Span::empty(), } diff --git a/src/keywords.rs b/src/keywords.rs index be3910f8f..bd97c3c98 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -664,6 +664,7 @@ define_keywords!( RIGHT, RLIKE, ROLE, + ROLES, ROLLBACK, ROLLUP, ROOT, @@ -682,6 +683,7 @@ define_keywords!( SCROLL, SEARCH, SECOND, + SECONDARY, SECRET, SECURITY, SELECT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 04a103c61..b5365b51d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10093,23 +10093,46 @@ impl<'a> Parser<'a> { } else if dialect_of!(self is DatabricksDialect) { self.parse_one_of_keywords(&[Keyword::CATALOG, Keyword::DATABASE, Keyword::SCHEMA]) } else if dialect_of!(self is SnowflakeDialect) { - self.parse_one_of_keywords(&[Keyword::DATABASE, Keyword::SCHEMA, Keyword::WAREHOUSE]) + self.parse_one_of_keywords(&[ + Keyword::DATABASE, + Keyword::SCHEMA, + Keyword::WAREHOUSE, + Keyword::ROLE, + Keyword::SECONDARY, + ]) } else { None // No specific keywords for other dialects, including GenericDialect }; - let obj_name = self.parse_object_name(false)?; - let result = match parsed_keyword { - Some(Keyword::CATALOG) => Use::Catalog(obj_name), - Some(Keyword::DATABASE) => Use::Database(obj_name), - Some(Keyword::SCHEMA) => Use::Schema(obj_name), - Some(Keyword::WAREHOUSE) => Use::Warehouse(obj_name), - _ => Use::Object(obj_name), + let result = if matches!(parsed_keyword, Some(Keyword::SECONDARY)) { + self.parse_secondary_roles()? + } else { + let obj_name = self.parse_object_name(false)?; + match parsed_keyword { + Some(Keyword::CATALOG) => Use::Catalog(obj_name), + Some(Keyword::DATABASE) => Use::Database(obj_name), + Some(Keyword::SCHEMA) => Use::Schema(obj_name), + Some(Keyword::WAREHOUSE) => Use::Warehouse(obj_name), + Some(Keyword::ROLE) => Use::Role(obj_name), + _ => Use::Object(obj_name), + } }; Ok(Statement::Use(result)) } + fn parse_secondary_roles(&mut self) -> Result { + self.expect_keyword(Keyword::ROLES)?; + if self.parse_keyword(Keyword::NONE) { + Ok(Use::SecondaryRoles(SecondaryRoles::None)) + } else if self.parse_keyword(Keyword::ALL) { + Ok(Use::SecondaryRoles(SecondaryRoles::All)) + } else { + let roles = self.parse_comma_separated(|parser| parser.parse_identifier(false))?; + Ok(Use::SecondaryRoles(SecondaryRoles::List(roles))) + } + } + pub fn parse_table_and_joins(&mut self) -> Result { let relation = self.parse_table_factor()?; // Note that for keywords to be properly handled here, they need to be diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 61c742dac..42616d51e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4066,8 +4066,8 @@ fn test_alter_table_with_on_cluster() { Statement::AlterTable { name, on_cluster, .. } => { - std::assert_eq!(name.to_string(), "t"); - std::assert_eq!(on_cluster, Some(Ident::with_quote('\'', "cluster"))); + assert_eq!(name.to_string(), "t"); + assert_eq!(on_cluster, Some(Ident::with_quote('\'', "cluster"))); } _ => unreachable!(), } @@ -4078,15 +4078,15 @@ fn test_alter_table_with_on_cluster() { Statement::AlterTable { name, on_cluster, .. } => { - std::assert_eq!(name.to_string(), "t"); - std::assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); + assert_eq!(name.to_string(), "t"); + assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); } _ => unreachable!(), } let res = all_dialects() .parse_sql_statements("ALTER TABLE t ON CLUSTER 123 ADD CONSTRAINT bar PRIMARY KEY (baz)"); - std::assert_eq!( + assert_eq!( res.unwrap_err(), ParserError::ParserError("Expected: identifier, found: 123".to_string()) ) @@ -11226,7 +11226,7 @@ fn test_group_by_nothing() { let Select { group_by, .. } = all_dialects_where(|d| d.supports_group_by_expr()) .verified_only_select("SELECT count(1) FROM t GROUP BY ()"); { - std::assert_eq!( + assert_eq!( GroupByExpr::Expressions(vec![Expr::Tuple(vec![])], vec![]), group_by ); @@ -11235,7 +11235,7 @@ fn test_group_by_nothing() { let Select { group_by, .. } = all_dialects_where(|d| d.supports_group_by_expr()) .verified_only_select("SELECT name, count(1) FROM t GROUP BY name, ()"); { - std::assert_eq!( + assert_eq!( GroupByExpr::Expressions( vec![ Identifier(Ident::new("name".to_string())), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e31811c2b..fb8a60cfa 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2649,7 +2649,7 @@ fn parse_use() { let quote_styles = ['\'', '"', '`']; for object_name in &valid_object_names { // Test single identifier without quotes - std::assert_eq!( + assert_eq!( snowflake().verified_stmt(&format!("USE {}", object_name)), Statement::Use(Use::Object(ObjectName(vec![Ident::new( object_name.to_string() @@ -2657,7 +2657,7 @@ fn parse_use() { ); for "e in "e_styles { // Test single identifier with different type of quotes - std::assert_eq!( + assert_eq!( snowflake().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( quote, @@ -2669,7 +2669,7 @@ fn parse_use() { for "e in "e_styles { // Test double identifier with different type of quotes - std::assert_eq!( + assert_eq!( snowflake().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), Statement::Use(Use::Object(ObjectName(vec![ Ident::with_quote(quote, "CATALOG"), @@ -2678,7 +2678,7 @@ fn parse_use() { ); } // Test double identifier without quotes - std::assert_eq!( + assert_eq!( snowflake().verified_stmt("USE mydb.my_schema"), Statement::Use(Use::Object(ObjectName(vec![ Ident::new("mydb"), @@ -2688,37 +2688,55 @@ fn parse_use() { for "e in "e_styles { // Test single and double identifier with keyword and different type of quotes - std::assert_eq!( + assert_eq!( snowflake().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)), Statement::Use(Use::Database(ObjectName(vec![Ident::with_quote( quote, "my_database".to_string(), )]))) ); - std::assert_eq!( + assert_eq!( snowflake().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)), Statement::Use(Use::Schema(ObjectName(vec![Ident::with_quote( quote, "my_schema".to_string(), )]))) ); - std::assert_eq!( + assert_eq!( snowflake().verified_stmt(&format!("USE SCHEMA {0}CATALOG{0}.{0}my_schema{0}", quote)), Statement::Use(Use::Schema(ObjectName(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)), + Statement::Use(Use::Role(ObjectName(vec![Ident::with_quote( + quote, + "my_role".to_string(), + )]))) + ); + assert_eq!( + snowflake().verified_stmt(&format!("USE WAREHOUSE {0}my_wh{0}", quote)), + Statement::Use(Use::Warehouse(ObjectName(vec![Ident::with_quote( + quote, + "my_wh".to_string(), + )]))) + ); } // Test invalid syntax - missing identifier let invalid_cases = ["USE SCHEMA", "USE DATABASE", "USE WAREHOUSE"]; for sql in &invalid_cases { - std::assert_eq!( + assert_eq!( snowflake().parse_sql_statements(sql).unwrap_err(), ParserError::ParserError("Expected: identifier, found: EOF".to_string()), ); } + + snowflake().verified_stmt("USE SECONDARY ROLES ALL"); + snowflake().verified_stmt("USE SECONDARY ROLES NONE"); + snowflake().verified_stmt("USE SECONDARY ROLES r1, r2, r3"); } #[test] From d0fcc06652ba9880622d0ef8b426c809cee752fe Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:41:01 +0100 Subject: [PATCH 630/806] Snowflake ALTER TABLE clustering options (#1579) --- src/ast/ddl.rs | 83 +++++++++++++++++++++++++++++------- src/ast/spans.rs | 4 ++ src/keywords.rs | 4 ++ src/parser/mod.rs | 11 +++++ tests/sqlparser_snowflake.rs | 36 ++++++++++++++++ 5 files changed, 123 insertions(+), 15 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6c930a422..849b583ed 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -70,7 +70,10 @@ pub enum AlterTableOperation { /// /// Note: this is a ClickHouse-specific operation. /// Please refer to [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#drop-projection) - DropProjection { if_exists: bool, name: Ident }, + DropProjection { + if_exists: bool, + name: Ident, + }, /// `MATERIALIZE PROJECTION [IF EXISTS] name [IN PARTITION partition_name]` /// @@ -99,11 +102,15 @@ pub enum AlterTableOperation { /// `DISABLE RULE rewrite_rule_name` /// /// Note: this is a PostgreSQL-specific operation. - DisableRule { name: Ident }, + DisableRule { + name: Ident, + }, /// `DISABLE TRIGGER [ trigger_name | ALL | USER ]` /// /// Note: this is a PostgreSQL-specific operation. - DisableTrigger { name: Ident }, + DisableTrigger { + name: Ident, + }, /// `DROP CONSTRAINT [ IF EXISTS ] ` DropConstraint { if_exists: bool, @@ -152,19 +159,27 @@ pub enum AlterTableOperation { /// `ENABLE ALWAYS RULE rewrite_rule_name` /// /// Note: this is a PostgreSQL-specific operation. - EnableAlwaysRule { name: Ident }, + EnableAlwaysRule { + name: Ident, + }, /// `ENABLE ALWAYS TRIGGER trigger_name` /// /// Note: this is a PostgreSQL-specific operation. - EnableAlwaysTrigger { name: Ident }, + EnableAlwaysTrigger { + name: Ident, + }, /// `ENABLE REPLICA RULE rewrite_rule_name` /// /// Note: this is a PostgreSQL-specific operation. - EnableReplicaRule { name: Ident }, + EnableReplicaRule { + name: Ident, + }, /// `ENABLE REPLICA TRIGGER trigger_name` /// /// Note: this is a PostgreSQL-specific operation. - EnableReplicaTrigger { name: Ident }, + EnableReplicaTrigger { + name: Ident, + }, /// `ENABLE ROW LEVEL SECURITY` /// /// Note: this is a PostgreSQL-specific operation. @@ -172,11 +187,15 @@ pub enum AlterTableOperation { /// `ENABLE RULE rewrite_rule_name` /// /// Note: this is a PostgreSQL-specific operation. - EnableRule { name: Ident }, + EnableRule { + name: Ident, + }, /// `ENABLE TRIGGER [ trigger_name | ALL | USER ]` /// /// Note: this is a PostgreSQL-specific operation. - EnableTrigger { name: Ident }, + EnableTrigger { + name: Ident, + }, /// `RENAME TO PARTITION (partition=val)` RenamePartitions { old_partitions: Vec, @@ -197,7 +216,9 @@ pub enum AlterTableOperation { new_column_name: Ident, }, /// `RENAME TO ` - RenameTable { table_name: ObjectName }, + RenameTable { + table_name: ObjectName, + }, // CHANGE [ COLUMN ] [ ] ChangeColumn { old_name: Ident, @@ -218,7 +239,10 @@ pub enum AlterTableOperation { /// `RENAME CONSTRAINT TO ` /// /// Note: this is a PostgreSQL-specific operation. - RenameConstraint { old_name: Ident, new_name: Ident }, + RenameConstraint { + old_name: Ident, + new_name: Ident, + }, /// `ALTER [ COLUMN ]` AlterColumn { column_name: Ident, @@ -227,14 +251,27 @@ pub enum AlterTableOperation { /// 'SWAP WITH ' /// /// Note: this is Snowflake specific - SwapWith { table_name: ObjectName }, + SwapWith { + table_name: ObjectName, + }, /// 'SET TBLPROPERTIES ( { property_key [ = ] property_val } [, ...] )' - SetTblProperties { table_properties: Vec }, - + SetTblProperties { + table_properties: Vec, + }, /// `OWNER TO { | CURRENT_ROLE | CURRENT_USER | SESSION_USER }` /// /// Note: this is PostgreSQL-specific - OwnerTo { new_owner: Owner }, + OwnerTo { + new_owner: Owner, + }, + /// Snowflake table clustering options + /// + ClusterBy { + exprs: Vec, + }, + DropClusteringKey, + SuspendRecluster, + ResumeRecluster, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -548,6 +585,22 @@ impl fmt::Display for AlterTableOperation { } Ok(()) } + AlterTableOperation::ClusterBy { exprs } => { + write!(f, "CLUSTER BY ({})", display_comma_separated(exprs))?; + Ok(()) + } + AlterTableOperation::DropClusteringKey => { + write!(f, "DROP CLUSTERING KEY")?; + Ok(()) + } + AlterTableOperation::SuspendRecluster => { + write!(f, "SUSPEND RECLUSTER")?; + Ok(()) + } + AlterTableOperation::ResumeRecluster => { + write!(f, "RESUME RECLUSTER")?; + Ok(()) + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index cd3bda1c2..de577c9b8 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1020,6 +1020,10 @@ impl Spanned for AlterTableOperation { union_spans(table_properties.iter().map(|i| i.span())) } AlterTableOperation::OwnerTo { .. } => Span::empty(), + AlterTableOperation::ClusterBy { exprs } => union_spans(exprs.iter().map(|e| e.span())), + AlterTableOperation::DropClusteringKey => Span::empty(), + AlterTableOperation::SuspendRecluster => Span::empty(), + AlterTableOperation::ResumeRecluster => Span::empty(), } } } diff --git a/src/keywords.rs b/src/keywords.rs index bd97c3c98..25a719d25 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -168,6 +168,7 @@ define_keywords!( CLOSE, CLUSTER, CLUSTERED, + CLUSTERING, COALESCE, COLLATE, COLLATION, @@ -622,6 +623,7 @@ define_keywords!( READS, READ_ONLY, REAL, + RECLUSTER, RECURSIVE, REF, REFERENCES, @@ -656,6 +658,7 @@ define_keywords!( RESTRICTIVE, RESULT, RESULTSET, + RESUME, RETAIN, RETURN, RETURNING, @@ -745,6 +748,7 @@ define_keywords!( SUM, SUPER, SUPERUSER, + SUSPEND, SWAP, SYMMETRIC, SYNC, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b5365b51d..ac76f6484 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7273,6 +7273,8 @@ impl<'a> Parser<'a> { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let name = self.parse_identifier(false)?; AlterTableOperation::DropProjection { if_exists, name } + } else if self.parse_keywords(&[Keyword::CLUSTERING, Keyword::KEY]) { + AlterTableOperation::DropClusteringKey } else { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); @@ -7444,6 +7446,15 @@ impl<'a> Parser<'a> { partition, with_name, } + } else if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { + self.expect_token(&Token::LParen)?; + let exprs = self.parse_comma_separated(|parser| parser.parse_expr())?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::ClusterBy { exprs } + } else if self.parse_keywords(&[Keyword::SUSPEND, Keyword::RECLUSTER]) { + AlterTableOperation::SuspendRecluster + } else if self.parse_keywords(&[Keyword::RESUME, Keyword::RECLUSTER]) { + AlterTableOperation::ResumeRecluster } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index fb8a60cfa..3cbd87bf7 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1411,6 +1411,42 @@ fn test_alter_table_swap_with() { }; } +#[test] +fn test_alter_table_clustering() { + let sql = r#"ALTER TABLE tab CLUSTER BY (c1, "c2", TO_DATE(c3))"#; + match alter_table_op(snowflake_and_generic().verified_stmt(sql)) { + AlterTableOperation::ClusterBy { exprs } => { + assert_eq!( + exprs, + [ + Expr::Identifier(Ident::new("c1")), + Expr::Identifier(Ident::with_quote('"', "c2")), + Expr::Function(Function { + name: ObjectName(vec![Ident::new("TO_DATE")]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("c3")) + ))], + duplicate_treatment: None, + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + }) + ], + ); + } + _ => unreachable!(), + } + + snowflake_and_generic().verified_stmt("ALTER TABLE tbl DROP CLUSTERING KEY"); + snowflake_and_generic().verified_stmt("ALTER TABLE tbl SUSPEND RECLUSTER"); + snowflake_and_generic().verified_stmt("ALTER TABLE tbl RESUME RECLUSTER"); +} + #[test] fn test_drop_stage() { match snowflake_and_generic().verified_stmt("DROP STAGE s1") { From 00abaf218735b6003af6eb4f482d6a6e2659a12c Mon Sep 17 00:00:00 2001 From: Yuval Shkolar <85674443+yuval-illumex@users.noreply.github.com> Date: Mon, 9 Dec 2024 22:25:10 +0200 Subject: [PATCH 631/806] Support INSERT OVERWRITE INTO syntax (#1584) --- src/parser/mod.rs | 5 ++--- tests/sqlparser_snowflake.rs | 6 ++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ac76f6484..e47e71b45 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11291,9 +11291,8 @@ impl<'a> Parser<'a> { let replace_into = false; - let action = self.parse_one_of_keywords(&[Keyword::INTO, Keyword::OVERWRITE]); - let into = action == Some(Keyword::INTO); - let overwrite = action == Some(Keyword::OVERWRITE); + let overwrite = self.parse_keyword(Keyword::OVERWRITE); + let into = self.parse_keyword(Keyword::INTO); let local = self.parse_keyword(Keyword::LOCAL); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 3cbd87bf7..5ad861f47 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2952,3 +2952,9 @@ fn test_sf_double_dot_notation() { #[test] fn test_parse_double_dot_notation_wrong_position() {} + +#[test] +fn parse_insert_overwrite() { + let insert_overwrite_into = r#"INSERT OVERWRITE INTO schema.table SELECT a FROM b"#; + snowflake().verified_stmt(insert_overwrite_into); +} From 04271b0e4eec304dd689bd9875b13dae15db1a3f Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Wed, 11 Dec 2024 23:31:24 +0100 Subject: [PATCH 632/806] Parse `INSERT` with subquery when lacking column names (#1586) --- src/parser/mod.rs | 25 +++++++++++++++++++------ tests/sqlparser_common.rs | 2 ++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e47e71b45..04d6edcd5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11329,14 +11329,19 @@ impl<'a> Parser<'a> { if self.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES]) { (vec![], None, vec![], None) } else { - let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?; + let (columns, partitioned, after_columns) = if !self.peek_subquery_start() { + let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?; - let partitioned = self.parse_insert_partition()?; - // Hive allows you to specify columns after partitions as well if you want. - let after_columns = if dialect_of!(self is HiveDialect) { - self.parse_parenthesized_column_list(Optional, false)? + let partitioned = self.parse_insert_partition()?; + // Hive allows you to specify columns after partitions as well if you want. + let after_columns = if dialect_of!(self is HiveDialect) { + self.parse_parenthesized_column_list(Optional, false)? + } else { + vec![] + }; + (columns, partitioned, after_columns) } else { - vec![] + Default::default() }; let source = Some(self.parse_query()?); @@ -11431,6 +11436,14 @@ impl<'a> Parser<'a> { } } + /// Returns true if the immediate tokens look like the + /// beginning of a subquery. `(SELECT ...` + fn peek_subquery_start(&mut self) -> bool { + let [maybe_lparen, maybe_select] = self.peek_tokens(); + Token::LParen == maybe_lparen + && matches!(maybe_select, Token::Word(w) if w.keyword == Keyword::SELECT) + } + fn parse_conflict_clause(&mut self) -> Option { if self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]) { Some(SqliteOnConflict::Replace) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 42616d51e..f76516ef4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10964,6 +10964,8 @@ fn insert_into_with_parentheses() { Box::new(GenericDialect {}), ]); dialects.verified_stmt("INSERT INTO t1 (id, name) (SELECT t2.id, t2.name FROM t2)"); + dialects.verified_stmt("INSERT INTO t1 (SELECT t2.id, t2.name FROM t2)"); + dialects.verified_stmt(r#"INSERT INTO t1 ("select", name) (SELECT t2.name FROM t2)"#); } #[test] From a13f8c6b931ac17cd245a23abfc412c18bfb23e2 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Wed, 11 Dec 2024 23:31:55 +0100 Subject: [PATCH 633/806] Add support for ODBC functions (#1585) --- src/ast/mod.rs | 17 +++++++++ src/ast/spans.rs | 1 + src/ast/visitor.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 65 +++++++++++++++++++++++++++++++---- src/test_utils.rs | 1 + tests/sqlparser_clickhouse.rs | 4 +++ tests/sqlparser_common.rs | 43 +++++++++++++++++++++++ tests/sqlparser_duckdb.rs | 1 + tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_postgres.rs | 7 ++++ tests/sqlparser_redshift.rs | 1 + tests/sqlparser_snowflake.rs | 2 ++ tests/sqlparser_sqlite.rs | 1 + 15 files changed, 142 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bc4dda349..cfd0ac089 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5523,6 +5523,15 @@ impl fmt::Display for CloseCursor { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Function { pub name: ObjectName, + /// Flags whether this function call uses the [ODBC syntax]. + /// + /// Example: + /// ```sql + /// SELECT {fn CONCAT('foo', 'bar')} + /// ``` + /// + /// [ODBC syntax]: https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/scalar-function-calls?view=sql-server-2017 + pub uses_odbc_syntax: bool, /// The parameters to the function, including any options specified within the /// delimiting parentheses. /// @@ -5561,6 +5570,10 @@ pub struct Function { impl fmt::Display for Function { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.uses_odbc_syntax { + write!(f, "{{fn ")?; + } + write!(f, "{}{}{}", self.name, self.parameters, self.args)?; if !self.within_group.is_empty() { @@ -5583,6 +5596,10 @@ impl fmt::Display for Function { write!(f, " OVER {o}")?; } + if self.uses_odbc_syntax { + write!(f, "}}")?; + } + Ok(()) } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index de577c9b8..7e45f838a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1478,6 +1478,7 @@ impl Spanned for Function { fn span(&self) -> Span { let Function { name, + uses_odbc_syntax: _, parameters, args, filter, diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index eacd268a4..f7562b66c 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -530,6 +530,7 @@ where /// let old_expr = std::mem::replace(expr, Expr::Value(Value::Null)); /// *expr = Expr::Function(Function { /// name: ObjectName(vec![Ident::new("f")]), +/// uses_odbc_syntax: false, /// args: FunctionArguments::List(FunctionArgumentList { /// duplicate_treatment: None, /// args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(old_expr))], diff --git a/src/keywords.rs b/src/keywords.rs index 25a719d25..d0cfcd05b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -333,6 +333,7 @@ define_keywords!( FLOAT8, FLOOR, FLUSH, + FN, FOLLOWING, FOR, FORCE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 04d6edcd5..39ab2db24 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1053,6 +1053,7 @@ impl<'a> Parser<'a> { { Ok(Some(Expr::Function(Function { name: ObjectName(vec![w.to_ident(w_span)]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, @@ -1111,6 +1112,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; Ok(Some(Expr::Function(Function { name: ObjectName(vec![w.to_ident(w_span)]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::Subquery(query), filter: None, @@ -1408,9 +1410,9 @@ impl<'a> Parser<'a> { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } - Token::LBrace if self.dialect.supports_dictionary_syntax() => { + Token::LBrace => { self.prev_token(); - self.parse_duckdb_struct_literal() + self.parse_lbrace_expr() } _ => self.expected("an expression", next_token), }?; @@ -1509,7 +1511,29 @@ impl<'a> Parser<'a> { } } + /// Tries to parse the body of an [ODBC function] call. + /// i.e. without the enclosing braces + /// + /// ```sql + /// fn myfunc(1,2,3) + /// ``` + /// + /// [ODBC function]: https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/scalar-function-calls?view=sql-server-2017 + fn maybe_parse_odbc_fn_body(&mut self) -> Result, ParserError> { + self.maybe_parse(|p| { + p.expect_keyword(Keyword::FN)?; + let fn_name = p.parse_object_name(false)?; + let mut fn_call = p.parse_function_call(fn_name)?; + fn_call.uses_odbc_syntax = true; + Ok(Expr::Function(fn_call)) + }) + } + pub fn parse_function(&mut self, name: ObjectName) -> Result { + self.parse_function_call(name).map(Expr::Function) + } + + fn parse_function_call(&mut self, name: ObjectName) -> Result { self.expect_token(&Token::LParen)?; // Snowflake permits a subquery to be passed as an argument without @@ -1517,15 +1541,16 @@ impl<'a> Parser<'a> { if dialect_of!(self is SnowflakeDialect) && self.peek_sub_query() { let subquery = self.parse_query()?; self.expect_token(&Token::RParen)?; - return Ok(Expr::Function(Function { + return Ok(Function { name, + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::Subquery(subquery), filter: None, null_treatment: None, over: None, within_group: vec![], - })); + }); } let mut args = self.parse_function_argument_list()?; @@ -1584,15 +1609,16 @@ impl<'a> Parser<'a> { None }; - Ok(Expr::Function(Function { + Ok(Function { name, + uses_odbc_syntax: false, parameters, args: FunctionArguments::List(args), null_treatment, filter, over, within_group, - })) + }) } /// Optionally parses a null treatment clause. @@ -1619,6 +1645,7 @@ impl<'a> Parser<'a> { }; Ok(Expr::Function(Function { name, + uses_odbc_syntax: false, parameters: FunctionArguments::None, args, filter: None, @@ -2211,6 +2238,31 @@ impl<'a> Parser<'a> { } } + /// Parse expression types that start with a left brace '{'. + /// Examples: + /// ```sql + /// -- Dictionary expr. + /// {'key1': 'value1', 'key2': 'value2'} + /// + /// -- Function call using the ODBC syntax. + /// { fn CONCAT('foo', 'bar') } + /// ``` + fn parse_lbrace_expr(&mut self) -> Result { + let token = self.expect_token(&Token::LBrace)?; + + if let Some(fn_expr) = self.maybe_parse_odbc_fn_body()? { + self.expect_token(&Token::RBrace)?; + return Ok(fn_expr); + } + + if self.dialect.supports_dictionary_syntax() { + self.prev_token(); // Put back the '{' + return self.parse_duckdb_struct_literal(); + } + + self.expected("an expression", token) + } + /// Parses fulltext expressions [`sqlparser::ast::Expr::MatchAgainst`] /// /// # Errors @@ -7578,6 +7630,7 @@ impl<'a> Parser<'a> { } else { Ok(Statement::Call(Function { name: object_name, + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, over: None, diff --git a/src/test_utils.rs b/src/test_utils.rs index aaee20c5f..6e60a31c1 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -376,6 +376,7 @@ pub fn join(relation: TableFactor) -> Join { pub fn call(function: &str, args: impl IntoIterator) -> Expr { Expr::Function(Function { name: ObjectName(vec![Ident::new(function)]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index ed0c74021..9d785576f 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -199,6 +199,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -821,6 +822,7 @@ fn parse_create_table_with_variant_default_expressions() { name: None, option: ColumnOption::Materialized(Expr::Function(Function { name: ObjectName(vec![Ident::new("now")]), + uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![], duplicate_treatment: None, @@ -842,6 +844,7 @@ fn parse_create_table_with_variant_default_expressions() { name: None, option: ColumnOption::Ephemeral(Some(Expr::Function(Function { name: ObjectName(vec![Ident::new("now")]), + uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![], duplicate_treatment: None, @@ -872,6 +875,7 @@ fn parse_create_table_with_variant_default_expressions() { name: None, option: ColumnOption::Alias(Expr::Function(Function { name: ObjectName(vec![Ident::new("toString")]), + uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Identifier(Ident::new("c")) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f76516ef4..7dfb98d6f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1108,6 +1108,7 @@ fn parse_select_count_wildcard() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -1130,6 +1131,7 @@ fn parse_select_count_distinct() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: Some(DuplicateTreatment::Distinct), @@ -2366,6 +2368,7 @@ fn parse_select_having() { Some(Expr::BinaryOp { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -2396,6 +2399,7 @@ fn parse_select_qualify() { Some(Expr::BinaryOp { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("ROW_NUMBER")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -2802,6 +2806,7 @@ fn parse_listagg() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("LISTAGG")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: Some(DuplicateTreatment::Distinct), @@ -4603,6 +4608,7 @@ fn parse_named_argument_function() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("FUN")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -4642,6 +4648,7 @@ fn parse_named_argument_function_with_eq_operator() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("FUN")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -4716,6 +4723,7 @@ fn parse_window_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -4846,6 +4854,7 @@ fn test_parse_named_window() { quote_style: None, span: Span::empty(), }]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -4880,6 +4889,7 @@ fn test_parse_named_window() { quote_style: None, span: Span::empty(), }]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -9008,6 +9018,7 @@ fn parse_time_functions() { let select = verified_only_select(&sql); let select_localtime_func_call_ast = Function { name: ObjectName(vec![Ident::new(func_name)]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -10021,6 +10032,7 @@ fn parse_call() { assert_eq!( verified_stmt("CALL my_procedure('a')"), Statement::Call(Function { + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -10511,6 +10523,7 @@ fn test_selective_aggregation() { vec![ SelectItem::UnnamedExpr(Expr::Function(Function { name: ObjectName(vec![Ident::new("ARRAY_AGG")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -10529,6 +10542,7 @@ fn test_selective_aggregation() { SelectItem::ExprWithAlias { expr: Expr::Function(Function { name: ObjectName(vec![Ident::new("ARRAY_AGG")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -10968,6 +10982,35 @@ fn insert_into_with_parentheses() { dialects.verified_stmt(r#"INSERT INTO t1 ("select", name) (SELECT t2.name FROM t2)"#); } +#[test] +fn parse_odbc_scalar_function() { + let select = verified_only_select("SELECT {fn my_func(1, 2)}"); + let Expr::Function(Function { + name, + uses_odbc_syntax, + args, + .. + }) = expr_from_projection(only(&select.projection)) + else { + unreachable!("expected function") + }; + assert_eq!(name, &ObjectName(vec![Ident::new("my_func")])); + assert!(uses_odbc_syntax); + matches!(args, FunctionArguments::List(l) if l.args.len() == 2); + + verified_stmt("SELECT {fn fna()} AS foo, fnb(1)"); + + // Testing invalid SQL with any-one dialect is intentional. + // Depending on dialect flags the error message may be different. + let pg = TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]); + assert_eq!( + pg.parse_sql_statements("SELECT {fn2 my_func()}") + .unwrap_err() + .to_string(), + "sql parser error: Expected: an expression, found: {" + ); +} + #[test] fn test_dictionary_syntax() { fn check(sql: &str, expect: Expr) { diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 01ac0649a..a0fc49b9f 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -606,6 +606,7 @@ fn test_duckdb_named_argument_function_with_assignment_operator() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("FUN")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 546b289ac..981218388 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -480,6 +480,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 31668c86a..66e40f46b 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -635,6 +635,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -1388,6 +1389,7 @@ fn parse_create_table_with_valid_options() { }, ], ), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List( FunctionArgumentList { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 92368e9ee..2e204d9bc 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2529,6 +2529,7 @@ fn parse_array_subquery_expr() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("ARRAY")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::Subquery(Box::new(Query { with: None, @@ -2911,6 +2912,7 @@ fn test_composite_value() { Ident::new("information_schema"), Ident::new("_pg_expandarray") ]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -3088,6 +3090,7 @@ fn parse_current_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_CATALOG")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, @@ -3100,6 +3103,7 @@ fn parse_current_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_USER")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, @@ -3112,6 +3116,7 @@ fn parse_current_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("SESSION_USER")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, @@ -3124,6 +3129,7 @@ fn parse_current_functions() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("USER")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, null_treatment: None, @@ -3599,6 +3605,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index f0c1f0c74..2fd855a09 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -154,6 +154,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5ad861f47..d6774c317 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1212,6 +1212,7 @@ fn parse_delimited_identifiers() { assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, @@ -1423,6 +1424,7 @@ fn test_alter_table_clustering() { Expr::Identifier(Ident::with_quote('"', "c2")), Expr::Function(Function { name: ObjectName(vec![Ident::new("TO_DATE")]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 4f23979c5..987b1263d 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -419,6 +419,7 @@ fn parse_window_function_with_filter() { select.projection, vec![SelectItem::UnnamedExpr(Expr::Function(Function { name: ObjectName(vec![Ident::new(func_name)]), + uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, From 5de5312406fae3f69b92b12dd63c68d7fce3ed74 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 12 Dec 2024 09:17:13 -0500 Subject: [PATCH 634/806] Update version to 0.53.0 and add release notes (#1592) --- Cargo.toml | 2 +- changelog/0.53.0.md | 95 +++++++++++++++++++++++++++++++++++++++++++ dev/release/README.md | 6 +++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 changelog/0.53.0.md diff --git a/Cargo.toml b/Cargo.toml index c4d0094f4..301a59c55 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.52.0" +version = "0.53.0" authors = ["Apache DataFusion "] homepage = "https://github.com/apache/datafusion-sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/changelog/0.53.0.md b/changelog/0.53.0.md new file mode 100644 index 000000000..5b9de07d3 --- /dev/null +++ b/changelog/0.53.0.md @@ -0,0 +1,95 @@ + + +# sqlparser-rs 0.53.0 Changelog + +This release consists of 47 commits from 16 contributors. See credits at the end of this changelog for more information. + +**Other:** + +- hive: support for special not expression `!a` and raise error for `a!` factorial operator [#1472](https://github.com/apache/datafusion-sqlparser-rs/pull/1472) (wugeer) +- Add support for MSSQL's `OPENJSON WITH` clause [#1498](https://github.com/apache/datafusion-sqlparser-rs/pull/1498) (gaoqiangz) +- Parse true and false as identifiers in mssql [#1510](https://github.com/apache/datafusion-sqlparser-rs/pull/1510) (lovasoa) +- Fix the parsing error in MSSQL for multiple statements that include `DECLARE` statements [#1497](https://github.com/apache/datafusion-sqlparser-rs/pull/1497) (wugeer) +- Add support for Snowflake SHOW DATABASES/SCHEMAS/TABLES/VIEWS/COLUMNS statements [#1501](https://github.com/apache/datafusion-sqlparser-rs/pull/1501) (yoavcloud) +- Add support of COMMENT ON syntax for Snowflake [#1516](https://github.com/apache/datafusion-sqlparser-rs/pull/1516) (git-hulk) +- Add support for MYSQL's `CREATE TABLE SELECT` expr [#1515](https://github.com/apache/datafusion-sqlparser-rs/pull/1515) (wugeer) +- Add support for MSSQL's `XQuery` methods [#1500](https://github.com/apache/datafusion-sqlparser-rs/pull/1500) (gaoqiangz) +- Add support for Hive's `LOAD DATA` expr [#1520](https://github.com/apache/datafusion-sqlparser-rs/pull/1520) (wugeer) +- Fix ClickHouse document link from `Russian` to `English` [#1527](https://github.com/apache/datafusion-sqlparser-rs/pull/1527) (git-hulk) +- Support ANTI and SEMI joins without LEFT/RIGHT [#1528](https://github.com/apache/datafusion-sqlparser-rs/pull/1528) (delamarch3) +- support sqlite's OR clauses in update statements [#1530](https://github.com/apache/datafusion-sqlparser-rs/pull/1530) (lovasoa) +- support column type definitions in table aliases [#1526](https://github.com/apache/datafusion-sqlparser-rs/pull/1526) (lovasoa) +- Add support for MSSQL's `JSON_ARRAY`/`JSON_OBJECT` expr [#1507](https://github.com/apache/datafusion-sqlparser-rs/pull/1507) (gaoqiangz) +- Add support for PostgreSQL `UNLISTEN` syntax and Add support for Postgres `LOAD extension` expr [#1531](https://github.com/apache/datafusion-sqlparser-rs/pull/1531) (wugeer) +- Parse byte/bit string literals in MySQL and Postgres [#1532](https://github.com/apache/datafusion-sqlparser-rs/pull/1532) (mvzink) +- Allow example CLI to read from stdin [#1536](https://github.com/apache/datafusion-sqlparser-rs/pull/1536) (mvzink) +- recursive select calls are parsed with bad trailing_commas parameter [#1521](https://github.com/apache/datafusion-sqlparser-rs/pull/1521) (tomershaniii) +- PartiQL queries in Redshift [#1534](https://github.com/apache/datafusion-sqlparser-rs/pull/1534) (yoavcloud) +- Include license file in sqlparser_derive crate [#1543](https://github.com/apache/datafusion-sqlparser-rs/pull/1543) (ankane) +- Fallback to identifier parsing if expression parsing fails [#1513](https://github.com/apache/datafusion-sqlparser-rs/pull/1513) (yoavcloud) +- support `json_object('k':'v')` in postgres [#1546](https://github.com/apache/datafusion-sqlparser-rs/pull/1546) (lovasoa) +- Document micro benchmarks [#1555](https://github.com/apache/datafusion-sqlparser-rs/pull/1555) (alamb) +- Implement `Spanned` to retrieve source locations on AST nodes [#1435](https://github.com/apache/datafusion-sqlparser-rs/pull/1435) (Nyrox) +- Fix error in benchmark queries [#1560](https://github.com/apache/datafusion-sqlparser-rs/pull/1560) (alamb) +- Fix clippy warnings on rust 1.83 [#1570](https://github.com/apache/datafusion-sqlparser-rs/pull/1570) (iffyio) +- Support relation visitor to visit the `Option` field [#1556](https://github.com/apache/datafusion-sqlparser-rs/pull/1556) (goldmedal) +- Rename `TokenWithLocation` to `TokenWithSpan`, in backwards compatible way [#1562](https://github.com/apache/datafusion-sqlparser-rs/pull/1562) (alamb) +- Support MySQL size variants for BLOB and TEXT columns [#1564](https://github.com/apache/datafusion-sqlparser-rs/pull/1564) (mvzink) +- Increase version of sqlparser_derive from 0.2.2 to 0.3.0 [#1571](https://github.com/apache/datafusion-sqlparser-rs/pull/1571) (alamb) +- `json_object('k' VALUE 'v')` in postgres [#1547](https://github.com/apache/datafusion-sqlparser-rs/pull/1547) (lovasoa) +- Support snowflake double dot notation for object name [#1540](https://github.com/apache/datafusion-sqlparser-rs/pull/1540) (ayman-sigma) +- Update comments / docs for `Spanned` [#1549](https://github.com/apache/datafusion-sqlparser-rs/pull/1549) (alamb) +- Support Databricks struct literal [#1542](https://github.com/apache/datafusion-sqlparser-rs/pull/1542) (ayman-sigma) +- Encapsulate CreateFunction [#1573](https://github.com/apache/datafusion-sqlparser-rs/pull/1573) (philipcristiano) +- Support BIT column types [#1577](https://github.com/apache/datafusion-sqlparser-rs/pull/1577) (mvzink) +- Support parsing optional nulls handling for unique constraint [#1567](https://github.com/apache/datafusion-sqlparser-rs/pull/1567) (mvzink) +- Fix displaying WORK or TRANSACTION after BEGIN [#1565](https://github.com/apache/datafusion-sqlparser-rs/pull/1565) (mvzink) +- Add support of the ENUM8|ENUM16 for ClickHouse dialect [#1574](https://github.com/apache/datafusion-sqlparser-rs/pull/1574) (git-hulk) +- Parse Snowflake USE ROLE and USE SECONDARY ROLES [#1578](https://github.com/apache/datafusion-sqlparser-rs/pull/1578) (yoavcloud) +- Snowflake ALTER TABLE clustering options [#1579](https://github.com/apache/datafusion-sqlparser-rs/pull/1579) (yoavcloud) +- Support INSERT OVERWRITE INTO syntax [#1584](https://github.com/apache/datafusion-sqlparser-rs/pull/1584) (yuval-illumex) +- Parse `INSERT` with subquery when lacking column names [#1586](https://github.com/apache/datafusion-sqlparser-rs/pull/1586) (iffyio) +- Add support for ODBC functions [#1585](https://github.com/apache/datafusion-sqlparser-rs/pull/1585) (iffyio) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 8 Andrew Lamb + 6 Michael Victor Zink + 5 Ophir LOJKINE + 5 Yoav Cohen + 5 wugeer + 3 Ifeanyi Ubah + 3 gaoqiangz + 3 hulk + 2 Ayman Elkfrawy + 1 Andrew Kane + 1 Jax Liu + 1 Mark-Oliver Junge + 1 Philip Cristiano + 1 Yuval Shkolar + 1 delamarch3 + 1 tomershaniii +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. + diff --git a/dev/release/README.md b/dev/release/README.md index c440f7387..c3018dd68 100644 --- a/dev/release/README.md +++ b/dev/release/README.md @@ -146,6 +146,12 @@ Move artifacts to the release location in SVN, using the `release-tarball.sh` sc ```shell ./dev/release/release-tarball.sh 0.52.0 1 ``` + +Promote the rc tag to the release tag +```shell +git tag v0.52.0 v0.52.0-rc3 +git push apache v0.52.0 +``` Congratulations! The release is now official! From 310882862147ad7ca43320da049a718c9a4538b0 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 13 Dec 2024 07:22:30 -0500 Subject: [PATCH 635/806] Run cargo fmt on `derive` crate (#1595) --- .github/workflows/rust.yml | 2 +- derive/src/lib.rs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2502abe9d..6c8130dc4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Rust Toolchain uses: ./.github/actions/setup-builder - - run: cargo fmt -- --check + - run: cargo fmt --all -- --check lint: runs-on: ubuntu-latest diff --git a/derive/src/lib.rs b/derive/src/lib.rs index dd4d37b41..b81623312 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -18,7 +18,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::spanned::Spanned; -use syn::{parse::{Parse, ParseStream}, parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Fields, GenericParam, Generics, Ident, Index, LitStr, Meta, Token, Type, TypePath}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Fields, GenericParam, Generics, + Ident, Index, LitStr, Meta, Token, Type, TypePath, +}; use syn::{Path, PathArguments}; /// Implementation of `[#derive(Visit)]` @@ -267,7 +271,11 @@ fn visit_children( } fn is_option(ty: &Type) -> bool { - if let Type::Path(TypePath { path: Path { segments, .. }, .. }) = ty { + if let Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) = ty + { if let Some(segment) = segments.last() { if segment.ident == "Option" { if let PathArguments::AngleBracketed(args) = &segment.arguments { From 885aa93465d0f984f4ff55cdff67f1be84472dc8 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 13 Dec 2024 13:01:56 -0500 Subject: [PATCH 636/806] Add Apache license header to spans.rs (#1594) --- src/ast/spans.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7e45f838a..88e0fbdf2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.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 core::iter; use crate::tokenizer::Span; From 7bc6ddb8fb0800111179a111fa281672285ce34f Mon Sep 17 00:00:00 2001 From: Martin Abelson Sahlen Date: Sun, 15 Dec 2024 11:39:42 +0200 Subject: [PATCH 637/806] Add support for BigQuery `ANY TYPE` data type (#1602) Co-authored-by: Martin Abelson Sahlen Co-authored-by: Martin Abelson Sahlen --- src/ast/data_type.rs | 6 +++++- src/parser/mod.rs | 4 ++++ tests/sqlparser_bigquery.rs | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 5b0239e17..b53b8f0d2 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -373,6 +373,10 @@ pub enum DataType { /// /// [postgresql]: https://www.postgresql.org/docs/current/plpgsql-trigger.html Trigger, + /// Any data type, used in BigQuery UDF definitions for templated parameters + /// + /// [bigquery]: https://cloud.google.com/bigquery/docs/user-defined-functions#templated-sql-udf-parameters + AnyType, } impl fmt::Display for DataType { @@ -383,7 +387,6 @@ impl fmt::Display for DataType { DataType::CharacterVarying(size) => { format_character_string_type(f, "CHARACTER VARYING", size) } - DataType::CharVarying(size) => format_character_string_type(f, "CHAR VARYING", size), DataType::Varchar(size) => format_character_string_type(f, "VARCHAR", size), DataType::Nvarchar(size) => format_character_string_type(f, "NVARCHAR", size), @@ -626,6 +629,7 @@ impl fmt::Display for DataType { } DataType::Unspecified => Ok(()), DataType::Trigger => write!(f, "TRIGGER"), + DataType::AnyType => write!(f, "ANY TYPE"), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 39ab2db24..37323084d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8382,6 +8382,10 @@ impl<'a> Parser<'a> { Ok(DataType::Tuple(field_defs)) } Keyword::TRIGGER => Ok(DataType::Trigger), + Keyword::ANY if self.peek_keyword(Keyword::TYPE) => { + let _ = self.parse_keyword(Keyword::TYPE); + Ok(DataType::AnyType) + } _ => { self.prev_token(); let type_name = self.parse_object_name(false)?; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 2be128a8c..34c14cc55 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2212,3 +2212,19 @@ fn test_any_value() { bigquery_and_generic().verified_expr("ANY_VALUE(fruit HAVING MAX sold)"); bigquery_and_generic().verified_expr("ANY_VALUE(fruit HAVING MIN sold)"); } + +#[test] +fn test_any_type() { + bigquery().verified_stmt(concat!( + "CREATE OR REPLACE TEMPORARY FUNCTION ", + "my_function(param1 ANY TYPE) ", + "AS (", + "(SELECT 1)", + ")", + )); +} + +#[test] +fn test_any_type_dont_break_custom_type() { + bigquery_and_generic().verified_stmt("CREATE TABLE foo (x ANY)"); +} From 316bb14135ce21f023ac8bb8f94d6bea23d03c37 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 15 Dec 2024 10:40:25 +0100 Subject: [PATCH 638/806] Add support for TABLESAMPLE (#1580) --- src/ast/mod.rs | 7 +- src/ast/query.rs | 188 ++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/hive.rs | 5 + src/dialect/mod.rs | 11 ++ src/keywords.rs | 8 + src/parser/mod.rs | 123 ++++++++++++ src/test_utils.rs | 16 ++ tests/sqlparser_bigquery.rs | 25 +-- tests/sqlparser_clickhouse.rs | 23 +-- tests/sqlparser_common.rs | 357 ++++++++-------------------------- tests/sqlparser_databricks.rs | 11 +- tests/sqlparser_duckdb.rs | 38 +--- tests/sqlparser_hive.rs | 10 + tests/sqlparser_mssql.rs | 29 ++- tests/sqlparser_mysql.rs | 47 ++--- tests/sqlparser_postgres.rs | 4 +- tests/sqlparser_redshift.rs | 70 +++---- tests/sqlparser_snowflake.rs | 20 +- tests/sqlparser_sqlite.rs | 11 +- 20 files changed, 546 insertions(+), 458 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cfd0ac089..ccb2ed1bc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -69,8 +69,11 @@ pub use self::query::{ OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, - Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, + TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableSample, + TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, + TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, + TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, + WithFill, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index ad7fd261e..948febd26 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1002,6 +1002,9 @@ pub enum TableFactor { partitions: Vec, /// Optional PartiQL JsonPath: json_path: Option, + /// Optional table sample modifier + /// See: + sample: Option, }, Derived { lateral: bool, @@ -1146,6 +1149,184 @@ pub enum TableFactor { }, } +/// The table sample modifier options +#[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), + /// Table sample located after the table alias option + AfterTableAlias(Box), +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TableSample { + pub modifier: TableSampleModifier, + pub name: Option, + pub quantity: Option, + pub seed: Option, + pub bucket: Option, + pub offset: 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 TableSampleModifier { + Sample, + TableSample, +} + +impl fmt::Display for TableSampleModifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TableSampleModifier::Sample => write!(f, "SAMPLE")?, + TableSampleModifier::TableSample => write!(f, "TABLESAMPLE")?, + } + 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 TableSampleQuantity { + pub parenthesized: bool, + pub value: Expr, + pub unit: Option, +} + +impl fmt::Display for TableSampleQuantity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.parenthesized { + write!(f, "(")?; + } + write!(f, "{}", self.value)?; + if let Some(unit) = &self.unit { + write!(f, " {}", unit)?; + } + if self.parenthesized { + write!(f, ")")?; + } + Ok(()) + } +} + +/// The table sample method names +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableSampleMethod { + Row, + Bernoulli, + System, + Block, +} + +impl fmt::Display for TableSampleMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TableSampleMethod::Bernoulli => write!(f, "BERNOULLI"), + TableSampleMethod::Row => write!(f, "ROW"), + TableSampleMethod::System => write!(f, "SYSTEM"), + TableSampleMethod::Block => write!(f, "BLOCK"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TableSampleSeed { + pub modifier: TableSampleSeedModifier, + pub value: Value, +} + +impl fmt::Display for TableSampleSeed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} ({})", self.modifier, self.value)?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableSampleSeedModifier { + Repeatable, + Seed, +} + +impl fmt::Display for TableSampleSeedModifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TableSampleSeedModifier::Repeatable => write!(f, "REPEATABLE"), + TableSampleSeedModifier::Seed => write!(f, "SEED"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableSampleUnit { + Rows, + Percent, +} + +impl fmt::Display for TableSampleUnit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TableSampleUnit::Percent => write!(f, "PERCENT"), + TableSampleUnit::Rows => write!(f, "ROWS"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TableSampleBucket { + pub bucket: Value, + pub total: Value, + pub on: Option, +} + +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)?; + } + Ok(()) + } +} +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)?; + } + if let Some(quantity) = &self.quantity { + write!(f, " {}", quantity)?; + } + if let Some(seed) = &self.seed { + write!(f, " {}", seed)?; + } + if let Some(bucket) = &self.bucket { + write!(f, " ({})", bucket)?; + } + if let Some(offset) = &self.offset { + write!(f, " OFFSET {}", offset)?; + } + Ok(()) + } +} + /// The source of values in a `PIVOT` operation. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1404,6 +1585,7 @@ impl fmt::Display for TableFactor { partitions, with_ordinality, json_path, + sample, } => { write!(f, "{name}")?; if let Some(json_path) = json_path { @@ -1426,6 +1608,9 @@ impl fmt::Display for TableFactor { if *with_ordinality { write!(f, " WITH ORDINALITY")?; } + if let Some(TableSampleKind::BeforeTableAlias(sample)) = sample { + write!(f, "{sample}")?; + } if let Some(alias) = alias { write!(f, " AS {alias}")?; } @@ -1435,6 +1620,9 @@ impl fmt::Display for TableFactor { if let Some(version) = version { write!(f, "{version}")?; } + if let Some(TableSampleKind::AfterTableAlias(sample)) = sample { + write!(f, "{sample}")?; + } Ok(()) } TableFactor::Derived { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 88e0fbdf2..c2c7c14f0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1699,6 +1699,7 @@ impl Spanned for TableFactor { with_ordinality: _, partitions: _, json_path: _, + sample: _, } => union_spans( name.0 .iter() diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index 571f9b9ba..80f44cf7c 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -61,4 +61,9 @@ impl Dialect for HiveDialect { fn supports_load_data(&self) -> bool { true } + + /// See Hive + fn supports_table_sample_before_alias(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f40cba719..8cce6a353 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -707,6 +707,17 @@ pub trait Dialect: Debug + Any { fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { keywords::RESERVED_FOR_IDENTIFIER.contains(&kw) } + + /// Returns true if this dialect supports the `TABLESAMPLE` option + /// before the table alias option. For example: + /// + /// Table sample before alias: `SELECT * FROM tbl AS t TABLESAMPLE (10)` + /// Table sample after alias: `SELECT * FROM tbl TABLESAMPLE (10) AS t` + /// + /// + fn supports_table_sample_before_alias(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/keywords.rs b/src/keywords.rs index d0cfcd05b..7e3354078 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -120,6 +120,7 @@ define_keywords!( BEGIN, BEGIN_FRAME, BEGIN_PARTITION, + BERNOULLI, BETWEEN, BIGDECIMAL, BIGINT, @@ -128,12 +129,14 @@ define_keywords!( BINDING, BIT, BLOB, + BLOCK, BLOOMFILTER, BOOL, BOOLEAN, BOTH, BROWSE, BTREE, + BUCKET, BUCKETS, BY, BYPASSRLS, @@ -680,6 +683,7 @@ define_keywords!( RUN, SAFE, SAFE_CAST, + SAMPLE, SAVEPOINT, SCHEMA, SCHEMAS, @@ -690,6 +694,7 @@ define_keywords!( SECONDARY, SECRET, SECURITY, + SEED, SELECT, SEMI, SENSITIVE, @@ -932,6 +937,9 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::CONNECT, // Reserved for snowflake MATCH_RECOGNIZE Keyword::MATCH_RECOGNIZE, + // Reserved for Snowflake table sample + Keyword::SAMPLE, + Keyword::TABLESAMPLE, ]; /// 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 37323084d..7d70460b4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10598,6 +10598,13 @@ impl<'a> Parser<'a> { let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); + let mut sample = None; + if self.dialect.supports_table_sample_before_alias() { + if let Some(parsed_sample) = self.maybe_parse_table_sample()? { + sample = Some(TableSampleKind::BeforeTableAlias(parsed_sample)); + } + } + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; // MSSQL-specific table hints: @@ -10612,6 +10619,12 @@ impl<'a> Parser<'a> { } }; + if !self.dialect.supports_table_sample_before_alias() { + if let Some(parsed_sample) = self.maybe_parse_table_sample()? { + sample = Some(TableSampleKind::AfterTableAlias(parsed_sample)); + } + } + let mut table = TableFactor::Table { name, alias, @@ -10621,6 +10634,7 @@ impl<'a> Parser<'a> { partitions, with_ordinality, json_path, + sample, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { @@ -10641,6 +10655,115 @@ impl<'a> Parser<'a> { } } + fn maybe_parse_table_sample(&mut self) -> Result>, ParserError> { + let modifier = if self.parse_keyword(Keyword::TABLESAMPLE) { + TableSampleModifier::TableSample + } else if self.parse_keyword(Keyword::SAMPLE) { + TableSampleModifier::Sample + } else { + return Ok(None); + }; + + let name = match self.parse_one_of_keywords(&[ + Keyword::BERNOULLI, + Keyword::ROW, + Keyword::SYSTEM, + Keyword::BLOCK, + ]) { + Some(Keyword::BERNOULLI) => Some(TableSampleMethod::Bernoulli), + Some(Keyword::ROW) => Some(TableSampleMethod::Row), + Some(Keyword::SYSTEM) => Some(TableSampleMethod::System), + Some(Keyword::BLOCK) => Some(TableSampleMethod::Block), + _ => None, + }; + + let parenthesized = self.consume_token(&Token::LParen); + + let (quantity, bucket) = if parenthesized && self.parse_keyword(Keyword::BUCKET) { + let selected_bucket = self.parse_number_value()?; + self.expect_keywords(&[Keyword::OUT, Keyword::OF])?; + let total = self.parse_number_value()?; + let on = if self.parse_keyword(Keyword::ON) { + Some(self.parse_expr()?) + } else { + None + }; + ( + None, + Some(TableSampleBucket { + bucket: selected_bucket, + total, + on, + }), + ) + } else { + let value = match self.maybe_parse(|p| p.parse_expr())? { + Some(num) => num, + None => { + if let Token::Word(w) = self.next_token().token { + Expr::Value(Value::Placeholder(w.value)) + } else { + return parser_err!( + "Expecting number or byte length e.g. 100M", + self.peek_token().span.start + ); + } + } + }; + let unit = if self.parse_keyword(Keyword::ROWS) { + Some(TableSampleUnit::Rows) + } else if self.parse_keyword(Keyword::PERCENT) { + Some(TableSampleUnit::Percent) + } else { + None + }; + ( + Some(TableSampleQuantity { + parenthesized, + value, + unit, + }), + None, + ) + }; + if parenthesized { + self.expect_token(&Token::RParen)?; + } + + let seed = if self.parse_keyword(Keyword::REPEATABLE) { + Some(self.parse_table_sample_seed(TableSampleSeedModifier::Repeatable)?) + } else if self.parse_keyword(Keyword::SEED) { + Some(self.parse_table_sample_seed(TableSampleSeedModifier::Seed)?) + } else { + None + }; + + let offset = if self.parse_keyword(Keyword::OFFSET) { + Some(self.parse_expr()?) + } else { + None + }; + + Ok(Some(Box::new(TableSample { + modifier, + name, + quantity, + seed, + bucket, + offset, + }))) + } + + fn parse_table_sample_seed( + &mut self, + modifier: TableSampleSeedModifier, + ) -> Result { + self.expect_token(&Token::LParen)?; + let value = self.parse_number_value()?; + self.expect_token(&Token::RParen)?; + Ok(TableSampleSeed { modifier, value }) + } + /// Parses `OPENJSON( jsonExpression [ , path ] ) [ ]` clause, /// assuming the `OPENJSON` keyword was already consumed. fn parse_open_json_table_factor(&mut self) -> Result { diff --git a/src/test_utils.rs b/src/test_utils.rs index 6e60a31c1..e76cdb87a 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -346,6 +346,21 @@ pub fn table(name: impl Into) -> TableFactor { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, + } +} + +pub fn table_from_name(name: ObjectName) -> TableFactor { + TableFactor::Table { + name, + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, } } @@ -362,6 +377,7 @@ pub fn table_with_alias(name: impl Into, alias: impl Into) -> Ta partitions: vec![], with_ordinality: false, json_path: None, + sample: None, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 34c14cc55..0311eba16 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -222,16 +222,7 @@ fn parse_delete_statement() { .. }) => { assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::with_quote('"', "table")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![Ident::with_quote('"', "table")])), from[0].relation ); } @@ -1379,16 +1370,7 @@ fn parse_table_identifiers() { assert_eq!( select.from, vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(expected), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(expected)), joins: vec![] },] ); @@ -1562,6 +1544,7 @@ fn parse_table_time_travel() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, joins: vec![] },] @@ -1661,6 +1644,7 @@ fn parse_merge() { partitions: Default::default(), with_ordinality: false, json_path: None, + sample: None, }, table ); @@ -1677,6 +1661,7 @@ fn parse_merge() { partitions: Default::default(), with_ordinality: false, json_path: None, + sample: None, }, source ); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 9d785576f..d60506d90 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -63,16 +63,7 @@ fn parse_map_access_expr() { })], into: None, from: vec![TableWithJoins { - relation: Table { - name: ObjectName(vec![Ident::new("foos")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::new("foos")])), joins: vec![], }], lateral_views: vec![], @@ -175,9 +166,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, - with_ordinality: _, - partitions: _, - json_path: _, + .. } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); @@ -1625,6 +1614,14 @@ fn parse_explain_table() { } } +#[test] +fn parse_table_sample() { + clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 0.1"); + clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1000"); + clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10"); + clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10 OFFSET 1 / 2"); +} + fn clickhouse() -> TestedDialects { TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7dfb98d6f..0f1813c2f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -41,7 +41,7 @@ use sqlparser::tokenizer::Span; use sqlparser::tokenizer::Tokenizer; use test_utils::{ all_dialects, all_dialects_where, alter_table_op, assert_eq_vec, call, expr_from_projection, - join, number, only, table, table_alias, TestedDialects, + join, number, only, table, table_alias, table_from_name, TestedDialects, }; #[macro_use] @@ -359,16 +359,7 @@ fn parse_update_set_from() { stmt, Statement::Update { table: TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t1")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::new("t1")])), joins: vec![], }, assignments: vec![Assignment { @@ -391,16 +382,7 @@ fn parse_update_set_from() { ], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t1")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::new("t1")])), joins: vec![], }], lateral_views: vec![], @@ -480,6 +462,7 @@ fn parse_update_with_table_alias() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, joins: vec![], }, @@ -572,6 +555,7 @@ fn parse_select_with_table_alias() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, joins: vec![], }] @@ -601,16 +585,7 @@ fn parse_delete_statement() { .. }) => { assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::with_quote('"', "table")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![Ident::with_quote('"', "table")])), from[0].relation ); } @@ -649,29 +624,17 @@ fn parse_delete_statement_for_multi_tables() { tables[1] ); assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![ + Ident::new("schema1"), + Ident::new("table1") + ])), from[0].relation ); assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![ + Ident::new("schema2"), + Ident::new("table2") + ])), from[0].joins[0].relation ); } @@ -689,55 +652,31 @@ fn parse_delete_statement_for_multi_tables_with_using() { .. }) => { assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![ + Ident::new("schema1"), + Ident::new("table1") + ])), from[0].relation ); assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![ + Ident::new("schema2"), + Ident::new("table2") + ])), from[1].relation ); assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![ + Ident::new("schema1"), + Ident::new("table1") + ])), using[0].relation ); assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![ + Ident::new("schema2"), + Ident::new("table2") + ])), using[0].joins[0].relation ); } @@ -760,16 +699,7 @@ fn parse_where_delete_statement() { .. }) => { assert_eq!( - TableFactor::Table { - name: ObjectName(vec![Ident::new("foo")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + table_from_name(ObjectName(vec![Ident::new("foo")])), from[0].relation, ); @@ -815,6 +745,7 @@ fn parse_where_delete_with_alias_statement() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, from[0].relation, ); @@ -832,6 +763,7 @@ fn parse_where_delete_with_alias_statement() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, joins: vec![], }]), @@ -4920,20 +4852,11 @@ fn test_parse_named_window() { ], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "aggregate_test_100".to_string(), - quote_style: None, - span: Span::empty(), - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident { + value: "aggregate_test_100".to_string(), + quote_style: None, + span: Span::empty(), + }])), joins: vec![], }], lateral_views: vec![], @@ -5511,20 +5434,11 @@ fn parse_interval_and_or_xor() { }))], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "test".to_string(), - quote_style: None, - span: Span::empty(), - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident { + value: "test".to_string(), + quote_style: None, + span: Span::empty(), + }])), joins: vec![], }], lateral_views: vec![], @@ -6132,29 +6046,11 @@ fn parse_implicit_join() { assert_eq!( vec![ TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec!["t1".into()]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec!["t1".into()])), joins: vec![], }, TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec!["t2".into()]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec!["t2".into()])), joins: vec![], }, ], @@ -6166,53 +6062,17 @@ fn parse_implicit_join() { assert_eq!( vec![ TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec!["t1a".into()]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec!["t1a".into()])), joins: vec![Join { - relation: TableFactor::Table { - name: ObjectName(vec!["t1b".into()]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec!["t1b".into()])), global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], }, TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec!["t2a".into()]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec!["t2a".into()])), joins: vec![Join { - relation: TableFactor::Table { - name: ObjectName(vec!["t2b".into()]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec!["t2b".into()])), global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -6228,16 +6088,7 @@ fn parse_cross_join() { let select = verified_only_select(sql); assert_eq!( Join { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t2")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::new("t2")])), global: false, join_operator: JoinOperator::CrossJoin, }, @@ -6263,6 +6114,7 @@ fn parse_joins_on() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, global, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -6391,6 +6243,7 @@ fn parse_joins_using() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, global: false, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), @@ -6465,6 +6318,7 @@ fn parse_natural_join() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, global: false, join_operator: f(JoinConstraint::Natural), @@ -6728,16 +6582,7 @@ fn parse_derived_tables() { }), }, joins: vec![Join { - relation: TableFactor::Table { - name: ObjectName(vec!["t2".into()]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec!["t2".into()])), global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -7668,20 +7513,11 @@ fn lateral_function() { top_before_distinct: false, into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "customer".to_string(), - quote_style: None, - span: Span::empty(), - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident { + value: "customer".to_string(), + quote_style: None, + span: Span::empty(), + }])), joins: vec![Join { relation: TableFactor::Function { lateral: true, @@ -8499,6 +8335,7 @@ fn parse_merge() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, } ); assert_eq!(table, table_no_into); @@ -8519,16 +8356,10 @@ fn parse_merge() { )], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("s"), Ident::new("foo")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![ + Ident::new("s"), + Ident::new("foo") + ])), joins: vec![], }], lateral_views: vec![], @@ -9611,6 +9442,7 @@ fn parse_pivot_table() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }), aggregate_functions: vec![ expected_function("a", None), @@ -9686,6 +9518,7 @@ fn parse_unpivot_table() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }), value: Ident { value: "quantity".to_string(), @@ -9756,6 +9589,7 @@ fn parse_pivot_unpivot_table() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }), value: Ident { value: "population".to_string(), @@ -10165,16 +9999,7 @@ fn parse_unload() { projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("tab")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::new("tab")])), joins: vec![], }], lateral_views: vec![], @@ -10348,16 +10173,7 @@ fn parse_connect_by() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), ], from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("employees")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::new("employees")])), joins: vec![], }], into: None, @@ -10437,16 +10253,7 @@ fn parse_connect_by() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), ], from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("employees")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::new("employees")])), joins: vec![], }], into: None, @@ -10601,16 +10408,7 @@ fn test_match_recognize() { use MatchRecognizeSymbol::*; use RepetitionQuantifier::*; - let table = TableFactor::Table { - name: ObjectName(vec![Ident::new("my_table")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }; + let table = table_from_name(ObjectName(vec![Ident::new("my_table")])); fn check(options: &str, expect: TableFactor) { let select = all_dialects_where(|d| d.supports_match_recognize()).verified_only_select( @@ -12585,3 +12383,16 @@ fn parse_create_table_with_enum_types() { ParserError::ParserError("Expected: literal string, found: 2".to_string()) ); } + +#[test] +fn test_table_sample() { + let dialects = all_dialects_where(|d| d.supports_table_sample_before_alias()); + dialects.verified_stmt("SELECT * FROM tbl TABLESAMPLE (50) AS t"); + dialects.verified_stmt("SELECT * FROM tbl TABLESAMPLE (50 ROWS) AS t"); + dialects.verified_stmt("SELECT * FROM tbl TABLESAMPLE (50 PERCENT) AS t"); + + let dialects = all_dialects_where(|d| !d.supports_table_sample_before_alias()); + dialects.verified_stmt("SELECT * FROM tbl AS t TABLESAMPLE BERNOULLI (50)"); + dialects.verified_stmt("SELECT * FROM tbl AS t TABLESAMPLE SYSTEM (50)"); + dialects.verified_stmt("SELECT * FROM tbl AS t TABLESAMPLE SYSTEM (50) REPEATABLE (10)"); +} diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index d73c088a7..b9ca55d13 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -185,16 +185,7 @@ fn test_values_clause() { "SELECT * FROM values", )); assert_eq!( - Some(&TableFactor::Table { - name: ObjectName(vec![Ident::new("values")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }), + Some(&table_from_name(ObjectName(vec![Ident::new("values")]))), query .body .as_select() diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a0fc49b9f..d441cd195 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -268,20 +268,11 @@ fn test_select_union_by_name() { top_before_distinct: false, into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "capitals".to_string(), - quote_style: None, - span: Span::empty(), - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident { + value: "capitals".to_string(), + quote_style: None, + span: Span::empty(), + }])), joins: vec![], }], lateral_views: vec![], @@ -306,20 +297,11 @@ fn test_select_union_by_name() { top_before_distinct: false, into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "weather".to_string(), - quote_style: None, - span: Span::empty(), - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident { + value: "weather".to_string(), + quote_style: None, + span: Span::empty(), + }])), joins: vec![], }], lateral_views: vec![], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 981218388..5349f1207 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -459,6 +459,7 @@ fn parse_delimited_identifiers() { with_ordinality: _, partitions: _, json_path: _, + sample: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); @@ -537,6 +538,15 @@ fn parse_use() { ); } +#[test] +fn test_tample_sample() { + hive().verified_stmt("SELECT * FROM source TABLESAMPLE (BUCKET 3 OUT OF 32 ON rand()) AS s"); + hive().verified_stmt("SELECT * FROM source TABLESAMPLE (BUCKET 3 OUT OF 16 ON id)"); + hive().verified_stmt("SELECT * FROM source TABLESAMPLE (100M) AS s"); + hive().verified_stmt("SELECT * FROM source TABLESAMPLE (0.1 PERCENT) AS s"); + hive().verified_stmt("SELECT * FROM source TABLESAMPLE (10 ROWS)"); +} + fn hive() -> TestedDialects { TestedDialects::new(vec![Box::new(HiveDialect {})]) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 66e40f46b..ecc874af8 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -73,6 +73,7 @@ fn parse_table_time_travel() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, joins: vec![] },] @@ -221,6 +222,7 @@ fn parse_mssql_openjson() { with_ordinality: false, partitions: vec![], json_path: None, + sample: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -279,6 +281,7 @@ fn parse_mssql_openjson() { with_ordinality: false, partitions: vec![], json_path: None, + sample: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -338,6 +341,7 @@ fn parse_mssql_openjson() { with_ordinality: false, partitions: vec![], json_path: None, + sample: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -396,6 +400,7 @@ fn parse_mssql_openjson() { with_ordinality: false, partitions: vec![], json_path: None, + sample: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -434,6 +439,7 @@ fn parse_mssql_openjson() { with_ordinality: false, partitions: vec![], json_path: None, + sample: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -611,9 +617,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, - with_ordinality: _, - partitions: _, - json_path: _, + .. } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); @@ -1082,20 +1086,11 @@ fn parse_substring_in_select() { })], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "test".to_string(), - quote_style: None, - span: Span::empty(), - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident { + value: "test".to_string(), + quote_style: None, + span: Span::empty(), + }])), joins: vec![] }], lateral_views: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index cac1af852..bc7bf2f88 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1884,16 +1884,9 @@ fn parse_select_with_numeric_prefix_column_name() { )))], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::with_quote('"', "table")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::with_quote( + '"', "table" + )])), joins: vec![] }], lateral_views: vec![], @@ -1943,16 +1936,9 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { ], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::with_quote('"', "table")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::with_quote( + '"', "table" + )])), joins: vec![] }], lateral_views: vec![], @@ -2020,6 +2006,7 @@ fn parse_update_with_joins() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -2034,6 +2021,7 @@ fn parse_update_with_joins() { partitions: vec![], with_ordinality: false, json_path: None, + sample: None, }, global: false, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { @@ -2464,20 +2452,11 @@ fn parse_substring_in_select() { })], into: None, from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident { - value: "test".to_string(), - quote_style: None, - span: Span::empty(), - }]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident { + value: "test".to_string(), + quote_style: None, + span: Span::empty(), + }])), joins: vec![] }], lateral_views: vec![], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2e204d9bc..aaf4e65db 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3581,9 +3581,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, - with_ordinality: _, - partitions: _, - json_path: _, + .. } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 2fd855a09..9492946d3 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -39,27 +39,18 @@ fn test_square_brackets_over_db_schema_table_name() { assert_eq!( select.from[0], TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![ - Ident { - value: "test_schema".to_string(), - quote_style: Some('['), - span: Span::empty(), - }, - Ident { - value: "test_table".to_string(), - quote_style: Some('['), - span: Span::empty(), - } - ]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![ + Ident { + value: "test_schema".to_string(), + quote_style: Some('['), + span: Span::empty(), + }, + Ident { + value: "test_table".to_string(), + quote_style: Some('['), + span: Span::empty(), + } + ])), joins: vec![], } ); @@ -90,27 +81,18 @@ fn test_double_quotes_over_db_schema_table_name() { assert_eq!( select.from[0], TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![ - Ident { - value: "test_schema".to_string(), - quote_style: Some('"'), - span: Span::empty(), - }, - Ident { - value: "test_table".to_string(), - quote_style: Some('"'), - span: Span::empty(), - } - ]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![ + Ident { + value: "test_schema".to_string(), + quote_style: Some('"'), + span: Span::empty(), + }, + Ident { + value: "test_table".to_string(), + quote_style: Some('"'), + span: Span::empty(), + } + ])), joins: vec![], } ); @@ -130,9 +112,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, - with_ordinality: _, - partitions: _, - json_path: _, + .. } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index d6774c317..adb8f8133 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1188,9 +1188,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, - with_ordinality: _, - partitions: _, - json_path: _, + .. } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); @@ -2960,3 +2958,19 @@ fn parse_insert_overwrite() { let insert_overwrite_into = r#"INSERT OVERWRITE INTO schema.table SELECT a FROM b"#; snowflake().verified_stmt(insert_overwrite_into); } + +#[test] +fn test_table_sample() { + snowflake_and_generic().verified_stmt("SELECT * FROM testtable SAMPLE (10)"); + snowflake_and_generic().verified_stmt("SELECT * FROM testtable TABLESAMPLE (10)"); + snowflake_and_generic() + .verified_stmt("SELECT * FROM testtable AS t TABLESAMPLE BERNOULLI (10)"); + snowflake_and_generic().verified_stmt("SELECT * FROM testtable AS t TABLESAMPLE ROW (10)"); + snowflake_and_generic().verified_stmt("SELECT * FROM testtable AS t TABLESAMPLE ROW (10 ROWS)"); + snowflake_and_generic() + .verified_stmt("SELECT * FROM testtable TABLESAMPLE BLOCK (3) SEED (82)"); + snowflake_and_generic() + .verified_stmt("SELECT * FROM testtable TABLESAMPLE SYSTEM (3) REPEATABLE (82)"); + snowflake_and_generic().verified_stmt("SELECT id FROM mytable TABLESAMPLE (10) REPEATABLE (1)"); + snowflake_and_generic().verified_stmt("SELECT id FROM mytable TABLESAMPLE (10) SEED (1)"); +} diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 987b1263d..ff0b54ef7 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -479,16 +479,7 @@ fn parse_update_tuple_row_values() { }], selection: None, table: TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("x")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - }, + relation: table_from_name(ObjectName(vec![Ident::new("x")])), joins: vec![], }, from: None, From 7867ba3cf04c9c8324bfa26403945f0d53c2119a Mon Sep 17 00:00:00 2001 From: Aleksei Piianin Date: Sun, 15 Dec 2024 10:56:11 +0100 Subject: [PATCH 639/806] Redshift: Fix parsing for quoted numbered columns (#1576) --- src/dialect/mod.rs | 53 +++++++++++++++++++------ src/dialect/redshift.rs | 48 ++++++++++++++++++----- src/tokenizer.rs | 77 ++++++++++++++++++++++++++++++++----- tests/sqlparser_redshift.rs | 58 +++++++++++++++++++++++++--- 4 files changed, 200 insertions(+), 36 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 8cce6a353..c32b763a4 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -128,14 +128,39 @@ pub trait Dialect: Debug + Any { ch == '"' || ch == '`' } - /// Return the character used to quote identifiers. - fn identifier_quote_style(&self, _identifier: &str) -> Option { + /// Determine if a character starts a potential nested quoted identifier. + /// Example: RedShift supports the following quote styles to all mean the same thing: + /// ```sql + /// SELECT 1 AS foo; + /// SELECT 1 AS "foo"; + /// SELECT 1 AS [foo]; + /// SELECT 1 AS ["foo"]; + /// ``` + fn is_nested_delimited_identifier_start(&self, _ch: char) -> bool { + false + } + + /// Only applicable whenever [`Self::is_nested_delimited_identifier_start`] returns true + /// If the next sequence of tokens potentially represent a nested identifier, then this method + /// returns a tuple containing the outer quote style, and if present, the inner (nested) quote style. + /// + /// Example (Redshift): + /// ```text + /// `["foo"]` => Some(`[`, Some(`"`)) + /// `[foo]` => Some(`[`, None) + /// `[0]` => None + /// `"foo"` => None + /// ``` + fn peek_nested_delimited_identifier_quotes( + &self, + mut _chars: Peekable>, + ) -> Option<(char, Option)> { None } - /// Determine if quoted characters are proper for identifier - fn is_proper_identifier_inside_quotes(&self, mut _chars: Peekable>) -> bool { - true + /// Return the character used to quote identifiers. + fn identifier_quote_style(&self, _identifier: &str) -> Option { + None } /// Determine if a character is a valid start character for an unquoted identifier @@ -869,6 +894,17 @@ mod tests { self.0.is_delimited_identifier_start(ch) } + fn is_nested_delimited_identifier_start(&self, ch: char) -> bool { + self.0.is_nested_delimited_identifier_start(ch) + } + + fn peek_nested_delimited_identifier_quotes( + &self, + chars: std::iter::Peekable>, + ) -> Option<(char, Option)> { + self.0.peek_nested_delimited_identifier_quotes(chars) + } + fn identifier_quote_style(&self, identifier: &str) -> Option { self.0.identifier_quote_style(identifier) } @@ -877,13 +913,6 @@ mod tests { self.0.supports_string_literal_backslash_escape() } - fn is_proper_identifier_inside_quotes( - &self, - chars: std::iter::Peekable>, - ) -> bool { - self.0.is_proper_identifier_inside_quotes(chars) - } - fn supports_filter_during_aggregation(&self) -> bool { self.0.supports_filter_during_aggregation() } diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 48eb00ab1..55405ba53 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -32,21 +32,51 @@ pub struct RedshiftSqlDialect {} // in the Postgres dialect, the query will be parsed as an array, while in the Redshift dialect it will // be a json path impl Dialect for RedshiftSqlDialect { - fn is_delimited_identifier_start(&self, ch: char) -> bool { - ch == '"' || ch == '[' + /// Determine if a character starts a potential nested quoted identifier. + /// Example: RedShift supports the following quote styles to all mean the same thing: + /// ```sql + /// SELECT 1 AS foo; + /// SELECT 1 AS "foo"; + /// SELECT 1 AS [foo]; + /// SELECT 1 AS ["foo"]; + /// ``` + fn is_nested_delimited_identifier_start(&self, ch: char) -> bool { + ch == '[' } - /// Determine if quoted characters are proper for identifier - /// It's needed to distinguish treating square brackets as quotes from - /// treating them as json path. If there is identifier then we assume - /// there is no json path. - fn is_proper_identifier_inside_quotes(&self, mut chars: Peekable>) -> bool { + /// Only applicable whenever [`Self::is_nested_delimited_identifier_start`] returns true + /// If the next sequence of tokens potentially represent a nested identifier, then this method + /// returns a tuple containing the outer quote style, and if present, the inner (nested) quote style. + /// + /// Example (Redshift): + /// ```text + /// `["foo"]` => Some(`[`, Some(`"`)) + /// `[foo]` => Some(`[`, None) + /// `[0]` => None + /// `"foo"` => None + /// ``` + fn peek_nested_delimited_identifier_quotes( + &self, + mut chars: Peekable>, + ) -> Option<(char, Option)> { + if chars.peek() != Some(&'[') { + return None; + } + chars.next(); + let mut not_white_chars = chars.skip_while(|ch| ch.is_whitespace()).peekable(); + if let Some(&ch) = not_white_chars.peek() { - return self.is_identifier_start(ch); + if ch == '"' { + return Some(('[', Some('"'))); + } + if self.is_identifier_start(ch) { + return Some(('[', None)); + } } - false + + None } fn is_identifier_start(&self, ch: char) -> bool { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index aacfc16fa..9269f4fe6 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1075,25 +1075,61 @@ impl<'a> Tokenizer<'a> { Ok(Some(Token::DoubleQuotedString(s))) } // delimited (quoted) identifier + quote_start if self.dialect.is_delimited_identifier_start(ch) => { + let word = self.tokenize_quoted_identifier(quote_start, chars)?; + Ok(Some(Token::make_word(&word, Some(quote_start)))) + } + // Potentially nested delimited (quoted) identifier quote_start - if self.dialect.is_delimited_identifier_start(ch) + if self + .dialect + .is_nested_delimited_identifier_start(quote_start) && self .dialect - .is_proper_identifier_inside_quotes(chars.peekable.clone()) => + .peek_nested_delimited_identifier_quotes(chars.peekable.clone()) + .is_some() => { - let error_loc = chars.location(); - chars.next(); // consume the opening quote + let Some((quote_start, nested_quote_start)) = self + .dialect + .peek_nested_delimited_identifier_quotes(chars.peekable.clone()) + else { + return self.tokenizer_error( + chars.location(), + format!("Expected nested delimiter '{quote_start}' before EOF."), + ); + }; + + let Some(nested_quote_start) = nested_quote_start else { + let word = self.tokenize_quoted_identifier(quote_start, chars)?; + return Ok(Some(Token::make_word(&word, Some(quote_start)))); + }; + + let mut word = vec![]; let quote_end = Word::matching_end_quote(quote_start); - let (s, last_char) = self.parse_quoted_ident(chars, quote_end); + let nested_quote_end = Word::matching_end_quote(nested_quote_start); + let error_loc = chars.location(); - if last_char == Some(quote_end) { - Ok(Some(Token::make_word(&s, Some(quote_start)))) - } else { - self.tokenizer_error( + chars.next(); // skip the first delimiter + peeking_take_while(chars, |ch| ch.is_whitespace()); + if chars.peek() != Some(&nested_quote_start) { + return self.tokenizer_error( + error_loc, + format!("Expected nested delimiter '{nested_quote_start}' before EOF."), + ); + } + word.push(nested_quote_start.into()); + word.push(self.tokenize_quoted_identifier(nested_quote_end, chars)?); + word.push(nested_quote_end.into()); + peeking_take_while(chars, |ch| ch.is_whitespace()); + if chars.peek() != Some("e_end) { + return self.tokenizer_error( error_loc, format!("Expected close delimiter '{quote_end}' before EOF."), - ) + ); } + chars.next(); // skip close delimiter + + Ok(Some(Token::make_word(&word.concat(), Some(quote_start)))) } // numbers and period '0'..='9' | '.' => { @@ -1597,6 +1633,27 @@ impl<'a> Tokenizer<'a> { s } + /// Read a quoted identifier + fn tokenize_quoted_identifier( + &self, + quote_start: char, + chars: &mut State, + ) -> Result { + let error_loc = chars.location(); + chars.next(); // consume the opening quote + let quote_end = Word::matching_end_quote(quote_start); + let (s, last_char) = self.parse_quoted_ident(chars, quote_end); + + if last_char == Some(quote_end) { + Ok(s) + } else { + self.tokenizer_error( + error_loc, + format!("Expected close delimiter '{quote_end}' before EOF."), + ) + } + } + /// Read a single quoted string, starting with the opening quote. fn tokenize_escaped_single_quoted_string( &self, diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 9492946d3..857d378bc 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -157,6 +157,8 @@ fn parse_delimited_identifiers() { } redshift().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); + // An alias starting with a number + redshift().verified_stmt(r#"CREATE TABLE "foo" ("1" INT)"#); redshift().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#); //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } @@ -203,7 +205,7 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + key: Expr::Value(number("0")) }, JsonPathElem::Dot { key: "o_orderkey".to_string(), @@ -226,7 +228,7 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + key: Expr::Value(number("0")) }, JsonPathElem::Bracket { key: Expr::Value(Value::SingleQuotedString("id".to_owned())) @@ -250,7 +252,7 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + key: Expr::Value(number("0")) }, JsonPathElem::Bracket { key: Expr::Value(Value::SingleQuotedString("id".to_owned())) @@ -260,6 +262,31 @@ fn test_redshift_json_path() { }, expr_from_projection(only(&select.projection)) ); + + let sql = r#"SELECT db1.sc1.tbl1.col1[0]."id" FROM customer_orders_lineitem"#; + let select = dialects.verified_only_select(sql); + assert_eq!( + &Expr::JsonAccess { + value: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("db1"), + Ident::new("sc1"), + Ident::new("tbl1"), + Ident::new("col1") + ])), + path: JsonPath { + path: vec![ + JsonPathElem::Bracket { + key: Expr::Value(number("0")) + }, + JsonPathElem::Dot { + key: "id".to_string(), + quoted: true, + } + ] + } + }, + expr_from_projection(only(&select.projection)) + ); } #[test] @@ -276,7 +303,7 @@ fn test_parse_json_path_from() { &Some(JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + key: Expr::Value(number("0")) }, JsonPathElem::Dot { key: "a".to_string(), @@ -300,7 +327,7 @@ fn test_parse_json_path_from() { &Some(JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(Value::Number("0".parse().unwrap(), false)) + key: Expr::Value(number("0")) }, JsonPathElem::Dot { key: "a".to_string(), @@ -334,3 +361,24 @@ fn test_parse_json_path_from() { _ => panic!(), } } + +#[test] +fn test_parse_select_numbered_columns() { + // An alias starting with a number + redshift_and_generic().verified_stmt(r#"SELECT 1 AS "1" FROM a"#); + redshift_and_generic().verified_stmt(r#"SELECT 1 AS "1abc" FROM a"#); +} + +#[test] +fn test_parse_nested_quoted_identifier() { + redshift().verified_stmt(r#"SELECT 1 AS ["1"] FROM a"#); + redshift().verified_stmt(r#"SELECT 1 AS ["[="] FROM a"#); + redshift().verified_stmt(r#"SELECT 1 AS ["=]"] FROM a"#); + redshift().verified_stmt(r#"SELECT 1 AS ["a[b]"] FROM a"#); + // trim spaces + redshift().one_statement_parses_to(r#"SELECT 1 AS [ " 1 " ]"#, r#"SELECT 1 AS [" 1 "]"#); + // invalid query + assert!(redshift() + .parse_sql_statements(r#"SELECT 1 AS ["1]"#) + .is_err()); +} From c69839102ae4854cd5d50a799ab7fd48c8919eda Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:39:59 +0100 Subject: [PATCH 640/806] Add the alter table ON COMMIT option to Snowflake (#1606) --- src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 36 ++++++++++++++++++++---------------- tests/sqlparser_snowflake.rs | 9 +++++++++ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 77d2ccff1..50e383db2 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -377,6 +377,10 @@ pub fn parse_create_table( parser.expect_token(&Token::RParen)?; builder = builder.with_tags(Some(tags)); } + Keyword::ON if parser.parse_keyword(Keyword::COMMIT) => { + let on_commit = Some(parser.parse_create_table_on_commit()?); + builder = builder.on_commit(on_commit); + } _ => { return parser.expected("end of statement", next_token); } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7d70460b4..ca46bb604 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6155,22 +6155,11 @@ impl<'a> Parser<'a> { None }; - let on_commit: Option = - if self.parse_keywords(&[Keyword::ON, Keyword::COMMIT, Keyword::DELETE, Keyword::ROWS]) - { - Some(OnCommit::DeleteRows) - } else if self.parse_keywords(&[ - Keyword::ON, - Keyword::COMMIT, - Keyword::PRESERVE, - Keyword::ROWS, - ]) { - Some(OnCommit::PreserveRows) - } else if self.parse_keywords(&[Keyword::ON, Keyword::COMMIT, Keyword::DROP]) { - Some(OnCommit::Drop) - } else { - None - }; + let on_commit = if self.parse_keywords(&[Keyword::ON, Keyword::COMMIT]) { + Some(self.parse_create_table_on_commit()?) + } else { + None + }; let strict = self.parse_keyword(Keyword::STRICT); @@ -6226,6 +6215,21 @@ impl<'a> Parser<'a> { .build()) } + pub(crate) fn parse_create_table_on_commit(&mut self) -> Result { + if self.parse_keywords(&[Keyword::DELETE, Keyword::ROWS]) { + Ok(OnCommit::DeleteRows) + } else if self.parse_keywords(&[Keyword::PRESERVE, Keyword::ROWS]) { + Ok(OnCommit::PreserveRows) + } else if self.parse_keywords(&[Keyword::DROP]) { + Ok(OnCommit::Drop) + } else { + parser_err!( + "Expecting DELETE ROWS, PRESERVE ROWS or DROP", + self.peek_token() + ) + } + } + /// Parse configuration like partitioning, clustering information during the table creation. /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_2) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index adb8f8133..9fe14783c 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -355,6 +355,15 @@ fn test_snowflake_create_table_column_comment() { } } +#[test] +fn test_snowflake_create_table_on_commit() { + snowflake().verified_stmt( + r#"CREATE LOCAL TEMPORARY TABLE "AAA"."foo" ("bar" INTEGER) ON COMMIT PRESERVE ROWS"#, + ); + snowflake().verified_stmt(r#"CREATE TABLE "AAA"."foo" ("bar" INTEGER) ON COMMIT DELETE ROWS"#); + snowflake().verified_stmt(r#"CREATE TABLE "AAA"."foo" ("bar" INTEGER) ON COMMIT DROP"#); +} + #[test] fn test_snowflake_create_local_table() { match snowflake().verified_stmt("CREATE TABLE my_table (a INT)") { From 8fcdf48e5c8325e0fdea2c5b2948bda69ba9b907 Mon Sep 17 00:00:00 2001 From: cjw Date: Tue, 17 Dec 2024 23:03:12 +0800 Subject: [PATCH 641/806] Support parsing `EXPLAIN ESTIMATE` of Clickhouse (#1605) Co-authored-by: Kermit --- src/ast/mod.rs | 7 +++++++ src/keywords.rs | 1 + src/parser/mod.rs | 4 ++++ tests/sqlparser_common.rs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ccb2ed1bc..6e3f20472 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3239,6 +3239,9 @@ pub enum Statement { /// /// [SQLite](https://sqlite.org/lang_explain.html) query_plan: bool, + /// `EXPLAIN ESTIMATE` + /// [Clickhouse](https://clickhouse.com/docs/en/sql-reference/statements/explain#explain-estimate) + estimate: bool, /// A SQL query that specifies what to explain statement: Box, /// Optional output format of explain @@ -3471,6 +3474,7 @@ impl fmt::Display for Statement { verbose, analyze, query_plan, + estimate, statement, format, options, @@ -3483,6 +3487,9 @@ impl fmt::Display for Statement { if *analyze { write!(f, "ANALYZE ")?; } + if *estimate { + write!(f, "ESTIMATE ")?; + } if *verbose { write!(f, "VERBOSE ")?; diff --git a/src/keywords.rs b/src/keywords.rs index 7e3354078..bbfd00ca0 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -298,6 +298,7 @@ define_keywords!( ERROR, ESCAPE, ESCAPED, + ESTIMATE, EVENT, EVERY, EXCEPT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ca46bb604..94d63cf80 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9091,6 +9091,7 @@ impl<'a> Parser<'a> { let mut analyze = false; let mut verbose = false; let mut query_plan = false; + let mut estimate = false; let mut format = None; let mut options = None; @@ -9103,6 +9104,8 @@ impl<'a> Parser<'a> { options = Some(self.parse_utility_options()?) } else if self.parse_keywords(&[Keyword::QUERY, Keyword::PLAN]) { query_plan = true; + } else if self.parse_keyword(Keyword::ESTIMATE) { + estimate = true; } else { analyze = self.parse_keyword(Keyword::ANALYZE); verbose = self.parse_keyword(Keyword::VERBOSE); @@ -9120,6 +9123,7 @@ impl<'a> Parser<'a> { analyze, verbose, query_plan, + estimate, statement: Box::new(statement), format, options, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0f1813c2f..1bf9383af 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4375,6 +4375,7 @@ fn run_explain_analyze( analyze, verbose, query_plan, + estimate, statement, format, options, @@ -4384,6 +4385,7 @@ fn run_explain_analyze( assert_eq!(format, expected_format); assert_eq!(options, exepcted_options); assert!(!query_plan); + assert!(!estimate); assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); } _ => panic!("Unexpected Statement, must be Explain"), @@ -4528,6 +4530,34 @@ fn parse_explain_query_plan() { ); } +#[test] +fn parse_explain_estimate() { + let statement = all_dialects().verified_stmt("EXPLAIN ESTIMATE SELECT sqrt(id) FROM foo"); + + match &statement { + Statement::Explain { + query_plan, + estimate, + analyze, + verbose, + statement, + .. + } => { + assert!(estimate); + assert!(!query_plan); + assert!(!analyze); + assert!(!verbose); + assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); + } + _ => unreachable!(), + } + + assert_eq!( + "EXPLAIN ESTIMATE SELECT sqrt(id) FROM foo", + statement.to_string() + ); +} + #[test] fn parse_named_argument_function() { let dialects = all_dialects_where(|d| { From e9ab4d6b94a81d4ed3e402750a5faf3860892c23 Mon Sep 17 00:00:00 2001 From: Ayman Elkfrawy <120422207+ayman-sigma@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:12:09 -0800 Subject: [PATCH 642/806] Fix BigQuery hyphenated ObjectName with numbers (#1598) --- src/parser/mod.rs | 4 +++- src/tokenizer.rs | 45 +++++++++++++++++++++++++++++++------ tests/sqlparser_bigquery.rs | 20 +++++++++++++++++ 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 94d63cf80..c0aa0acba 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8755,7 +8755,9 @@ impl<'a> Parser<'a> { } Token::Number(s, false) if s.chars().all(|c| c.is_ascii_digit()) => { ident.value.push_str(&s); - true + // If next token is period, then it is part of an ObjectName and we don't expect whitespace + // after the number. + !matches!(self.peek_token().token, Token::Period) } _ => { return self diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 9269f4fe6..3c2f70edf 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1144,15 +1144,29 @@ impl<'a> Tokenizer<'a> { // match one period if let Some('.') = chars.peek() { - s.push('.'); - chars.next(); + // Check if this actually is a float point number + let mut char_clone = chars.peekable.clone(); + char_clone.next(); + // Next char should be a digit, otherwise, it is not a float point number + if char_clone + .peek() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + { + s.push('.'); + chars.next(); + } else if !s.is_empty() { + // Number might be part of period separated construct. Keep the period for next token + // e.g. a-12.b + return Ok(Some(Token::Number(s, false))); + } else { + // No number -> Token::Period + chars.next(); + return Ok(Some(Token::Period)); + } } - s += &peeking_take_while(chars, |ch| ch.is_ascii_digit()); - // No number -> Token::Period - if s == "." { - return Ok(Some(Token::Period)); - } + s += &peeking_take_while(chars, |ch| ch.is_ascii_digit()); let mut exponent_part = String::new(); // Parse exponent as number @@ -2185,6 +2199,23 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_select_float_hyphenated_identifier() { + let sql = String::from("SELECT a-12.b"); + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::make_word("a", None), + Token::Minus, + Token::Number(String::from("12"), false), + Token::Period, + Token::make_word("b", None), + ]; + compare(expected, tokens); + } + #[test] fn tokenize_clickhouse_double_equal() { let sql = String::from("SELECT foo=='1'"); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 0311eba16..c8173759d 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1504,6 +1504,26 @@ fn parse_hyphenated_table_identifiers() { "SELECT * FROM foo-bar AS f JOIN baz-qux AS b ON f.id = b.id", ); + assert_eq!( + bigquery() + .verified_only_select_with_canonical( + "select * from foo-123.bar", + "SELECT * FROM foo-123.bar" + ) + .from[0] + .relation, + TableFactor::Table { + name: ObjectName(vec![Ident::new("foo-123"), Ident::new("bar")]), + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + } + ); + assert_eq!( bigquery() .verified_only_select_with_canonical( From fac84d81ee7882ed2a2244febfd45b4e8ba21490 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Thu, 19 Dec 2024 00:04:25 +0100 Subject: [PATCH 643/806] Fix test compilation issue (#1609) --- tests/sqlparser_bigquery.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index c8173759d..be383b476 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1512,16 +1512,7 @@ fn parse_hyphenated_table_identifiers() { ) .from[0] .relation, - TableFactor::Table { - name: ObjectName(vec![Ident::new("foo-123"), Ident::new("bar")]), - alias: None, - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - } + table_from_name(ObjectName(vec![Ident::new("foo-123"), Ident::new("bar")])), ); assert_eq!( From 6523dabcf886c69be58ca8e486683fb6e02e0f16 Mon Sep 17 00:00:00 2001 From: Ramnivas Laddad Date: Thu, 19 Dec 2024 01:10:53 -0800 Subject: [PATCH 644/806] Allow foreign table constraint without columns (#1608) --- src/ast/ddl.rs | 6 ++++-- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 849b583ed..c87960679 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -885,12 +885,14 @@ impl fmt::Display for TableConstraint { } => { write!( f, - "{}FOREIGN KEY ({}) REFERENCES {}({})", + "{}FOREIGN KEY ({}) REFERENCES {}", display_constraint_name(name), display_comma_separated(columns), foreign_table, - display_comma_separated(referred_columns), )?; + if !referred_columns.is_empty() { + write!(f, "({})", display_comma_separated(referred_columns))?; + } if let Some(action) = on_delete { write!(f, " ON DELETE {action}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c0aa0acba..ae44de1b5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6830,7 +6830,7 @@ impl<'a> Parser<'a> { let columns = self.parse_parenthesized_column_list(Mandatory, false)?; self.expect_keyword(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name(false)?; - let referred_columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let referred_columns = self.parse_parenthesized_column_list(Optional, false)?; let mut on_delete = None; let mut on_update = None; loop { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1bf9383af..d04066c70 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4167,6 +4167,7 @@ fn parse_alter_table_constraints() { check_one("UNIQUE (id)"); check_one("FOREIGN KEY (foo, bar) REFERENCES AnotherTable(foo, bar)"); check_one("CHECK (end_date > start_date OR end_date IS NULL)"); + check_one("CONSTRAINT fk FOREIGN KEY (lng) REFERENCES othertable4"); fn check_one(constraint_text: &str) { match alter_table_op(verified_stmt(&format!( From eae5629fb86f5e262063c0bc99ff628a5855168f Mon Sep 17 00:00:00 2001 From: yuyang <96557710+yuyang-ok@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:18:45 +0800 Subject: [PATCH 645/806] Support optional table for `ANALYZE` statement (#1599) --- src/ast/mod.rs | 8 +++++++- src/ast/spans.rs | 1 + src/parser/mod.rs | 3 ++- tests/sqlparser_common.rs | 6 ++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6e3f20472..39b974635 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2354,6 +2354,7 @@ pub enum Statement { cache_metadata: bool, noscan: bool, compute_statistics: bool, + has_table_keyword: bool, }, /// ```sql /// TRUNCATE @@ -3651,8 +3652,13 @@ impl fmt::Display for Statement { cache_metadata, noscan, compute_statistics, + has_table_keyword, } => { - write!(f, "ANALYZE TABLE {table_name}")?; + write!( + f, + "ANALYZE{}{table_name}", + if *has_table_keyword { " TABLE " } else { " " } + )?; if let Some(ref parts) = partitions { if !parts.is_empty() { write!(f, " PARTITION ({})", display_comma_separated(parts))?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index c2c7c14f0..6168587c5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -284,6 +284,7 @@ impl Spanned for Statement { cache_metadata: _, noscan: _, compute_statistics: _, + has_table_keyword: _, } => union_spans( core::iter::once(table_name.span()) .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ae44de1b5..570b2397d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -851,7 +851,7 @@ impl<'a> Parser<'a> { } pub fn parse_analyze(&mut self) -> Result { - self.expect_keyword(Keyword::TABLE)?; + let has_table_keyword = self.parse_keyword(Keyword::TABLE); let table_name = self.parse_object_name(false)?; let mut for_columns = false; let mut cache_metadata = false; @@ -896,6 +896,7 @@ impl<'a> Parser<'a> { } Ok(Statement::Analyze { + has_table_keyword, table_name, for_columns, columns, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d04066c70..f18daa52e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -562,6 +562,12 @@ fn parse_select_with_table_alias() { ); } +#[test] +fn parse_analyze() { + verified_stmt("ANALYZE TABLE test_table"); + verified_stmt("ANALYZE test_table"); +} + #[test] fn parse_invalid_table_name() { let ast = all_dialects().run_parser_method("db.public..customer", |parser| { From c973df35d69f156acda80fa60c34f9f15d7ff104 Mon Sep 17 00:00:00 2001 From: artorias1024 <82564604+artorias1024@users.noreply.github.com> Date: Fri, 20 Dec 2024 01:11:39 +0800 Subject: [PATCH 646/806] Support DOUBLE data types with precision for Mysql (#1611) --- src/ast/data_type.rs | 4 ++-- src/parser/mod.rs | 4 +++- tests/sqlparser_common.rs | 12 ++++++------ tests/sqlparser_mysql.rs | 10 ++++++++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index b53b8f0d2..02aa6cc9f 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -254,7 +254,7 @@ pub enum DataType { /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html Float8, /// Double - Double, + Double(ExactNumberInfo), /// Double PRECISION e.g. [standard], [postgresql] /// /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#approximate-numeric-type @@ -508,7 +508,7 @@ impl fmt::Display for DataType { DataType::Float4 => write!(f, "FLOAT4"), DataType::Float32 => write!(f, "Float32"), DataType::Float64 => write!(f, "FLOAT64"), - DataType::Double => write!(f, "DOUBLE"), + DataType::Double(info) => write!(f, "DOUBLE{info}"), DataType::Float8 => write!(f, "FLOAT8"), DataType::DoublePrecision => write!(f, "DOUBLE PRECISION"), DataType::Bool => write!(f, "BOOL"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 570b2397d..df4af538f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8115,7 +8115,9 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::PRECISION) { Ok(DataType::DoublePrecision) } else { - Ok(DataType::Double) + Ok(DataType::Double( + self.parse_exact_number_optional_precision_scale()?, + )) } } Keyword::TINYINT => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f18daa52e..507c9c77c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3009,7 +3009,7 @@ fn parse_create_table() { }, ColumnDef { name: "lat".into(), - data_type: DataType::Double, + data_type: DataType::Double(ExactNumberInfo::None), collation: None, options: vec![ColumnOptionDef { name: None, @@ -3018,7 +3018,7 @@ fn parse_create_table() { }, ColumnDef { name: "lng".into(), - data_type: DataType::Double, + data_type: DataType::Double(ExactNumberInfo::None), collation: None, options: vec![], }, @@ -3198,7 +3198,7 @@ fn parse_create_table_with_constraint_characteristics() { }, ColumnDef { name: "lat".into(), - data_type: DataType::Double, + data_type: DataType::Double(ExactNumberInfo::None), collation: None, options: vec![ColumnOptionDef { name: None, @@ -3207,7 +3207,7 @@ fn parse_create_table_with_constraint_characteristics() { }, ColumnDef { name: "lng".into(), - data_type: DataType::Double, + data_type: DataType::Double(ExactNumberInfo::None), collation: None, options: vec![], }, @@ -3838,7 +3838,7 @@ fn parse_create_external_table() { }, ColumnDef { name: "lat".into(), - data_type: DataType::Double, + data_type: DataType::Double(ExactNumberInfo::None), collation: None, options: vec![ColumnOptionDef { name: None, @@ -3847,7 +3847,7 @@ fn parse_create_external_table() { }, ColumnDef { name: "lng".into(), - data_type: DataType::Double, + data_type: DataType::Double(ExactNumberInfo::None), collation: None, options: vec![], }, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index bc7bf2f88..4a4e79611 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3022,3 +3022,13 @@ fn parse_longblob_type() { fn parse_begin_without_transaction() { mysql().verified_stmt("BEGIN"); } + +#[test] +fn parse_double_precision() { + mysql().verified_stmt("CREATE TABLE foo (bar DOUBLE)"); + mysql().verified_stmt("CREATE TABLE foo (bar DOUBLE(11,0))"); + mysql().one_statement_parses_to( + "CREATE TABLE foo (bar DOUBLE(11, 0))", + "CREATE TABLE foo (bar DOUBLE(11,0))", + ); +} From 84e82e6e2ebb95f573ed258865752217f7ae5a6f Mon Sep 17 00:00:00 2001 From: Dmitrii Blaginin Date: Thu, 19 Dec 2024 22:17:20 +0300 Subject: [PATCH 647/806] Add `#[recursive]` (#1522) Co-authored-by: Ifeanyi Ubah --- Cargo.toml | 5 ++- README.md | 2 +- derive/src/lib.rs | 3 ++ sqlparser_bench/benches/sqlparser_bench.rs | 40 ++++++++++++++++++++++ src/ast/mod.rs | 1 + src/ast/visitor.rs | 25 ++++++++++++++ src/parser/mod.rs | 6 ++++ tests/sqlparser_common.rs | 13 +++++++ 8 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 301a59c55..8ff0ceb55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,8 +37,9 @@ name = "sqlparser" path = "src/lib.rs" [features] -default = ["std"] +default = ["std", "recursive-protection"] std = [] +recursive-protection = ["std", "recursive"] # Enable JSON output in the `cli` example: json_example = ["serde_json", "serde"] visitor = ["sqlparser_derive"] @@ -46,6 +47,8 @@ visitor = ["sqlparser_derive"] [dependencies] bigdecimal = { version = "0.4.1", features = ["serde"], optional = true } log = "0.4" +recursive = { version = "0.1.1", optional = true} + serde = { version = "1.0", features = ["derive"], optional = true } # serde_json is only used in examples/cli, but we have to put it outside # of dev-dependencies because of diff --git a/README.md b/README.md index fd676d115..997aec585 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ The following optional [crate features](https://doc.rust-lang.org/cargo/referen * `serde`: Adds [Serde](https://serde.rs/) support by implementing `Serialize` and `Deserialize` for all AST nodes. * `visitor`: Adds a `Visitor` capable of recursively walking the AST tree. - +* `recursive-protection` (enabled by default), uses [recursive](https://docs.rs/recursive/latest/recursive/) for stack overflow protection. ## Syntax vs Semantics diff --git a/derive/src/lib.rs b/derive/src/lib.rs index b81623312..08c5c5db4 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -78,7 +78,10 @@ fn derive_visit(input: proc_macro::TokenStream, visit_type: &VisitType) -> proc_ let expanded = quote! { // The generated impl. + // Note that it uses [`recursive::recursive`] to protect from stack overflow. + // See tests in https://github.com/apache/datafusion-sqlparser-rs/pull/1522/ for more info. impl #impl_generics sqlparser::ast::#visit_trait for #name #ty_generics #where_clause { + #[cfg_attr(feature = "recursive-protection", recursive::recursive)] fn visit( &#modifier self, visitor: &mut V diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs index 32a6da1bc..74cac5c97 100644 --- a/sqlparser_bench/benches/sqlparser_bench.rs +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -42,6 +42,46 @@ fn basic_queries(c: &mut Criterion) { group.bench_function("sqlparser::with_select", |b| { b.iter(|| Parser::parse_sql(&dialect, with_query).unwrap()); }); + + let large_statement = { + let expressions = (0..1000) + .map(|n| format!("FN_{}(COL_{})", n, n)) + .collect::>() + .join(", "); + let tables = (0..1000) + .map(|n| format!("TABLE_{}", n)) + .collect::>() + .join(" JOIN "); + let where_condition = (0..1000) + .map(|n| format!("COL_{} = {}", n, n)) + .collect::>() + .join(" OR "); + let order_condition = (0..1000) + .map(|n| format!("COL_{} DESC", n)) + .collect::>() + .join(", "); + + format!( + "SELECT {} FROM {} WHERE {} ORDER BY {}", + expressions, tables, where_condition, order_condition + ) + }; + + group.bench_function("parse_large_statement", |b| { + b.iter(|| Parser::parse_sql(&dialect, criterion::black_box(large_statement.as_str()))); + }); + + let large_statement = Parser::parse_sql(&dialect, large_statement.as_str()) + .unwrap() + .pop() + .unwrap(); + + group.bench_function("format_large_statement", |b| { + b.iter(|| { + let formatted_query = large_statement.to_string(); + assert_eq!(formatted_query, large_statement); + }); + }); } criterion_group!(benches, basic_queries); diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 39b974635..3157a0609 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1291,6 +1291,7 @@ impl fmt::Display for CastFormat { } impl fmt::Display for Expr { + #[cfg_attr(feature = "recursive-protection", recursive::recursive)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Expr::Identifier(s) => write!(f, "{s}"), diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index f7562b66c..c824ad2f3 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -894,4 +894,29 @@ mod tests { assert_eq!(actual, expected) } } + + struct QuickVisitor; // [`TestVisitor`] is too slow to iterate over thousands of nodes + + impl Visitor for QuickVisitor { + type Break = (); + } + + #[test] + fn overflow() { + let cond = (0..1000) + .map(|n| format!("X = {}", n)) + .collect::>() + .join(" OR "); + let sql = format!("SELECT x where {0}", cond); + + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, sql.as_str()).tokenize().unwrap(); + let s = Parser::new(&dialect) + .with_tokens(tokens) + .parse_statement() + .unwrap(); + + let mut visitor = QuickVisitor {}; + s.visit(&mut visitor); + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index df4af538f..e809ffba5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -73,6 +73,9 @@ mod recursion { /// Note: Uses an [`std::rc::Rc`] and [`std::cell::Cell`] in order to satisfy the Rust /// borrow checker so the automatic [`DepthGuard`] decrement a /// reference to the counter. + /// + /// Note: when "recursive-protection" feature is enabled, this crate uses additional stack overflow protection + /// for some of its recursive methods. See [`recursive::recursive`] for more information. pub(crate) struct RecursionCounter { remaining_depth: Rc>, } @@ -326,6 +329,9 @@ impl<'a> Parser<'a> { /// # Ok(()) /// # } /// ``` + /// + /// Note: when "recursive-protection" feature is enabled, this crate uses additional stack overflow protection + // for some of its recursive methods. See [`recursive::recursive`] for more information. pub fn with_recursion_limit(mut self, recursion_limit: usize) -> Self { self.recursion_counter = RecursionCounter::new(recursion_limit); self diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 507c9c77c..e7e2e3bc2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12433,3 +12433,16 @@ fn test_table_sample() { dialects.verified_stmt("SELECT * FROM tbl AS t TABLESAMPLE SYSTEM (50)"); dialects.verified_stmt("SELECT * FROM tbl AS t TABLESAMPLE SYSTEM (50) REPEATABLE (10)"); } + +#[test] +fn overflow() { + let expr = std::iter::repeat("1") + .take(1000) + .collect::>() + .join(" + "); + let sql = format!("SELECT {}", expr); + + let mut statements = Parser::parse_sql(&GenericDialect {}, sql.as_str()).unwrap(); + let statement = statements.pop().unwrap(); + assert_eq!(statement.to_string(), sql); +} From cd898cb6a4e9d819f18649a4515bfb507678e64b Mon Sep 17 00:00:00 2001 From: Ayman Elkfrawy <120422207+ayman-sigma@users.noreply.github.com> Date: Sun, 22 Dec 2024 06:22:16 -0800 Subject: [PATCH 648/806] Support arbitrary composite access expressions (#1600) --- src/ast/mod.rs | 2 +- src/parser/mod.rs | 28 ++++--------- tests/sqlparser_common.rs | 86 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 20 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3157a0609..45dbba2a2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -640,7 +640,7 @@ pub enum Expr { /// The path to the data to extract. path: JsonPath, }, - /// CompositeAccess (postgres) eg: SELECT (information_schema._pg_expandarray(array['i','i'])).n + /// CompositeAccess eg: SELECT foo(bar).z, (information_schema._pg_expandarray(array['i','i'])).n CompositeAccess { expr: Box, key: Ident, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e809ffba5..5ee8ae213 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -969,6 +969,14 @@ impl<'a> Parser<'a> { let _guard = self.recursion_counter.try_decrease()?; debug!("parsing expr"); let mut expr = self.parse_prefix()?; + // Attempt to parse composite access. Example `SELECT f(x).a` + while self.consume_token(&Token::Period) { + expr = Expr::CompositeAccess { + expr: Box::new(expr), + key: self.parse_identifier(false)?, + } + } + debug!("prefix: {:?}", expr); loop { let next_precedence = self.get_next_precedence()?; @@ -1393,25 +1401,7 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; - let expr = self.try_parse_method(expr)?; - if !self.consume_token(&Token::Period) { - Ok(expr) - } else { - let tok = self.next_token(); - let key = match tok.token { - Token::Word(word) => word.to_ident(tok.span), - _ => { - return parser_err!( - format!("Expected identifier, found: {tok}"), - tok.span.start - ) - } - }; - Ok(Expr::CompositeAccess { - expr: Box::new(expr), - key, - }) - } + self.try_parse_method(expr) } Token::Placeholder(_) | Token::Colon | Token::AtSign => { self.prev_token(); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e7e2e3bc2..8cc161f1e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12341,6 +12341,92 @@ fn parse_create_table_with_bit_types() { } } +#[test] +fn parse_composite_access_expr() { + assert_eq!( + verified_expr("f(a).b"), + Expr::CompositeAccess { + expr: Box::new(Expr::Function(Function { + name: ObjectName(vec![Ident::new("f")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("a")) + ))], + clauses: vec![], + }), + null_treatment: None, + filter: None, + over: None, + within_group: vec![] + })), + key: Ident::new("b") + } + ); + + // Nested Composite Access + assert_eq!( + verified_expr("f(a).b.c"), + Expr::CompositeAccess { + expr: Box::new(Expr::CompositeAccess { + expr: Box::new(Expr::Function(Function { + name: ObjectName(vec![Ident::new("f")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("a")) + ))], + clauses: vec![], + }), + null_treatment: None, + filter: None, + over: None, + within_group: vec![] + })), + key: Ident::new("b") + }), + key: Ident::new("c") + } + ); + + // Composite Access in Select and Where Clauses + let stmt = verified_only_select("SELECT f(a).b FROM t WHERE f(a).b IS NOT NULL"); + let expr = Expr::CompositeAccess { + expr: Box::new(Expr::Function(Function { + name: ObjectName(vec![Ident::new("f")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("a")), + ))], + clauses: vec![], + }), + null_treatment: None, + filter: None, + over: None, + within_group: vec![], + })), + key: Ident::new("b"), + }; + + assert_eq!(stmt.projection[0], SelectItem::UnnamedExpr(expr.clone())); + assert_eq!(stmt.selection.unwrap(), Expr::IsNotNull(Box::new(expr))); + + // Composite Access with quoted identifier + verified_only_select("SELECT f(a).\"an id\""); + + // Composite Access in struct literal + all_dialects_where(|d| d.supports_struct_literal()).verified_stmt( + "SELECT * FROM t WHERE STRUCT(STRUCT(1 AS a, NULL AS b) AS c, NULL AS d).c.a IS NOT NULL", + ); +} + #[test] fn parse_create_table_with_enum_types() { let sql = "CREATE TABLE t0 (foo ENUM8('a' = 1, 'b' = 2), bar ENUM16('a' = 1, 'b' = 2), baz ENUM('a', 'b'))"; From 0647a4aa829954397bd72369865f44bbab19ba2b Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Sun, 22 Dec 2024 22:28:44 +0800 Subject: [PATCH 649/806] Consolidate `MapAccess`, and `Subscript` into `CompoundExpr` to handle the complex field access chain (#1551) --- src/ast/mod.rs | 106 ++++++------- src/ast/spans.rs | 44 +++--- src/dialect/snowflake.rs | 4 + src/parser/mod.rs | 284 +++++++++++++++++++++++----------- tests/sqlparser_bigquery.rs | 58 ++++--- tests/sqlparser_clickhouse.rs | 24 ++- tests/sqlparser_common.rs | 76 +++++++-- tests/sqlparser_duckdb.rs | 8 +- tests/sqlparser_postgres.rs | 138 +++++++++-------- 9 files changed, 455 insertions(+), 287 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 45dbba2a2..9fb2bb9c9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -459,40 +459,6 @@ pub enum CastFormat { ValueAtTimeZone(Value, Value), } -/// Represents the syntax/style used in a map access. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum MapAccessSyntax { - /// Access using bracket notation. `mymap[mykey]` - Bracket, - /// Access using period notation. `mymap.mykey` - Period, -} - -/// Expression used to access a value in a nested structure. -/// -/// Example: `SAFE_OFFSET(0)` in -/// ```sql -/// SELECT mymap[SAFE_OFFSET(0)]; -/// ``` -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct MapAccessKey { - pub key: Expr, - pub syntax: MapAccessSyntax, -} - -impl fmt::Display for MapAccessKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.syntax { - MapAccessSyntax::Bracket => write!(f, "[{}]", self.key), - MapAccessSyntax::Period => write!(f, ".{}", self.key), - } - } -} - /// An element of a JSON path. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -629,6 +595,28 @@ pub enum Expr { Identifier(Ident), /// Multi-part identifier, e.g. `table_alias.column` or `schema.table.col` CompoundIdentifier(Vec), + /// Multi-part expression access. + /// + /// This structure represents an access chain in structured / nested types + /// such as maps, arrays, and lists: + /// - Array + /// - A 1-dim array `a[1]` will be represented like: + /// `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)]` + /// - Map or Struct (Bracket-style) + /// - A map `a['field1']` will be represented like: + /// `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')]` + /// - 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')]` + /// - If a struct access likes `a.field1.field2`, it will be represented by CompoundIdentifier([a, field1, field2]) + CompoundFieldAccess { + root: Box, + access_chain: Vec, + }, /// Access data nested in a value containing semi-structured data, such as /// the `VARIANT` type on Snowflake. for example `src:customer[0].name`. /// @@ -882,14 +870,6 @@ pub enum Expr { data_type: DataType, value: String, }, - /// Access a map-like object by field (e.g. `column['field']` or `column[4]` - /// Note that depending on the dialect, struct like accesses may be - /// parsed as [`Subscript`](Self::Subscript) or [`MapAccess`](Self::MapAccess) - /// - MapAccess { - column: Box, - keys: Vec, - }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), /// Arbitrary expr method call @@ -978,11 +958,6 @@ pub enum Expr { /// ``` /// [1]: https://duckdb.org/docs/sql/data_types/map#creating-maps Map(Map), - /// An access of nested data using subscript syntax, for example `array[2]`. - Subscript { - expr: Box, - subscript: Box, - }, /// An array expression e.g. `ARRAY[1, 2]` Array(Array), /// An interval expression e.g. `INTERVAL '1' YEAR` @@ -1099,6 +1074,27 @@ impl fmt::Display for Subscript { } } +/// An element of a [`Expr::CompoundFieldAccess`]. +/// It can be an expression or a subscript. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AccessExpr { + /// Accesses a field using dot notation, e.g. `foo.bar.baz`. + Dot(Expr), + /// Accesses a field or array element using bracket notation, e.g. `foo['bar']`. + Subscript(Subscript), +} + +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), + } + } +} + /// A lambda function. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1295,12 +1291,16 @@ impl fmt::Display for Expr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Expr::Identifier(s) => write!(f, "{s}"), - Expr::MapAccess { column, keys } => { - write!(f, "{column}{}", display_separated(keys, "")) - } Expr::Wildcard(_) => f.write_str("*"), Expr::QualifiedWildcard(prefix, _) => write!(f, "{}.*", prefix), Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")), + Expr::CompoundFieldAccess { root, access_chain } => { + write!(f, "{}", root)?; + for field in access_chain { + write!(f, "{}", field)?; + } + Ok(()) + } Expr::IsTrue(ast) => write!(f, "{ast} IS TRUE"), Expr::IsNotTrue(ast) => write!(f, "{ast} IS NOT TRUE"), Expr::IsFalse(ast) => write!(f, "{ast} IS FALSE"), @@ -1720,12 +1720,6 @@ impl fmt::Display for Expr { Expr::Map(map) => { write!(f, "{map}") } - Expr::Subscript { - expr, - subscript: key, - } => { - write!(f, "{expr}[{key}]") - } Expr::Array(set) => { write!(f, "{set}") } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 6168587c5..9ba3bdd9b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -20,20 +20,20 @@ use core::iter; use crate::tokenizer::Span; use super::{ - dcl::SecondaryRoles, 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, MatchRecognizePattern, Measure, NamedWindowDefinition, - ObjectName, Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, Partition, - PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem, - ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, - Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, - TableFactor, TableOptionsClustered, TableWithJoins, Use, Value, Values, ViewColumnDef, - WildcardAdditionalOptions, With, WithFill, + dcl::SecondaryRoles, 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, MatchRecognizePattern, + Measure, NamedWindowDefinition, ObjectName, Offset, OnConflict, OnConflictAction, OnInsert, + OrderBy, OrderByExpr, Partition, PivotValueSource, ProjectionSelect, Query, ReferentialAction, + RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, + SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, + TableConstraint, TableFactor, TableOptionsClustered, TableWithJoins, Use, Value, Values, + ViewColumnDef, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -1262,6 +1262,9 @@ impl Spanned for Expr { Expr::Identifier(ident) => ident.span, Expr::CompoundIdentifier(vec) => union_spans(vec.iter().map(|i| i.span)), Expr::CompositeAccess { expr, key } => expr.span().union(&key.span), + Expr::CompoundFieldAccess { root, access_chain } => { + union_spans(iter::once(root.span()).chain(access_chain.iter().map(|i| i.span()))) + } Expr::IsFalse(expr) => expr.span(), Expr::IsNotFalse(expr) => expr.span(), Expr::IsTrue(expr) => expr.span(), @@ -1336,9 +1339,6 @@ impl Spanned for Expr { Expr::Nested(expr) => expr.span(), Expr::Value(value) => value.span(), Expr::TypedString { .. } => Span::empty(), - Expr::MapAccess { column, keys } => column - .span() - .union(&union_spans(keys.iter().map(|i| i.key.span()))), Expr::Function(function) => function.span(), Expr::GroupingSets(vec) => { union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span()))) @@ -1434,7 +1434,6 @@ impl Spanned for Expr { Expr::Named { .. } => Span::empty(), Expr::Dictionary(_) => Span::empty(), Expr::Map(_) => Span::empty(), - Expr::Subscript { expr, subscript } => expr.span().union(&subscript.span()), Expr::Interval(interval) => interval.value.span(), Expr::Wildcard(token) => token.0.span, Expr::QualifiedWildcard(object_name, token) => union_spans( @@ -1473,6 +1472,15 @@ impl Spanned for Subscript { } } +impl Spanned for AccessExpr { + fn span(&self) -> Span { + match self { + AccessExpr::Dot(ident) => ident.span(), + AccessExpr::Subscript(subscript) => subscript.span(), + } + } +} + impl Spanned for ObjectName { fn span(&self) -> Span { let ObjectName(segments) = self; diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 50e383db2..045e5062d 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -234,6 +234,10 @@ impl Dialect for SnowflakeDialect { RESERVED_FOR_IDENTIFIER.contains(&kw) } } + + fn supports_partiql(&self) -> bool { + true + } } /// Parse snowflake create table statement. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5ee8ae213..af4b7b450 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1161,53 +1161,39 @@ impl<'a> Parser<'a> { w_span: Span, ) -> Result { match self.peek_token().token { - Token::LParen | Token::Period => { - let mut id_parts: Vec = vec![w.to_ident(w_span)]; - let mut ending_wildcard: Option = None; - while self.consume_token(&Token::Period) { - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => id_parts.push(w.to_ident(next_token.span)), - Token::Mul => { - // Postgres explicitly allows funcnm(tablenm.*) and the - // function array_agg traverses this control flow - if dialect_of!(self is PostgreSqlDialect) { - ending_wildcard = Some(next_token); - break; - } else { - return self.expected("an identifier after '.'", next_token); - } - } - Token::SingleQuotedString(s) => id_parts.push(Ident::with_quote('\'', s)), - _ => { - return self.expected("an identifier or a '*' after '.'", next_token); - } - } - } - - if let Some(wildcard_token) = ending_wildcard { - Ok(Expr::QualifiedWildcard( - ObjectName(id_parts), - AttachedToken(wildcard_token), - )) - } else if self.consume_token(&Token::LParen) { - if dialect_of!(self is SnowflakeDialect | MsSqlDialect) - && self.consume_tokens(&[Token::Plus, Token::RParen]) - { - Ok(Expr::OuterJoin(Box::new( - match <[Ident; 1]>::try_from(id_parts) { - Ok([ident]) => Expr::Identifier(ident), - Err(parts) => Expr::CompoundIdentifier(parts), - }, - ))) - } else { - self.prev_token(); - self.parse_function(ObjectName(id_parts)) - } + Token::Period => { + self.parse_compound_field_access(Expr::Identifier(w.to_ident(w_span)), vec![]) + } + Token::LParen => { + let id_parts = vec![w.to_ident(w_span)]; + if let Some(expr) = self.parse_outer_join_expr(&id_parts) { + Ok(expr) } else { - Ok(Expr::CompoundIdentifier(id_parts)) + let mut expr = self.parse_function(ObjectName(id_parts))?; + // consume all period if it's a method chain + expr = self.try_parse_method(expr)?; + let fields = vec![]; + self.parse_compound_field_access(expr, fields) } } + Token::LBracket if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) => + { + let ident = Expr::Identifier(w.to_ident(w_span)); + let mut fields = vec![]; + self.parse_multi_dim_subscript(&mut fields)?; + self.parse_compound_field_access(ident, fields) + } + // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html + Token::SingleQuotedString(_) + | Token::DoubleQuotedString(_) + | Token::HexStringLiteral(_) + if w.value.starts_with('_') => + { + Ok(Expr::IntroducedString { + introducer: w.value.clone(), + value: self.parse_introduced_string_value()?, + }) + } // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html Token::SingleQuotedString(_) | Token::DoubleQuotedString(_) @@ -1426,6 +1412,144 @@ impl<'a> Parser<'a> { } } + /// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`. + /// If all the fields are `Expr::Identifier`s, return an [Expr::CompoundIdentifier] instead. + /// If only the root exists, return the root. + /// If self supports [Dialect::supports_partiql], it will fall back when occurs [Token::LBracket] for JsonAccess parsing. + pub fn parse_compound_field_access( + &mut self, + root: Expr, + mut chain: Vec, + ) -> Result { + let mut ending_wildcard: Option = None; + let mut ending_lbracket = false; + while self.consume_token(&Token::Period) { + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) => { + let expr = Expr::Identifier(w.to_ident(next_token.span)); + chain.push(AccessExpr::Dot(expr)); + if self.peek_token().token == Token::LBracket { + if self.dialect.supports_partiql() { + self.next_token(); + ending_lbracket = true; + break; + } else { + self.parse_multi_dim_subscript(&mut chain)? + } + } + } + Token::Mul => { + // Postgres explicitly allows funcnm(tablenm.*) and the + // function array_agg traverses this control flow + if dialect_of!(self is PostgreSqlDialect) { + ending_wildcard = Some(next_token); + break; + } else { + return self.expected("an identifier after '.'", next_token); + } + } + Token::SingleQuotedString(s) => { + let expr = Expr::Identifier(Ident::with_quote('\'', s)); + chain.push(AccessExpr::Dot(expr)); + } + _ => { + return self.expected("an identifier or a '*' after '.'", next_token); + } + } + } + + // if dialect supports partiql, we need to go back one Token::LBracket for the JsonAccess parsing + if self.dialect.supports_partiql() && ending_lbracket { + self.prev_token(); + } + + if let Some(wildcard_token) = ending_wildcard { + if !Self::is_all_ident(&root, &chain) { + return self.expected("an identifier or a '*' after '.'", self.peek_token()); + }; + Ok(Expr::QualifiedWildcard( + ObjectName(Self::exprs_to_idents(root, chain)?), + AttachedToken(wildcard_token), + )) + } else if self.peek_token().token == Token::LParen { + if !Self::is_all_ident(&root, &chain) { + // consume LParen + self.next_token(); + return self.expected("an identifier or a '*' after '.'", self.peek_token()); + }; + let id_parts = Self::exprs_to_idents(root, chain)?; + if let Some(expr) = self.parse_outer_join_expr(&id_parts) { + Ok(expr) + } else { + self.parse_function(ObjectName(id_parts)) + } + } else { + if Self::is_all_ident(&root, &chain) { + return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents( + root, chain, + )?)); + } + if chain.is_empty() { + return Ok(root); + } + Ok(Expr::CompoundFieldAccess { + root: Box::new(root), + access_chain: chain.clone(), + }) + } + } + + /// 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(_)) { + return false; + } + fields + .iter() + .all(|x| matches!(x, AccessExpr::Dot(Expr::Identifier(_)))) + } + + /// Convert a root and a list of fields to a list of identifiers. + fn exprs_to_idents(root: Expr, fields: Vec) -> Result, ParserError> { + let mut idents = vec![]; + if let Expr::Identifier(root) = root { + idents.push(root); + for x in fields { + if let AccessExpr::Dot(Expr::Identifier(ident)) = x { + idents.push(ident); + } else { + return parser_err!( + format!("Expected identifier, found: {}", x), + x.span().start + ); + } + } + Ok(idents) + } else { + parser_err!( + format!("Expected identifier, found: {}", root), + root.span().start + ) + } + } + + /// Try to parse OuterJoin expression `(+)` + fn parse_outer_join_expr(&mut self, id_parts: &[Ident]) -> Option { + if dialect_of!(self is SnowflakeDialect | MsSqlDialect) + && self.consume_tokens(&[Token::LParen, Token::Plus, Token::RParen]) + { + Some(Expr::OuterJoin(Box::new( + match <[Ident; 1]>::try_from(id_parts.to_vec()) { + Ok([ident]) => Expr::Identifier(ident), + Err(parts) => Expr::CompoundIdentifier(parts), + }, + ))) + } else { + None + } + } + pub fn parse_utility_options(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let options = self.parse_comma_separated(Self::parse_utility_option)?; @@ -3042,13 +3166,18 @@ impl<'a> Parser<'a> { expr: Box::new(expr), }) } else if Token::LBracket == tok { - if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) { - self.parse_subscript(expr) - } else if dialect_of!(self is SnowflakeDialect) || self.dialect.supports_partiql() { + if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) + { + let mut chain = vec![]; + // back to LBracket + self.prev_token(); + self.parse_multi_dim_subscript(&mut chain)?; + self.parse_compound_field_access(expr, chain) + } else if self.dialect.supports_partiql() { self.prev_token(); self.parse_json_access(expr) } else { - self.parse_map_access(expr) + parser_err!("Array subscripting is not supported", tok.span.start) } } else if dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == tok { self.prev_token(); @@ -3144,15 +3273,24 @@ impl<'a> Parser<'a> { }) } + /// Parse a multi-dimension array accessing like `[1:3][1][1]` + pub fn parse_multi_dim_subscript( + &mut self, + chain: &mut Vec, + ) -> Result<(), ParserError> { + while self.consume_token(&Token::LBracket) { + self.parse_subscript(chain)?; + } + Ok(()) + } + /// Parses an array subscript like `[1:3]` /// /// Parser is right after `[` - pub fn parse_subscript(&mut self, expr: Expr) -> Result { + fn parse_subscript(&mut self, chain: &mut Vec) -> Result<(), ParserError> { let subscript = self.parse_subscript_inner()?; - Ok(Expr::Subscript { - expr: Box::new(expr), - subscript: Box::new(subscript), - }) + chain.push(AccessExpr::Subscript(subscript)); + Ok(()) } fn parse_json_path_object_key(&mut self) -> Result { @@ -3214,46 +3352,6 @@ impl<'a> Parser<'a> { Ok(JsonPath { path }) } - pub fn parse_map_access(&mut self, expr: Expr) -> Result { - let key = self.parse_expr()?; - self.expect_token(&Token::RBracket)?; - - let mut keys = vec![MapAccessKey { - key, - syntax: MapAccessSyntax::Bracket, - }]; - loop { - let key = match self.peek_token().token { - Token::LBracket => { - self.next_token(); // consume `[` - let key = self.parse_expr()?; - self.expect_token(&Token::RBracket)?; - MapAccessKey { - key, - syntax: MapAccessSyntax::Bracket, - } - } - // Access on BigQuery nested and repeated expressions can - // mix notations in the same expression. - // https://cloud.google.com/bigquery/docs/nested-repeated#query_nested_and_repeated_columns - Token::Period if dialect_of!(self is BigQueryDialect) => { - self.next_token(); // consume `.` - MapAccessKey { - key: self.parse_expr()?, - syntax: MapAccessSyntax::Period, - } - } - _ => break, - }; - keys.push(key); - } - - Ok(Expr::MapAccess { - column: Box::new(expr), - keys, - }) - } - /// Parses the parens following the `[ NOT ] IN` operator. pub fn parse_in(&mut self, expr: Expr, negated: bool) -> Result { // BigQuery allows `IN UNNEST(array_expression)` diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index be383b476..9dfabc014 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -23,7 +23,7 @@ use std::ops::Deref; use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; use sqlparser::parser::{ParserError, ParserOptions}; -use sqlparser::tokenizer::Span; +use sqlparser::tokenizer::{Location, Span}; use test_utils::*; #[test] @@ -1965,27 +1965,47 @@ fn parse_map_access_expr() { let sql = "users[-1][safe_offset(2)].a.b"; let expr = bigquery().verified_expr(sql); - fn map_access_key(key: Expr, syntax: MapAccessSyntax) -> MapAccessKey { - MapAccessKey { key, syntax } - } - let expected = Expr::MapAccess { - column: Expr::Identifier(Ident::new("users")).into(), - keys: vec![ - map_access_key( - Expr::UnaryOp { + let expected = Expr::CompoundFieldAccess { + root: Box::new(Expr::Identifier(Ident::with_span( + Span::new(Location::of(1, 1), Location::of(1, 6)), + "users", + ))), + access_chain: vec![ + AccessExpr::Subscript(Subscript::Index { + index: Expr::UnaryOp { op: UnaryOperator::Minus, expr: Expr::Value(number("1")).into(), }, - MapAccessSyntax::Bracket, - ), - map_access_key( - call("safe_offset", [Expr::Value(number("2"))]), - MapAccessSyntax::Bracket, - ), - map_access_key( - Expr::CompoundIdentifier(vec![Ident::new("a"), Ident::new("b")]), - MapAccessSyntax::Period, - ), + }), + AccessExpr::Subscript(Subscript::Index { + index: Expr::Function(Function { + name: ObjectName(vec![Ident::with_span( + Span::new(Location::of(1, 11), Location::of(1, 22)), + "safe_offset", + )]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + number("2"), + )))], + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + uses_odbc_syntax: false, + }), + }), + AccessExpr::Dot(Expr::Identifier(Ident::with_span( + Span::new(Location::of(1, 24), Location::of(1, 25)), + "a", + ))), + AccessExpr::Dot(Expr::Identifier(Ident::with_span( + Span::new(Location::of(1, 26), Location::of(1, 27)), + "b", + ))), ], }; assert_eq!(expr, expected); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index d60506d90..2f1b043b6 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -25,7 +25,7 @@ use helpers::attached_token::AttachedToken; use sqlparser::tokenizer::Span; use test_utils::*; -use sqlparser::ast::Expr::{BinaryOp, Identifier, MapAccess}; +use sqlparser::ast::Expr::{BinaryOp, Identifier}; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Table; use sqlparser::ast::Value::Number; @@ -44,22 +44,21 @@ fn parse_map_access_expr() { select_token: AttachedToken::empty(), top: None, top_before_distinct: false, - projection: vec![UnnamedExpr(MapAccess { - column: Box::new(Identifier(Ident { + projection: vec![UnnamedExpr(Expr::CompoundFieldAccess { + root: Box::new(Identifier(Ident { value: "string_values".to_string(), quote_style: None, span: Span::empty(), })), - keys: vec![MapAccessKey { - key: call( + access_chain: vec![AccessExpr::Subscript(Subscript::Index { + index: call( "indexOf", [ Expr::Identifier(Ident::new("string_names")), Expr::Value(Value::SingleQuotedString("endpoint".to_string())) ] ), - syntax: MapAccessSyntax::Bracket - }], + })], })], into: None, from: vec![TableWithJoins { @@ -76,18 +75,17 @@ fn parse_map_access_expr() { }), op: BinaryOperator::And, right: Box::new(BinaryOp { - left: Box::new(MapAccess { - column: Box::new(Identifier(Ident::new("string_value"))), - keys: vec![MapAccessKey { - key: call( + left: Box::new(Expr::CompoundFieldAccess { + root: Box::new(Identifier(Ident::new("string_value"))), + access_chain: vec![AccessExpr::Subscript(Subscript::Index { + index: call( "indexOf", [ Expr::Identifier(Ident::new("string_name")), Expr::Value(Value::SingleQuotedString("app".to_string())) ] ), - syntax: MapAccessSyntax::Bracket - }], + })], }), op: BinaryOperator::NotEq, right: Box::new(Expr::Value(Value::SingleQuotedString("foo".to_string()))), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8cc161f1e..c294eab0a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -37,8 +37,8 @@ use sqlparser::dialect::{ }; use sqlparser::keywords::{Keyword, ALL_KEYWORDS}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; -use sqlparser::tokenizer::Span; 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, @@ -2939,6 +2939,31 @@ fn parse_window_function_null_treatment_arg() { ); } +#[test] +fn test_compound_expr() { + let supported_dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(DuckDbDialect {}), + Box::new(BigQueryDialect {}), + ]); + let sqls = [ + "SELECT abc[1].f1 FROM t", + "SELECT abc[1].f1.f2 FROM t", + "SELECT f1.abc[1] FROM t", + "SELECT f1.f2.abc[1] FROM t", + "SELECT f1.abc[1].f2 FROM t", + "SELECT named_struct('a', 1, 'b', 2).a", + "SELECT named_struct('a', 1, 'b', 2).a", + "SELECT make_array(1, 2, 3)[1]", + "SELECT make_array(named_struct('a', 1))[1].a", + "SELECT abc[1][-1].a.b FROM t", + "SELECT abc[1][-1].a.b[1] FROM t", + ]; + for sql in sqls { + supported_dialects.verified_stmt(sql); + } +} + #[test] fn parse_negative_value() { let sql1 = "SELECT -1"; @@ -10174,20 +10199,39 @@ fn parse_map_access_expr() { Box::new(ClickHouseDialect {}), ]); let expr = dialects.verified_expr(sql); - let expected = Expr::MapAccess { - column: Expr::Identifier(Ident::new("users")).into(), - keys: vec![ - MapAccessKey { - key: Expr::UnaryOp { + let expected = Expr::CompoundFieldAccess { + root: Box::new(Expr::Identifier(Ident::with_span( + Span::new(Location::of(1, 1), Location::of(1, 6)), + "users", + ))), + access_chain: vec![ + AccessExpr::Subscript(Subscript::Index { + index: Expr::UnaryOp { op: UnaryOperator::Minus, expr: Expr::Value(number("1")).into(), }, - syntax: MapAccessSyntax::Bracket, - }, - MapAccessKey { - key: call("safe_offset", [Expr::Value(number("2"))]), - syntax: MapAccessSyntax::Bracket, - }, + }), + AccessExpr::Subscript(Subscript::Index { + index: Expr::Function(Function { + name: ObjectName(vec![Ident::with_span( + Span::new(Location::of(1, 11), Location::of(1, 22)), + "safe_offset", + )]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + number("2"), + )))], + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + uses_odbc_syntax: false, + }), + }), ], }; assert_eq!(expr, expected); @@ -10977,8 +11021,8 @@ fn test_map_syntax() { check( "MAP {'a': 10, 'b': 20}['a']", - Expr::Subscript { - expr: Box::new(Expr::Map(Map { + Expr::CompoundFieldAccess { + root: Box::new(Expr::Map(Map { entries: vec![ MapEntry { key: Box::new(Expr::Value(Value::SingleQuotedString("a".to_owned()))), @@ -10990,9 +11034,9 @@ fn test_map_syntax() { }, ], })), - subscript: Box::new(Subscript::Index { + access_chain: vec![AccessExpr::Subscript(Subscript::Index { index: Expr::Value(Value::SingleQuotedString("a".to_owned())), - }), + })], }, ); diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index d441cd195..db4ffb6f6 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -630,8 +630,8 @@ fn test_array_index() { _ => panic!("Expected an expression with alias"), }; assert_eq!( - &Expr::Subscript { - expr: Box::new(Expr::Array(Array { + &Expr::CompoundFieldAccess { + root: Box::new(Expr::Array(Array { elem: vec![ Expr::Value(Value::SingleQuotedString("a".to_owned())), Expr::Value(Value::SingleQuotedString("b".to_owned())), @@ -639,9 +639,9 @@ fn test_array_index() { ], named: false })), - subscript: Box::new(Subscript::Index { + access_chain: vec![AccessExpr::Subscript(Subscript::Index { index: Expr::Value(number("3")) - }) + })] }, expr ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index aaf4e65db..557e70bff 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2095,11 +2095,11 @@ fn parse_array_index_expr() { let sql = "SELECT foo[0] FROM foos"; let select = pg_and_generic().verified_only_select(sql); assert_eq!( - &Expr::Subscript { - expr: Box::new(Expr::Identifier(Ident::new("foo"))), - subscript: Box::new(Subscript::Index { + &Expr::CompoundFieldAccess { + root: Box::new(Expr::Identifier(Ident::new("foo"))), + access_chain: vec![AccessExpr::Subscript(Subscript::Index { index: num[0].clone() - }), + })], }, expr_from_projection(only(&select.projection)), ); @@ -2107,16 +2107,16 @@ fn parse_array_index_expr() { let sql = "SELECT foo[0][0] FROM foos"; let select = pg_and_generic().verified_only_select(sql); assert_eq!( - &Expr::Subscript { - expr: Box::new(Expr::Subscript { - expr: Box::new(Expr::Identifier(Ident::new("foo"))), - subscript: Box::new(Subscript::Index { + &Expr::CompoundFieldAccess { + root: Box::new(Expr::Identifier(Ident::new("foo"))), + access_chain: vec![ + AccessExpr::Subscript(Subscript::Index { index: num[0].clone() }), - }), - subscript: Box::new(Subscript::Index { - index: num[0].clone() - }), + AccessExpr::Subscript(Subscript::Index { + index: num[0].clone() + }) + ], }, expr_from_projection(only(&select.projection)), ); @@ -2124,29 +2124,27 @@ fn parse_array_index_expr() { let sql = r#"SELECT bar[0]["baz"]["fooz"] FROM foos"#; let select = pg_and_generic().verified_only_select(sql); assert_eq!( - &Expr::Subscript { - expr: Box::new(Expr::Subscript { - expr: Box::new(Expr::Subscript { - expr: Box::new(Expr::Identifier(Ident::new("bar"))), - subscript: Box::new(Subscript::Index { - index: num[0].clone() - }) + &Expr::CompoundFieldAccess { + root: Box::new(Expr::Identifier(Ident::new("bar"))), + access_chain: vec![ + AccessExpr::Subscript(Subscript::Index { + index: num[0].clone() }), - subscript: Box::new(Subscript::Index { + AccessExpr::Subscript(Subscript::Index { index: Expr::Identifier(Ident { value: "baz".to_string(), quote_style: Some('"'), span: Span::empty(), }) - }) - }), - subscript: Box::new(Subscript::Index { - index: Expr::Identifier(Ident { - value: "fooz".to_string(), - quote_style: Some('"'), - span: Span::empty(), - }) - }) + }), + AccessExpr::Subscript(Subscript::Index { + index: Expr::Identifier(Ident { + value: "fooz".to_string(), + quote_style: Some('"'), + span: Span::empty(), + }) + }), + ], }, expr_from_projection(only(&select.projection)), ); @@ -2154,33 +2152,33 @@ fn parse_array_index_expr() { let sql = "SELECT (CAST(ARRAY[ARRAY[2, 3]] AS INT[][]))[1][2]"; let select = pg_and_generic().verified_only_select(sql); assert_eq!( - &Expr::Subscript { - expr: Box::new(Expr::Subscript { - expr: Box::new(Expr::Nested(Box::new(Expr::Cast { - kind: CastKind::Cast, - expr: Box::new(Expr::Array(Array { - elem: vec![Expr::Array(Array { - elem: vec![num[2].clone(), num[3].clone(),], - named: true, - })], + &Expr::CompoundFieldAccess { + root: Box::new(Expr::Nested(Box::new(Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::Array(Array { + elem: vec![Expr::Array(Array { + elem: vec![num[2].clone(), num[3].clone(),], named: true, - })), - data_type: DataType::Array(ArrayElemTypeDef::SquareBracket( - Box::new(DataType::Array(ArrayElemTypeDef::SquareBracket( - Box::new(DataType::Int(None)), - None - ))), + })], + named: true, + })), + data_type: DataType::Array(ArrayElemTypeDef::SquareBracket( + Box::new(DataType::Array(ArrayElemTypeDef::SquareBracket( + Box::new(DataType::Int(None)), None - )), - format: None, - }))), - subscript: Box::new(Subscript::Index { + ))), + None + )), + format: None, + }))), + access_chain: vec![ + AccessExpr::Subscript(Subscript::Index { index: num[1].clone() }), - }), - subscript: Box::new(Subscript::Index { - index: num[2].clone() - }), + AccessExpr::Subscript(Subscript::Index { + index: num[2].clone() + }), + ], }, expr_from_projection(only(&select.projection)), ); @@ -2269,9 +2267,13 @@ fn parse_array_subscript() { ), ]; for (sql, expect) in tests { - let Expr::Subscript { subscript, .. } = pg_and_generic().verified_expr(sql) else { + let Expr::CompoundFieldAccess { access_chain, .. } = pg_and_generic().verified_expr(sql) + else { panic!("expected subscript expr"); }; + let Some(AccessExpr::Subscript(subscript)) = access_chain.last() else { + panic!("expected subscript"); + }; assert_eq!(expect, *subscript); } @@ -2282,25 +2284,25 @@ fn parse_array_subscript() { fn parse_array_multi_subscript() { let expr = pg_and_generic().verified_expr("make_array(1, 2, 3)[1:2][2]"); assert_eq!( - Expr::Subscript { - expr: Box::new(Expr::Subscript { - expr: Box::new(call( - "make_array", - vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")) - ] - )), - subscript: Box::new(Subscript::Slice { + Expr::CompoundFieldAccess { + root: Box::new(call( + "make_array", + vec![ + Expr::Value(number("1")), + Expr::Value(number("2")), + Expr::Value(number("3")) + ] + )), + access_chain: vec![ + AccessExpr::Subscript(Subscript::Slice { lower_bound: Some(Expr::Value(number("1"))), upper_bound: Some(Expr::Value(number("2"))), stride: None, }), - }), - subscript: Box::new(Subscript::Index { - index: Expr::Value(number("2")), - }), + AccessExpr::Subscript(Subscript::Index { + index: Expr::Value(number("2")), + }), + ], }, expr, ); From 27822e254b65525fb934c3f04d15389acf06a47e Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 23 Dec 2024 02:19:43 +1000 Subject: [PATCH 650/806] Handle empty projection in Postgres SELECT statements (#1613) --- src/ast/query.rs | 4 +++- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 10 ++++++++++ src/dialect/postgresql.rs | 10 ++++++++++ src/parser/mod.rs | 7 ++++++- tests/sqlparser_common.rs | 6 ++++++ 6 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 948febd26..69b7ea1c1 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -350,7 +350,9 @@ impl fmt::Display for Select { } } - write!(f, " {}", display_comma_separated(&self.projection))?; + if !self.projection.is_empty() { + write!(f, " {}", display_comma_separated(&self.projection))?; + } if let Some(ref into) = self.into { write!(f, " {into}")?; diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 61e5070fb..f852152a0 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -127,4 +127,8 @@ impl Dialect for GenericDialect { fn supports_struct_literal(&self) -> bool { true } + + fn supports_empty_projections(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c32b763a4..7a71d662f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -410,6 +410,16 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports empty projections in SELECT statements + /// + /// Example + /// ```sql + /// SELECT from table_name + /// ``` + fn supports_empty_projections(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index dcdcc88c1..80c14c8da 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -231,6 +231,16 @@ impl Dialect for PostgreSqlDialect { fn supports_named_fn_args_with_expr_name(&self) -> bool { true } + + /// Return true if the dialect supports empty projections in SELECT statements + /// + /// Example + /// ```sql + /// SELECT from table_name + /// ``` + fn supports_empty_projections(&self) -> bool { + true + } } pub fn parse_create(parser: &mut Parser) -> Option> { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index af4b7b450..cc0a57e4d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9692,7 +9692,12 @@ impl<'a> Parser<'a> { top = Some(self.parse_top()?); } - let projection = self.parse_projection()?; + let projection = + if self.dialect.supports_empty_projections() && self.peek_keyword(Keyword::FROM) { + vec![] + } else { + self.parse_projection()? + }; let into = if self.parse_keyword(Keyword::INTO) { let temporary = self diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c294eab0a..b8ea7586a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12576,3 +12576,9 @@ fn overflow() { let statement = statements.pop().unwrap(); assert_eq!(statement.to_string(), sql); } + +#[test] +fn parse_select_without_projection() { + let dialects = all_dialects_where(|d| d.supports_empty_projections()); + dialects.verified_stmt("SELECT FROM users"); +} From 14cefc47ed4c53f5616935281f512ecb4ebdc5c2 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Sun, 22 Dec 2024 22:40:08 +0100 Subject: [PATCH 651/806] Merge composite and compound expr test cases (#1615) --- tests/sqlparser_common.rs | 59 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b8ea7586a..79f5c8d32 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12389,8 +12389,8 @@ fn parse_create_table_with_bit_types() { fn parse_composite_access_expr() { assert_eq!( verified_expr("f(a).b"), - Expr::CompositeAccess { - expr: Box::new(Expr::Function(Function { + Expr::CompoundFieldAccess { + root: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("f")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, @@ -12406,41 +12406,41 @@ fn parse_composite_access_expr() { over: None, within_group: vec![] })), - key: Ident::new("b") + access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("b")))] } ); // Nested Composite Access assert_eq!( verified_expr("f(a).b.c"), - Expr::CompositeAccess { - expr: Box::new(Expr::CompositeAccess { - expr: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("f")]), - uses_odbc_syntax: false, - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( - Expr::Identifier(Ident::new("a")) - ))], - clauses: vec![], - }), - null_treatment: None, - filter: None, - over: None, - within_group: vec![] - })), - key: Ident::new("b") - }), - key: Ident::new("c") + Expr::CompoundFieldAccess { + root: Box::new(Expr::Function(Function { + name: ObjectName(vec![Ident::new("f")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("a")) + ))], + clauses: vec![], + }), + null_treatment: None, + filter: None, + over: None, + within_group: vec![] + })), + access_chain: vec![ + AccessExpr::Dot(Expr::Identifier(Ident::new("b"))), + AccessExpr::Dot(Expr::Identifier(Ident::new("c"))), + ] } ); // Composite Access in Select and Where Clauses let stmt = verified_only_select("SELECT f(a).b FROM t WHERE f(a).b IS NOT NULL"); - let expr = Expr::CompositeAccess { - expr: Box::new(Expr::Function(Function { + let expr = Expr::CompoundFieldAccess { + root: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("f")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, @@ -12456,14 +12456,15 @@ fn parse_composite_access_expr() { over: None, within_group: vec![], })), - key: Ident::new("b"), + access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("b")))], }; assert_eq!(stmt.projection[0], SelectItem::UnnamedExpr(expr.clone())); assert_eq!(stmt.selection.unwrap(), Expr::IsNotNull(Box::new(expr))); - // Composite Access with quoted identifier - verified_only_select("SELECT f(a).\"an id\""); + // Compound access with quoted identifier. + all_dialects_where(|d| d.is_delimited_identifier_start('"')) + .verified_only_select("SELECT f(a).\"an id\""); // Composite Access in struct literal all_dialects_where(|d| d.supports_struct_literal()).verified_stmt( From 024a878ee7f027ca2f9c635c9398ba59653f1a4e Mon Sep 17 00:00:00 2001 From: Yuval Shkolar <85674443+yuval-illumex@users.noreply.github.com> Date: Tue, 24 Dec 2024 17:00:59 +0200 Subject: [PATCH 652/806] Support Snowflake Update-From-Select (#1604) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 11 +++++++---- src/ast/query.rs | 13 +++++++++++++ src/ast/spans.rs | 13 +++++++++++-- src/keywords.rs | 1 + src/parser/mod.rs | 15 ++++++++++----- tests/sqlparser_common.rs | 20 ++++++++++++++++---- 6 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9fb2bb9c9..5bdce21ef 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -72,8 +72,8 @@ pub use self::query::{ TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, - TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, - WithFill, + TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, + WildcardAdditionalOptions, With, WithFill, }; pub use self::trigger::{ @@ -2473,7 +2473,7 @@ pub enum Statement { /// Column assignments assignments: Vec, /// Table which provide value to be set - from: Option, + from: Option, /// WHERE selection: Option, /// RETURNING @@ -3745,10 +3745,13 @@ impl fmt::Display for Statement { write!(f, "{or} ")?; } write!(f, "{table}")?; + if let Some(UpdateTableFromKind::BeforeSet(from)) = from { + write!(f, " FROM {from}")?; + } if !assignments.is_empty() { write!(f, " SET {}", display_comma_separated(assignments))?; } - if let Some(from) = from { + if let Some(UpdateTableFromKind::AfterSet(from)) = from { write!(f, " FROM {from}")?; } if let Some(selection) = selection { diff --git a/src/ast/query.rs b/src/ast/query.rs index 69b7ea1c1..9e4e9e2ef 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2790,3 +2790,16 @@ impl fmt::Display for ValueTableMode { } } } + +/// The `FROM` clause of an `UPDATE TABLE` 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 UpdateTableFromKind { + /// Update Statment where the 'FROM' clause is before the 'SET' keyword (Supported by Snowflake) + /// For Example: `UPDATE FROM t1 SET t1.name='aaa'` + BeforeSet(TableWithJoins), + /// Update Statment where the 'FROM' clause is after the 'SET' keyword (Which is the standard way) + /// For Example: `UPDATE SET t1.name='aaa' FROM t1` + AfterSet(TableWithJoins), +} diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 9ba3bdd9b..521b5399a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -32,8 +32,8 @@ use super::{ OrderBy, OrderByExpr, Partition, PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, - TableConstraint, TableFactor, TableOptionsClustered, TableWithJoins, Use, Value, Values, - ViewColumnDef, WildcardAdditionalOptions, With, WithFill, + TableConstraint, TableFactor, TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, + Value, Values, ViewColumnDef, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -2106,6 +2106,15 @@ impl Spanned for SelectInto { } } +impl Spanned for UpdateTableFromKind { + fn span(&self) -> Span { + match self { + UpdateTableFromKind::BeforeSet(from) => from.span(), + UpdateTableFromKind::AfterSet(from) => from.span(), + } + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; diff --git a/src/keywords.rs b/src/keywords.rs index bbfd00ca0..43abc2b03 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -941,6 +941,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ // Reserved for Snowflake table sample Keyword::SAMPLE, Keyword::TABLESAMPLE, + Keyword::FROM, ]; /// 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 cc0a57e4d..57c4dc6e7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11791,14 +11791,19 @@ impl<'a> Parser<'a> { pub fn parse_update(&mut self) -> Result { let or = self.parse_conflict_clause(); let table = self.parse_table_and_joins()?; + let from_before_set = if self.parse_keyword(Keyword::FROM) { + Some(UpdateTableFromKind::BeforeSet( + self.parse_table_and_joins()?, + )) + } else { + None + }; self.expect_keyword(Keyword::SET)?; let assignments = self.parse_comma_separated(Parser::parse_assignment)?; - let from = if self.parse_keyword(Keyword::FROM) - && dialect_of!(self is GenericDialect | PostgreSqlDialect | DuckDbDialect | BigQueryDialect | SnowflakeDialect | RedshiftSqlDialect | MsSqlDialect | SQLiteDialect ) - { - Some(self.parse_table_and_joins()?) + let from = if from_before_set.is_none() && self.parse_keyword(Keyword::FROM) { + Some(UpdateTableFromKind::AfterSet(self.parse_table_and_joins()?)) } else { - None + from_before_set }; let selection = if self.parse_keyword(Keyword::WHERE) { Some(self.parse_expr()?) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 79f5c8d32..cbbbb45f9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -366,7 +366,7 @@ fn parse_update_set_from() { target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("name")])), value: Expr::CompoundIdentifier(vec![Ident::new("t2"), Ident::new("name")]) }], - from: Some(TableWithJoins { + from: Some(UpdateTableFromKind::AfterSet(TableWithJoins { relation: TableFactor::Derived { lateral: false, subquery: Box::new(Query { @@ -417,8 +417,8 @@ fn parse_update_set_from() { columns: vec![], }) }, - joins: vec![], - }), + joins: vec![] + })), selection: Some(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("t1"), @@ -12577,9 +12577,21 @@ fn overflow() { let statement = statements.pop().unwrap(); assert_eq!(statement.to_string(), sql); } - #[test] fn parse_select_without_projection() { let dialects = all_dialects_where(|d| d.supports_empty_projections()); dialects.verified_stmt("SELECT FROM users"); } + +#[test] +fn parse_update_from_before_select() { + all_dialects() + .verified_stmt("UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name WHERE t1.id = t2.id"); + + let query = + "UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name FROM (SELECT name from t2) AS t2"; + assert_eq!( + ParserError::ParserError("Expected: end of statement, found: FROM".to_string()), + parse_sql_statements(query).unwrap_err() + ); +} From df3c5652b10493df4db484f358514bb210673744 Mon Sep 17 00:00:00 2001 From: "Paul J. Davis" Date: Tue, 24 Dec 2024 16:19:35 -0600 Subject: [PATCH 653/806] Improve parsing performance by reducing token cloning (#1587) Co-authored-by: Andrew Lamb --- README.md | 7 +- src/dialect/mod.rs | 9 + src/dialect/postgresql.rs | 8 +- src/dialect/snowflake.rs | 16 +- src/parser/alter.rs | 8 +- src/parser/mod.rs | 540 ++++++++++++++++++++++---------------- 6 files changed, 344 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 997aec585..41a44d3d7 100644 --- a/README.md +++ b/README.md @@ -240,11 +240,14 @@ You can run them with: ``` git checkout main cd sqlparser_bench -cargo bench +cargo bench -- --save-baseline main git checkout -cargo bench +cargo bench -- --baseline main ``` +By adding the `--save-baseline main` and `--baseline main` you can track the +progress of your improvements as you continue working on the feature branch. + ## Licensing All code in this repository is licensed under the [Apache Software License 2.0](LICENSE.txt). diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 7a71d662f..aee7b5994 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -75,6 +75,15 @@ macro_rules! dialect_of { }; } +// Similar to above, but for applying directly against an instance of dialect +// instead of a struct member named dialect. This avoids lifetime issues when +// mixing match guards and token references. +macro_rules! dialect_is { + ($dialect:ident is $($dialect_type:ty)|+) => { + ($($dialect.is::<$dialect_type>())||+) + } +} + /// Encapsulates the differences between SQL implementations. /// /// # SQL Dialects diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 80c14c8da..32a56743b 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -245,11 +245,11 @@ impl Dialect for PostgreSqlDialect { pub fn parse_create(parser: &mut Parser) -> Option> { let name = parser.maybe_parse(|parser| -> Result { - parser.expect_keyword(Keyword::CREATE)?; - parser.expect_keyword(Keyword::TYPE)?; + parser.expect_keyword_is(Keyword::CREATE)?; + parser.expect_keyword_is(Keyword::TYPE)?; let name = parser.parse_object_name(false)?; - parser.expect_keyword(Keyword::AS)?; - parser.expect_keyword(Keyword::ENUM)?; + parser.expect_keyword_is(Keyword::AS)?; + parser.expect_keyword_is(Keyword::ENUM)?; Ok(name) }); diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 045e5062d..c6f92daea 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -273,7 +273,7 @@ pub fn parse_create_table( match &next_token.token { Token::Word(word) => match word.keyword { Keyword::COPY => { - parser.expect_keyword(Keyword::GRANTS)?; + parser.expect_keyword_is(Keyword::GRANTS)?; builder = builder.copy_grants(true); } Keyword::COMMENT => { @@ -297,7 +297,7 @@ pub fn parse_create_table( break; } Keyword::CLUSTER => { - parser.expect_keyword(Keyword::BY)?; + 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(false))?, @@ -360,14 +360,14 @@ pub fn parse_create_table( parser.prev_token(); } Keyword::AGGREGATION => { - parser.expect_keyword(Keyword::POLICY)?; + parser.expect_keyword_is(Keyword::POLICY)?; let aggregation_policy = parser.parse_object_name(false)?; builder = builder.with_aggregation_policy(Some(aggregation_policy)); } Keyword::ROW => { parser.expect_keywords(&[Keyword::ACCESS, Keyword::POLICY])?; let policy = parser.parse_object_name(false)?; - parser.expect_keyword(Keyword::ON)?; + parser.expect_keyword_is(Keyword::ON)?; parser.expect_token(&Token::LParen)?; let columns = parser.parse_comma_separated(|p| p.parse_identifier(false))?; parser.expect_token(&Token::RParen)?; @@ -536,15 +536,15 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { let from_stage: ObjectName; let stage_params: StageParamsObject; - parser.expect_keyword(Keyword::FROM)?; + parser.expect_keyword_is(Keyword::FROM)?; // check if data load transformations are present match parser.next_token().token { Token::LParen => { // data load with transformations - parser.expect_keyword(Keyword::SELECT)?; + parser.expect_keyword_is(Keyword::SELECT)?; from_transformations = parse_select_items_for_data_load(parser)?; - parser.expect_keyword(Keyword::FROM)?; + parser.expect_keyword_is(Keyword::FROM)?; from_stage = parse_snowflake_stage_name(parser)?; stage_params = parse_stage_params(parser)?; @@ -860,7 +860,7 @@ fn parse_identity_property(parser: &mut Parser) -> Result { /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterpolicy.html) pub fn parse_alter_policy(&mut self) -> Result { let name = self.parse_identifier(false)?; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; if self.parse_keyword(Keyword::RENAME) { - self.expect_keyword(Keyword::TO)?; + self.expect_keyword_is(Keyword::TO)?; let new_name = self.parse_identifier(false)?; Ok(Statement::AlterPolicy { name, @@ -232,7 +232,7 @@ impl Parser<'_> { Some(Keyword::BYPASSRLS) => RoleOption::BypassRLS(true), Some(Keyword::NOBYPASSRLS) => RoleOption::BypassRLS(false), Some(Keyword::CONNECTION) => { - self.expect_keyword(Keyword::LIMIT)?; + self.expect_keyword_is(Keyword::LIMIT)?; RoleOption::ConnectionLimit(Expr::Value(self.parse_number_value()?)) } Some(Keyword::CREATEDB) => RoleOption::CreateDB(true), @@ -256,7 +256,7 @@ impl Parser<'_> { Some(Keyword::SUPERUSER) => RoleOption::SuperUser(true), Some(Keyword::NOSUPERUSER) => RoleOption::SuperUser(false), Some(Keyword::VALID) => { - self.expect_keyword(Keyword::UNTIL)?; + self.expect_keyword_is(Keyword::UNTIL)?; RoleOption::ValidUntil(Expr::Value(self.parse_value()?)) } _ => self.expected("option", self.peek_token())?, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 57c4dc6e7..2190b51d8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -189,6 +189,15 @@ impl std::error::Error for ParserError {} // By default, allow expressions up to this deep before erroring const DEFAULT_REMAINING_DEPTH: usize = 50; +// A constant EOF token that can be referenced. +const EOF_TOKEN: TokenWithSpan = TokenWithSpan { + token: Token::EOF, + span: Span { + start: Location { line: 0, column: 0 }, + end: Location { line: 0, column: 0 }, + }, +}; + /// Composite types declarations using angle brackets syntax can be arbitrary /// nested such that the following declaration is possible: /// `ARRAY>` @@ -571,7 +580,7 @@ impl<'a> Parser<'a> { pub fn parse_comment(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; let token = self.next_token(); let (object_type, object_name) = match token.token { @@ -599,7 +608,7 @@ impl<'a> Parser<'a> { _ => self.expected("comment object_type", token)?, }; - self.expect_keyword(Keyword::IS)?; + self.expect_keyword_is(Keyword::IS)?; let comment = if self.parse_keyword(Keyword::NULL) { None } else { @@ -702,7 +711,7 @@ impl<'a> Parser<'a> { pub fn parse_msck(&mut self) -> Result { let repair = self.parse_keyword(Keyword::REPAIR); - self.expect_keyword(Keyword::TABLE)?; + self.expect_keyword_is(Keyword::TABLE)?; let table_name = self.parse_object_name(false)?; let partition_action = self .maybe_parse(|parser| { @@ -716,7 +725,7 @@ impl<'a> Parser<'a> { Some(Keyword::SYNC) => Some(AddDropSync::SYNC), _ => None, }; - parser.expect_keyword(Keyword::PARTITIONS)?; + parser.expect_keyword_is(Keyword::PARTITIONS)?; Ok(pa) })? .unwrap_or_default(); @@ -847,7 +856,7 @@ impl<'a> Parser<'a> { pub fn parse_attach_database(&mut self) -> Result { let database = self.parse_keyword(Keyword::DATABASE); let database_file_name = self.parse_expr()?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let schema_name = self.parse_identifier(false)?; Ok(Statement::AttachDatabase { database, @@ -880,7 +889,7 @@ impl<'a> Parser<'a> { } Some(Keyword::NOSCAN) => noscan = true, Some(Keyword::FOR) => { - self.expect_keyword(Keyword::COLUMNS)?; + self.expect_keyword_is(Keyword::COLUMNS)?; columns = self .maybe_parse(|parser| { @@ -890,11 +899,11 @@ impl<'a> Parser<'a> { for_columns = true } Some(Keyword::CACHE) => { - self.expect_keyword(Keyword::METADATA)?; + self.expect_keyword_is(Keyword::METADATA)?; cache_metadata = true } Some(Keyword::COMPUTE) => { - self.expect_keyword(Keyword::STATISTICS)?; + self.expect_keyword_is(Keyword::STATISTICS)?; compute_statistics = true } _ => break, @@ -1094,7 +1103,7 @@ impl<'a> Parser<'a> { // Support parsing Databricks has a function named `exists`. if !dialect_of!(self is DatabricksDialect) || matches!( - self.peek_nth_token(1).token, + self.peek_nth_token_ref(1).token, Token::Word(Word { keyword: Keyword::SELECT | Keyword::WITH, .. @@ -1106,7 +1115,7 @@ impl<'a> Parser<'a> { Keyword::EXTRACT => Ok(Some(self.parse_extract_expr()?)), Keyword::CEIL => Ok(Some(self.parse_ceil_floor_expr(true)?)), Keyword::FLOOR => Ok(Some(self.parse_ceil_floor_expr(false)?)), - Keyword::POSITION if self.peek_token().token == Token::LParen => { + Keyword::POSITION if self.peek_token_ref().token == Token::LParen => { Ok(Some(self.parse_position_expr(w.to_ident(w_span))?)) } Keyword::SUBSTRING => Ok(Some(self.parse_substring_expr()?)), @@ -1114,7 +1123,7 @@ impl<'a> Parser<'a> { Keyword::TRIM => Ok(Some(self.parse_trim_expr()?)), Keyword::INTERVAL => Ok(Some(self.parse_interval()?)), // Treat ARRAY[1,2,3] as an array [1,2,3], otherwise try as subquery or a function call - Keyword::ARRAY if self.peek_token() == Token::LBracket => { + Keyword::ARRAY if *self.peek_token_ref() == Token::LBracket => { self.expect_token(&Token::LBracket)?; Ok(Some(self.parse_array_expr(true)?)) } @@ -1147,7 +1156,7 @@ impl<'a> Parser<'a> { let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?; Ok(Some(Expr::Prior(Box::new(expr)))) } - Keyword::MAP if self.peek_token() == Token::LBrace && self.dialect.support_map_literal_syntax() => { + Keyword::MAP if *self.peek_token_ref() == Token::LBrace && self.dialect.support_map_literal_syntax() => { Ok(Some(self.parse_duckdb_map_literal()?)) } _ => Ok(None) @@ -1239,7 +1248,7 @@ impl<'a> Parser<'a> { // Note also that naively `SELECT date` looks like a syntax error because the `date` type // name is not followed by a string literal, but in fact in PostgreSQL it is a valid // expression that should parse as the column name "date". - let loc = self.peek_token().span.start; + let loc = self.peek_token_ref().span.start; let opt_expr = self.maybe_parse(|parser| { match parser.parse_data_type()? { DataType::Interval => parser.parse_interval(), @@ -1262,8 +1271,14 @@ impl<'a> Parser<'a> { return Ok(expr); } - let next_token = self.next_token(); - let expr = match next_token.token { + // Cache some dialect properties to avoid lifetime issues with the + // next_token reference. + + let dialect = self.dialect; + + let (next_token, next_token_index) = self.next_token_ref_with_index(); + let span = next_token.span; + let expr = match &next_token.token { Token::Word(w) => { // The word we consumed may fall into one of two cases: it has a special meaning, or not. // For example, in Snowflake, the word `interval` may have two meanings depending on the context: @@ -1273,14 +1288,13 @@ impl<'a> Parser<'a> { // // We first try to parse the word and following tokens as a special expression, and if that fails, // we rollback and try to parse it as an identifier. - match self.try_parse(|parser| { - parser.parse_expr_prefix_by_reserved_word(&w, next_token.span) - }) { + let w = w.clone(); + match self.try_parse(|parser| parser.parse_expr_prefix_by_reserved_word(&w, span)) { // This word indicated an expression prefix and parsing was successful Ok(Some(expr)) => Ok(expr), // No expression prefix associated with this word - Ok(None) => Ok(self.parse_expr_prefix_by_unreserved_word(&w, next_token.span)?), + Ok(None) => Ok(self.parse_expr_prefix_by_unreserved_word(&w, span)?), // If parsing of the word as a special expression failed, we are facing two options: // 1. The statement is malformed, e.g. `SELECT INTERVAL '1 DAI` (`DAI` instead of `DAY`) @@ -1291,7 +1305,7 @@ impl<'a> Parser<'a> { Err(e) => { if !self.dialect.is_reserved_for_identifier(w.keyword) { if let Ok(Some(expr)) = self.maybe_parse(|parser| { - parser.parse_expr_prefix_by_unreserved_word(&w, next_token.span) + parser.parse_expr_prefix_by_unreserved_word(&w, span) }) { return Ok(expr); } @@ -1303,7 +1317,7 @@ impl<'a> Parser<'a> { // array `[1, 2, 3]` Token::LBracket => self.parse_array_expr(false), tok @ Token::Minus | tok @ Token::Plus => { - let op = if tok == Token::Plus { + let op = if *tok == Token::Plus { UnaryOperator::Plus } else { UnaryOperator::Minus @@ -1315,20 +1329,16 @@ impl<'a> Parser<'a> { ), }) } - Token::ExclamationMark if self.dialect.supports_bang_not_operator() => { - Ok(Expr::UnaryOp { - op: UnaryOperator::BangNot, - expr: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryNot))?, - ), - }) - } + Token::ExclamationMark if dialect.supports_bang_not_operator() => Ok(Expr::UnaryOp { + op: UnaryOperator::BangNot, + expr: Box::new(self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryNot))?), + }), tok @ Token::DoubleExclamationMark | tok @ Token::PGSquareRoot | tok @ Token::PGCubeRoot | tok @ Token::AtSign | tok @ Token::Tilde - if dialect_of!(self is PostgreSqlDialect) => + if dialect_is!(dialect is PostgreSqlDialect) => { let op = match tok { Token::DoubleExclamationMark => UnaryOperator::PGPrefixFactorial, @@ -1345,7 +1355,7 @@ impl<'a> Parser<'a> { ), }) } - Token::EscapedStringLiteral(_) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => + Token::EscapedStringLiteral(_) if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) @@ -1397,7 +1407,7 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_lbrace_expr() } - _ => self.expected("an expression", next_token), + _ => self.expected_at("an expression", next_token_index), }?; let expr = self.try_parse_method(expr)?; @@ -1746,7 +1756,7 @@ impl<'a> Parser<'a> { fn parse_null_treatment(&mut self) -> Result, ParserError> { match self.parse_one_of_keywords(&[Keyword::RESPECT, Keyword::IGNORE]) { Some(keyword) => { - self.expect_keyword(Keyword::NULLS)?; + self.expect_keyword_is(Keyword::NULLS)?; Ok(match keyword { Keyword::RESPECT => Some(NullTreatment::RespectNulls), @@ -1793,7 +1803,7 @@ impl<'a> Parser<'a> { let units = self.parse_window_frame_units()?; let (start_bound, end_bound) = if self.parse_keyword(Keyword::BETWEEN) { let start_bound = self.parse_window_frame_bound()?; - self.expect_keyword(Keyword::AND)?; + self.expect_keyword_is(Keyword::AND)?; let end_bound = Some(self.parse_window_frame_bound()?); (start_bound, end_bound) } else { @@ -1899,13 +1909,13 @@ impl<'a> Parser<'a> { let mut operand = None; if !self.parse_keyword(Keyword::WHEN) { operand = Some(Box::new(self.parse_expr()?)); - self.expect_keyword(Keyword::WHEN)?; + self.expect_keyword_is(Keyword::WHEN)?; } let mut conditions = vec![]; let mut results = vec![]; loop { conditions.push(self.parse_expr()?); - self.expect_keyword(Keyword::THEN)?; + self.expect_keyword_is(Keyword::THEN)?; results.push(self.parse_expr()?); if !self.parse_keyword(Keyword::WHEN) { break; @@ -1916,7 +1926,7 @@ impl<'a> Parser<'a> { } else { None }; - self.expect_keyword(Keyword::END)?; + self.expect_keyword_is(Keyword::END)?; Ok(Expr::Case { operand, conditions, @@ -2011,7 +2021,7 @@ impl<'a> Parser<'a> { pub fn parse_cast_expr(&mut self, kind: CastKind) -> Result { self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let data_type = self.parse_data_type()?; let format = self.parse_optional_cast_format()?; self.expect_token(&Token::RParen)?; @@ -2101,7 +2111,7 @@ impl<'a> Parser<'a> { // Parse the subexpr till the IN keyword let expr = p.parse_subexpr(between_prec)?; - p.expect_keyword(Keyword::IN)?; + p.expect_keyword_is(Keyword::IN)?; let from = p.parse_expr()?; p.expect_token(&Token::RParen)?; Ok(Expr::Position { @@ -2145,9 +2155,9 @@ impl<'a> Parser<'a> { // PARSE OVERLAY (EXPR PLACING EXPR FROM 1 [FOR 3]) self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; - self.expect_keyword(Keyword::PLACING)?; + self.expect_keyword_is(Keyword::PLACING)?; let what_expr = self.parse_expr()?; - self.expect_keyword(Keyword::FROM)?; + self.expect_keyword_is(Keyword::FROM)?; let from_expr = self.parse_expr()?; let mut for_expr = None; if self.parse_keyword(Keyword::FOR) { @@ -2238,7 +2248,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::ERROR) { Ok(Some(ListAggOnOverflow::Error)) } else { - self.expect_keyword(Keyword::TRUNCATE)?; + self.expect_keyword_is(Keyword::TRUNCATE)?; let filler = match self.peek_token().token { Token::Word(w) if w.keyword == Keyword::WITH || w.keyword == Keyword::WITHOUT => @@ -2259,7 +2269,7 @@ impl<'a> Parser<'a> { if !with_count && !self.parse_keyword(Keyword::WITHOUT) { self.expected("either WITH or WITHOUT in LISTAGG", self.peek_token())?; } - self.expect_keyword(Keyword::COUNT)?; + self.expect_keyword_is(Keyword::COUNT)?; Ok(Some(ListAggOnOverflow::Truncate { filler, with_count })) } } else { @@ -2392,7 +2402,7 @@ impl<'a> Parser<'a> { pub fn parse_match_against(&mut self) -> Result { let columns = self.parse_parenthesized_column_list(Mandatory, false)?; - self.expect_keyword(Keyword::AGAINST)?; + self.expect_keyword_is(Keyword::AGAINST)?; self.expect_token(&Token::LParen)?; @@ -2635,7 +2645,7 @@ impl<'a> Parser<'a> { F: FnMut(&mut Parser<'a>) -> Result<(StructField, MatchedTrailingBracket), ParserError>, { let start_token = self.peek_token(); - self.expect_keyword(Keyword::STRUCT)?; + self.expect_keyword_is(Keyword::STRUCT)?; // Nothing to do if we have no type information. if Token::Lt != self.peek_token() { @@ -2667,7 +2677,7 @@ impl<'a> Parser<'a> { /// Duckdb Struct Data Type fn parse_duckdb_struct_type_def(&mut self) -> Result, ParserError> { - self.expect_keyword(Keyword::STRUCT)?; + self.expect_keyword_is(Keyword::STRUCT)?; self.expect_token(&Token::LParen)?; let struct_body = self.parse_comma_separated(|parser| { let field_name = parser.parse_identifier(false)?; @@ -2728,7 +2738,7 @@ impl<'a> Parser<'a> { /// /// [1]: https://duckdb.org/docs/sql/data_types/union.html fn parse_union_type_def(&mut self) -> Result, ParserError> { - self.expect_keyword(Keyword::UNION)?; + self.expect_keyword_is(Keyword::UNION)?; self.expect_token(&Token::LParen)?; @@ -2833,7 +2843,7 @@ impl<'a> Parser<'a> { /// /// [map]: https://clickhouse.com/docs/en/sql-reference/data-types/map fn parse_click_house_map_def(&mut self) -> Result<(DataType, DataType), ParserError> { - self.expect_keyword(Keyword::MAP)?; + self.expect_keyword_is(Keyword::MAP)?; self.expect_token(&Token::LParen)?; let key_data_type = self.parse_data_type()?; self.expect_token(&Token::Comma)?; @@ -2853,7 +2863,7 @@ impl<'a> Parser<'a> { /// /// [tuple]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple fn parse_click_house_tuple_def(&mut self) -> Result, ParserError> { - self.expect_keyword(Keyword::TUPLE)?; + self.expect_keyword_is(Keyword::TUPLE)?; self.expect_token(&Token::LParen)?; let mut field_defs = vec![]; loop { @@ -2902,8 +2912,11 @@ impl<'a> Parser<'a> { return infix; } - let mut tok = self.next_token(); - let regular_binary_operator = match &mut tok.token { + let dialect = self.dialect; + + let (tok, tok_index) = self.next_token_ref_with_index(); + let span = tok.span; + let regular_binary_operator = match &tok.token { Token::Spaceship => Some(BinaryOperator::Spaceship), Token::DoubleEq => Some(BinaryOperator::Eq), Token::Eq => Some(BinaryOperator::Eq), @@ -2921,7 +2934,7 @@ impl<'a> Parser<'a> { Token::Caret => { // In PostgreSQL, ^ stands for the exponentiation operation, // and # stands for XOR. See https://www.postgresql.org/docs/current/functions-math.html - if dialect_of!(self is PostgreSqlDialect) { + if dialect_is!(dialect is PostgreSqlDialect) { Some(BinaryOperator::PGExp) } else { Some(BinaryOperator::BitwiseXor) @@ -2929,22 +2942,22 @@ impl<'a> Parser<'a> { } Token::Ampersand => Some(BinaryOperator::BitwiseAnd), Token::Div => Some(BinaryOperator::Divide), - Token::DuckIntDiv if dialect_of!(self is DuckDbDialect | GenericDialect) => { + Token::DuckIntDiv if dialect_is!(dialect is DuckDbDialect | GenericDialect) => { Some(BinaryOperator::DuckIntegerDivide) } - Token::ShiftLeft if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { + Token::ShiftLeft if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { Some(BinaryOperator::PGBitwiseShiftLeft) } - Token::ShiftRight if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { + Token::ShiftRight if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { Some(BinaryOperator::PGBitwiseShiftRight) } - Token::Sharp if dialect_of!(self is PostgreSqlDialect) => { + Token::Sharp if dialect_is!(dialect is PostgreSqlDialect) => { Some(BinaryOperator::PGBitwiseXor) } - Token::Overlap if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { + Token::Overlap if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { Some(BinaryOperator::PGOverlap) } - Token::CaretAt if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { + Token::CaretAt if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { Some(BinaryOperator::PGStartsWith) } Token::Tilde => Some(BinaryOperator::PGRegexMatch), @@ -2967,13 +2980,13 @@ impl<'a> Parser<'a> { Token::Question => Some(BinaryOperator::Question), Token::QuestionAnd => Some(BinaryOperator::QuestionAnd), Token::QuestionPipe => Some(BinaryOperator::QuestionPipe), - Token::CustomBinaryOperator(s) => Some(BinaryOperator::Custom(core::mem::take(s))), + Token::CustomBinaryOperator(s) => Some(BinaryOperator::Custom(s.clone())), Token::Word(w) => match w.keyword { Keyword::AND => Some(BinaryOperator::And), Keyword::OR => Some(BinaryOperator::Or), Keyword::XOR => Some(BinaryOperator::Xor), - Keyword::OPERATOR if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { + Keyword::OPERATOR if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { self.expect_token(&Token::LParen)?; // there are special rules for operator names in // postgres so we can not use 'parse_object' @@ -2981,7 +2994,7 @@ impl<'a> Parser<'a> { // See https://www.postgresql.org/docs/current/sql-createoperator.html let mut idents = vec![]; loop { - idents.push(self.next_token().to_string()); + idents.push(self.next_token_ref().to_string()); if !self.consume_token(&Token::Period) { break; } @@ -2994,6 +3007,7 @@ impl<'a> Parser<'a> { _ => None, }; + let tok = self.token_at(tok_index); if let Some(op) = regular_binary_operator { if let Some(keyword) = self.parse_one_of_keywords(&[Keyword::ANY, Keyword::ALL, Keyword::SOME]) @@ -3024,7 +3038,7 @@ impl<'a> Parser<'a> { format!( "Expected one of [=, >, <, =>, =<, !=] as comparison operator, found: {op}" ), - tok.span.start + span.start ); }; @@ -3153,19 +3167,19 @@ impl<'a> Parser<'a> { tok.span.start ), } - } else if Token::DoubleColon == tok { + } else if Token::DoubleColon == *tok { Ok(Expr::Cast { kind: CastKind::DoubleColon, expr: Box::new(expr), data_type: self.parse_data_type()?, format: None, }) - } else if Token::ExclamationMark == tok && self.dialect.supports_factorial_operator() { + } else if Token::ExclamationMark == *tok && self.dialect.supports_factorial_operator() { Ok(Expr::UnaryOp { op: UnaryOperator::PGPostfixFactorial, expr: Box::new(expr), }) - } else if Token::LBracket == tok { + } else if Token::LBracket == *tok { if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) { let mut chain = vec![]; @@ -3179,7 +3193,7 @@ impl<'a> Parser<'a> { } else { parser_err!("Array subscripting is not supported", tok.span.start) } - } else if dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == tok { + } else if dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == *tok { self.prev_token(); self.parse_json_access(expr) } else { @@ -3394,7 +3408,7 @@ impl<'a> Parser<'a> { // Stop parsing subexpressions for and on tokens with // precedence lower than that of `BETWEEN`, such as `AND`, `IS`, etc. let low = self.parse_subexpr(self.dialect.prec_value(Precedence::Between))?; - self.expect_keyword(Keyword::AND)?; + self.expect_keyword_is(Keyword::AND)?; let high = self.parse_subexpr(self.dialect.prec_value(Precedence::Between))?; Ok(Expr::Between { expr: Box::new(expr), @@ -3419,12 +3433,24 @@ impl<'a> Parser<'a> { self.dialect.get_next_precedence_default(self) } + /// Return the token at the given location, or EOF if the index is beyond + /// the length of the current set of tokens. + pub fn token_at(&self, index: usize) -> &TokenWithSpan { + self.tokens.get(index).unwrap_or(&EOF_TOKEN) + } + /// Return the first non-whitespace token that has not yet been processed - /// (or None if reached end-of-file) + /// or Token::EOF pub fn peek_token(&self) -> TokenWithSpan { self.peek_nth_token(0) } + /// Return a reference to the first non-whitespace token that has not yet + /// been processed or Token::EOF + pub fn peek_token_ref(&self) -> &TokenWithSpan { + self.peek_nth_token_ref(0) + } + /// Returns the `N` next non-whitespace tokens that have not yet been /// processed. /// @@ -3476,7 +3502,12 @@ impl<'a> Parser<'a> { } /// Return nth non-whitespace token that has not yet been processed - pub fn peek_nth_token(&self, mut n: usize) -> TokenWithSpan { + pub fn peek_nth_token(&self, n: usize) -> TokenWithSpan { + self.peek_nth_token_ref(n).clone() + } + + /// Return nth non-whitespace token that has not yet been processed + pub fn peek_nth_token_ref(&self, mut n: usize) -> &TokenWithSpan { let mut index = self.index; loop { index += 1; @@ -3487,10 +3518,7 @@ impl<'a> Parser<'a> { }) => continue, non_whitespace => { if n == 0 { - return non_whitespace.cloned().unwrap_or(TokenWithSpan { - token: Token::EOF, - span: Span::empty(), - }); + return non_whitespace.unwrap_or(&EOF_TOKEN); } n -= 1; } @@ -3515,7 +3543,9 @@ impl<'a> Parser<'a> { }) } - /// Look for all of the expected keywords in sequence, without consuming them + /// Return true if the next tokens exactly `expected` + /// + /// Does not advance the current token. fn peek_keywords(&mut self, expected: &[Keyword]) -> bool { let index = self.index; let matched = self.parse_keywords(expected); @@ -3523,10 +3553,23 @@ impl<'a> Parser<'a> { matched } - /// Return the first non-whitespace token that has not yet been processed - /// (or None if reached end-of-file) and mark it as processed. OK to call - /// repeatedly after reaching EOF. + /// Advances to the next non-whitespace token and returns a copy. + /// + /// See [`Self::next_token_ref`] to avoid the copy. pub fn next_token(&mut self) -> TokenWithSpan { + self.next_token_ref().clone() + } + + pub fn next_token_ref(&mut self) -> &TokenWithSpan { + self.next_token_ref_with_index().0 + } + + /// Return the first non-whitespace token that has not yet been processed + /// and that tokens index and advances the tokens + /// + /// # Notes: + /// OK to call repeatedly after reaching EOF. + pub fn next_token_ref_with_index(&mut self) -> (&TokenWithSpan, usize) { loop { self.index += 1; match self.tokens.get(self.index - 1) { @@ -3534,15 +3577,16 @@ impl<'a> Parser<'a> { token: Token::Whitespace(_), span: _, }) => continue, - token => { - return token - .cloned() - .unwrap_or_else(|| TokenWithSpan::wrap(Token::EOF)) - } + token => return (token.unwrap_or(&EOF_TOKEN), self.index - 1), } } } + /// Returns a reference to the current token + pub fn current_token(&self) -> &TokenWithSpan { + self.tokens.get(self.index - 1).unwrap_or(&EOF_TOKEN) + } + /// Return the first unprocessed token, possibly whitespace. pub fn next_token_no_skip(&mut self) -> Option<&TokenWithSpan> { self.index += 1; @@ -3575,24 +3619,51 @@ impl<'a> Parser<'a> { ) } + /// report `found` was encountered instead of `expected` + pub fn expected_ref(&self, expected: &str, found: &TokenWithSpan) -> Result { + parser_err!( + format!("Expected: {expected}, found: {found}"), + found.span.start + ) + } + + /// Report that the current token was found instead of `expected`. + pub fn expected_at(&self, expected: &str, index: usize) -> Result { + let found = self.tokens.get(index).unwrap_or(&EOF_TOKEN); + parser_err!( + format!("Expected: {expected}, found: {found}"), + found.span.start + ) + } + /// If the current token is the `expected` keyword, consume it and returns /// true. Otherwise, no tokens are consumed and returns false. #[must_use] pub fn parse_keyword(&mut self, expected: Keyword) -> bool { - self.parse_keyword_token(expected).is_some() + if self.peek_keyword(expected) { + self.next_token_ref(); + true + } else { + false + } } #[must_use] pub fn parse_keyword_token(&mut self, expected: Keyword) -> Option { - match self.peek_token().token { - Token::Word(w) if expected == w.keyword => Some(self.next_token()), + self.parse_keyword_token_ref(expected).cloned() + } + + #[must_use] + pub fn parse_keyword_token_ref(&mut self, expected: Keyword) -> Option<&TokenWithSpan> { + match &self.peek_token_ref().token { + Token::Word(w) if expected == w.keyword => Some(self.next_token_ref()), _ => None, } } #[must_use] - pub fn peek_keyword(&mut self, expected: Keyword) -> bool { - matches!(self.peek_token().token, Token::Word(w) if expected == w.keyword) + pub fn peek_keyword(&self, expected: Keyword) -> bool { + matches!(&self.peek_token_ref().token, Token::Word(w) if expected == w.keyword) } /// If the current token is the `expected` keyword followed by @@ -3603,16 +3674,16 @@ impl<'a> Parser<'a> { /// not be efficient as it does a loop on the tokens with `peek_nth_token` /// each time. pub fn parse_keyword_with_tokens(&mut self, expected: Keyword, tokens: &[Token]) -> bool { - match self.peek_token().token { + match &self.peek_token_ref().token { Token::Word(w) if expected == w.keyword => { for (idx, token) in tokens.iter().enumerate() { - if self.peek_nth_token(idx + 1).token != *token { + if self.peek_nth_token_ref(idx + 1).token != *token { return false; } } // consume all tokens for _ in 0..(tokens.len() + 1) { - self.next_token(); + self.next_token_ref(); } true } @@ -3642,13 +3713,13 @@ impl<'a> Parser<'a> { /// and returns [`None`]. #[must_use] pub fn parse_one_of_keywords(&mut self, keywords: &[Keyword]) -> Option { - match self.peek_token().token { + match &self.peek_token_ref().token { Token::Word(w) => { keywords .iter() .find(|keyword| **keyword == w.keyword) .map(|keyword| { - self.next_token(); + self.next_token_ref(); *keyword }) } @@ -3663,9 +3734,9 @@ impl<'a> Parser<'a> { Ok(keyword) } else { let keywords: Vec = keywords.iter().map(|x| format!("{x:?}")).collect(); - self.expected( + self.expected_ref( &format!("one of {}", keywords.join(" or ")), - self.peek_token(), + self.peek_token_ref(), ) } } @@ -3673,10 +3744,23 @@ impl<'a> Parser<'a> { /// If the current token is the `expected` keyword, consume the token. /// Otherwise, return an error. pub fn expect_keyword(&mut self, expected: Keyword) -> Result { - if let Some(token) = self.parse_keyword_token(expected) { - Ok(token) + if let Some(token) = self.parse_keyword_token_ref(expected) { + Ok(token.clone()) + } else { + self.expected_ref(format!("{:?}", &expected).as_str(), self.peek_token_ref()) + } + } + + /// If the current token is the `expected` keyword, consume the token. + /// Otherwise, return an error. + /// + /// This differs from expect_keyword only in that the matched keyword + /// token is not returned. + pub fn expect_keyword_is(&mut self, expected: Keyword) -> Result<(), ParserError> { + if self.parse_keyword_token_ref(expected).is_some() { + Ok(()) } else { - self.expected(format!("{:?}", &expected).as_str(), self.peek_token()) + self.expected_ref(format!("{:?}", &expected).as_str(), self.peek_token_ref()) } } @@ -3684,7 +3768,7 @@ impl<'a> Parser<'a> { /// sequence, consume them and returns Ok. Otherwise, return an Error. pub fn expect_keywords(&mut self, expected: &[Keyword]) -> Result<(), ParserError> { for &kw in expected { - self.expect_keyword(kw)?; + self.expect_keyword_is(kw)?; } Ok(()) } @@ -3692,8 +3776,8 @@ impl<'a> Parser<'a> { /// Consume the next token if it matches the expected token, otherwise return false #[must_use] pub fn consume_token(&mut self, expected: &Token) -> bool { - if self.peek_token() == *expected { - self.next_token(); + if self.peek_token_ref() == expected { + self.next_token_ref(); true } else { false @@ -3717,10 +3801,10 @@ impl<'a> Parser<'a> { /// Bail out if the current token is not an expected keyword, or consume it if it is pub fn expect_token(&mut self, expected: &Token) -> Result { - if self.peek_token() == *expected { + if self.peek_token_ref() == expected { Ok(self.next_token()) } else { - self.expected(&expected.to_string(), self.peek_token()) + self.expected_ref(&expected.to_string(), self.peek_token_ref()) } } @@ -4034,7 +4118,7 @@ impl<'a> Parser<'a> { } self.expect_token(&Token::LParen)?; - self.expect_keyword(Keyword::TYPE)?; + self.expect_keyword_is(Keyword::TYPE)?; let secret_type = self.parse_identifier(false)?; let mut options = Vec::new(); @@ -4157,7 +4241,7 @@ impl<'a> Parser<'a> { /// Parse a UNCACHE TABLE statement pub fn parse_uncache_table(&mut self) -> Result { - self.expect_keyword(Keyword::TABLE)?; + self.expect_keyword_is(Keyword::TABLE)?; let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let table_name = self.parse_object_name(false)?; Ok(Statement::UNCache { @@ -4168,10 +4252,10 @@ impl<'a> Parser<'a> { /// SQLite-specific `CREATE VIRTUAL TABLE` pub fn parse_create_virtual_table(&mut self) -> Result { - self.expect_keyword(Keyword::TABLE)?; + self.expect_keyword_is(Keyword::TABLE)?; let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let table_name = self.parse_object_name(false)?; - self.expect_keyword(Keyword::USING)?; + self.expect_keyword_is(Keyword::USING)?; let module_name = self.parse_identifier(false)?; // SQLite docs note that module "arguments syntax is sufficiently // general that the arguments can be made to appear as column @@ -4415,7 +4499,7 @@ impl<'a> Parser<'a> { temporary: bool, ) -> Result { let name = self.parse_object_name(false)?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let as_ = self.parse_create_function_body_string()?; let using = self.parse_optional_create_function_using()?; @@ -4497,7 +4581,7 @@ impl<'a> Parser<'a> { let mut options = self.maybe_parse_options(Keyword::OPTIONS)?; let function_body = if remote_connection.is_none() { - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let expr = self.parse_expr()?; if options.is_none() { options = self.maybe_parse_options(Keyword::OPTIONS)?; @@ -4574,7 +4658,7 @@ impl<'a> Parser<'a> { } let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let trigger_name = self.parse_object_name(false)?; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; let option = self .parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) @@ -4605,7 +4689,7 @@ impl<'a> Parser<'a> { let period = self.parse_trigger_period()?; let events = self.parse_keyword_separated(Keyword::OR, Parser::parse_trigger_event)?; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; let referenced_table_name = if self.parse_keyword(Keyword::FROM) { @@ -4623,7 +4707,7 @@ impl<'a> Parser<'a> { } } - self.expect_keyword(Keyword::FOR)?; + self.expect_keyword_is(Keyword::FOR)?; let include_each = self.parse_keyword(Keyword::EACH); let trigger_object = match self.expect_one_of_keywords(&[Keyword::ROW, Keyword::STATEMENT])? { @@ -4637,7 +4721,7 @@ impl<'a> Parser<'a> { .then(|| self.parse_expr()) .transpose()?; - self.expect_keyword(Keyword::EXECUTE)?; + self.expect_keyword_is(Keyword::EXECUTE)?; let exec_body = self.parse_trigger_exec_body()?; @@ -4668,7 +4752,7 @@ impl<'a> Parser<'a> { Keyword::BEFORE => TriggerPeriod::Before, Keyword::AFTER => TriggerPeriod::After, Keyword::INSTEAD => self - .expect_keyword(Keyword::OF) + .expect_keyword_is(Keyword::OF) .map(|_| TriggerPeriod::InsteadOf)?, _ => unreachable!(), }, @@ -4752,7 +4836,7 @@ impl<'a> Parser<'a> { }; self.expect_token(&Token::RParen)?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; Ok(Statement::CreateMacro { or_replace, @@ -4787,7 +4871,7 @@ impl<'a> Parser<'a> { &mut self, or_replace: bool, ) -> Result { - self.expect_keyword(Keyword::TABLE)?; + self.expect_keyword_is(Keyword::TABLE)?; let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let table_name = self.parse_object_name(false)?; let (columns, constraints) = self.parse_columns()?; @@ -4855,7 +4939,7 @@ impl<'a> Parser<'a> { temporary: bool, ) -> Result { let materialized = self.parse_keyword(Keyword::MATERIALIZED); - self.expect_keyword(Keyword::VIEW)?; + self.expect_keyword_is(Keyword::VIEW)?; let if_not_exists = dialect_of!(self is BigQueryDialect|SQLiteDialect|GenericDialect) && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). @@ -4870,7 +4954,7 @@ impl<'a> Parser<'a> { } let cluster_by = if self.parse_keyword(Keyword::CLUSTER) { - self.expect_keyword(Keyword::BY)?; + self.expect_keyword_is(Keyword::BY)?; self.parse_parenthesized_column_list(Optional, false)? } else { vec![] @@ -4905,7 +4989,7 @@ impl<'a> Parser<'a> { None }; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let query = self.parse_query()?; // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. @@ -5071,7 +5155,7 @@ impl<'a> Parser<'a> { } } Keyword::CONNECTION => { - self.expect_keyword(Keyword::LIMIT)?; + self.expect_keyword_is(Keyword::LIMIT)?; if connection_limit.is_some() { parser_err!("Found multiple CONNECTION LIMIT", loc) } else { @@ -5080,7 +5164,7 @@ impl<'a> Parser<'a> { } } Keyword::VALID => { - self.expect_keyword(Keyword::UNTIL)?; + self.expect_keyword_is(Keyword::UNTIL)?; if valid_until.is_some() { parser_err!("Found multiple VALID UNTIL", loc) } else { @@ -5186,7 +5270,7 @@ impl<'a> Parser<'a> { /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createpolicy.html) pub fn parse_create_policy(&mut self) -> Result { let name = self.parse_identifier(false)?; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; let policy_type = if self.parse_keyword(Keyword::AS) { @@ -5357,7 +5441,7 @@ impl<'a> Parser<'a> { fn parse_drop_policy(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let name = self.parse_identifier(false)?; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; let option = self.parse_optional_referential_action(); Ok(Statement::DropPolicy { @@ -5467,12 +5551,12 @@ impl<'a> Parser<'a> { None }; - self.expect_keyword(Keyword::CURSOR)?; + self.expect_keyword_is(Keyword::CURSOR)?; let declare_type = Some(DeclareType::Cursor); let hold = match self.parse_one_of_keywords(&[Keyword::WITH, Keyword::WITHOUT]) { Some(keyword) => { - self.expect_keyword(Keyword::HOLD)?; + self.expect_keyword_is(Keyword::HOLD)?; match keyword { Keyword::WITH => Some(true), @@ -5483,7 +5567,7 @@ impl<'a> Parser<'a> { None => None, }; - self.expect_keyword(Keyword::FOR)?; + self.expect_keyword_is(Keyword::FOR)?; let query = Some(self.parse_query()?); @@ -5526,7 +5610,7 @@ impl<'a> Parser<'a> { } else { // If no variable type - default expression must be specified, per BQ docs. // i.e `DECLARE foo;` is invalid. - self.expect_keyword(Keyword::DEFAULT)?; + self.expect_keyword_is(Keyword::DEFAULT)?; Some(self.parse_expr()?) }; @@ -5575,7 +5659,7 @@ impl<'a> Parser<'a> { let name = self.parse_identifier(false)?; let (declare_type, for_query, assigned_expr, data_type) = if self.parse_keyword(Keyword::CURSOR) { - self.expect_keyword(Keyword::FOR)?; + self.expect_keyword_is(Keyword::FOR)?; match self.peek_token().token { Token::Word(w) if w.keyword == Keyword::SELECT => ( Some(DeclareType::Cursor), @@ -5859,7 +5943,7 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let index_name = if if_not_exists || !self.parse_keyword(Keyword::ON) { let index_name = self.parse_object_name(false)?; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; Some(index_name) } else { None @@ -5885,7 +5969,7 @@ impl<'a> Parser<'a> { let nulls_distinct = if self.parse_keyword(Keyword::NULLS) { let not = self.parse_keyword(Keyword::NOT); - self.expect_keyword(Keyword::DISTINCT)?; + self.expect_keyword_is(Keyword::DISTINCT)?; Some(!not) } else { None @@ -5981,10 +6065,10 @@ impl<'a> Parser<'a> { hive_format.row_format = Some(self.parse_row_format()?); } Some(Keyword::STORED) => { - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; if self.parse_keyword(Keyword::INPUTFORMAT) { let input_format = self.parse_expr()?; - self.expect_keyword(Keyword::OUTPUTFORMAT)?; + self.expect_keyword_is(Keyword::OUTPUTFORMAT)?; let output_format = self.parse_expr()?; hive_format.storage = Some(HiveIOFormat::IOF { input_format, @@ -6017,7 +6101,7 @@ impl<'a> Parser<'a> { } pub fn parse_row_format(&mut self) -> Result { - self.expect_keyword(Keyword::FORMAT)?; + self.expect_keyword_is(Keyword::FORMAT)?; match self.parse_one_of_keywords(&[Keyword::SERDE, Keyword::DELIMITED]) { Some(Keyword::SERDE) => { let class = self.parse_literal_string()?; @@ -6790,9 +6874,9 @@ impl<'a> Parser<'a> { None }; - self.expect_keyword(Keyword::INTO)?; + self.expect_keyword_is(Keyword::INTO)?; let num_buckets = self.parse_number_value()?; - self.expect_keyword(Keyword::BUCKETS)?; + self.expect_keyword_is(Keyword::BUCKETS)?; Some(ClusteredBy { columns, sorted_by, @@ -6902,7 +6986,7 @@ impl<'a> Parser<'a> { } Token::Word(w) if w.keyword == Keyword::PRIMARY => { // after `PRIMARY` always stay `KEY` - self.expect_keyword(Keyword::KEY)?; + self.expect_keyword_is(Keyword::KEY)?; // optional index name let index_name = self.parse_optional_indent()?; @@ -6921,9 +7005,9 @@ impl<'a> Parser<'a> { })) } Token::Word(w) if w.keyword == Keyword::FOREIGN => { - self.expect_keyword(Keyword::KEY)?; + self.expect_keyword_is(Keyword::KEY)?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; - self.expect_keyword(Keyword::REFERENCES)?; + self.expect_keyword_is(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name(false)?; let referred_columns = self.parse_parenthesized_column_list(Optional, false)?; let mut on_delete = None; @@ -7023,7 +7107,7 @@ impl<'a> Parser<'a> { fn parse_optional_nulls_distinct(&mut self) -> Result { Ok(if self.parse_keyword(Keyword::NULLS) { let not = self.parse_keyword(Keyword::NOT); - self.expect_keyword(Keyword::DISTINCT)?; + self.expect_keyword_is(Keyword::DISTINCT)?; if not { NullsDistinctOption::NotDistinct } else { @@ -7191,11 +7275,11 @@ impl<'a> Parser<'a> { } pub fn parse_option_partition(&mut self) -> Result { - self.expect_keyword(Keyword::PARTITION)?; + self.expect_keyword_is(Keyword::PARTITION)?; self.expect_token(&Token::LParen)?; let column_name = self.parse_identifier(false)?; - self.expect_keyword(Keyword::RANGE)?; + self.expect_keyword_is(Keyword::RANGE)?; let range_direction = if self.parse_keyword(Keyword::LEFT) { Some(PartitionRangeDirection::Left) } else if self.parse_keyword(Keyword::RIGHT) { @@ -7228,7 +7312,7 @@ impl<'a> Parser<'a> { pub fn parse_projection_select(&mut self) -> Result { self.expect_token(&Token::LParen)?; - self.expect_keyword(Keyword::SELECT)?; + self.expect_keyword_is(Keyword::SELECT)?; let projection = self.parse_projection()?; let group_by = self.parse_optional_group_by()?; let order_by = self.parse_optional_order_by()?; @@ -7300,7 +7384,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::RENAME) { if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::CONSTRAINT) { let old_name = self.parse_identifier(false)?; - self.expect_keyword(Keyword::TO)?; + self.expect_keyword_is(Keyword::TO)?; let new_name = self.parse_identifier(false)?; AlterTableOperation::RenameConstraint { old_name, new_name } } else if self.parse_keyword(Keyword::TO) { @@ -7309,7 +7393,7 @@ impl<'a> Parser<'a> { } else { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let old_column_name = self.parse_identifier(false)?; - self.expect_keyword(Keyword::TO)?; + self.expect_keyword_is(Keyword::TO)?; let new_column_name = self.parse_identifier(false)?; AlterTableOperation::RenameColumn { old_column_name, @@ -7441,7 +7525,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let before = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; - self.expect_keyword(Keyword::RENAME)?; + self.expect_keyword_is(Keyword::RENAME)?; self.expect_keywords(&[Keyword::TO, Keyword::PARTITION])?; self.expect_token(&Token::LParen)?; let renames = self.parse_comma_separated(Parser::parse_expr)?; @@ -7549,7 +7633,7 @@ impl<'a> Parser<'a> { }; AlterTableOperation::AlterColumn { column_name, op } } else if self.parse_keyword(Keyword::SWAP) { - self.expect_keyword(Keyword::WITH)?; + self.expect_keyword_is(Keyword::WITH)?; let table_name = self.parse_object_name(false)?; AlterTableOperation::SwapWith { table_name } } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) @@ -7574,7 +7658,7 @@ impl<'a> Parser<'a> { { let partition = self.parse_part_or_partition()?; let with_name = if self.parse_keyword(Keyword::WITH) { - self.expect_keyword(Keyword::NAME)?; + self.expect_keyword_is(Keyword::NAME)?; Some(self.parse_identifier(false)?) } else { None @@ -7588,7 +7672,7 @@ impl<'a> Parser<'a> { { let partition = self.parse_part_or_partition()?; let with_name = if self.parse_keyword(Keyword::WITH) { - self.expect_keyword(Keyword::NAME)?; + self.expect_keyword_is(Keyword::NAME)?; Some(self.parse_identifier(false)?) } else { None @@ -7703,7 +7787,7 @@ impl<'a> Parser<'a> { let with_options = self.parse_options(Keyword::WITH)?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let query = self.parse_query()?; Ok(Statement::AlterView { @@ -8193,9 +8277,13 @@ impl<'a> Parser<'a> { fn parse_data_type_helper( &mut self, ) -> Result<(DataType, MatchedTrailingBracket), ParserError> { - let next_token = self.next_token(); + let dialect = self.dialect; + let (next_token, next_token_index) = self.next_token_ref_with_index(); + let _ = next_token; // release ref + let next_token = self.current_token(); + let mut trailing_bracket: MatchedTrailingBracket = false.into(); - let mut data = match next_token.token { + let mut data = match &next_token.token { Token::Word(w) => match w.keyword { Keyword::BOOLEAN => Ok(DataType::Boolean), Keyword::BOOL => Ok(DataType::Bool), @@ -8437,12 +8525,12 @@ impl<'a> Parser<'a> { )))) } } - Keyword::STRUCT if dialect_of!(self is DuckDbDialect) => { + Keyword::STRUCT if dialect_is!(dialect is DuckDbDialect) => { self.prev_token(); let field_defs = self.parse_duckdb_struct_type_def()?; Ok(DataType::Struct(field_defs, StructBracketKind::Parentheses)) } - Keyword::STRUCT if dialect_of!(self is BigQueryDialect | GenericDialect) => { + Keyword::STRUCT if dialect_is!(dialect is BigQueryDialect | GenericDialect) => { self.prev_token(); let (field_defs, _trailing_bracket) = self.parse_struct_type_def(Self::parse_struct_field_def)?; @@ -8452,18 +8540,18 @@ impl<'a> Parser<'a> { StructBracketKind::AngleBrackets, )) } - Keyword::UNION if dialect_of!(self is DuckDbDialect | GenericDialect) => { + Keyword::UNION if dialect_is!(dialect is DuckDbDialect | GenericDialect) => { self.prev_token(); let fields = self.parse_union_type_def()?; Ok(DataType::Union(fields)) } - Keyword::NULLABLE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + Keyword::NULLABLE if dialect_is!(dialect is ClickHouseDialect | GenericDialect) => { Ok(self.parse_sub_type(DataType::Nullable)?) } - Keyword::LOWCARDINALITY if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + Keyword::LOWCARDINALITY if dialect_is!(dialect is ClickHouseDialect | GenericDialect) => { Ok(self.parse_sub_type(DataType::LowCardinality)?) } - Keyword::MAP if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + Keyword::MAP if dialect_is!(dialect is ClickHouseDialect | GenericDialect) => { self.prev_token(); let (key_data_type, value_data_type) = self.parse_click_house_map_def()?; Ok(DataType::Map( @@ -8471,13 +8559,13 @@ impl<'a> Parser<'a> { Box::new(value_data_type), )) } - Keyword::NESTED if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + Keyword::NESTED if dialect_is!(dialect is ClickHouseDialect | GenericDialect) => { self.expect_token(&Token::LParen)?; let field_defs = self.parse_comma_separated(Parser::parse_column_def)?; self.expect_token(&Token::RParen)?; Ok(DataType::Nested(field_defs)) } - Keyword::TUPLE if dialect_of!(self is ClickHouseDialect | GenericDialect) => { + Keyword::TUPLE if dialect_is!(dialect is ClickHouseDialect | GenericDialect) => { self.prev_token(); let field_defs = self.parse_click_house_tuple_def()?; Ok(DataType::Tuple(field_defs)) @@ -8497,7 +8585,7 @@ impl<'a> Parser<'a> { } } }, - _ => self.expected("a data type name", next_token), + _ => self.expected_at("a data type name", next_token_index), }?; // Parse array data types. Note: this is postgresql-specific and different from @@ -8536,7 +8624,7 @@ impl<'a> Parser<'a> { /// Strictly parse `identifier AS identifier` pub fn parse_identifier_with_alias(&mut self) -> Result { let ident = self.parse_identifier(false)?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let alias = self.parse_identifier(false)?; Ok(IdentWithAlias { ident, alias }) } @@ -8711,14 +8799,14 @@ impl<'a> Parser<'a> { pub fn parse_identifiers(&mut self) -> Result, ParserError> { let mut idents = vec![]; loop { - match self.peek_token().token { + match &self.peek_token_ref().token { Token::Word(w) => { - idents.push(w.to_ident(self.peek_token().span)); + idents.push(w.to_ident(self.peek_token_ref().span)); } Token::EOF | Token::Eq => break, _ => {} } - self.next_token(); + self.next_token_ref(); } Ok(idents) } @@ -8985,7 +9073,7 @@ impl<'a> Parser<'a> { /// /// [1]: https://clickhouse.com/docs/en/sql-reference/data-types/datetime64 pub fn parse_datetime_64(&mut self) -> Result<(u64, Option), ParserError> { - self.expect_keyword(Keyword::DATETIME64)?; + self.expect_keyword_is(Keyword::DATETIME64)?; self.expect_token(&Token::LParen)?; let precision = self.parse_literal_uint()?; let time_zone = if self.consume_token(&Token::Comma) { @@ -9108,7 +9196,7 @@ impl<'a> Parser<'a> { (vec![], false) } else { let tables = self.parse_comma_separated(|p| p.parse_object_name(false))?; - self.expect_keyword(Keyword::FROM)?; + self.expect_keyword_is(Keyword::FROM)?; (tables, true) } } else { @@ -9259,9 +9347,9 @@ impl<'a> Parser<'a> { /// expect the initial keyword to be already consumed pub fn parse_query(&mut self) -> Result, ParserError> { let _guard = self.recursion_counter.try_decrease()?; - let with = if let Some(with_token) = self.parse_keyword_token(Keyword::WITH) { + let with = if let Some(with_token) = self.parse_keyword_token_ref(Keyword::WITH) { Some(With { - with_token: with_token.into(), + with_token: with_token.clone().into(), recursive: self.parse_keyword(Keyword::RECURSIVE), cte_tables: self.parse_comma_separated(Parser::parse_cte)?, }) @@ -9452,7 +9540,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::ELEMENTS) { elements = true; } else if self.parse_keyword(Keyword::BINARY) { - self.expect_keyword(Keyword::BASE64)?; + self.expect_keyword_is(Keyword::BASE64)?; binary_base64 = true; } else if self.parse_keyword(Keyword::ROOT) { self.expect_token(&Token::LParen)?; @@ -9536,7 +9624,7 @@ impl<'a> Parser<'a> { } } else { let columns = self.parse_table_alias_column_defs()?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let mut is_materialized = None; if dialect_of!(self is PostgreSqlDialect) { if self.parse_keyword(Keyword::MATERIALIZED) { @@ -9943,7 +10031,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(Keyword::ROLE)?; + self.expect_keyword_is(Keyword::ROLE)?; let context_modifier = match modifier { Some(Keyword::LOCAL) => ContextModifier::Local, Some(Keyword::SESSION) => ContextModifier::Session, @@ -10302,7 +10390,7 @@ impl<'a> Parser<'a> { } fn parse_secondary_roles(&mut self) -> Result { - self.expect_keyword(Keyword::ROLES)?; + self.expect_keyword_is(Keyword::ROLES)?; if self.parse_keyword(Keyword::NONE) { Ok(Use::SecondaryRoles(SecondaryRoles::None)) } else if self.parse_keyword(Keyword::ALL) { @@ -10337,16 +10425,16 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::OUTER) { // MSSQL extension, similar to LEFT JOIN LATERAL .. ON 1=1 - self.expect_keyword(Keyword::APPLY)?; + self.expect_keyword_is(Keyword::APPLY)?; Join { relation: self.parse_table_factor()?, global, join_operator: JoinOperator::OuterApply, } } else if self.parse_keyword(Keyword::ASOF) { - self.expect_keyword(Keyword::JOIN)?; + self.expect_keyword_is(Keyword::JOIN)?; let relation = self.parse_table_factor()?; - self.expect_keyword(Keyword::MATCH_CONDITION)?; + self.expect_keyword_is(Keyword::MATCH_CONDITION)?; let match_condition = self.parse_parenthesized(Self::parse_expr)?; Join { relation, @@ -10367,7 +10455,7 @@ impl<'a> Parser<'a> { let join_operator_type = match peek_keyword { Keyword::INNER | Keyword::JOIN => { let _ = self.parse_keyword(Keyword::INNER); // [ INNER ] - self.expect_keyword(Keyword::JOIN)?; + self.expect_keyword_is(Keyword::JOIN)?; JoinOperator::Inner } kw @ Keyword::LEFT | kw @ Keyword::RIGHT => { @@ -10381,7 +10469,7 @@ impl<'a> Parser<'a> { ]); match join_type { Some(Keyword::OUTER) => { - self.expect_keyword(Keyword::JOIN)?; + self.expect_keyword_is(Keyword::JOIN)?; if is_left { JoinOperator::LeftOuter } else { @@ -10389,7 +10477,7 @@ impl<'a> Parser<'a> { } } Some(Keyword::SEMI) => { - self.expect_keyword(Keyword::JOIN)?; + self.expect_keyword_is(Keyword::JOIN)?; if is_left { JoinOperator::LeftSemi } else { @@ -10397,7 +10485,7 @@ impl<'a> Parser<'a> { } } Some(Keyword::ANTI) => { - self.expect_keyword(Keyword::JOIN)?; + self.expect_keyword_is(Keyword::JOIN)?; if is_left { JoinOperator::LeftAnti } else { @@ -10420,18 +10508,18 @@ impl<'a> Parser<'a> { } Keyword::ANTI => { let _ = self.next_token(); // consume ANTI - self.expect_keyword(Keyword::JOIN)?; + self.expect_keyword_is(Keyword::JOIN)?; JoinOperator::Anti } Keyword::SEMI => { let _ = self.next_token(); // consume SEMI - self.expect_keyword(Keyword::JOIN)?; + self.expect_keyword_is(Keyword::JOIN)?; JoinOperator::Semi } Keyword::FULL => { let _ = self.next_token(); // consume FULL let _ = self.parse_keyword(Keyword::OUTER); // [ OUTER ] - self.expect_keyword(Keyword::JOIN)?; + self.expect_keyword_is(Keyword::JOIN)?; JoinOperator::FullOuter } Keyword::OUTER => { @@ -10603,7 +10691,7 @@ impl<'a> Parser<'a> { ] ) { - self.expect_keyword(Keyword::VALUES)?; + self.expect_keyword_is(Keyword::VALUES)?; // Snowflake and Databricks allow syntax like below: // SELECT * FROM VALUES (1, 'a'), (2, 'b') AS t (col1, col2) @@ -10667,7 +10755,7 @@ impl<'a> Parser<'a> { let json_expr = self.parse_expr()?; self.expect_token(&Token::Comma)?; let json_path = self.parse_value()?; - self.expect_keyword(Keyword::COLUMNS)?; + self.expect_keyword_is(Keyword::COLUMNS)?; self.expect_token(&Token::LParen)?; let columns = self.parse_comma_separated(Parser::parse_json_table_column_def)?; self.expect_token(&Token::RParen)?; @@ -10980,14 +11068,14 @@ impl<'a> Parser<'a> { None }; - self.expect_keyword(Keyword::PATTERN)?; + self.expect_keyword_is(Keyword::PATTERN)?; let pattern = self.parse_parenthesized(Self::parse_pattern)?; - self.expect_keyword(Keyword::DEFINE)?; + self.expect_keyword_is(Keyword::DEFINE)?; let symbols = self.parse_comma_separated(|p| { let symbol = p.parse_identifier(false)?; - p.expect_keyword(Keyword::AS)?; + p.expect_keyword_is(Keyword::AS)?; let definition = p.parse_expr()?; Ok(SymbolDefinition { symbol, definition }) })?; @@ -11152,7 +11240,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::NESTED) { let _has_path_keyword = self.parse_keyword(Keyword::PATH); let path = self.parse_value()?; - self.expect_keyword(Keyword::COLUMNS)?; + self.expect_keyword_is(Keyword::COLUMNS)?; let columns = self.parse_parenthesized(|p| { p.parse_comma_separated(Self::parse_json_table_column_def) })?; @@ -11163,12 +11251,12 @@ impl<'a> Parser<'a> { } let name = self.parse_identifier(false)?; if self.parse_keyword(Keyword::FOR) { - self.expect_keyword(Keyword::ORDINALITY)?; + self.expect_keyword_is(Keyword::ORDINALITY)?; return Ok(JsonTableColumn::ForOrdinality(name)); } let r#type = self.parse_data_type()?; let exists = self.parse_keyword(Keyword::EXISTS); - self.expect_keyword(Keyword::PATH)?; + self.expect_keyword_is(Keyword::PATH)?; let path = self.parse_value()?; let mut on_empty = None; let mut on_error = None; @@ -11176,7 +11264,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::EMPTY) { on_empty = Some(error_handling); } else { - self.expect_keyword(Keyword::ERROR)?; + self.expect_keyword_is(Keyword::ERROR)?; on_error = Some(error_handling); } } @@ -11208,7 +11296,7 @@ impl<'a> Parser<'a> { }; let as_json = self.parse_keyword(Keyword::AS); if as_json { - self.expect_keyword(Keyword::JSON)?; + self.expect_keyword_is(Keyword::JSON)?; } Ok(OpenJsonTableColumn { name, @@ -11230,7 +11318,7 @@ impl<'a> Parser<'a> { } else { return Ok(None); }; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; Ok(Some(res)) } @@ -11304,9 +11392,9 @@ impl<'a> Parser<'a> { ) -> Result { self.expect_token(&Token::LParen)?; let aggregate_functions = self.parse_comma_separated(Self::parse_aliased_function_call)?; - self.expect_keyword(Keyword::FOR)?; + self.expect_keyword_is(Keyword::FOR)?; let value_column = self.parse_object_name(false)?.0; - self.expect_keyword(Keyword::IN)?; + self.expect_keyword_is(Keyword::IN)?; self.expect_token(&Token::LParen)?; let value_source = if self.parse_keyword(Keyword::ANY) { @@ -11351,9 +11439,9 @@ impl<'a> Parser<'a> { ) -> Result { self.expect_token(&Token::LParen)?; let value = self.parse_identifier(false)?; - self.expect_keyword(Keyword::FOR)?; + self.expect_keyword_is(Keyword::FOR)?; let name = self.parse_identifier(false)?; - self.expect_keyword(Keyword::IN)?; + self.expect_keyword_is(Keyword::IN)?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; self.expect_token(&Token::RParen)?; let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; @@ -11385,7 +11473,7 @@ impl<'a> Parser<'a> { pub fn parse_grant(&mut self) -> Result { let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; - self.expect_keyword(Keyword::TO)?; + self.expect_keyword_is(Keyword::TO)?; let grantees = self.parse_comma_separated(|p| p.parse_identifier(false))?; let with_grant_option = @@ -11445,7 +11533,7 @@ impl<'a> Parser<'a> { Privileges::Actions(act) }; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; let objects = if self.parse_keywords(&[ Keyword::ALL, @@ -11516,7 +11604,7 @@ impl<'a> Parser<'a> { pub fn parse_revoke(&mut self) -> Result { let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; - self.expect_keyword(Keyword::FROM)?; + self.expect_keyword_is(Keyword::FROM)?; let grantees = self.parse_comma_separated(|p| p.parse_identifier(false))?; let granted_by = self @@ -11667,12 +11755,12 @@ impl<'a> Parser<'a> { None }; - self.expect_keyword(Keyword::DO)?; + self.expect_keyword_is(Keyword::DO)?; let action = if self.parse_keyword(Keyword::NOTHING) { OnConflictAction::DoNothing } else { - self.expect_keyword(Keyword::UPDATE)?; - self.expect_keyword(Keyword::SET)?; + self.expect_keyword_is(Keyword::UPDATE)?; + self.expect_keyword_is(Keyword::SET)?; let assignments = self.parse_comma_separated(Parser::parse_assignment)?; let selection = if self.parse_keyword(Keyword::WHERE) { Some(self.parse_expr()?) @@ -11690,9 +11778,9 @@ impl<'a> Parser<'a> { action, })) } else { - self.expect_keyword(Keyword::DUPLICATE)?; - self.expect_keyword(Keyword::KEY)?; - self.expect_keyword(Keyword::UPDATE)?; + self.expect_keyword_is(Keyword::DUPLICATE)?; + self.expect_keyword_is(Keyword::KEY)?; + self.expect_keyword_is(Keyword::UPDATE)?; let l = self.parse_comma_separated(Parser::parse_assignment)?; Some(OnInsert::DuplicateKeyUpdate(l)) @@ -11770,7 +11858,7 @@ impl<'a> Parser<'a> { ) -> Result, ParserError> { if self.parse_keyword(Keyword::INPUTFORMAT) { let input_format = self.parse_expr()?; - self.expect_keyword(Keyword::SERDE)?; + self.expect_keyword_is(Keyword::SERDE)?; let serde = self.parse_expr()?; Ok(Some(HiveLoadDataFormat { input_format, @@ -12481,7 +12569,7 @@ impl<'a> Parser<'a> { } pub fn parse_start_transaction(&mut self) -> Result { - self.expect_keyword(Keyword::TRANSACTION)?; + self.expect_keyword_is(Keyword::TRANSACTION)?; Ok(Statement::StartTransaction { modes: self.parse_transaction_modes()?, begin: false, @@ -12574,7 +12662,7 @@ impl<'a> Parser<'a> { let _ = self.parse_one_of_keywords(&[Keyword::TRANSACTION, Keyword::WORK]); if self.parse_keyword(Keyword::AND) { let chain = !self.parse_keyword(Keyword::NO); - self.expect_keyword(Keyword::CHAIN)?; + self.expect_keyword_is(Keyword::CHAIN)?; Ok(chain) } else { Ok(false) @@ -12642,7 +12730,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; } - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let statement = Box::new(self.parse_statement()?); Ok(Statement::Prepare { name, @@ -12656,7 +12744,7 @@ impl<'a> Parser<'a> { let query = self.parse_query()?; self.expect_token(&Token::RParen)?; - self.expect_keyword(Keyword::TO)?; + self.expect_keyword_is(Keyword::TO)?; let to = self.parse_identifier(false)?; let with_options = self.parse_options(Keyword::WITH)?; @@ -12674,13 +12762,13 @@ impl<'a> Parser<'a> { if self.peek_token() == Token::EOF || self.peek_token() == Token::SemiColon { break; } - self.expect_keyword(Keyword::WHEN)?; + self.expect_keyword_is(Keyword::WHEN)?; let mut clause_kind = MergeClauseKind::Matched; if self.parse_keyword(Keyword::NOT) { clause_kind = MergeClauseKind::NotMatched; } - self.expect_keyword(Keyword::MATCHED)?; + self.expect_keyword_is(Keyword::MATCHED)?; if matches!(clause_kind, MergeClauseKind::NotMatched) && self.parse_keywords(&[Keyword::BY, Keyword::SOURCE]) @@ -12698,7 +12786,7 @@ impl<'a> Parser<'a> { None }; - self.expect_keyword(Keyword::THEN)?; + self.expect_keyword_is(Keyword::THEN)?; let merge_clause = match self.parse_one_of_keywords(&[ Keyword::UPDATE, @@ -12714,7 +12802,7 @@ impl<'a> Parser<'a> { "UPDATE is not allowed in a {clause_kind} merge clause" ))); } - self.expect_keyword(Keyword::SET)?; + self.expect_keyword_is(Keyword::SET)?; MergeAction::Update { assignments: self.parse_comma_separated(Parser::parse_assignment)?, } @@ -12747,7 +12835,7 @@ impl<'a> Parser<'a> { { MergeInsertKind::Row } else { - self.expect_keyword(Keyword::VALUES)?; + self.expect_keyword_is(Keyword::VALUES)?; let values = self.parse_values(is_mysql)?; MergeInsertKind::Values(values) }; @@ -12773,9 +12861,9 @@ impl<'a> Parser<'a> { let table = self.parse_table_factor()?; - self.expect_keyword(Keyword::USING)?; + self.expect_keyword_is(Keyword::USING)?; let source = self.parse_table_factor()?; - self.expect_keyword(Keyword::ON)?; + self.expect_keyword_is(Keyword::ON)?; let on = self.parse_expr()?; let clauses = self.parse_merge_clauses()?; @@ -12841,11 +12929,11 @@ impl<'a> Parser<'a> { Ok(Statement::Load { extension_name }) } else if self.parse_keyword(Keyword::DATA) && self.dialect.supports_load_data() { let local = self.parse_one_of_keywords(&[Keyword::LOCAL]).is_some(); - self.expect_keyword(Keyword::INPATH)?; + self.expect_keyword_is(Keyword::INPATH)?; let inpath = self.parse_literal_string()?; let overwrite = self.parse_one_of_keywords(&[Keyword::OVERWRITE]).is_some(); - self.expect_keyword(Keyword::INTO)?; - self.expect_keyword(Keyword::TABLE)?; + self.expect_keyword_is(Keyword::INTO)?; + self.expect_keyword_is(Keyword::TABLE)?; let table_name = self.parse_object_name(false)?; let partitioned = self.parse_insert_partition()?; let table_format = self.parse_load_data_table_format()?; @@ -12870,7 +12958,7 @@ impl<'a> Parser<'a> { /// ``` /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/optimize) pub fn parse_optimize_table(&mut self) -> Result { - self.expect_keyword(Keyword::TABLE)?; + self.expect_keyword_is(Keyword::TABLE)?; let name = self.parse_object_name(false)?; let on_cluster = self.parse_optional_on_cluster()?; @@ -12992,7 +13080,7 @@ impl<'a> Parser<'a> { pub fn parse_named_window(&mut self) -> Result { let ident = self.parse_identifier(false)?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let window_expr = if self.consume_token(&Token::LParen) { NamedWindowExpr::WindowSpec(self.parse_window_spec()?) @@ -13008,10 +13096,10 @@ 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()?; - self.expect_keyword(Keyword::AS)?; - self.expect_keyword(Keyword::BEGIN)?; + self.expect_keyword_is(Keyword::AS)?; + self.expect_keyword_is(Keyword::BEGIN)?; let statements = self.parse_statements()?; - self.expect_keyword(Keyword::END)?; + self.expect_keyword_is(Keyword::END)?; Ok(Statement::CreateProcedure { name, or_alter, @@ -13056,7 +13144,7 @@ impl<'a> Parser<'a> { pub fn parse_create_type(&mut self) -> Result { let name = self.parse_object_name(false)?; - self.expect_keyword(Keyword::AS)?; + self.expect_keyword_is(Keyword::AS)?; let mut attributes = vec![]; if !self.consume_token(&Token::LParen) || self.consume_token(&Token::RParen) { From d89cf801dde7a3b0c33246bf65061d2bc6a9fea2 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 27 Dec 2024 06:24:58 -0500 Subject: [PATCH 654/806] Improve Parser documentation (#1617) --- src/parser/mod.rs | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2190b51d8..3b582701f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -276,19 +276,58 @@ enum ParserState { ConnectBy, } +/// A SQL Parser +/// +/// This struct is the main entry point for parsing SQL queries. +/// +/// # Functionality: +/// * Parsing SQL: see examples on [`Parser::new`] and [`Parser::parse_sql`] +/// * Controlling recursion: See [`Parser::with_recursion_limit`] +/// * Controlling parser options: See [`Parser::with_options`] +/// * Providing your own tokens: See [`Parser::with_tokens`] +/// +/// # Internals +/// +/// The parser uses a [`Tokenizer`] to tokenize the input SQL string into a +/// `Vec` of [`TokenWithSpan`]s and maintains an `index` to the current token +/// being processed. The token vec may contain multiple SQL statements. +/// +/// * The "current" token is the token at `index - 1` +/// * The "next" token is the token at `index` +/// * The "previous" token is the token at `index - 2` +/// +/// If `index` is equal to the length of the token stream, the 'next' token is +/// [`Token::EOF`]. +/// +/// For example, the SQL string "SELECT * FROM foo" will be tokenized into +/// following tokens: +/// ```text +/// [ +/// "SELECT", // token index 0 +/// " ", // whitespace +/// "*", +/// " ", +/// "FROM", +/// " ", +/// "foo" +/// ] +/// ``` +/// +/// pub struct Parser<'a> { + /// The tokens tokens: Vec, /// The index of the first unprocessed token in [`Parser::tokens`]. index: usize, /// The current state of the parser. state: ParserState, - /// The current dialect to use. + /// The SQL dialect to use. dialect: &'a dyn Dialect, /// Additional options that allow you to mix & match behavior /// otherwise constrained to certain dialects (e.g. trailing /// commas) and/or format of parse (e.g. unescaping). options: ParserOptions, - /// Ensure the stack does not overflow by limiting recursion depth. + /// Ensures the stack does not overflow by limiting recursion depth. recursion_counter: RecursionCounter, } From 7dbf31b5875c4643dee493367203d4f29e7f1033 Mon Sep 17 00:00:00 2001 From: Ramnivas Laddad Date: Fri, 27 Dec 2024 04:19:42 -0800 Subject: [PATCH 655/806] Add support for DROP EXTENSION (#1610) --- src/ast/mod.rs | 27 +++++++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 23 ++++++++- tests/sqlparser_postgres.rs | 94 +++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5bdce21ef..d99f68886 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2755,6 +2755,18 @@ pub enum Statement { version: Option, }, /// ```sql + /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + /// + /// Note: this is a PostgreSQL-specific statement. + /// https://www.postgresql.org/docs/current/sql-dropextension.html + /// ``` + DropExtension { + names: Vec, + if_exists: bool, + /// `CASCADE` or `RESTRICT` + cascade_or_restrict: Option, + }, + /// ```sql /// FETCH /// ``` /// Retrieve rows from a query using a cursor @@ -4029,6 +4041,21 @@ impl fmt::Display for Statement { Ok(()) } + Statement::DropExtension { + names, + if_exists, + cascade_or_restrict, + } => { + write!(f, "DROP EXTENSION")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", display_comma_separated(names))?; + if let Some(cascade_or_restrict) = cascade_or_restrict { + write!(f, " {cascade_or_restrict}")?; + } + Ok(()) + } Statement::CreateRole { names, if_not_exists, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 521b5399a..574830ef5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -430,6 +430,7 @@ impl Spanned for Statement { Statement::DropSecret { .. } => Span::empty(), Statement::Declare { .. } => Span::empty(), Statement::CreateExtension { .. } => Span::empty(), + Statement::DropExtension { .. } => Span::empty(), Statement::Fetch { .. } => Span::empty(), Statement::Flush { .. } => Span::empty(), Statement::Discard { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3b582701f..2756ed6ca 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5414,9 +5414,11 @@ impl<'a> Parser<'a> { return self.parse_drop_secret(temporary, persistent); } else if self.parse_keyword(Keyword::TRIGGER) { return self.parse_drop_trigger(); + } else if self.parse_keyword(Keyword::EXTENSION) { + return self.parse_drop_extension(); } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, SCHEMA, DATABASE, FUNCTION, PROCEDURE, STAGE, TRIGGER, SECRET, SEQUENCE, or TYPE after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, DATABASE, FUNCTION, PROCEDURE, STAGE, TRIGGER, SECRET, SEQUENCE, TYPE, or EXTENSION after DROP", self.peek_token(), ); }; @@ -6079,6 +6081,25 @@ impl<'a> Parser<'a> { }) } + /// Parse a PostgreSQL-specific [Statement::DropExtension] statement. + pub fn parse_drop_extension(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let names = self.parse_comma_separated(|p| p.parse_identifier(false))?; + let cascade_or_restrict = + self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]); + Ok(Statement::DropExtension { + names, + if_exists, + cascade_or_restrict: cascade_or_restrict + .map(|k| match k { + Keyword::CASCADE => Ok(ReferentialAction::Cascade), + Keyword::RESTRICT => Ok(ReferentialAction::Restrict), + _ => self.expected("CASCADE or RESTRICT", self.peek_token()), + }) + .transpose()?, + }) + } + //TODO: Implement parsing for Skewed pub fn parse_hive_distribution(&mut self) -> Result { if self.parse_keywords(&[Keyword::PARTITIONED, Keyword::BY]) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 557e70bff..fd520d507 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -662,6 +662,100 @@ fn parse_create_extension() { .verified_stmt("CREATE EXTENSION extension_name WITH SCHEMA schema_name VERSION version"); } +#[test] +fn parse_drop_extension() { + assert_eq!( + pg_and_generic().verified_stmt("DROP EXTENSION extension_name"), + Statement::DropExtension { + names: vec!["extension_name".into()], + if_exists: false, + cascade_or_restrict: None, + } + ); + assert_eq!( + pg_and_generic().verified_stmt("DROP EXTENSION extension_name CASCADE"), + Statement::DropExtension { + names: vec!["extension_name".into()], + if_exists: false, + cascade_or_restrict: Some(ReferentialAction::Cascade), + } + ); + + assert_eq!( + pg_and_generic().verified_stmt("DROP EXTENSION extension_name RESTRICT"), + Statement::DropExtension { + names: vec!["extension_name".into()], + if_exists: false, + cascade_or_restrict: Some(ReferentialAction::Restrict), + } + ); + + assert_eq!( + pg_and_generic().verified_stmt("DROP EXTENSION extension_name, extension_name2 CASCADE"), + Statement::DropExtension { + names: vec!["extension_name".into(), "extension_name2".into()], + if_exists: false, + cascade_or_restrict: Some(ReferentialAction::Cascade), + } + ); + + assert_eq!( + pg_and_generic().verified_stmt("DROP EXTENSION extension_name, extension_name2 RESTRICT"), + Statement::DropExtension { + names: vec!["extension_name".into(), "extension_name2".into()], + if_exists: false, + cascade_or_restrict: Some(ReferentialAction::Restrict), + } + ); + + assert_eq!( + pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name"), + Statement::DropExtension { + names: vec!["extension_name".into()], + if_exists: true, + cascade_or_restrict: None, + } + ); + + assert_eq!( + pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name CASCADE"), + Statement::DropExtension { + names: vec!["extension_name".into()], + if_exists: true, + cascade_or_restrict: Some(ReferentialAction::Cascade), + } + ); + + assert_eq!( + pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name RESTRICT"), + Statement::DropExtension { + names: vec!["extension_name".into()], + if_exists: true, + cascade_or_restrict: Some(ReferentialAction::Restrict), + } + ); + + assert_eq!( + pg_and_generic() + .verified_stmt("DROP EXTENSION IF EXISTS extension_name1, extension_name2 CASCADE"), + Statement::DropExtension { + names: vec!["extension_name1".into(), "extension_name2".into()], + if_exists: true, + cascade_or_restrict: Some(ReferentialAction::Cascade), + } + ); + + assert_eq!( + pg_and_generic() + .verified_stmt("DROP EXTENSION IF EXISTS extension_name1, extension_name2 RESTRICT"), + Statement::DropExtension { + names: vec!["extension_name1".into(), "extension_name2".into()], + if_exists: true, + cascade_or_restrict: Some(ReferentialAction::Restrict), + } + ); +} + #[test] fn parse_alter_table_alter_column() { pg().one_statement_parses_to( From 6daa4b059cde8b77b67a3699b174ef0f8edff350 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 27 Dec 2024 09:17:52 -0500 Subject: [PATCH 656/806] Refactor advancing token to avoid duplication, avoid borrow checker issues (#1618) Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 102 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2756ed6ca..65991d324 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1315,7 +1315,9 @@ impl<'a> Parser<'a> { let dialect = self.dialect; - let (next_token, next_token_index) = self.next_token_ref_with_index(); + self.advance_token(); + let next_token_index = self.get_current_index(); + let next_token = self.get_current_token(); let span = next_token.span; let expr = match &next_token.token { Token::Word(w) => { @@ -2953,7 +2955,9 @@ impl<'a> Parser<'a> { let dialect = self.dialect; - let (tok, tok_index) = self.next_token_ref_with_index(); + self.advance_token(); + let tok = self.get_current_token(); + let tok_index = self.get_current_index(); let span = tok.span; let regular_binary_operator = match &tok.token { Token::Spaceship => Some(BinaryOperator::Spaceship), @@ -3033,7 +3037,8 @@ impl<'a> Parser<'a> { // See https://www.postgresql.org/docs/current/sql-createoperator.html let mut idents = vec![]; loop { - idents.push(self.next_token_ref().to_string()); + self.advance_token(); + idents.push(self.get_current_token().to_string()); if !self.consume_token(&Token::Period) { break; } @@ -3480,6 +3485,8 @@ impl<'a> Parser<'a> { /// Return the first non-whitespace token that has not yet been processed /// or Token::EOF + /// + /// See [`Self::peek_token_ref`] to avoid the copy. pub fn peek_token(&self) -> TokenWithSpan { self.peek_nth_token(0) } @@ -3594,21 +3601,31 @@ impl<'a> Parser<'a> { /// Advances to the next non-whitespace token and returns a copy. /// - /// See [`Self::next_token_ref`] to avoid the copy. + /// Please use [`Self::advance_token`] and [`Self::get_current_token`] to + /// avoid the copy. pub fn next_token(&mut self) -> TokenWithSpan { - self.next_token_ref().clone() + self.advance_token(); + self.get_current_token().clone() } - pub fn next_token_ref(&mut self) -> &TokenWithSpan { - self.next_token_ref_with_index().0 + /// Returns the index of the current token + /// + /// This can be used with APIs that expect an index, such as + /// [`Self::token_at`] + pub fn get_current_index(&self) -> usize { + self.index.saturating_sub(1) } - /// Return the first non-whitespace token that has not yet been processed - /// and that tokens index and advances the tokens + /// Return the next unprocessed token, possibly whitespace. + pub fn next_token_no_skip(&mut self) -> Option<&TokenWithSpan> { + self.index += 1; + self.tokens.get(self.index - 1) + } + + /// Advances the current token to the next non-whitespace token /// - /// # Notes: - /// OK to call repeatedly after reaching EOF. - pub fn next_token_ref_with_index(&mut self) -> (&TokenWithSpan, usize) { + /// See [`Self::get_current_token`] to get the current token after advancing + pub fn advance_token(&mut self) { loop { self.index += 1; match self.tokens.get(self.index - 1) { @@ -3616,25 +3633,38 @@ impl<'a> Parser<'a> { token: Token::Whitespace(_), span: _, }) => continue, - token => return (token.unwrap_or(&EOF_TOKEN), self.index - 1), + _ => break, } } } /// Returns a reference to the current token - pub fn current_token(&self) -> &TokenWithSpan { - self.tokens.get(self.index - 1).unwrap_or(&EOF_TOKEN) + /// + /// Does not advance the current token. + pub fn get_current_token(&self) -> &TokenWithSpan { + self.token_at(self.index.saturating_sub(1)) } - /// Return the first unprocessed token, possibly whitespace. - pub fn next_token_no_skip(&mut self) -> Option<&TokenWithSpan> { - self.index += 1; - self.tokens.get(self.index - 1) + /// Returns a reference to the previous token + /// + /// Does not advance the current token. + pub fn get_previous_token(&self) -> &TokenWithSpan { + self.token_at(self.index.saturating_sub(2)) } - /// Push back the last one non-whitespace token. Must be called after - /// `next_token()`, otherwise might panic. OK to call after - /// `next_token()` indicates an EOF. + /// Returns a reference to the next token + /// + /// Does not advance the current token. + pub fn get_next_token(&self) -> &TokenWithSpan { + self.token_at(self.index) + } + + /// Seek back the last one non-whitespace token. + /// + /// Must be called after `next_token()`, otherwise might panic. OK to call + /// after `next_token()` indicates an EOF. + /// + // TODO rename to backup_token and deprecate prev_token? pub fn prev_token(&mut self) { loop { assert!(self.index > 0); @@ -3680,22 +3710,30 @@ impl<'a> Parser<'a> { #[must_use] pub fn parse_keyword(&mut self, expected: Keyword) -> bool { if self.peek_keyword(expected) { - self.next_token_ref(); + self.advance_token(); true } else { false } } + /// If the current token is the `expected` keyword, consume it and returns + /// + /// See [`Self::parse_keyword_token_ref`] to avoid the copy. #[must_use] pub fn parse_keyword_token(&mut self, expected: Keyword) -> Option { self.parse_keyword_token_ref(expected).cloned() } + /// If the current token is the `expected` keyword, consume it and returns a reference to the next token. + /// #[must_use] pub fn parse_keyword_token_ref(&mut self, expected: Keyword) -> Option<&TokenWithSpan> { match &self.peek_token_ref().token { - Token::Word(w) if expected == w.keyword => Some(self.next_token_ref()), + Token::Word(w) if expected == w.keyword => { + self.advance_token(); + Some(self.get_current_token()) + } _ => None, } } @@ -3722,7 +3760,7 @@ impl<'a> Parser<'a> { } // consume all tokens for _ in 0..(tokens.len() + 1) { - self.next_token_ref(); + self.advance_token(); } true } @@ -3758,7 +3796,7 @@ impl<'a> Parser<'a> { .iter() .find(|keyword| **keyword == w.keyword) .map(|keyword| { - self.next_token_ref(); + self.advance_token(); *keyword }) } @@ -3813,10 +3851,12 @@ impl<'a> Parser<'a> { } /// Consume the next token if it matches the expected token, otherwise return false + /// + /// See [Self::advance_token] to consume the token unconditionally #[must_use] pub fn consume_token(&mut self, expected: &Token) -> bool { if self.peek_token_ref() == expected { - self.next_token_ref(); + self.advance_token(); true } else { false @@ -8338,9 +8378,9 @@ impl<'a> Parser<'a> { &mut self, ) -> Result<(DataType, MatchedTrailingBracket), ParserError> { let dialect = self.dialect; - let (next_token, next_token_index) = self.next_token_ref_with_index(); - let _ = next_token; // release ref - let next_token = self.current_token(); + self.advance_token(); + let next_token = self.get_current_token(); + let next_token_index = self.get_current_index(); let mut trailing_bracket: MatchedTrailingBracket = false.into(); let mut data = match &next_token.token { @@ -8866,7 +8906,7 @@ impl<'a> Parser<'a> { Token::EOF | Token::Eq => break, _ => {} } - self.next_token_ref(); + self.advance_token(); } Ok(idents) } From d0d4153137849027867526e270cc0a9464166194 Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Sat, 28 Dec 2024 21:16:30 +0800 Subject: [PATCH 657/806] Fix the parsing result for the special double number (#1621) --- src/dialect/mysql.rs | 2 +- src/dialect/postgresql.rs | 2 +- src/dialect/snowflake.rs | 8 +- src/parser/alter.rs | 16 +- src/parser/mod.rs | 460 ++++++++++++++++++++------------------ src/tokenizer.rs | 45 +--- tests/sqlparser_common.rs | 142 ++++++++++++ 7 files changed, 410 insertions(+), 265 deletions(-) diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 197ce48d4..1ede59f5a 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -113,7 +113,7 @@ fn parse_lock_tables(parser: &mut Parser) -> Result { // tbl_name [[AS] alias] lock_type fn parse_lock_table(parser: &mut Parser) -> Result { - let table = parser.parse_identifier(false)?; + let table = parser.parse_identifier()?; let alias = parser.parse_optional_alias(&[Keyword::READ, Keyword::WRITE, Keyword::LOW_PRIORITY])?; let lock_type = parse_lock_tables_type(parser)?; diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 32a56743b..6a13a386a 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -268,7 +268,7 @@ pub fn parse_create_type_as_enum( return parser.expected("'(' after CREATE TYPE AS ENUM", parser.peek_token()); } - let labels = parser.parse_comma_separated0(|p| p.parse_identifier(false), Token::RParen)?; + let labels = parser.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; parser.expect_token(&Token::RParen)?; Ok(Statement::CreateType { diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index c6f92daea..249241d73 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -300,7 +300,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(false))?, + parser.parse_comma_separated(|p| p.parse_identifier())?, )); parser.expect_token(&Token::RParen)?; @@ -369,7 +369,7 @@ pub fn parse_create_table( let policy = parser.parse_object_name(false)?; parser.expect_keyword_is(Keyword::ON)?; parser.expect_token(&Token::LParen)?; - let columns = parser.parse_comma_separated(|p| p.parse_identifier(false))?; + let columns = parser.parse_comma_separated(|p| p.parse_identifier())?; parser.expect_token(&Token::RParen)?; builder = @@ -887,10 +887,10 @@ fn parse_column_policy_property( parser: &mut Parser, with: bool, ) -> Result { - let policy_name = parser.parse_identifier(false)?; + let policy_name = parser.parse_identifier()?; let using_columns = if parser.parse_keyword(Keyword::USING) { parser.expect_token(&Token::LParen)?; - let columns = parser.parse_comma_separated(|p| p.parse_identifier(false))?; + let columns = parser.parse_comma_separated(|p| p.parse_identifier())?; parser.expect_token(&Token::RParen)?; Some(columns) } else { diff --git a/src/parser/alter.rs b/src/parser/alter.rs index a32e93d9f..bb6782c13 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -51,13 +51,13 @@ impl Parser<'_> { /// /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterpolicy.html) pub fn parse_alter_policy(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; if self.parse_keyword(Keyword::RENAME) { self.expect_keyword_is(Keyword::TO)?; - let new_name = self.parse_identifier(false)?; + let new_name = self.parse_identifier()?; Ok(Statement::AlterPolicy { name, table_name, @@ -100,17 +100,17 @@ impl Parser<'_> { } fn parse_mssql_alter_role(&mut self) -> Result { - let role_name = self.parse_identifier(false)?; + let role_name = self.parse_identifier()?; let operation = if self.parse_keywords(&[Keyword::ADD, Keyword::MEMBER]) { - let member_name = self.parse_identifier(false)?; + let member_name = self.parse_identifier()?; AlterRoleOperation::AddMember { member_name } } else if self.parse_keywords(&[Keyword::DROP, Keyword::MEMBER]) { - let member_name = self.parse_identifier(false)?; + let member_name = self.parse_identifier()?; AlterRoleOperation::DropMember { member_name } } else if self.parse_keywords(&[Keyword::WITH, Keyword::NAME]) { if self.consume_token(&Token::Eq) { - let role_name = self.parse_identifier(false)?; + let role_name = self.parse_identifier()?; AlterRoleOperation::RenameRole { role_name } } else { return self.expected("= after WITH NAME ", self.peek_token()); @@ -126,7 +126,7 @@ impl Parser<'_> { } fn parse_pg_alter_role(&mut self) -> Result { - let role_name = self.parse_identifier(false)?; + let role_name = self.parse_identifier()?; // [ IN DATABASE _`database_name`_ ] let in_database = if self.parse_keywords(&[Keyword::IN, Keyword::DATABASE]) { @@ -137,7 +137,7 @@ impl Parser<'_> { let operation = if self.parse_keyword(Keyword::RENAME) { if self.parse_keyword(Keyword::TO) { - let role_name = self.parse_identifier(false)?; + let role_name = self.parse_identifier()?; AlterRoleOperation::RenameRole { role_name } } else { return self.expected("TO after RENAME", self.peek_token()); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 65991d324..5d1b1c37b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -845,7 +845,7 @@ impl<'a> Parser<'a> { }; options.push(AttachDuckDBDatabaseOption::ReadOnly(boolean)); } else if self.parse_keyword(Keyword::TYPE) { - let ident = self.parse_identifier(false)?; + let ident = self.parse_identifier()?; options.push(AttachDuckDBDatabaseOption::Type(ident)); } else { return self.expected("expected one of: ), READ_ONLY, TYPE", self.peek_token()); @@ -864,9 +864,9 @@ impl<'a> Parser<'a> { pub fn parse_attach_duckdb_database(&mut self) -> Result { let database = self.parse_keyword(Keyword::DATABASE); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let database_path = self.parse_identifier(false)?; + let database_path = self.parse_identifier()?; let database_alias = if self.parse_keyword(Keyword::AS) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -884,7 +884,7 @@ impl<'a> Parser<'a> { pub fn parse_detach_duckdb_database(&mut self) -> Result { let database = self.parse_keyword(Keyword::DATABASE); let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let database_alias = self.parse_identifier(false)?; + let database_alias = self.parse_identifier()?; Ok(Statement::DetachDuckDBDatabase { if_exists, database, @@ -896,7 +896,7 @@ impl<'a> Parser<'a> { let database = self.parse_keyword(Keyword::DATABASE); let database_file_name = self.parse_expr()?; self.expect_keyword_is(Keyword::AS)?; - let schema_name = self.parse_identifier(false)?; + let schema_name = self.parse_identifier()?; Ok(Statement::AttachDatabase { database, schema_name, @@ -932,7 +932,7 @@ impl<'a> Parser<'a> { columns = self .maybe_parse(|parser| { - parser.parse_comma_separated(|p| p.parse_identifier(false)) + parser.parse_comma_separated(|p| p.parse_identifier()) })? .unwrap_or_default(); for_columns = true @@ -1017,13 +1017,6 @@ impl<'a> Parser<'a> { let _guard = self.recursion_counter.try_decrease()?; debug!("parsing expr"); let mut expr = self.parse_prefix()?; - // Attempt to parse composite access. Example `SELECT f(x).a` - while self.consume_token(&Token::Period) { - expr = Expr::CompositeAccess { - expr: Box::new(expr), - key: self.parse_identifier(false)?, - } - } debug!("prefix: {:?}", expr); loop { @@ -1051,19 +1044,19 @@ impl<'a> Parser<'a> { } pub fn parse_savepoint(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; Ok(Statement::Savepoint { name }) } pub fn parse_release(&mut self) -> Result { let _ = self.parse_keyword(Keyword::SAVEPOINT); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; Ok(Statement::ReleaseSavepoint { name }) } pub fn parse_listen(&mut self) -> Result { - let channel = self.parse_identifier(false)?; + let channel = self.parse_identifier()?; Ok(Statement::LISTEN { channel }) } @@ -1071,7 +1064,7 @@ impl<'a> Parser<'a> { let channel = if self.consume_token(&Token::Mul) { Ident::new(Expr::Wildcard(AttachedToken::empty()).to_string()) } else { - match self.parse_identifier(false) { + match self.parse_identifier() { Ok(expr) => expr, _ => { self.prev_token(); @@ -1083,7 +1076,7 @@ impl<'a> Parser<'a> { } pub fn parse_notify(&mut self) -> Result { - let channel = self.parse_identifier(false)?; + let channel = self.parse_identifier()?; let payload = if self.consume_token(&Token::Comma) { Some(self.parse_literal_string()?) } else { @@ -1189,7 +1182,8 @@ impl<'a> Parser<'a> { Ok(Some(self.parse_match_against()?)) } Keyword::STRUCT if self.dialect.supports_struct_literal() => { - Ok(Some(self.parse_struct_literal()?)) + let struct_expr = self.parse_struct_literal()?; + Ok(Some(self.parse_compound_field_access(struct_expr, vec![])?)) } Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => { let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?; @@ -1438,7 +1432,25 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; - self.try_parse_method(expr) + let expr = self.try_parse_method(expr)?; + if !self.consume_token(&Token::Period) { + Ok(expr) + } else { + let tok = self.next_token(); + let key = match tok.token { + Token::Word(word) => word.to_ident(tok.span), + _ => { + return parser_err!( + format!("Expected identifier, found: {tok}"), + tok.span.start + ) + } + }; + Ok(Expr::CompositeAccess { + expr: Box::new(expr), + key, + }) + } } Token::Placeholder(_) | Token::Colon | Token::AtSign => { self.prev_token(); @@ -1610,7 +1622,7 @@ impl<'a> Parser<'a> { } fn parse_utility_option(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let next_token = self.peek_token(); if next_token == Token::Comma || next_token == Token::RParen { @@ -1637,7 +1649,7 @@ impl<'a> Parser<'a> { return Ok(None); } self.maybe_parse(|p| { - let params = p.parse_comma_separated(|p| p.parse_identifier(false))?; + let params = p.parse_comma_separated(|p| p.parse_identifier())?; p.expect_token(&Token::RParen)?; p.expect_token(&Token::Arrow)?; let expr = p.parse_expr()?; @@ -1775,7 +1787,7 @@ impl<'a> Parser<'a> { let window_spec = self.parse_window_spec()?; Some(WindowType::WindowSpec(window_spec)) } else { - Some(WindowType::NamedWindow(self.parse_identifier(false)?)) + Some(WindowType::NamedWindow(self.parse_identifier()?)) } } else { None @@ -2332,7 +2344,7 @@ impl<'a> Parser<'a> { let week_day = if dialect_of!(self is BigQueryDialect | GenericDialect) && self.consume_token(&Token::LParen) { - let week_day = self.parse_identifier(false)?; + let week_day = self.parse_identifier()?; self.expect_token(&Token::RParen)?; Some(week_day) } else { @@ -2374,14 +2386,14 @@ impl<'a> Parser<'a> { Keyword::TIMEZONE_REGION => Ok(DateTimeField::TimezoneRegion), _ if self.dialect.allow_extract_custom() => { self.prev_token(); - let custom = self.parse_identifier(false)?; + let custom = self.parse_identifier()?; Ok(DateTimeField::Custom(custom)) } _ => self.expected("date/time field", next_token), }, Token::SingleQuotedString(_) if self.dialect.allow_extract_single_quotes() => { self.prev_token(); - let custom = self.parse_identifier(false)?; + let custom = self.parse_identifier()?; Ok(DateTimeField::Custom(custom)) } _ => self.expected("date/time field", next_token), @@ -2656,7 +2668,7 @@ impl<'a> Parser<'a> { self.peek_token().span.start }); } - let field_name = self.parse_identifier(false)?; + let field_name = self.parse_identifier()?; Ok(Expr::Named { expr: expr.into(), name: field_name, @@ -2721,7 +2733,7 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::STRUCT)?; self.expect_token(&Token::LParen)?; let struct_body = self.parse_comma_separated(|parser| { - let field_name = parser.parse_identifier(false)?; + let field_name = parser.parse_identifier()?; let field_type = parser.parse_data_type()?; Ok(StructField { @@ -2755,7 +2767,7 @@ impl<'a> Parser<'a> { let field_name = if is_anonymous_field { None } else { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) }; let (field_type, trailing_bracket) = self.parse_data_type_helper()?; @@ -2785,7 +2797,7 @@ impl<'a> Parser<'a> { let fields = self.parse_comma_separated(|p| { Ok(UnionField { - field_name: p.parse_identifier(false)?, + field_name: p.parse_identifier()?, field_type: p.parse_data_type()?, }) })?; @@ -2824,7 +2836,7 @@ impl<'a> Parser<'a> { /// /// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs fn parse_duckdb_dictionary_field(&mut self) -> Result { - let key = self.parse_identifier(false)?; + let key = self.parse_identifier()?; self.expect_token(&Token::Colon)?; @@ -4182,9 +4194,9 @@ impl<'a> Parser<'a> { let mut name = None; if self.peek_token() != Token::LParen { if self.parse_keyword(Keyword::IN) { - storage_specifier = self.parse_identifier(false).ok() + storage_specifier = self.parse_identifier().ok() } else { - name = self.parse_identifier(false).ok(); + name = self.parse_identifier().ok(); } // Storage specifier may follow the name @@ -4192,19 +4204,19 @@ impl<'a> Parser<'a> { && self.peek_token() != Token::LParen && self.parse_keyword(Keyword::IN) { - storage_specifier = self.parse_identifier(false).ok(); + storage_specifier = self.parse_identifier().ok(); } } self.expect_token(&Token::LParen)?; self.expect_keyword_is(Keyword::TYPE)?; - let secret_type = self.parse_identifier(false)?; + let secret_type = self.parse_identifier()?; let mut options = Vec::new(); if self.consume_token(&Token::Comma) { options.append(&mut self.parse_comma_separated(|p| { - let key = p.parse_identifier(false)?; - let value = p.parse_identifier(false)?; + let key = p.parse_identifier()?; + let value = p.parse_identifier()?; Ok(SecretOption { key, value }) })?); } @@ -4335,7 +4347,7 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let table_name = self.parse_object_name(false)?; self.expect_keyword_is(Keyword::USING)?; - let module_name = self.parse_identifier(false)?; + let module_name = self.parse_identifier()?; // SQLite docs note that module "arguments syntax is sufficiently // general that the arguments can be made to appear as column // definitions in a traditional CREATE TABLE statement", but @@ -4362,16 +4374,14 @@ impl<'a> Parser<'a> { fn parse_schema_name(&mut self) -> Result { if self.parse_keyword(Keyword::AUTHORIZATION) { - Ok(SchemaName::UnnamedAuthorization( - self.parse_identifier(false)?, - )) + Ok(SchemaName::UnnamedAuthorization(self.parse_identifier()?)) } else { let name = self.parse_object_name(false)?; if self.parse_keyword(Keyword::AUTHORIZATION) { Ok(SchemaName::NamedAuthorization( name, - self.parse_identifier(false)?, + self.parse_identifier()?, )) } else { Ok(SchemaName::Simple(name)) @@ -4492,7 +4502,7 @@ impl<'a> Parser<'a> { )); } else if self.parse_keyword(Keyword::LANGUAGE) { ensure_not_set(&body.language, "LANGUAGE")?; - body.language = Some(self.parse_identifier(false)?); + body.language = Some(self.parse_identifier()?); } else if self.parse_keyword(Keyword::IMMUTABLE) { ensure_not_set(&body.behavior, "IMMUTABLE | STABLE | VOLATILE")?; body.behavior = Some(FunctionBehavior::Immutable); @@ -4615,7 +4625,7 @@ impl<'a> Parser<'a> { let parse_function_param = |parser: &mut Parser| -> Result { - let name = parser.parse_identifier(false)?; + let name = parser.parse_identifier()?; let data_type = parser.parse_data_type()?; Ok(OperateFunctionArg { mode: None, @@ -4643,7 +4653,7 @@ impl<'a> Parser<'a> { }; let language = if self.parse_keyword(Keyword::LANGUAGE) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -4849,9 +4859,7 @@ impl<'a> Parser<'a> { Keyword::INSERT => TriggerEvent::Insert, Keyword::UPDATE => { if self.parse_keyword(Keyword::OF) { - let cols = self.parse_comma_separated(|ident| { - Parser::parse_identifier(ident, false) - })?; + let cols = self.parse_comma_separated(Parser::parse_identifier)?; TriggerEvent::Update(cols) } else { TriggerEvent::Update(vec![]) @@ -4935,7 +4943,7 @@ impl<'a> Parser<'a> { } fn parse_macro_arg(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let default_expr = if self.consume_token(&Token::Assignment) || self.consume_token(&Token::RArrow) { @@ -5256,14 +5264,14 @@ impl<'a> Parser<'a> { if !in_role.is_empty() { parser_err!("Found multiple IN ROLE", loc) } else { - in_role = self.parse_comma_separated(|p| p.parse_identifier(false))?; + in_role = self.parse_comma_separated(|p| p.parse_identifier())?; Ok(()) } } else if self.parse_keyword(Keyword::GROUP) { if !in_group.is_empty() { parser_err!("Found multiple IN GROUP", loc) } else { - in_group = self.parse_comma_separated(|p| p.parse_identifier(false))?; + in_group = self.parse_comma_separated(|p| p.parse_identifier())?; Ok(()) } } else { @@ -5274,7 +5282,7 @@ impl<'a> Parser<'a> { if !role.is_empty() { parser_err!("Found multiple ROLE", loc) } else { - role = self.parse_comma_separated(|p| p.parse_identifier(false))?; + role = self.parse_comma_separated(|p| p.parse_identifier())?; Ok(()) } } @@ -5282,7 +5290,7 @@ impl<'a> Parser<'a> { if !user.is_empty() { parser_err!("Found multiple USER", loc) } else { - user = self.parse_comma_separated(|p| p.parse_identifier(false))?; + user = self.parse_comma_separated(|p| p.parse_identifier())?; Ok(()) } } @@ -5290,7 +5298,7 @@ impl<'a> Parser<'a> { if !admin.is_empty() { parser_err!("Found multiple ADMIN", loc) } else { - admin = self.parse_comma_separated(|p| p.parse_identifier(false))?; + admin = self.parse_comma_separated(|p| p.parse_identifier())?; Ok(()) } } @@ -5327,7 +5335,7 @@ impl<'a> Parser<'a> { Some(Keyword::SESSION_USER) => Owner::SessionUser, Some(_) => unreachable!(), None => { - match self.parse_identifier(false) { + match self.parse_identifier() { Ok(ident) => Owner::Ident(ident), Err(e) => { return Err(ParserError::ParserError(format!("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. {e}"))) @@ -5348,7 +5356,7 @@ impl<'a> Parser<'a> { /// /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createpolicy.html) pub fn parse_create_policy(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; @@ -5521,7 +5529,7 @@ impl<'a> Parser<'a> { /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-droppolicy.html) fn parse_drop_policy(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; let option = self.parse_optional_referential_action(); @@ -5573,9 +5581,9 @@ impl<'a> Parser<'a> { persistent: bool, ) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let storage_specifier = if self.parse_keyword(Keyword::FROM) { - self.parse_identifier(false).ok() + self.parse_identifier().ok() } else { None }; @@ -5614,7 +5622,7 @@ impl<'a> Parser<'a> { return self.parse_mssql_declare(); } - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let binary = Some(self.parse_keyword(Keyword::BINARY)); let sensitive = if self.parse_keyword(Keyword::INSENSITIVE) { @@ -5675,7 +5683,7 @@ impl<'a> Parser<'a> { /// ``` /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#declare pub fn parse_big_query_declare(&mut self) -> Result { - let names = self.parse_comma_separated(|parser| Parser::parse_identifier(parser, false))?; + let names = self.parse_comma_separated(Parser::parse_identifier)?; let data_type = match self.peek_token().token { Token::Word(w) if w.keyword == Keyword::DEFAULT => None, @@ -5737,7 +5745,7 @@ impl<'a> Parser<'a> { pub fn parse_snowflake_declare(&mut self) -> Result { let mut stmts = vec![]; loop { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let (declare_type, for_query, assigned_expr, data_type) = if self.parse_keyword(Keyword::CURSOR) { self.expect_keyword_is(Keyword::FOR)?; @@ -5855,7 +5863,7 @@ impl<'a> Parser<'a> { /// [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(false)?; + let ident = self.parse_identifier()?; if !ident.value.starts_with('@') { Err(ParserError::TokenizerError( "Invalid MsSql variable declaration.".to_string(), @@ -5986,7 +5994,7 @@ impl<'a> Parser<'a> { self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let into = if self.parse_keyword(Keyword::INTO) { Some(self.parse_object_name(false)?) @@ -6031,7 +6039,7 @@ impl<'a> Parser<'a> { }; let table_name = self.parse_object_name(false)?; let using = if self.parse_keyword(Keyword::USING) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -6041,7 +6049,7 @@ impl<'a> Parser<'a> { let include = if self.parse_keyword(Keyword::INCLUDE) { self.expect_token(&Token::LParen)?; - let columns = self.parse_comma_separated(|p| p.parse_identifier(false))?; + let columns = self.parse_comma_separated(|p| p.parse_identifier())?; self.expect_token(&Token::RParen)?; columns } else { @@ -6090,17 +6098,17 @@ impl<'a> Parser<'a> { pub fn parse_create_extension(&mut self) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let (schema, version, cascade) = if self.parse_keyword(Keyword::WITH) { let schema = if self.parse_keyword(Keyword::SCHEMA) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; let version = if self.parse_keyword(Keyword::VERSION) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -6124,7 +6132,7 @@ impl<'a> Parser<'a> { /// Parse a PostgreSQL-specific [Statement::DropExtension] statement. pub fn parse_drop_extension(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let names = self.parse_comma_separated(|p| p.parse_identifier(false))?; + let names = self.parse_comma_separated(|p| p.parse_identifier())?; let cascade_or_restrict = self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]); Ok(Statement::DropExtension { @@ -6222,13 +6230,13 @@ impl<'a> Parser<'a> { if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) { row_delimiters.push(HiveRowDelimiter { delimiter: HiveDelimiter::FieldsTerminatedBy, - char: self.parse_identifier(false)?, + char: self.parse_identifier()?, }); if self.parse_keywords(&[Keyword::ESCAPED, Keyword::BY]) { row_delimiters.push(HiveRowDelimiter { delimiter: HiveDelimiter::FieldsEscapedBy, - char: self.parse_identifier(false)?, + char: self.parse_identifier()?, }); } } else { @@ -6243,7 +6251,7 @@ impl<'a> Parser<'a> { ]) { row_delimiters.push(HiveRowDelimiter { delimiter: HiveDelimiter::CollectionItemsTerminatedBy, - char: self.parse_identifier(false)?, + char: self.parse_identifier()?, }); } else { break; @@ -6257,7 +6265,7 @@ impl<'a> Parser<'a> { ]) { row_delimiters.push(HiveRowDelimiter { delimiter: HiveDelimiter::MapKeysTerminatedBy, - char: self.parse_identifier(false)?, + char: self.parse_identifier()?, }); } else { break; @@ -6267,7 +6275,7 @@ impl<'a> Parser<'a> { if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) { row_delimiters.push(HiveRowDelimiter { delimiter: HiveDelimiter::LinesTerminatedBy, - char: self.parse_identifier(false)?, + char: self.parse_identifier()?, }); } else { break; @@ -6277,7 +6285,7 @@ impl<'a> Parser<'a> { if self.parse_keywords(&[Keyword::DEFINED, Keyword::AS]) { row_delimiters.push(HiveRowDelimiter { delimiter: HiveDelimiter::NullDefinedAs, - char: self.parse_identifier(false)?, + char: self.parse_identifier()?, }); } else { break; @@ -6298,7 +6306,7 @@ impl<'a> Parser<'a> { fn parse_optional_on_cluster(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::ON, Keyword::CLUSTER]) { - Ok(Some(self.parse_identifier(false)?)) + Ok(Some(self.parse_identifier()?)) } else { Ok(None) } @@ -6529,7 +6537,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(false))?, + self.parse_comma_separated(|p| p.parse_identifier())?, )); }; @@ -6620,13 +6628,13 @@ impl<'a> Parser<'a> { } pub fn parse_procedure_param(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let data_type = self.parse_data_type()?; Ok(ProcedureParam { name, data_type }) } pub fn parse_column_def(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let data_type = if self.is_column_type_sqlite_unspecified() { DataType::Unspecified } else { @@ -6640,7 +6648,7 @@ impl<'a> Parser<'a> { let mut options = vec![]; loop { if self.parse_keyword(Keyword::CONSTRAINT) { - let name = Some(self.parse_identifier(false)?); + let name = Some(self.parse_identifier()?); if let Some(option) = self.parse_optional_column_option()? { options.push(ColumnOptionDef { name, option }); } else { @@ -6859,7 +6867,7 @@ impl<'a> Parser<'a> { } pub(crate) fn parse_tag(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; self.expect_token(&Token::Eq)?; let value = self.parse_literal_string()?; @@ -7048,7 +7056,7 @@ impl<'a> Parser<'a> { &mut self, ) -> Result, ParserError> { let name = if self.parse_keyword(Keyword::CONSTRAINT) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -7279,7 +7287,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> { - self.maybe_parse(|parser| parser.parse_identifier(false)) + self.maybe_parse(|parser| parser.parse_identifier()) } #[must_use] @@ -7320,7 +7328,7 @@ impl<'a> Parser<'a> { match self.peek_token().token { Token::Word(w) if w.keyword == Keyword::HEAP && is_mssql => { - Ok(SqlOption::Ident(self.parse_identifier(false)?)) + Ok(SqlOption::Ident(self.parse_identifier()?)) } Token::Word(w) if w.keyword == Keyword::PARTITION && is_mssql => { self.parse_option_partition() @@ -7329,7 +7337,7 @@ impl<'a> Parser<'a> { self.parse_option_clustered() } _ => { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; self.expect_token(&Token::Eq)?; let value = self.parse_expr()?; @@ -7358,7 +7366,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let columns = self.parse_comma_separated(|p| { - let name = p.parse_identifier(false)?; + let name = p.parse_identifier()?; let asc = p.parse_asc_desc(); Ok(ClusteredIndex { name, asc }) @@ -7377,7 +7385,7 @@ impl<'a> Parser<'a> { pub fn parse_option_partition(&mut self) -> Result { self.expect_keyword_is(Keyword::PARTITION)?; self.expect_token(&Token::LParen)?; - let column_name = self.parse_identifier(false)?; + let column_name = self.parse_identifier()?; self.expect_keyword_is(Keyword::RANGE)?; let range_direction = if self.parse_keyword(Keyword::LEFT) { @@ -7425,7 +7433,7 @@ impl<'a> Parser<'a> { } pub fn parse_alter_table_add_projection(&mut self) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let query = self.parse_projection_select()?; Ok(AlterTableOperation::AddProjection { if_not_exists, @@ -7483,18 +7491,18 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::RENAME) { if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::CONSTRAINT) { - let old_name = self.parse_identifier(false)?; + let old_name = self.parse_identifier()?; self.expect_keyword_is(Keyword::TO)?; - let new_name = self.parse_identifier(false)?; + let new_name = self.parse_identifier()?; AlterTableOperation::RenameConstraint { old_name, new_name } } else if self.parse_keyword(Keyword::TO) { let table_name = self.parse_object_name(false)?; AlterTableOperation::RenameTable { table_name } } else { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] - let old_column_name = self.parse_identifier(false)?; + let old_column_name = self.parse_identifier()?; self.expect_keyword_is(Keyword::TO)?; - let new_column_name = self.parse_identifier(false)?; + let new_column_name = self.parse_identifier()?; AlterTableOperation::RenameColumn { old_column_name, new_column_name, @@ -7504,10 +7512,10 @@ impl<'a> Parser<'a> { if self.parse_keywords(&[Keyword::ROW, Keyword::LEVEL, Keyword::SECURITY]) { AlterTableOperation::DisableRowLevelSecurity {} } else if self.parse_keyword(Keyword::RULE) { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::DisableRule { name } } else if self.parse_keyword(Keyword::TRIGGER) { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::DisableTrigger { name } } else { return self.expected( @@ -7517,24 +7525,24 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::ENABLE) { if self.parse_keywords(&[Keyword::ALWAYS, Keyword::RULE]) { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::EnableAlwaysRule { name } } else if self.parse_keywords(&[Keyword::ALWAYS, Keyword::TRIGGER]) { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::EnableAlwaysTrigger { name } } else if self.parse_keywords(&[Keyword::ROW, Keyword::LEVEL, Keyword::SECURITY]) { AlterTableOperation::EnableRowLevelSecurity {} } else if self.parse_keywords(&[Keyword::REPLICA, Keyword::RULE]) { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::EnableReplicaRule { name } } else if self.parse_keywords(&[Keyword::REPLICA, Keyword::TRIGGER]) { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::EnableReplicaTrigger { name } } else if self.parse_keyword(Keyword::RULE) { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::EnableRule { name } } else if self.parse_keyword(Keyword::TRIGGER) { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::EnableTrigger { name } } else { return self.expected( @@ -7546,9 +7554,9 @@ impl<'a> Parser<'a> { && dialect_of!(self is ClickHouseDialect|GenericDialect) { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let partition = if self.parse_keywords(&[Keyword::IN, Keyword::PARTITION]) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -7561,9 +7569,9 @@ impl<'a> Parser<'a> { && dialect_of!(self is ClickHouseDialect|GenericDialect) { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let partition = if self.parse_keywords(&[Keyword::IN, Keyword::PARTITION]) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -7591,7 +7599,7 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::CONSTRAINT) { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let cascade = self.parse_keyword(Keyword::CASCADE); AlterTableOperation::DropConstraint { if_exists, @@ -7606,14 +7614,14 @@ impl<'a> Parser<'a> { && dialect_of!(self is ClickHouseDialect|GenericDialect) { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; AlterTableOperation::DropProjection { if_exists, name } } else if self.parse_keywords(&[Keyword::CLUSTERING, Keyword::KEY]) { AlterTableOperation::DropClusteringKey } else { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let column_name = self.parse_identifier(false)?; + let column_name = self.parse_identifier()?; let cascade = self.parse_keyword(Keyword::CASCADE); AlterTableOperation::DropColumn { column_name, @@ -7636,8 +7644,8 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::CHANGE) { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] - let old_name = self.parse_identifier(false)?; - let new_name = self.parse_identifier(false)?; + let old_name = self.parse_identifier()?; + let new_name = self.parse_identifier()?; let data_type = self.parse_data_type()?; let mut options = vec![]; while let Some(option) = self.parse_optional_column_option()? { @@ -7655,7 +7663,7 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::MODIFY) { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] - let col_name = self.parse_identifier(false)?; + let col_name = self.parse_identifier()?; let data_type = self.parse_data_type()?; let mut options = vec![]; while let Some(option) = self.parse_optional_column_option()? { @@ -7672,7 +7680,7 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::ALTER) { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] - let column_name = self.parse_identifier(false)?; + let column_name = self.parse_identifier()?; let is_postgresql = dialect_of!(self is PostgreSqlDialect); let op: AlterColumnOperation = if self.parse_keywords(&[ @@ -7759,7 +7767,7 @@ impl<'a> Parser<'a> { let partition = self.parse_part_or_partition()?; let with_name = if self.parse_keyword(Keyword::WITH) { self.expect_keyword_is(Keyword::NAME)?; - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -7773,7 +7781,7 @@ impl<'a> Parser<'a> { let partition = self.parse_part_or_partition()?; let with_name = if self.parse_keyword(Keyword::WITH) { self.expect_keyword_is(Keyword::NAME)?; - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -7838,12 +7846,12 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::LOCATION) { location = Some(HiveSetLocation { has_set: false, - location: self.parse_identifier(false)?, + location: self.parse_identifier()?, }); } else if self.parse_keywords(&[Keyword::SET, Keyword::LOCATION]) { location = Some(HiveSetLocation { has_set: true, - location: self.parse_identifier(false)?, + location: self.parse_identifier()?, }); } @@ -7995,7 +8003,7 @@ impl<'a> Parser<'a> { let cursor = if self.parse_keyword(Keyword::ALL) { CloseCursor::All } else { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; CloseCursor::Specific { name } }; @@ -8017,7 +8025,7 @@ impl<'a> Parser<'a> { Keyword::FORCE_NULL, Keyword::ENCODING, ]) { - Some(Keyword::FORMAT) => CopyOption::Format(self.parse_identifier(false)?), + Some(Keyword::FORMAT) => CopyOption::Format(self.parse_identifier()?), Some(Keyword::FREEZE) => CopyOption::Freeze(!matches!( self.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]), Some(Keyword::FALSE) @@ -8093,12 +8101,12 @@ impl<'a> Parser<'a> { } Some(Keyword::FORCE) if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) => { CopyLegacyCsvOption::ForceNotNull( - self.parse_comma_separated(|p| p.parse_identifier(false))?, + self.parse_comma_separated(|p| p.parse_identifier())?, ) } Some(Keyword::FORCE) if self.parse_keywords(&[Keyword::QUOTE]) => { CopyLegacyCsvOption::ForceQuote( - self.parse_comma_separated(|p| p.parse_identifier(false))?, + self.parse_comma_separated(|p| p.parse_identifier())?, ) } _ => self.expected("csv option", self.peek_token())?, @@ -8723,9 +8731,9 @@ impl<'a> Parser<'a> { /// Strictly parse `identifier AS identifier` pub fn parse_identifier_with_alias(&mut self) -> Result { - let ident = self.parse_identifier(false)?; + let ident = self.parse_identifier()?; self.expect_keyword_is(Keyword::AS)?; - let alias = self.parse_identifier(false)?; + let alias = self.parse_identifier()?; Ok(IdentWithAlias { ident, alias }) } @@ -8857,17 +8865,28 @@ impl<'a> Parser<'a> { /// in this context on BigQuery. pub fn parse_object_name(&mut self, in_table_clause: bool) -> Result { let mut idents = vec![]; - loop { - 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("")); + + 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; + } } - idents.push(self.parse_identifier(in_table_clause)?); - if !self.consume_token(&Token::Period) { - break; + } else { + loop { + 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("")); + } + idents.push(self.parse_identifier()?); + if !self.consume_token(&Token::Period) { + break; + } } } @@ -9002,29 +9021,32 @@ impl<'a> Parser<'a> { } /// Parse a simple one-word identifier (possibly quoted, possibly a keyword) - /// - /// The `in_table_clause` parameter indicates whether the identifier 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_identifier(&mut self, in_table_clause: bool) -> Result { + pub fn parse_identifier(&mut self) -> Result { let next_token = self.next_token(); match next_token.token { - Token::Word(w) => { - let mut ident = w.to_ident(next_token.span); + Token::Word(w) => Ok(w.to_ident(next_token.span)), + Token::SingleQuotedString(s) => Ok(Ident::with_quote('\'', s)), + Token::DoubleQuotedString(s) => Ok(Ident::with_quote('\"', s)), + _ => self.expected("identifier", next_token), + } + } - // On BigQuery, hyphens are permitted in unquoted identifiers inside of a FROM or - // TABLE clause [0]. - // - // The first segment must be an ordinary unquoted identifier, e.g. it must not start - // with a digit. Subsequent segments are either must either be valid identifiers or - // integers, e.g. foo-123 is allowed, but foo-123a is not. - // - // [0] https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical - if dialect_of!(self is BigQueryDialect) - && w.quote_style.is_none() - && in_table_clause - { - let mut requires_whitespace = false; + /// On BigQuery, hyphens are permitted in unquoted identifiers inside of a FROM or + /// TABLE clause. + /// + /// The first segment must be an ordinary unquoted identifier, e.g. it must not start + /// with a digit. Subsequent segments are either must either be valid identifiers or + /// integers, e.g. foo-123 is allowed, but foo-123a is not. + /// + /// [BigQuery-lexical](https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical) + /// + /// Return a tuple of the identifier and a boolean indicating it ends with a period. + fn parse_unquoted_hyphenated_identifier(&mut self) -> Result<(Ident, bool), ParserError> { + match self.peek_token().token { + Token::Word(w) => { + let mut requires_whitespace = false; + let mut ident = w.to_ident(self.next_token().span); + if w.quote_style.is_none() { while matches!(self.peek_token_no_skip().token, Token::Minus) { self.next_token(); ident.value.push('-'); @@ -9038,8 +9060,27 @@ impl<'a> Parser<'a> { ident.value.push_str(&next_word.value); false } - Token::Number(s, false) if s.chars().all(|c| c.is_ascii_digit()) => { - ident.value.push_str(&s); + Token::Number(s, false) => { + // A number token can represent a decimal value ending with a period, e.g., `Number('123.')`. + // However, for an [ObjectName], it is part of a hyphenated identifier, e.g., `foo-123.bar`. + // + // If a number token is followed by a period, it is part of an [ObjectName]. + // Return the identifier with `true` if the number token is followed by a period, indicating that + // parsing should continue for the next part of the hyphenated identifier. + if s.ends_with('.') { + let Some(s) = s.split('.').next().filter(|s| { + !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) + }) else { + return self.expected( + "continuation of hyphenated identifier", + TokenWithSpan::new(Token::Number(s, false), token.span), + ); + }; + ident.value.push_str(s); + return Ok((ident, true)); + } else { + ident.value.push_str(&s); + } // If next token is period, then it is part of an ObjectName and we don't expect whitespace // after the number. !matches!(self.peek_token().token, Token::Period) @@ -9061,11 +9102,9 @@ impl<'a> Parser<'a> { } } } - Ok(ident) + Ok((ident, false)) } - Token::SingleQuotedString(s) => Ok(Ident::with_quote('\'', s)), - Token::DoubleQuotedString(s) => Ok(Ident::with_quote('\"', s)), - _ => self.expected("identifier", next_token), + _ => Ok((self.parse_identifier()?, false)), } } @@ -9087,7 +9126,7 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { - let name = self.parse_identifier(false)?; + 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) @@ -9122,7 +9161,7 @@ impl<'a> Parser<'a> { self.next_token(); Ok(vec![]) } else { - let cols = self.parse_comma_separated(|p| p.parse_identifier(false))?; + let cols = self.parse_comma_separated(|p| p.parse_identifier())?; self.expect_token(&Token::RParen)?; Ok(cols) } @@ -9137,7 +9176,7 @@ impl<'a> Parser<'a> { fn parse_table_alias_column_defs(&mut self) -> Result, ParserError> { if self.consume_token(&Token::LParen) { let cols = self.parse_comma_separated(|p| { - let name = p.parse_identifier(false)?; + let name = p.parse_identifier()?; let data_type = p.maybe_parse(|p| p.parse_data_type())?; Ok(TableAliasColumnDef { name, data_type }) })?; @@ -9550,7 +9589,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::NULL) { Some(FormatClause::Null) } else { - let ident = self.parse_identifier(false)?; + let ident = self.parse_identifier()?; Some(FormatClause::Identifier(ident)) } } else { @@ -9579,7 +9618,7 @@ impl<'a> Parser<'a> { && self.parse_keyword(Keyword::SETTINGS) { let key_values = self.parse_comma_separated(|p| { - let key = p.parse_identifier(false)?; + let key = p.parse_identifier()?; p.expect_token(&Token::Eq)?; let value = p.parse_value()?; Ok(Setting { key, value }) @@ -9695,7 +9734,7 @@ impl<'a> Parser<'a> { /// Parse a CTE (`alias [( col1, col2, ... )] AS (subquery)`) pub fn parse_cte(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let mut cte = if self.parse_keyword(Keyword::AS) { let mut is_materialized = None; @@ -9748,7 +9787,7 @@ impl<'a> Parser<'a> { } }; if self.parse_keyword(Keyword::FROM) { - cte.from = Some(self.parse_identifier(false)?); + cte.from = Some(self.parse_identifier()?); } Ok(cte) } @@ -10141,7 +10180,7 @@ impl<'a> Parser<'a> { let role_name = if self.parse_keyword(Keyword::NONE) { None } else { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) }; Ok(Statement::SetRole { context_modifier, @@ -10166,12 +10205,10 @@ impl<'a> Parser<'a> { && self.consume_token(&Token::LParen) { let variables = OneOrManyWithParens::Many( - self.parse_comma_separated(|parser: &mut Parser<'a>| { - parser.parse_identifier(false) - })? - .into_iter() - .map(|ident| ObjectName(vec![ident])) - .collect(), + self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())? + .into_iter() + .map(|ident| ObjectName(vec![ident])) + .collect(), ); self.expect_token(&Token::RParen)?; variables @@ -10496,7 +10533,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::ALL) { Ok(Use::SecondaryRoles(SecondaryRoles::All)) } else { - let roles = self.parse_comma_separated(|parser| parser.parse_identifier(false))?; + let roles = self.parse_comma_separated(|parser| parser.parse_identifier())?; Ok(Use::SecondaryRoles(SecondaryRoles::List(roles))) } } @@ -11111,7 +11148,7 @@ impl<'a> Parser<'a> { self.parse_comma_separated(|p| { let expr = p.parse_expr()?; let _ = p.parse_keyword(Keyword::AS); - let alias = p.parse_identifier(false)?; + let alias = p.parse_identifier()?; Ok(Measure { expr, alias }) })? } else { @@ -11157,9 +11194,9 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::TO, Keyword::NEXT, Keyword::ROW]) { Some(AfterMatchSkip::ToNextRow) } else if self.parse_keywords(&[Keyword::TO, Keyword::FIRST]) { - Some(AfterMatchSkip::ToFirst(self.parse_identifier(false)?)) + Some(AfterMatchSkip::ToFirst(self.parse_identifier()?)) } else if self.parse_keywords(&[Keyword::TO, Keyword::LAST]) { - Some(AfterMatchSkip::ToLast(self.parse_identifier(false)?)) + Some(AfterMatchSkip::ToLast(self.parse_identifier()?)) } else { let found = self.next_token(); return self.expected("after match skip option", found); @@ -11174,7 +11211,7 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::DEFINE)?; let symbols = self.parse_comma_separated(|p| { - let symbol = p.parse_identifier(false)?; + let symbol = p.parse_identifier()?; p.expect_keyword_is(Keyword::AS)?; let definition = p.parse_expr()?; Ok(SymbolDefinition { symbol, definition }) @@ -11205,9 +11242,7 @@ impl<'a> Parser<'a> { } Token::LBrace => { self.expect_token(&Token::Minus)?; - let symbol = self - .parse_identifier(false) - .map(MatchRecognizeSymbol::Named)?; + let symbol = self.parse_identifier().map(MatchRecognizeSymbol::Named)?; self.expect_token(&Token::Minus)?; self.expect_token(&Token::RBrace)?; Ok(MatchRecognizePattern::Exclude(symbol)) @@ -11219,7 +11254,7 @@ impl<'a> Parser<'a> { }) if value == "PERMUTE" => { self.expect_token(&Token::LParen)?; let symbols = self.parse_comma_separated(|p| { - p.parse_identifier(false).map(MatchRecognizeSymbol::Named) + p.parse_identifier().map(MatchRecognizeSymbol::Named) })?; self.expect_token(&Token::RParen)?; Ok(MatchRecognizePattern::Permute(symbols)) @@ -11231,7 +11266,7 @@ impl<'a> Parser<'a> { } _ => { self.prev_token(); - self.parse_identifier(false) + self.parse_identifier() .map(MatchRecognizeSymbol::Named) .map(MatchRecognizePattern::Symbol) } @@ -11349,7 +11384,7 @@ impl<'a> Parser<'a> { columns, })); } - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; if self.parse_keyword(Keyword::FOR) { self.expect_keyword_is(Keyword::ORDINALITY)?; return Ok(JsonTableColumn::ForOrdinality(name)); @@ -11386,7 +11421,7 @@ impl<'a> Parser<'a> { /// /// Reference: pub fn parse_openjson_table_column_def(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let r#type = self.parse_data_type()?; let path = if let Token::SingleQuotedString(path) = self.peek_token().token { self.next_token(); @@ -11446,7 +11481,7 @@ impl<'a> Parser<'a> { }?; let expr = self.parse_function(ObjectName(vec![Ident::new(function_name)]))?; let alias = if self.parse_keyword(Keyword::AS) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -11478,7 +11513,7 @@ impl<'a> Parser<'a> { pub fn parse_expr_with_alias(&mut self) -> Result { let expr = self.parse_expr()?; let alias = if self.parse_keyword(Keyword::AS) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -11538,9 +11573,9 @@ impl<'a> Parser<'a> { table: TableFactor, ) -> Result { self.expect_token(&Token::LParen)?; - let value = self.parse_identifier(false)?; + let value = self.parse_identifier()?; self.expect_keyword_is(Keyword::FOR)?; - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::IN)?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; self.expect_token(&Token::RParen)?; @@ -11574,14 +11609,14 @@ impl<'a> Parser<'a> { let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::TO)?; - let grantees = self.parse_comma_separated(|p| p.parse_identifier(false))?; + let grantees = self.parse_comma_separated(|p| p.parse_identifier())?; 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(false).unwrap()); + .then(|| self.parse_identifier().unwrap()); Ok(Statement::Grant { privileges, @@ -11705,11 +11740,11 @@ impl<'a> Parser<'a> { let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::FROM)?; - let grantees = self.parse_comma_separated(|p| p.parse_identifier(false))?; + let grantees = self.parse_comma_separated(|p| p.parse_identifier())?; let granted_by = self .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) - .then(|| self.parse_identifier(false).unwrap()); + .then(|| self.parse_identifier().unwrap()); let loc = self.peek_token().span.start; let cascade = self.parse_keyword(Keyword::CASCADE); @@ -11798,7 +11833,7 @@ impl<'a> Parser<'a> { let table_alias = if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::AS) { - Some(self.parse_identifier(false)?) + Some(self.parse_identifier()?) } else { None }; @@ -12047,7 +12082,7 @@ impl<'a> Parser<'a> { })? } else { self.maybe_parse(|p| { - let name = p.parse_identifier(false)?; + let name = p.parse_identifier()?; let operator = p.parse_function_named_arg_operator()?; let arg = p.parse_wildcard_expr()?.into(); Ok(FunctionArg::Named { @@ -12342,12 +12377,11 @@ impl<'a> Parser<'a> { ) -> Result, ParserError> { let opt_exclude = if self.parse_keyword(Keyword::EXCLUDE) { if self.consume_token(&Token::LParen) { - let columns = - self.parse_comma_separated(|parser| parser.parse_identifier(false))?; + let columns = self.parse_comma_separated(|parser| parser.parse_identifier())?; self.expect_token(&Token::RParen)?; Some(ExcludeSelectItem::Multiple(columns)) } else { - let column = self.parse_identifier(false)?; + let column = self.parse_identifier()?; Some(ExcludeSelectItem::Single(column)) } } else { @@ -12380,7 +12414,7 @@ impl<'a> Parser<'a> { } } else { // Clickhouse allows EXCEPT column_name - let ident = self.parse_identifier(false)?; + let ident = self.parse_identifier()?; Some(ExceptSelectItem { first_element: ident, additional_elements: vec![], @@ -12438,7 +12472,7 @@ impl<'a> Parser<'a> { pub fn parse_replace_elements(&mut self) -> Result { let expr = self.parse_expr()?; let as_keyword = self.parse_keyword(Keyword::AS); - let ident = self.parse_identifier(false)?; + let ident = self.parse_identifier()?; Ok(ReplaceSelectElement { expr, column_name: ident, @@ -12535,7 +12569,7 @@ impl<'a> Parser<'a> { // Parse a INTERPOLATE expression (ClickHouse dialect) pub fn parse_interpolation(&mut self) -> Result { - let column = self.parse_identifier(false)?; + let column = self.parse_identifier()?; let expr = if self.parse_keyword(Keyword::AS) { Some(self.parse_expr()?) } else { @@ -12772,7 +12806,7 @@ impl<'a> Parser<'a> { pub fn parse_rollback_savepoint(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::TO) { let _ = self.parse_keyword(Keyword::SAVEPOINT); - let savepoint = self.parse_identifier(false)?; + let savepoint = self.parse_identifier()?; Ok(Some(savepoint)) } else { @@ -12782,7 +12816,7 @@ impl<'a> Parser<'a> { pub fn parse_deallocate(&mut self) -> Result { let prepare = self.parse_keyword(Keyword::PREPARE); - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; Ok(Statement::Deallocate { name, prepare }) } @@ -12822,7 +12856,7 @@ impl<'a> Parser<'a> { } pub fn parse_prepare(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_identifier()?; let mut data_types = vec![]; if self.consume_token(&Token::LParen) { @@ -12845,7 +12879,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; self.expect_keyword_is(Keyword::TO)?; - let to = self.parse_identifier(false)?; + let to = self.parse_identifier()?; let with_options = self.parse_options(Keyword::WITH)?; @@ -13017,7 +13051,7 @@ impl<'a> Parser<'a> { /// `INSTALL [extension_name]` pub fn parse_install(&mut self) -> Result { - let extension_name = self.parse_identifier(false)?; + let extension_name = self.parse_identifier()?; Ok(Statement::Install { extension_name }) } @@ -13025,7 +13059,7 @@ impl<'a> Parser<'a> { /// Parse a SQL LOAD statement pub fn parse_load(&mut self) -> Result { if self.dialect.supports_load_extension() { - let extension_name = self.parse_identifier(false)?; + let extension_name = self.parse_identifier()?; Ok(Statement::Load { extension_name }) } else if self.parse_keyword(Keyword::DATA) && self.dialect.supports_load_data() { let local = self.parse_one_of_keywords(&[Keyword::LOCAL]).is_some(); @@ -13064,7 +13098,7 @@ impl<'a> Parser<'a> { let partition = if self.parse_keyword(Keyword::PARTITION) { if self.parse_keyword(Keyword::ID) { - Some(Partition::Identifier(self.parse_identifier(false)?)) + Some(Partition::Identifier(self.parse_identifier()?)) } else { Some(Partition::Expr(self.parse_expr()?)) } @@ -13179,13 +13213,13 @@ impl<'a> Parser<'a> { } pub fn parse_named_window(&mut self) -> Result { - let ident = self.parse_identifier(false)?; + let ident = self.parse_identifier()?; self.expect_keyword_is(Keyword::AS)?; let window_expr = if self.consume_token(&Token::LParen) { NamedWindowExpr::WindowSpec(self.parse_window_spec()?) } else if self.dialect.supports_window_clause_named_window_reference() { - NamedWindowExpr::NamedWindow(self.parse_identifier(false)?) + NamedWindowExpr::NamedWindow(self.parse_identifier()?) } else { return self.expected("(", self.peek_token()); }; @@ -13255,7 +13289,7 @@ impl<'a> Parser<'a> { } loop { - let attr_name = self.parse_identifier(false)?; + let attr_name = self.parse_identifier()?; let attr_data_type = self.parse_data_type()?; let attr_collation = if self.parse_keyword(Keyword::COLLATE) { Some(self.parse_object_name(false)?) @@ -13284,7 +13318,7 @@ 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(false))?; + let partitions = self.parse_comma_separated(|p| p.parse_identifier())?; self.expect_token(&Token::RParen)?; Ok(partitions) } @@ -13294,7 +13328,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::FIRST) { Ok(Some(MySQLColumnPosition::First)) } else if self.parse_keyword(Keyword::AFTER) { - let ident = self.parse_identifier(false)?; + let ident = self.parse_identifier()?; Ok(Some(MySQLColumnPosition::After(ident))) } else { Ok(None) @@ -13402,7 +13436,7 @@ impl<'a> Parser<'a> { .parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) .is_some() { - parent_name.0.insert(0, self.parse_identifier(false)?); + parent_name.0.insert(0, self.parse_identifier()?); } (None, Some(parent_name)) } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 3c2f70edf..9269f4fe6 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1144,30 +1144,16 @@ impl<'a> Tokenizer<'a> { // match one period if let Some('.') = chars.peek() { - // Check if this actually is a float point number - let mut char_clone = chars.peekable.clone(); - char_clone.next(); - // Next char should be a digit, otherwise, it is not a float point number - if char_clone - .peek() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false) - { - s.push('.'); - chars.next(); - } else if !s.is_empty() { - // Number might be part of period separated construct. Keep the period for next token - // e.g. a-12.b - return Ok(Some(Token::Number(s, false))); - } else { - // No number -> Token::Period - chars.next(); - return Ok(Some(Token::Period)); - } + s.push('.'); + chars.next(); } - s += &peeking_take_while(chars, |ch| ch.is_ascii_digit()); + // No number -> Token::Period + if s == "." { + return Ok(Some(Token::Period)); + } + let mut exponent_part = String::new(); // Parse exponent as number if chars.peek() == Some(&'e') || chars.peek() == Some(&'E') { @@ -2199,23 +2185,6 @@ mod tests { compare(expected, tokens); } - #[test] - fn tokenize_select_float_hyphenated_identifier() { - let sql = String::from("SELECT a-12.b"); - let dialect = GenericDialect {}; - let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); - let expected = vec![ - Token::make_keyword("SELECT"), - Token::Whitespace(Whitespace::Space), - Token::make_word("a", None), - Token::Minus, - Token::Number(String::from("12"), false), - Token::Period, - Token::make_word("b", None), - ]; - compare(expected, tokens); - } - #[test] fn tokenize_clickhouse_double_equal() { let sql = String::from("SELECT foo=='1'"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index cbbbb45f9..3b21160b9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2964,6 +2964,113 @@ fn test_compound_expr() { } } +#[test] +fn test_double_value() { + let dialects = all_dialects(); + let test_cases = vec![ + gen_number_case_with_sign("0."), + gen_number_case_with_sign("0.0"), + gen_number_case_with_sign("0000."), + gen_number_case_with_sign("0000.00"), + gen_number_case_with_sign(".0"), + gen_number_case_with_sign(".00"), + gen_number_case_with_sign("0e0"), + gen_number_case_with_sign("0e+0"), + gen_number_case_with_sign("0e-0"), + gen_number_case_with_sign("0.e-0"), + gen_number_case_with_sign("0.e+0"), + gen_number_case_with_sign(".0e-0"), + gen_number_case_with_sign(".0e+0"), + gen_number_case_with_sign("00.0e+0"), + gen_number_case_with_sign("00.0e-0"), + ]; + + 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), "") + { + if let SetExpr::Select(select) = *query.body { + assert_eq!(expected[i], select.projection[0]); + } else { + panic!("Expected a SELECT statement"); + } + } else { + panic!("Expected a SELECT statement"); + } + } + } +} + +fn gen_number_case(value: &str) -> (Vec, Vec) { + let input = vec![ + value.to_string(), + format!("{} col_alias", value), + format!("{} AS col_alias", value), + ]; + let expected = vec![ + SelectItem::UnnamedExpr(Expr::Value(number(value))), + SelectItem::ExprWithAlias { + expr: Expr::Value(number(value)), + alias: Ident::new("col_alias"), + }, + SelectItem::ExprWithAlias { + expr: Expr::Value(number(value)), + alias: Ident::new("col_alias"), + }, + ]; + (input, expected) +} + +fn gen_sign_number_case(value: &str, op: UnaryOperator) -> (Vec, Vec) { + match op { + UnaryOperator::Plus | UnaryOperator::Minus => {} + _ => panic!("Invalid sign"), + } + + let input = vec![ + format!("{}{}", op, value), + format!("{}{} col_alias", op, value), + format!("{}{} AS col_alias", op, value), + ]; + let expected = vec![ + SelectItem::UnnamedExpr(Expr::UnaryOp { + op, + expr: Box::new(Expr::Value(number(value))), + }), + SelectItem::ExprWithAlias { + expr: Expr::UnaryOp { + op, + expr: Box::new(Expr::Value(number(value))), + }, + alias: Ident::new("col_alias"), + }, + SelectItem::ExprWithAlias { + expr: Expr::UnaryOp { + op, + expr: Box::new(Expr::Value(number(value))), + }, + alias: Ident::new("col_alias"), + }, + ]; + (input, expected) +} + +/// generate the test cases for signed and unsigned numbers +/// For example, given "0.0", the test cases will be: +/// - "0.0" +/// - "+0.0" +/// - "-0.0" +fn gen_number_case_with_sign(number: &str) -> (Vec, Vec) { + let (mut input, mut expected) = gen_number_case(number); + for op in [UnaryOperator::Plus, UnaryOperator::Minus] { + let (input_sign, expected_sign) = gen_sign_number_case(number, op); + input.extend(input_sign); + expected.extend(expected_sign); + } + (input, expected) +} + #[test] fn parse_negative_value() { let sql1 = "SELECT -1"; @@ -12470,6 +12577,41 @@ fn parse_composite_access_expr() { all_dialects_where(|d| d.supports_struct_literal()).verified_stmt( "SELECT * FROM t WHERE STRUCT(STRUCT(1 AS a, NULL AS b) AS c, NULL AS d).c.a IS NOT NULL", ); + let support_struct = all_dialects_where(|d| d.supports_struct_literal()); + let stmt = support_struct + .verified_only_select("SELECT STRUCT(STRUCT(1 AS a, NULL AS b) AS c, NULL AS d).c.a"); + let expected = SelectItem::UnnamedExpr(Expr::CompoundFieldAccess { + root: Box::new(Expr::Struct { + values: vec![ + Expr::Named { + name: Ident::new("c"), + expr: Box::new(Expr::Struct { + values: vec![ + Expr::Named { + name: Ident::new("a"), + expr: Box::new(Expr::Value(Number("1".parse().unwrap(), false))), + }, + Expr::Named { + name: Ident::new("b"), + expr: Box::new(Expr::Value(Value::Null)), + }, + ], + fields: vec![], + }), + }, + Expr::Named { + name: Ident::new("d"), + expr: Box::new(Expr::Value(Value::Null)), + }, + ], + fields: vec![], + }), + access_chain: vec![ + AccessExpr::Dot(Expr::Identifier(Ident::new("c"))), + AccessExpr::Dot(Expr::Identifier(Ident::new("a"))), + ], + }); + assert_eq!(stmt.projection[0], expected); } #[test] From 48f025f65861b1187702ef5aa6614b84baf1bcef Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 28 Dec 2024 14:20:48 +0100 Subject: [PATCH 658/806] SQLite: Allow dollar signs in placeholder names (#1620) --- src/dialect/mod.rs | 6 ++++++ src/dialect/sqlite.rs | 4 ++++ src/tokenizer.rs | 37 +++++++++++++++++++++++++++++++++---- tests/sqlparser_sqlite.rs | 10 ++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index aee7b5994..1343efca6 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -636,6 +636,12 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect allows dollar placeholders + /// e.g. `SELECT $var` (SQLite) + fn supports_dollar_placeholder(&self) -> bool { + false + } + /// Does the dialect support with clause in create index statement? /// e.g. `CREATE INDEX idx ON t WITH (key = value, key2)` fn supports_create_index_with_clause(&self) -> bool { diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 95717f9fd..138c4692c 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -81,4 +81,8 @@ impl Dialect for SQLiteDialect { fn supports_asc_desc_in_column_definition(&self) -> bool { true } + + fn supports_dollar_placeholder(&self) -> bool { + true + } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 9269f4fe6..da61303b4 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1509,7 +1509,8 @@ impl<'a> Tokenizer<'a> { chars.next(); - if let Some('$') = chars.peek() { + // If the dialect does not support dollar-quoted strings, then `$$` is rather a placeholder. + if matches!(chars.peek(), Some('$')) && !self.dialect.supports_dollar_placeholder() { chars.next(); let mut is_terminated = false; @@ -1543,10 +1544,14 @@ impl<'a> Tokenizer<'a> { }; } else { value.push_str(&peeking_take_while(chars, |ch| { - ch.is_alphanumeric() || ch == '_' + ch.is_alphanumeric() + || ch == '_' + // Allow $ as a placeholder character if the dialect supports it + || matches!(ch, '$' if self.dialect.supports_dollar_placeholder()) })); - if let Some('$') = chars.peek() { + // If the dialect does not support dollar-quoted strings, don't look for the end delimiter. + if matches!(chars.peek(), Some('$')) && !self.dialect.supports_dollar_placeholder() { chars.next(); 'searching_for_end: loop { @@ -2137,7 +2142,7 @@ fn take_char_from_hex_digits( mod tests { use super::*; use crate::dialect::{ - BigQueryDialect, ClickHouseDialect, HiveDialect, MsSqlDialect, MySqlDialect, + BigQueryDialect, ClickHouseDialect, HiveDialect, MsSqlDialect, MySqlDialect, SQLiteDialect, }; use core::fmt::Debug; @@ -2573,6 +2578,30 @@ mod tests { ); } + #[test] + fn tokenize_dollar_placeholder() { + let sql = String::from("SELECT $$, $$ABC$$, $ABC$, $ABC"); + let dialect = SQLiteDialect {}; + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + assert_eq!( + tokens, + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Placeholder("$$".into()), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Placeholder("$$ABC$$".into()), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Placeholder("$ABC$".into()), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Placeholder("$ABC".into()), + ] + ); + } + #[test] fn tokenize_dollar_quoted_string_untagged() { let sql = diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index ff0b54ef7..0adf7f755 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -561,6 +561,16 @@ fn test_dollar_identifier_as_placeholder() { } _ => unreachable!(), } + + // $$ is a valid placeholder in SQLite + match sqlite().verified_expr("id = $$") { + Expr::BinaryOp { op, left, right } => { + assert_eq!(op, BinaryOperator::Eq); + assert_eq!(left, Box::new(Expr::Identifier(Ident::new("id")))); + assert_eq!(right, Box::new(Expr::Value(Placeholder("$$".to_string())))); + } + _ => unreachable!(), + } } fn sqlite() -> TestedDialects { From 77b5bd6fa8b85b22cb712d9995d2512b5b74038c Mon Sep 17 00:00:00 2001 From: Ramnivas Laddad Date: Sat, 28 Dec 2024 05:29:28 -0800 Subject: [PATCH 659/806] Improve error for an unexpected token after DROP (#1623) --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5d1b1c37b..ab8379ad6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5466,7 +5466,7 @@ impl<'a> Parser<'a> { return self.parse_drop_extension(); } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, SCHEMA, DATABASE, FUNCTION, PROCEDURE, STAGE, TRIGGER, SECRET, SEQUENCE, TYPE, or EXTENSION after DROP", + "DATABASE, EXTENSION, FUNCTION, INDEX, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, or VIEW after DROP", self.peek_token(), ); }; From 539db9fb1a7c2463e2efd41c87137a4c86d15610 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 29 Dec 2024 08:14:06 -0500 Subject: [PATCH 660/806] Fix `sqlparser_bench` benchmark compilation (#1625) --- sqlparser_bench/benches/sqlparser_bench.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sqlparser_bench/benches/sqlparser_bench.rs b/sqlparser_bench/benches/sqlparser_bench.rs index 74cac5c97..a7768cbc9 100644 --- a/sqlparser_bench/benches/sqlparser_bench.rs +++ b/sqlparser_bench/benches/sqlparser_bench.rs @@ -78,8 +78,7 @@ fn basic_queries(c: &mut Criterion) { group.bench_function("format_large_statement", |b| { b.iter(|| { - let formatted_query = large_statement.to_string(); - assert_eq!(formatted_query, large_statement); + let _formatted_query = large_statement.to_string(); }); }); } From 3db1b4430f26ccb5077d4e28d3b6d8caa345cc49 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 29 Dec 2024 08:17:52 -0500 Subject: [PATCH 661/806] Improve parsing speed by avoiding some clones in parse_identifier (#1624) --- src/parser/mod.rs | 60 ++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ab8379ad6..ec91bf00e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -970,7 +970,7 @@ impl<'a> Parser<'a> { t @ (Token::Word(_) | Token::SingleQuotedString(_)) => { if self.peek_token().token == Token::Period { let mut id_parts: Vec = vec![match t { - Token::Word(w) => w.to_ident(next_token.span), + Token::Word(w) => w.into_ident(next_token.span), Token::SingleQuotedString(s) => Ident::with_quote('\'', s), _ => unreachable!(), // We matched above }]; @@ -978,7 +978,7 @@ impl<'a> Parser<'a> { while self.consume_token(&Token::Period) { let next_token = self.next_token(); match next_token.token { - Token::Word(w) => id_parts.push(w.to_ident(next_token.span)), + Token::Word(w) => id_parts.push(w.into_ident(next_token.span)), Token::SingleQuotedString(s) => { // SQLite has single-quoted identifiers id_parts.push(Ident::with_quote('\'', s)) @@ -1108,7 +1108,7 @@ impl<'a> Parser<'a> { if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.to_ident(w_span)]), + name: ObjectName(vec![w.clone().into_ident(w_span)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -1123,7 +1123,7 @@ impl<'a> Parser<'a> { | Keyword::CURRENT_DATE | Keyword::LOCALTIME | Keyword::LOCALTIMESTAMP => { - Ok(Some(self.parse_time_functions(ObjectName(vec![w.to_ident(w_span)]))?)) + Ok(Some(self.parse_time_functions(ObjectName(vec![w.clone().into_ident(w_span)]))?)) } Keyword::CASE => Ok(Some(self.parse_case_expr()?)), Keyword::CONVERT => Ok(Some(self.parse_convert_expr(false)?)), @@ -1148,7 +1148,7 @@ impl<'a> Parser<'a> { Keyword::CEIL => Ok(Some(self.parse_ceil_floor_expr(true)?)), Keyword::FLOOR => Ok(Some(self.parse_ceil_floor_expr(false)?)), Keyword::POSITION if self.peek_token_ref().token == Token::LParen => { - Ok(Some(self.parse_position_expr(w.to_ident(w_span))?)) + Ok(Some(self.parse_position_expr(w.clone().into_ident(w_span))?)) } Keyword::SUBSTRING => Ok(Some(self.parse_substring_expr()?)), Keyword::OVERLAY => Ok(Some(self.parse_overlay_expr()?)), @@ -1167,7 +1167,7 @@ impl<'a> Parser<'a> { let query = self.parse_query()?; self.expect_token(&Token::RParen)?; Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.to_ident(w_span)]), + name: ObjectName(vec![w.clone().into_ident(w_span)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::Subquery(query), @@ -1203,11 +1203,12 @@ impl<'a> Parser<'a> { w_span: Span, ) -> Result { match self.peek_token().token { - Token::Period => { - self.parse_compound_field_access(Expr::Identifier(w.to_ident(w_span)), vec![]) - } + Token::Period => self.parse_compound_field_access( + Expr::Identifier(w.clone().into_ident(w_span)), + vec![], + ), Token::LParen => { - let id_parts = vec![w.to_ident(w_span)]; + let id_parts = vec![w.clone().into_ident(w_span)]; if let Some(expr) = self.parse_outer_join_expr(&id_parts) { Ok(expr) } else { @@ -1220,7 +1221,7 @@ impl<'a> Parser<'a> { } Token::LBracket if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) => { - let ident = Expr::Identifier(w.to_ident(w_span)); + let ident = Expr::Identifier(w.clone().into_ident(w_span)); let mut fields = vec![]; self.parse_multi_dim_subscript(&mut fields)?; self.parse_compound_field_access(ident, fields) @@ -1250,11 +1251,11 @@ impl<'a> Parser<'a> { Token::Arrow if self.dialect.supports_lambda_functions() => { self.expect_token(&Token::Arrow)?; Ok(Expr::Lambda(LambdaFunction { - params: OneOrManyWithParens::One(w.to_ident(w_span)), + params: OneOrManyWithParens::One(w.clone().into_ident(w_span)), body: Box::new(self.parse_expr()?), })) } - _ => Ok(Expr::Identifier(w.to_ident(w_span))), + _ => Ok(Expr::Identifier(w.clone().into_ident(w_span))), } } @@ -1438,7 +1439,7 @@ impl<'a> Parser<'a> { } else { let tok = self.next_token(); let key = match tok.token { - Token::Word(word) => word.to_ident(tok.span), + Token::Word(word) => word.into_ident(tok.span), _ => { return parser_err!( format!("Expected identifier, found: {tok}"), @@ -1490,7 +1491,7 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { Token::Word(w) => { - let expr = Expr::Identifier(w.to_ident(next_token.span)); + let expr = Expr::Identifier(w.into_ident(next_token.span)); chain.push(AccessExpr::Dot(expr)); if self.peek_token().token == Token::LBracket { if self.dialect.supports_partiql() { @@ -1670,7 +1671,7 @@ impl<'a> Parser<'a> { while p.consume_token(&Token::Period) { let tok = p.next_token(); let name = match tok.token { - Token::Word(word) => word.to_ident(tok.span), + Token::Word(word) => word.into_ident(tok.span), _ => return p.expected("identifier", tok), }; let func = match p.parse_function(ObjectName(vec![name]))? { @@ -8242,7 +8243,7 @@ impl<'a> Parser<'a> { // This because snowflake allows numbers as placeholders let next_token = self.next_token(); let ident = match next_token.token { - Token::Word(w) => Ok(w.to_ident(next_token.span)), + Token::Word(w) => Ok(w.into_ident(next_token.span)), Token::Number(w, false) => Ok(Ident::new(w)), _ => self.expected("placeholder", next_token), }?; @@ -8753,7 +8754,7 @@ impl<'a> Parser<'a> { // (For example, in `FROM t1 JOIN` the `JOIN` will always be parsed as a keyword, // not an alias.) Token::Word(w) if after_as || !reserved_kwds.contains(&w.keyword) => { - Ok(Some(w.to_ident(next_token.span))) + Ok(Some(w.into_ident(next_token.span))) } // MSSQL supports single-quoted strings as aliases for columns // We accept them as table aliases too, although MSSQL does not. @@ -8920,7 +8921,7 @@ impl<'a> Parser<'a> { loop { match &self.peek_token_ref().token { Token::Word(w) => { - idents.push(w.to_ident(self.peek_token_ref().span)); + idents.push(w.clone().into_ident(self.peek_token_ref().span)); } Token::EOF | Token::Eq => break, _ => {} @@ -8975,7 +8976,7 @@ impl<'a> Parser<'a> { // expecting at least one word for identifier let next_token = self.next_token(); match next_token.token { - Token::Word(w) => idents.push(w.to_ident(next_token.span)), + Token::Word(w) => idents.push(w.into_ident(next_token.span)), Token::EOF => { return Err(ParserError::ParserError( "Empty input when parsing identifier".to_string(), @@ -8995,7 +8996,7 @@ impl<'a> Parser<'a> { Token::Period => { let next_token = self.next_token(); match next_token.token { - Token::Word(w) => idents.push(w.to_ident(next_token.span)), + Token::Word(w) => idents.push(w.into_ident(next_token.span)), Token::EOF => { return Err(ParserError::ParserError( "Trailing period in identifier".to_string(), @@ -9024,7 +9025,7 @@ impl<'a> Parser<'a> { pub fn parse_identifier(&mut self) -> Result { let next_token = self.next_token(); match next_token.token { - Token::Word(w) => Ok(w.to_ident(next_token.span)), + Token::Word(w) => Ok(w.into_ident(next_token.span)), Token::SingleQuotedString(s) => Ok(Ident::with_quote('\'', s)), Token::DoubleQuotedString(s) => Ok(Ident::with_quote('\"', s)), _ => self.expected("identifier", next_token), @@ -9044,9 +9045,10 @@ impl<'a> Parser<'a> { fn parse_unquoted_hyphenated_identifier(&mut self) -> Result<(Ident, bool), ParserError> { match self.peek_token().token { Token::Word(w) => { + let quote_style_is_none = w.quote_style.is_none(); let mut requires_whitespace = false; - let mut ident = w.to_ident(self.next_token().span); - if w.quote_style.is_none() { + let mut ident = w.into_ident(self.next_token().span); + if quote_style_is_none { while matches!(self.peek_token_no_skip().token, Token::Minus) { self.next_token(); ident.value.push('-'); @@ -13475,6 +13477,7 @@ impl<'a> Parser<'a> { } impl Word { + #[deprecated(since = "0.54.0", note = "please use `into_ident` instead")] pub fn to_ident(&self, span: Span) -> Ident { Ident { value: self.value.clone(), @@ -13482,6 +13485,15 @@ impl Word { span, } } + + /// Convert this word into an [`Ident`] identifier + pub fn into_ident(self, span: Span) -> Ident { + Ident { + value: self.value, + quote_style: self.quote_style, + span, + } + } } #[cfg(test)] From fe360208900a287872fc9875e9e6cb7d3a662302 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 29 Dec 2024 08:20:17 -0500 Subject: [PATCH 662/806] Simplify `parse_keyword_apis` more (#1626) --- src/parser/mod.rs | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ec91bf00e..012314b42 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3709,7 +3709,7 @@ impl<'a> Parser<'a> { ) } - /// Report that the current token was found instead of `expected`. + /// Report that the token at `index` was found instead of `expected`. pub fn expected_at(&self, expected: &str, index: usize) -> Result { let found = self.tokens.get(index).unwrap_or(&EOF_TOKEN); parser_err!( @@ -3730,27 +3730,6 @@ impl<'a> Parser<'a> { } } - /// If the current token is the `expected` keyword, consume it and returns - /// - /// See [`Self::parse_keyword_token_ref`] to avoid the copy. - #[must_use] - pub fn parse_keyword_token(&mut self, expected: Keyword) -> Option { - self.parse_keyword_token_ref(expected).cloned() - } - - /// If the current token is the `expected` keyword, consume it and returns a reference to the next token. - /// - #[must_use] - pub fn parse_keyword_token_ref(&mut self, expected: Keyword) -> Option<&TokenWithSpan> { - match &self.peek_token_ref().token { - Token::Word(w) if expected == w.keyword => { - self.advance_token(); - Some(self.get_current_token()) - } - _ => None, - } - } - #[must_use] pub fn peek_keyword(&self, expected: Keyword) -> bool { matches!(&self.peek_token_ref().token, Token::Word(w) if expected == w.keyword) @@ -3833,9 +3812,11 @@ impl<'a> Parser<'a> { /// If the current token is the `expected` keyword, consume the token. /// Otherwise, return an error. + /// + // todo deprecate infavor of expected_keyword_is pub fn expect_keyword(&mut self, expected: Keyword) -> Result { - if let Some(token) = self.parse_keyword_token_ref(expected) { - Ok(token.clone()) + if self.parse_keyword(expected) { + Ok(self.get_current_token().clone()) } else { self.expected_ref(format!("{:?}", &expected).as_str(), self.peek_token_ref()) } @@ -3847,7 +3828,7 @@ impl<'a> Parser<'a> { /// This differs from expect_keyword only in that the matched keyword /// token is not returned. pub fn expect_keyword_is(&mut self, expected: Keyword) -> Result<(), ParserError> { - if self.parse_keyword_token_ref(expected).is_some() { + if self.parse_keyword(expected) { Ok(()) } else { self.expected_ref(format!("{:?}", &expected).as_str(), self.peek_token_ref()) @@ -9488,7 +9469,8 @@ impl<'a> Parser<'a> { /// expect the initial keyword to be already consumed pub fn parse_query(&mut self) -> Result, ParserError> { let _guard = self.recursion_counter.try_decrease()?; - let with = if let Some(with_token) = self.parse_keyword_token_ref(Keyword::WITH) { + let with = if self.parse_keyword(Keyword::WITH) { + let with_token = self.get_current_token(); Some(With { with_token: with_token.clone().into(), recursive: self.parse_keyword(Keyword::RECURSIVE), From 3bad04e9e872e2f41b44bbd24a0c340dee13b584 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Jan 2025 15:47:59 -0500 Subject: [PATCH 663/806] Test benchmarks and Improve benchmark README.md (#1627) --- .github/workflows/rust.yml | 8 + sqlparser_bench/README.md | 24 +- sqlparser_bench/img/flamegraph.svg | 491 +++++++++++++++++++++++++++++ 3 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 sqlparser_bench/img/flamegraph.svg diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6c8130dc4..b5744e863 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,6 +37,14 @@ jobs: uses: ./.github/actions/setup-builder - run: cargo clippy --all-targets --all-features -- -D warnings + benchmark-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: ./.github/actions/setup-builder + - run: cd sqlparser_bench && cargo clippy --all-targets --all-features -- -D warnings + compile: runs-on: ubuntu-latest steps: diff --git a/sqlparser_bench/README.md b/sqlparser_bench/README.md index 4cdcfb29c..7f2c26254 100644 --- a/sqlparser_bench/README.md +++ b/sqlparser_bench/README.md @@ -17,4 +17,26 @@ under the License. --> -Benchmarks for sqlparser. See [the main README](../README.md) for more information. \ No newline at end of file +Benchmarks for sqlparser. See [the main README](../README.md) for more information. + +Note: this is in a separate, non workspace crate to avoid adding a dependency +on `criterion` to the main crate (which complicates testing without std). + +# Running Benchmarks + +```shell +cargo bench --bench sqlparser_bench +``` + +# Profiling + +Note you can generate a [flamegraph] using the following command: + +```shell +cargo flamegraph --bench sqlparser_bench +``` + +[flamegraph]: https://crates.io/crates/flamegraph + +Here is an example flamegraph: +![flamegraph](img/flamegraph.svg) diff --git a/sqlparser_bench/img/flamegraph.svg b/sqlparser_bench/img/flamegraph.svg new file mode 100644 index 000000000..0aaa17e06 --- /dev/null +++ b/sqlparser_bench/img/flamegraph.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<plotters_svg::svg::SVGBackend> (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<plotters_svg::svg::SVGBackend as core::ops::drop::Drop>::drop (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::kde::sweep_and_estimate (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<criterion::plot::plotters_backend::PlottersBackend as criterion::plot::Plotter>::abs_distributions (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`criterion::plot::plotters_backend::distributions::abs_distributions (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<criterion::plot::plotters_backend::PlottersBackend as criterion::plot::Plotter>::pdf (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<criterion::html::Html as criterion::report::Report>::measurement_complete (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`criterion::estimate::build_estimates (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates (7 samples, 0.01%)libsystem_malloc.dylib`_free (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_malloc.dylib`_free (38 samples, 0.08%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (32 samples, 0.07%)libsystem_malloc.dylib`_szone_free (13 samples, 0.03%)libsystem_malloc.dylib`small_free_list_add_ptr (12 samples, 0.03%)libsystem_malloc.dylib`small_free_list_remove_ptr (5 samples, 0.01%)libsystem_malloc.dylib`free_small (55 samples, 0.12%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::OrderByExpr as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (10 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (21 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (11 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (53 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (164 samples, 0.34%)sqlparser_bench-959bc5267970ca34`core::fmt::write (128 samples, 0.27%)libdyld.dylib`tlv_get_addr (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`psm::stack_pointer (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rust_psm_stack_pointer (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt (58 samples, 0.12%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::write (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`recursive::get_minimum_stack_size (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`recursive::get_stack_allocation_size (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::OrderByExpr as core::fmt::Display>::fmt (435 samples, 0.91%)sqlparser_bench-959bc5267970ca34`core::fmt::write (309 samples, 0.65%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (629 samples, 1.32%)sqlparser_bench-959bc5267970ca34`core::fmt::write (578 samples, 1.21%)sqlparser_bench-959bc5267970ca34`core::fmt::write (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::OrderBy as core::fmt::Display>::fmt (661 samples, 1.39%)sqlparser_bench-959bc5267970ca34`core::fmt::write (661 samples, 1.39%)sqlparser_bench-959bc5267970ca34`core::fmt::write (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::SelectItem as core::fmt::Display>::fmt (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (14 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::Join as core::fmt::Display>::fmt (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<<sqlparser::ast::query::Join as core::fmt::Display>::fmt::suffix::Suffix as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::TableFactor as core::fmt::Display>::fmt (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (77 samples, 0.16%)sqlparser_bench-959bc5267970ca34`<<sqlparser::ast::query::Join as core::fmt::Display>::fmt::suffix::Suffix as core::fmt::Display>::fmt (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (23 samples, 0.05%)libsystem_platform.dylib`_platform_memmove (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::fmt::write (192 samples, 0.40%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (347 samples, 0.73%)sqlparser_bench-959bc5267970ca34`core::fmt::write (321 samples, 0.67%)sqlparser_bench-959bc5267970ca34`core::fmt::write (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::TableFactor as core::fmt::Display>::fmt (473 samples, 0.99%)sqlparser_bench-959bc5267970ca34`core::fmt::write (401 samples, 0.84%)sqlparser_bench-959bc5267970ca34`core::fmt::write (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::Join as core::fmt::Display>::fmt (859 samples, 1.80%)s..sqlparser_bench-959bc5267970ca34`core::fmt::write (753 samples, 1.58%)sqlparser_bench-959bc5267970ca34`core::fmt::write (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (931 samples, 1.96%)s..sqlparser_bench-959bc5267970ca34`core::fmt::write (902 samples, 1.89%)s..sqlparser_bench-959bc5267970ca34`core::fmt::write (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (11 samples, 0.02%)libdyld.dylib`tlv_get_addr (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`psm::stack_pointer (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rust_psm_stack_pointer (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (53 samples, 0.11%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Function as core::fmt::Display>::fmt (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArguments as core::fmt::Display>::fmt (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::ObjectName as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArgumentList as core::fmt::Display>::fmt (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArgExpr as core::fmt::Display>::fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (10 samples, 0.02%)libdyld.dylib`tlv_get_addr (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`psm::stack_pointer (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rust_psm_stack_pointer (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (63 samples, 0.13%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (65 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (231 samples, 0.49%)sqlparser_bench-959bc5267970ca34`core::fmt::write (188 samples, 0.39%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_str (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::write (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`recursive::get_minimum_stack_size (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`recursive::get_stack_allocation_size (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArgExpr as core::fmt::Display>::fmt (450 samples, 0.95%)sqlparser_bench-959bc5267970ca34`core::fmt::write (408 samples, 0.86%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArg as core::fmt::Display>::fmt (528 samples, 1.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (500 samples, 1.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (711 samples, 1.49%)sqlparser_bench-959bc5267970ca34`core::fmt::write (645 samples, 1.35%)sqlparser_bench-959bc5267970ca34`core::fmt::write (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArgumentList as core::fmt::Display>::fmt (805 samples, 1.69%)sqlparser_bench-959bc5267970ca34`core::fmt::write (772 samples, 1.62%)sqlparser_bench-959bc5267970ca34`core::fmt::write (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArguments as core::fmt::Display>::fmt (1,024 samples, 2.15%)s..sqlparser_bench-959bc5267970ca34`core::fmt::write (972 samples, 2.04%)s..sqlparser_bench-959bc5267970ca34`core::fmt::write (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (18 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (50 samples, 0.11%)libsystem_malloc.dylib`szone_realloc (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_realloc (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (8 samples, 0.02%)libsystem_malloc.dylib`_realloc (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (321 samples, 0.67%)sqlparser_bench-959bc5267970ca34`core::fmt::write (219 samples, 0.46%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_str (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::ObjectName as core::fmt::Display>::fmt (425 samples, 0.89%)sqlparser_bench-959bc5267970ca34`core::fmt::write (390 samples, 0.82%)sqlparser_bench-959bc5267970ca34`core::fmt::write (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Function as core::fmt::Display>::fmt (1,627 samples, 3.42%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,551 samples, 3.26%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,728 samples, 3.63%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,678 samples, 3.52%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`recursive::get_minimum_stack_size (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`recursive::get_stack_allocation_size (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::SelectItem as core::fmt::Display>::fmt (1,910 samples, 4.01%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,868 samples, 3.92%)sqlp..sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (3,038 samples, 6.38%)sqlparse..sqlparser_bench-959bc5267970ca34`core::fmt::write (2,973 samples, 6.24%)sqlparse..sqlparser_bench-959bc5267970ca34`core::fmt::write (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::write (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::write (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::write (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::write (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`core::fmt::write (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (48 samples, 0.10%)sqlparser_bench-959bc5267970ca34`core::fmt::write (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`core::fmt::write (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`core::fmt::write (54 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (59 samples, 0.12%)sqlparser_bench-959bc5267970ca34`core::fmt::write (59 samples, 0.12%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::write (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::write (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (63 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::write (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (65 samples, 0.14%)sqlparser_bench-959bc5267970ca34`core::fmt::write (65 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (67 samples, 0.14%)sqlparser_bench-959bc5267970ca34`core::fmt::write (67 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (68 samples, 0.14%)sqlparser_bench-959bc5267970ca34`core::fmt::write (68 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (72 samples, 0.15%)sqlparser_bench-959bc5267970ca34`core::fmt::write (71 samples, 0.15%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (79 samples, 0.17%)sqlparser_bench-959bc5267970ca34`core::fmt::write (79 samples, 0.17%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (78 samples, 0.16%)sqlparser_bench-959bc5267970ca34`core::fmt::write (78 samples, 0.16%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (75 samples, 0.16%)sqlparser_bench-959bc5267970ca34`core::fmt::write (75 samples, 0.16%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (74 samples, 0.16%)sqlparser_bench-959bc5267970ca34`core::fmt::write (74 samples, 0.16%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (82 samples, 0.17%)sqlparser_bench-959bc5267970ca34`core::fmt::write (82 samples, 0.17%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (81 samples, 0.17%)sqlparser_bench-959bc5267970ca34`core::fmt::write (81 samples, 0.17%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::Query as core::fmt::Display>::fmt (3,806 samples, 7.99%)sqlparser_b..sqlparser_bench-959bc5267970ca34`core::fmt::write (3,806 samples, 7.99%)sqlparser_b..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::SetExpr as core::fmt::Display>::fmt (3,145 samples, 6.60%)sqlparser..sqlparser_bench-959bc5267970ca34`core::fmt::write (3,145 samples, 6.60%)sqlparser..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::Select as core::fmt::Display>::fmt (3,144 samples, 6.60%)sqlparser..sqlparser_bench-959bc5267970ca34`core::fmt::write (3,143 samples, 6.60%)sqlparser..sqlparser_bench-959bc5267970ca34`core::fmt::write (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Statement as core::fmt::Display>::fmt (3,809 samples, 8.00%)sqlparser_b..sqlparser_bench-959bc5267970ca34`core::fmt::write (3,808 samples, 8.00%)sqlparser_b..sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (32 samples, 0.07%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`_free (19 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (17 samples, 0.04%)libsystem_malloc.dylib`_szone_free (12 samples, 0.03%)libsystem_malloc.dylib`small_free_list_add_ptr (18 samples, 0.04%)libsystem_malloc.dylib`small_free_list_remove_ptr (8 samples, 0.02%)libsystem_malloc.dylib`free_small (83 samples, 0.17%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (11 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_malloc.dylib`free_small (10 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)libsystem_malloc.dylib`free_small (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::vec::Vec<T,A> as core::ops::drop::Drop>::drop (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::TableFactor> (11 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (13 samples, 0.03%)libsystem_malloc.dylib`free_small (12 samples, 0.03%)libsystem_malloc.dylib`small_free_list_add_ptr (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::Cte> (128 samples, 0.27%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::Query> (112 samples, 0.24%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::SetExpr> (91 samples, 0.19%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::SelectItem> (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Function> (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::FunctionArgumentList> (17 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)libsystem_malloc.dylib`free_medium (39 samples, 0.08%)libsystem_kernel.dylib`madvise (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::OrderBy> (58 samples, 0.12%)libsystem_malloc.dylib`nanov2_madvise_block (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_madvise_block_locked (5 samples, 0.01%)libsystem_kernel.dylib`madvise (5 samples, 0.01%)libsystem_malloc.dylib`_free (16 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (16 samples, 0.03%)libsystem_malloc.dylib`_szone_free (12 samples, 0.03%)libsystem_malloc.dylib`free_medium (18 samples, 0.04%)libsystem_kernel.dylib`madvise (18 samples, 0.04%)libsystem_malloc.dylib`small_free_list_add_ptr (10 samples, 0.02%)libsystem_malloc.dylib`small_free_list_find_by_ptr (9 samples, 0.02%)libsystem_malloc.dylib`free_small (48 samples, 0.10%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (8 samples, 0.02%)libsystem_malloc.dylib`tiny_free_list_add_ptr (8 samples, 0.02%)libsystem_malloc.dylib`free_tiny (44 samples, 0.09%)libsystem_malloc.dylib`tiny_free_no_lock (30 samples, 0.06%)libsystem_malloc.dylib`tiny_free_list_remove_ptr (10 samples, 0.02%)libsystem_malloc.dylib`_free (10 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (10 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (52 samples, 0.11%)libsystem_malloc.dylib`nanov2_madvise_block (13 samples, 0.03%)libsystem_malloc.dylib`nanov2_madvise_block_locked (13 samples, 0.03%)libsystem_kernel.dylib`madvise (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::vec::Vec<T,A> as core::ops::drop::Drop>::drop (131 samples, 0.28%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::TableFactor> (96 samples, 0.20%)libsystem_platform.dylib`_platform_memset (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`tiny_free_list_add_ptr (5 samples, 0.01%)libsystem_malloc.dylib`free_tiny (28 samples, 0.06%)libsystem_malloc.dylib`tiny_free_no_lock (16 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (70 samples, 0.15%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (13 samples, 0.03%)libsystem_malloc.dylib`nanov2_madvise_block (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_madvise_block_locked (6 samples, 0.01%)libsystem_kernel.dylib`madvise (6 samples, 0.01%)libsystem_platform.dylib`_platform_memset (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`small_free_list_add_ptr (12 samples, 0.03%)libsystem_malloc.dylib`free_small (38 samples, 0.08%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Function> (95 samples, 0.20%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::FunctionArgumentList> (54 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::SetExpr> (510 samples, 1.07%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::query::Query>> (873 samples, 1.83%)s..sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::Query> (854 samples, 1.79%)s..sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (175 samples, 0.37%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Whitespace> (57 samples, 0.12%)libsystem_malloc.dylib`_nanov2_free (123 samples, 0.26%)libsystem_malloc.dylib`free_medium (62 samples, 0.13%)libsystem_kernel.dylib`madvise (62 samples, 0.13%)libsystem_malloc.dylib`free_small (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_madvise_block (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_madvise_block_locked (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (83 samples, 0.17%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (17 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`small_free_list_add_ptr (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (67 samples, 0.14%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (48 samples, 0.10%)libsystem_malloc.dylib`szone_malloc_should_clear (31 samples, 0.07%)libsystem_malloc.dylib`small_malloc_should_clear (25 samples, 0.05%)libsystem_malloc.dylib`small_malloc_from_free_list (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (12 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_malloc (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (154 samples, 0.32%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (6 samples, 0.01%)libsystem_malloc.dylib`small_free_list_add_ptr (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::alloc::exchange_malloc (48 samples, 0.10%)libsystem_malloc.dylib`szone_malloc_should_clear (48 samples, 0.10%)libsystem_malloc.dylib`small_malloc_should_clear (36 samples, 0.08%)libsystem_malloc.dylib`small_malloc_from_free_list (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (8 samples, 0.02%)libsystem_malloc.dylib`szone_malloc_should_clear (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_token (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::alloc::exchange_malloc (11 samples, 0.02%)libsystem_malloc.dylib`szone_malloc_should_clear (11 samples, 0.02%)libsystem_malloc.dylib`small_malloc_should_clear (8 samples, 0.02%)libsystem_malloc.dylib`small_malloc_from_free_list (7 samples, 0.01%)libsystem_malloc.dylib`szone_malloc_should_clear (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (16 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_keyword (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_all_or_distinct (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (14 samples, 0.03%)libsystem_malloc.dylib`szone_malloc_should_clear (6 samples, 0.01%)libsystem_malloc.dylib`_free (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (13 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_alias (41 samples, 0.09%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (28 samples, 0.06%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (9 samples, 0.02%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (13 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_pointer_size (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)libsystem_malloc.dylib`nanov2_realloc (25 samples, 0.05%)libsystem_malloc.dylib`nanov2_malloc (10 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (47 samples, 0.10%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_malloc.dylib`_realloc (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (71 samples, 0.15%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (62 samples, 0.13%)libsystem_malloc.dylib`nanov2_size (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (76 samples, 0.16%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (115 samples, 0.24%)sqlparser_bench-959bc5267970ca34`core::fmt::write (109 samples, 0.23%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (106 samples, 0.22%)sqlparser_bench-959bc5267970ca34`core::fmt::write (104 samples, 0.22%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (16 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (16 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (81 samples, 0.17%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (16 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_type_modifiers (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (158 samples, 0.33%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (168 samples, 0.35%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_compound_field_access (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_token (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (14 samples, 0.03%)libsystem_malloc.dylib`szone_malloc_should_clear (14 samples, 0.03%)libsystem_malloc.dylib`small_malloc_should_clear (11 samples, 0.02%)libsystem_malloc.dylib`small_malloc_from_free_list (9 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (13 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (23 samples, 0.05%)libsystem_malloc.dylib`_realloc (21 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_realloc (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (54 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (53 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (119 samples, 0.25%)sqlparser_bench-959bc5267970ca34`core::fmt::write (105 samples, 0.22%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_named_arg_operator (134 samples, 0.28%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (19 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (5 samples, 0.01%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_realloc (24 samples, 0.05%)libsystem_malloc.dylib`nanov2_malloc (10 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (34 samples, 0.07%)libsystem_malloc.dylib`_realloc (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (48 samples, 0.10%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (69 samples, 0.14%)sqlparser_bench-959bc5267970ca34`core::fmt::write (69 samples, 0.14%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (80 samples, 0.17%)sqlparser_bench-959bc5267970ca34`core::fmt::write (77 samples, 0.16%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (48 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::try_parse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (198 samples, 0.42%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_args (436 samples, 0.92%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_expr (264 samples, 0.55%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (241 samples, 0.51%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (480 samples, 1.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_argument_list (541 samples, 1.14%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_call (564 samples, 1.18%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (641 samples, 1.35%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (1,035 samples, 2.17%)s..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select_item (1,271 samples, 2.67%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_expr (1,200 samples, 2.52%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (1,148 samples, 2.41%)sq..libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_table_alias (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_alias (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_factor (90 samples, 0.19%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_tokens (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (1,454 samples, 3.05%)sql..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_and_joins (122 samples, 0.26%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (12 samples, 0.03%)libsystem_malloc.dylib`nanov2_realloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (18 samples, 0.04%)libsystem_malloc.dylib`_realloc (11 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (106 samples, 0.22%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_group_by (154 samples, 0.32%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (135 samples, 0.28%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_query_body (1,727 samples, 3.63%)sqlp..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select (1,681 samples, 3.53%)sql..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_cte (1,854 samples, 3.89%)sqlp..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_query (1,798 samples, 3.78%)sqlp..libsystem_platform.dylib`_platform_memmove (81 samples, 0.17%)libsystem_platform.dylib`_platform_memmove (18 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_realloc (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (10 samples, 0.02%)libsystem_malloc.dylib`_realloc (10 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)libsystem_malloc.dylib`_free (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (12 samples, 0.03%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_realloc (26 samples, 0.05%)libsystem_malloc.dylib`_realloc (49 samples, 0.10%)libsystem_malloc.dylib`_malloc_zone_realloc (46 samples, 0.10%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (64 samples, 0.13%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (60 samples, 0.13%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (54 samples, 0.11%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (106 samples, 0.22%)sqlparser_bench-959bc5267970ca34`core::fmt::write (99 samples, 0.21%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (92 samples, 0.19%)sqlparser_bench-959bc5267970ca34`core::fmt::write (90 samples, 0.19%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (13 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (10 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (58 samples, 0.12%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (73 samples, 0.15%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (71 samples, 0.15%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (277 samples, 0.58%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::try_parse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_order_by_expr (357 samples, 0.75%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (343 samples, 0.72%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_order_by (481 samples, 1.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (397 samples, 0.83%)libsystem_malloc.dylib`szone_malloc_should_clear (23 samples, 0.05%)libsystem_malloc.dylib`small_malloc_should_clear (18 samples, 0.04%)libsystem_malloc.dylib`small_malloc_from_free_list (15 samples, 0.03%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (173 samples, 0.36%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (11 samples, 0.02%)libsystem_malloc.dylib`rack_get_thread_index (5 samples, 0.01%)libsystem_malloc.dylib`small_free_list_add_ptr (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::FnOnce::call_once (42 samples, 0.09%)libsystem_malloc.dylib`szone_malloc_should_clear (36 samples, 0.08%)libsystem_malloc.dylib`small_malloc_should_clear (26 samples, 0.05%)libsystem_malloc.dylib`small_malloc_from_free_list (21 samples, 0.04%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keywords (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_one_of_keywords (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_group_by (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (106 samples, 0.22%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (13 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_keyword (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::is_parse_comma_separated_end_with_trailing_commas (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_all_or_distinct (11 samples, 0.02%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (195 samples, 0.41%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::cmp::PartialEq>::eq (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (10 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (9 samples, 0.02%)libsystem_malloc.dylib`small_free_list_add_ptr (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (94 samples, 0.20%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (56 samples, 0.12%)libsystem_malloc.dylib`szone_malloc_should_clear (28 samples, 0.06%)libsystem_malloc.dylib`small_malloc_should_clear (19 samples, 0.04%)libsystem_malloc.dylib`small_malloc_from_free_list (13 samples, 0.03%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::is_parse_comma_separated_end_with_trailing_commas (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_select_item_exclude (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_select_item_ilike (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_select_item_rename (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_additional_options (48 samples, 0.10%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (20 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (11 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (22 samples, 0.05%)libsystem_platform.dylib`_platform_memmove (18 samples, 0.04%)libsystem_platform.dylib`_platform_memset (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_malloc.dylib`nanov2_pointer_size (8 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc (8 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (29 samples, 0.06%)libsystem_malloc.dylib`nanov2_realloc (16 samples, 0.03%)libsystem_malloc.dylib`_realloc (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (59 samples, 0.12%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (94 samples, 0.20%)sqlparser_bench-959bc5267970ca34`core::fmt::write (93 samples, 0.20%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (111 samples, 0.23%)sqlparser_bench-959bc5267970ca34`core::fmt::write (102 samples, 0.21%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (26 samples, 0.05%)libsystem_malloc.dylib`_nanov2_free (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (48 samples, 0.10%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (10 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (9 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_type_modifiers (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (111 samples, 0.23%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (118 samples, 0.25%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_compound_field_access (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_token (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_token (23 samples, 0.05%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (16 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (21 samples, 0.04%)libsystem_malloc.dylib`szone_malloc_should_clear (18 samples, 0.04%)libsystem_malloc.dylib`small_malloc_should_clear (17 samples, 0.04%)libsystem_malloc.dylib`small_malloc_from_free_list (15 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (17 samples, 0.04%)libsystem_platform.dylib`_platform_memset (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (14 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (11 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (15 samples, 0.03%)libsystem_malloc.dylib`_realloc (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (63 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::write (60 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (106 samples, 0.22%)sqlparser_bench-959bc5267970ca34`core::fmt::write (97 samples, 0.20%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_named_arg_operator (117 samples, 0.25%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (31 samples, 0.07%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (14 samples, 0.03%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (8 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (15 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_realloc (33 samples, 0.07%)libsystem_platform.dylib`_platform_memmove (10 samples, 0.02%)libsystem_malloc.dylib`_realloc (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (48 samples, 0.10%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (79 samples, 0.17%)sqlparser_bench-959bc5267970ca34`core::fmt::write (78 samples, 0.16%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (102 samples, 0.21%)sqlparser_bench-959bc5267970ca34`core::fmt::write (94 samples, 0.20%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (19 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (64 samples, 0.13%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (69 samples, 0.14%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::try_parse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (259 samples, 0.54%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_args (554 samples, 1.16%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_expr (354 samples, 0.74%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (318 samples, 0.67%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (616 samples, 1.29%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_argument_list (706 samples, 1.48%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_json_null_clause (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_call (781 samples, 1.64%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (824 samples, 1.73%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (1,166 samples, 2.45%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select_item (1,371 samples, 2.88%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_expr (1,283 samples, 2.69%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (1,236 samples, 2.60%)sq..libsystem_malloc.dylib`_free (19 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (20 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<T as core::any::Any>::type_id (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (12 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (15 samples, 0.03%)libsystem_malloc.dylib`szone_malloc_should_clear (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_join_constraint (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_parenthesized_column_list (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keywords (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_one_of_keywords (9 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::cmp::PartialEq>::eq (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::consume_token (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::cmp::PartialEq>::eq (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::maybe_parse_table_sample (12 samples, 0.03%)libsystem_malloc.dylib`_free (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (27 samples, 0.06%)libsystem_malloc.dylib`nanov2_malloc_type (25 samples, 0.05%)libsystem_malloc.dylib`nanov2_allocate_outlined (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (6 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (19 samples, 0.04%)libsystem_malloc.dylib`nanov2_malloc_type (13 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (13 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (170 samples, 0.36%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (98 samples, 0.21%)libsystem_malloc.dylib`_free (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (14 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_table_alias (111 samples, 0.23%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_alias (95 samples, 0.20%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (37 samples, 0.08%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (12 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (12 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (22 samples, 0.05%)libsystem_malloc.dylib`nanov2_malloc_type (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_factor (711 samples, 1.49%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_tokens (208 samples, 0.44%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (91 samples, 0.19%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_and_joins (1,055 samples, 2.22%)s..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_tokens (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (2,821 samples, 5.92%)sqlparse..libsystem_malloc.dylib`_malloc_zone_malloc (15 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (73 samples, 0.15%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (10 samples, 0.02%)libsystem_malloc.dylib`rack_get_thread_index (10 samples, 0.02%)libsystem_malloc.dylib`tiny_malloc_from_free_list (8 samples, 0.02%)libsystem_malloc.dylib`set_tiny_meta_header_in_use (18 samples, 0.04%)libsystem_malloc.dylib`szone_malloc_should_clear (129 samples, 0.27%)libsystem_malloc.dylib`tiny_malloc_should_clear (95 samples, 0.20%)libsystem_malloc.dylib`tiny_malloc_from_free_list (59 samples, 0.12%)libsystem_malloc.dylib`tiny_free_list_add_ptr (8 samples, 0.02%)libsystem_malloc.dylib`tiny_malloc_should_clear (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (6 samples, 0.01%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (83 samples, 0.17%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (10 samples, 0.02%)libsystem_malloc.dylib`set_tiny_meta_header_in_use (8 samples, 0.02%)libsystem_malloc.dylib`szone_malloc_should_clear (50 samples, 0.11%)libsystem_malloc.dylib`tiny_malloc_should_clear (42 samples, 0.09%)libsystem_malloc.dylib`tiny_malloc_from_free_list (30 samples, 0.06%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_malloc.dylib`_free (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (10 samples, 0.02%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memset (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (8 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (19 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)libsystem_malloc.dylib`_realloc (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (64 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::write (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::fmt::Display>::fmt (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::write (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (142 samples, 0.30%)sqlparser_bench-959bc5267970ca34`core::fmt::write (130 samples, 0.27%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (168 samples, 0.35%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (159 samples, 0.33%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_value (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (213 samples, 0.45%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_infix (349 samples, 0.73%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (271 samples, 0.57%)libsystem_malloc.dylib`_free (6 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (9 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (20 samples, 0.04%)libsystem_platform.dylib`_platform_memset (11 samples, 0.02%)libsystem_malloc.dylib`nanov2_pointer_size (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (17 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_realloc (36 samples, 0.08%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_malloc.dylib`_realloc (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (59 samples, 0.12%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (63 samples, 0.13%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (91 samples, 0.19%)sqlparser_bench-959bc5267970ca34`core::fmt::write (89 samples, 0.19%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (105 samples, 0.22%)sqlparser_bench-959bc5267970ca34`core::fmt::write (99 samples, 0.21%)libsystem_malloc.dylib`_nanov2_free (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (13 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memset (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_malloc.dylib`_realloc (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_pointer_size (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc (8 samples, 0.02%)libsystem_malloc.dylib`nanov2_realloc (25 samples, 0.05%)libsystem_platform.dylib`_platform_memset (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_realloc (47 samples, 0.10%)libsystem_platform.dylib`_platform_memmove (9 samples, 0.02%)libsystem_malloc.dylib`_realloc (53 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (80 samples, 0.17%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (73 samples, 0.15%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (58 samples, 0.12%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (120 samples, 0.25%)sqlparser_bench-959bc5267970ca34`core::fmt::write (118 samples, 0.25%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::fmt::Display>::fmt (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (261 samples, 0.55%)sqlparser_bench-959bc5267970ca34`core::fmt::write (205 samples, 0.43%)sqlparser_bench-959bc5267970ca34`core::fmt::write (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (12 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (54 samples, 0.11%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (379 samples, 0.80%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (360 samples, 0.76%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (17 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_value (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::try_parse (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (673 samples, 1.41%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_infix (1,392 samples, 2.92%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (1,215 samples, 2.55%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (24 samples, 0.05%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (23 samples, 0.05%)libsystem_platform.dylib`_platform_memset (8 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (35 samples, 0.07%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)libsystem_malloc.dylib`_realloc (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (47 samples, 0.10%)libsystem_malloc.dylib`nanov2_size (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (128 samples, 0.27%)sqlparser_bench-959bc5267970ca34`core::fmt::write (123 samples, 0.26%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral::write_prefix (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::fmt::Display>::fmt (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (246 samples, 0.52%)sqlparser_bench-959bc5267970ca34`core::fmt::write (223 samples, 0.47%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (273 samples, 0.57%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (269 samples, 0.56%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_value (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (343 samples, 0.72%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (1,947 samples, 4.09%)sqlp..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select (5,109 samples, 10.73%)sqlparser_bench-..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_query_body (5,450 samples, 11.45%)sqlparser_bench-9..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_query (8,118 samples, 17.05%)sqlparser_bench-959bc52679..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_settings (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_statements (8,347 samples, 17.53%)sqlparser_bench-959bc526797..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_statement (8,222 samples, 17.27%)sqlparser_bench-959bc526797..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::next_token (177 samples, 0.37%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_identifier_or_keyword (36 samples, 0.08%)libsystem_malloc.dylib`_free (161 samples, 0.34%)libsystem_malloc.dylib`_nanov2_free (39 samples, 0.08%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::iter::traits::collect::FromIterator<char>>::from_iter (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::dialect::generic::GenericDialect as sqlparser::dialect::Dialect>::is_delimited_identifier_start (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::dialect::mssql::MsSqlDialect as sqlparser::dialect::Dialect>::is_identifier_start (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`__rdl_dealloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$realloc (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_realloc (7 samples, 0.01%)libsystem_malloc.dylib`szone_good_size (6 samples, 0.01%)libsystem_malloc.dylib`_szone_free (5 samples, 0.01%)libsystem_malloc.dylib`tiny_free_list_add_ptr (6 samples, 0.01%)libsystem_malloc.dylib`free_tiny (29 samples, 0.06%)libsystem_malloc.dylib`tiny_free_no_lock (20 samples, 0.04%)libsystem_malloc.dylib`small_try_realloc_in_place (14 samples, 0.03%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (7 samples, 0.01%)libsystem_malloc.dylib`small_free_list_add_ptr (5 samples, 0.01%)libsystem_malloc.dylib`szone_malloc_should_clear (43 samples, 0.09%)libsystem_malloc.dylib`small_malloc_should_clear (33 samples, 0.07%)libsystem_malloc.dylib`small_malloc_from_free_list (28 samples, 0.06%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (7 samples, 0.01%)libsystem_malloc.dylib`szone_size (23 samples, 0.05%)libsystem_malloc.dylib`tiny_size (22 samples, 0.05%)libsystem_malloc.dylib`tiny_try_realloc_in_place (23 samples, 0.05%)libsystem_malloc.dylib`tiny_free_list_remove_ptr (6 samples, 0.01%)libsystem_malloc.dylib`szone_realloc (186 samples, 0.39%)libsystem_platform.dylib`_platform_memset (9 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (252 samples, 0.53%)libsystem_platform.dylib`_platform_memmove (29 samples, 0.06%)libsystem_malloc.dylib`_realloc (330 samples, 0.69%)libsystem_malloc.dylib`szone_size (42 samples, 0.09%)libsystem_malloc.dylib`tiny_size (40 samples, 0.08%)libsystem_malloc.dylib`szone_malloc_should_clear (47 samples, 0.10%)libsystem_malloc.dylib`tiny_malloc_should_clear (34 samples, 0.07%)libsystem_malloc.dylib`tiny_malloc_from_free_list (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (436 samples, 0.92%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (405 samples, 0.85%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::is_custom_operator_part (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::State::next (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::State::peek (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::consume_and_return (25 samples, 0.05%)libsystem_malloc.dylib`_free (9 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (13 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (30 samples, 0.06%)libsystem_platform.dylib`_platform_memcmp (80 samples, 0.17%)libsystem_platform.dylib`_platform_memmove (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::dialect::mssql::MsSqlDialect as sqlparser::dialect::Dialect>::is_identifier_part (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcmp (21 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_malloc (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (18 samples, 0.04%)libsystem_malloc.dylib`nanov2_malloc_type (17 samples, 0.04%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Token::make_word (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::str::_<impl str>::to_uppercase (12 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_pointer_size (11 samples, 0.02%)libsystem_malloc.dylib`_realloc (30 samples, 0.06%)libsystem_malloc.dylib`_malloc_zone_realloc (23 samples, 0.05%)libsystem_malloc.dylib`nanov2_realloc (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (39 samples, 0.08%)libsystem_malloc.dylib`nanov2_size (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::next_token (884 samples, 1.86%)s..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_word (116 samples, 0.24%)libsystem_malloc.dylib`_free (37 samples, 0.08%)libsystem_malloc.dylib`_malloc_zone_malloc (39 samples, 0.08%)libsystem_malloc.dylib`_nanov2_free (231 samples, 0.49%)libsystem_platform.dylib`_platform_memcmp (593 samples, 1.25%)libsystem_platform.dylib`_platform_memmove (110 samples, 0.23%)libsystem_malloc.dylib`_malloc_zone_malloc (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (183 samples, 0.38%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (83 samples, 0.17%)libsystem_malloc.dylib`nanov2_malloc_type (66 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::iter::traits::collect::FromIterator<char>>::from_iter (235 samples, 0.49%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::dialect::mssql::MsSqlDialect as sqlparser::dialect::Dialect>::is_identifier_part (119 samples, 0.25%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$malloc (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcmp (194 samples, 0.41%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::str::_<impl str>::to_uppercase (38 samples, 0.08%)libsystem_malloc.dylib`_malloc_zone_malloc (42 samples, 0.09%)libsystem_malloc.dylib`nanov2_malloc_type (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$malloc (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Token::make_word (363 samples, 0.76%)sqlparser_bench-959bc5267970ca34`alloc::str::_<impl str>::to_uppercase (137 samples, 0.29%)libsystem_malloc.dylib`nanov2_malloc_type (59 samples, 0.12%)libsystem_malloc.dylib`_nanov2_free (78 samples, 0.16%)libsystem_malloc.dylib`_malloc_zone_malloc (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$malloc (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (158 samples, 0.33%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (55 samples, 0.12%)libsystem_malloc.dylib`nanov2_malloc_type (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_identifier_or_keyword (2,644 samples, 5.55%)sqlpars..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_word (528 samples, 1.11%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_with_location (5,051 samples, 10.61%)sqlparser_bench..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_with_location_into_buf (4,835 samples, 10.15%)sqlparser_bench..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_word (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::try_with_sql (5,081 samples, 10.67%)sqlparser_bench-..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_sql (13,813 samples, 29.01%)sqlparser_bench-959bc5267970ca34`sqlparser::par..sqlparser_bench-959bc5267970ca34`criterion::bencher::Bencher<M>::iter (18,926 samples, 39.75%)sqlparser_bench-959bc5267970ca34`criterion::bencher::Bencher<M>::..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::try_with_sql (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (18,946 samples, 39.79%)sqlparser_bench-959bc5267970ca34`<alloc::vec::Vec<T> as alloc::ve..libsystem_malloc.dylib`_free (8 samples, 0.02%)libsystem_malloc.dylib`_free (57 samples, 0.12%)libsystem_malloc.dylib`_nanov2_free (33 samples, 0.07%)libsystem_malloc.dylib`_szone_free (12 samples, 0.03%)libsystem_malloc.dylib`small_free_list_add_ptr (14 samples, 0.03%)libsystem_malloc.dylib`small_free_list_find_by_ptr (8 samples, 0.02%)libsystem_malloc.dylib`free_small (60 samples, 0.13%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::OrderByExpr as core::fmt::Display>::fmt (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (11 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (22 samples, 0.05%)libsystem_platform.dylib`_platform_memmove (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (143 samples, 0.30%)sqlparser_bench-959bc5267970ca34`core::fmt::write (113 samples, 0.24%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_str (8 samples, 0.02%)libdyld.dylib`tlv_get_addr (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`psm::stack_pointer (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rust_psm_stack_pointer (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_fmt (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::write (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`recursive::get_minimum_stack_size (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`recursive::get_stack_allocation_size (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::OrderByExpr as core::fmt::Display>::fmt (376 samples, 0.79%)sqlparser_bench-959bc5267970ca34`core::fmt::write (282 samples, 0.59%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (542 samples, 1.14%)sqlparser_bench-959bc5267970ca34`core::fmt::write (500 samples, 1.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::OrderBy as core::fmt::Display>::fmt (570 samples, 1.20%)sqlparser_bench-959bc5267970ca34`core::fmt::write (570 samples, 1.20%)sqlparser_bench-959bc5267970ca34`core::fmt::write (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (11 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::Join as core::fmt::Display>::fmt (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<<sqlparser::ast::query::Join as core::fmt::Display>::fmt::suffix::Suffix as core::fmt::Display>::fmt (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::TableFactor as core::fmt::Display>::fmt (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (65 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<<sqlparser::ast::query::Join as core::fmt::Display>::fmt::suffix::Suffix as core::fmt::Display>::fmt (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (18 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (232 samples, 0.49%)sqlparser_bench-959bc5267970ca34`core::fmt::write (161 samples, 0.34%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (311 samples, 0.65%)sqlparser_bench-959bc5267970ca34`core::fmt::write (272 samples, 0.57%)sqlparser_bench-959bc5267970ca34`core::fmt::write (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::TableFactor as core::fmt::Display>::fmt (412 samples, 0.87%)sqlparser_bench-959bc5267970ca34`core::fmt::write (347 samples, 0.73%)sqlparser_bench-959bc5267970ca34`core::fmt::write (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::Join as core::fmt::Display>::fmt (733 samples, 1.54%)sqlparser_bench-959bc5267970ca34`core::fmt::write (648 samples, 1.36%)sqlparser_bench-959bc5267970ca34`core::fmt::write (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (801 samples, 1.68%)sqlparser_bench-959bc5267970ca34`core::fmt::write (780 samples, 1.64%)sqlparser_bench-959bc5267970ca34`core::fmt::write (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (11 samples, 0.02%)libdyld.dylib`tlv_get_addr (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`psm::stack_pointer (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rust_psm_stack_pointer (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Function as core::fmt::Display>::fmt (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArguments as core::fmt::Display>::fmt (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (23 samples, 0.05%)libsystem_platform.dylib`_platform_memmove (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArg as core::fmt::Display>::fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (21 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArgExpr as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (11 samples, 0.02%)libdyld.dylib`tlv_get_addr (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`psm::stack_pointer (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rust_psm_stack_pointer (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (178 samples, 0.37%)sqlparser_bench-959bc5267970ca34`core::fmt::write (133 samples, 0.28%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_str (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_fmt (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::write (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`recursive::get_minimum_stack_size (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`recursive::get_stack_allocation_size (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArgExpr as core::fmt::Display>::fmt (340 samples, 0.71%)sqlparser_bench-959bc5267970ca34`core::fmt::write (311 samples, 0.65%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArg as core::fmt::Display>::fmt (401 samples, 0.84%)sqlparser_bench-959bc5267970ca34`core::fmt::write (378 samples, 0.79%)sqlparser_bench-959bc5267970ca34`core::fmt::write (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (580 samples, 1.22%)sqlparser_bench-959bc5267970ca34`core::fmt::write (508 samples, 1.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArgumentList as core::fmt::Display>::fmt (655 samples, 1.38%)sqlparser_bench-959bc5267970ca34`core::fmt::write (637 samples, 1.34%)sqlparser_bench-959bc5267970ca34`core::fmt::write (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::FunctionArguments as core::fmt::Display>::fmt (813 samples, 1.71%)sqlparser_bench-959bc5267970ca34`core::fmt::write (772 samples, 1.62%)sqlparser_bench-959bc5267970ca34`core::fmt::write (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (15 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (239 samples, 0.50%)sqlparser_bench-959bc5267970ca34`core::fmt::write (152 samples, 0.32%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::ObjectName as core::fmt::Display>::fmt (320 samples, 0.67%)sqlparser_bench-959bc5267970ca34`core::fmt::write (294 samples, 0.62%)sqlparser_bench-959bc5267970ca34`core::fmt::write (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Function as core::fmt::Display>::fmt (1,284 samples, 2.70%)sq..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,221 samples, 2.56%)sq..sqlparser_bench-959bc5267970ca34`core::fmt::write (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,364 samples, 2.86%)sq..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,331 samples, 2.80%)sq..sqlparser_bench-959bc5267970ca34`core::fmt::write (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`recursive::get_minimum_stack_size (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`recursive::get_stack_allocation_size (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::SelectItem as core::fmt::Display>::fmt (1,540 samples, 3.23%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,504 samples, 3.16%)sql..sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::DisplaySeparated<T> as core::fmt::Display>::fmt (2,493 samples, 5.24%)sqlpar..sqlparser_bench-959bc5267970ca34`core::fmt::write (2,446 samples, 5.14%)sqlpar..sqlparser_bench-959bc5267970ca34`core::fmt::write (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::write (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::write (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::fmt::write (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::write (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`core::fmt::write (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`core::fmt::write (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (48 samples, 0.10%)sqlparser_bench-959bc5267970ca34`core::fmt::write (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`core::fmt::write (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`core::fmt::write (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Statement as core::fmt::Display>::fmt (3,140 samples, 6.59%)sqlparser..sqlparser_bench-959bc5267970ca34`core::fmt::write (3,140 samples, 6.59%)sqlparser..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::Query as core::fmt::Display>::fmt (3,140 samples, 6.59%)sqlparser..sqlparser_bench-959bc5267970ca34`core::fmt::write (3,140 samples, 6.59%)sqlparser..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::SetExpr as core::fmt::Display>::fmt (2,570 samples, 5.40%)sqlpars..sqlparser_bench-959bc5267970ca34`core::fmt::write (2,570 samples, 5.40%)sqlpars..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::query::Select as core::fmt::Display>::fmt (2,570 samples, 5.40%)sqlpars..sqlparser_bench-959bc5267970ca34`core::fmt::write (2,570 samples, 5.40%)sqlpars..sqlparser_bench-959bc5267970ca34`core::fmt::write (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (24 samples, 0.05%)libsystem_malloc.dylib`_free (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (5 samples, 0.01%)libsystem_malloc.dylib`_free (14 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)libsystem_malloc.dylib`_szone_free (18 samples, 0.04%)libsystem_malloc.dylib`small_free_list_add_ptr (8 samples, 0.02%)libsystem_malloc.dylib`small_free_list_remove_ptr (7 samples, 0.01%)libsystem_malloc.dylib`free_small (64 samples, 0.13%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (8 samples, 0.02%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`free_small (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (8 samples, 0.02%)libsystem_malloc.dylib`free_small (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Function> (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::FunctionArgumentList> (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::Cte> (77 samples, 0.16%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::Query> (69 samples, 0.14%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::SetExpr> (53 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::SelectItem> (32 samples, 0.07%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)libsystem_malloc.dylib`free_medium (28 samples, 0.06%)libsystem_kernel.dylib`madvise (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::OrderBy> (42 samples, 0.09%)libsystem_malloc.dylib`_free (10 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (20 samples, 0.04%)libsystem_malloc.dylib`_szone_free (10 samples, 0.02%)libsystem_malloc.dylib`free_medium (27 samples, 0.06%)libsystem_kernel.dylib`madvise (27 samples, 0.06%)libsystem_malloc.dylib`small_free_list_add_ptr (14 samples, 0.03%)libsystem_malloc.dylib`small_free_list_find_by_ptr (5 samples, 0.01%)libsystem_malloc.dylib`free_small (64 samples, 0.13%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (8 samples, 0.02%)libsystem_malloc.dylib`tiny_free_list_add_ptr (5 samples, 0.01%)libsystem_malloc.dylib`free_tiny (40 samples, 0.08%)libsystem_malloc.dylib`tiny_free_no_lock (23 samples, 0.05%)libsystem_malloc.dylib`tiny_free_list_remove_ptr (6 samples, 0.01%)libsystem_malloc.dylib`_free (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)libsystem_malloc.dylib`free_medium (12 samples, 0.03%)libsystem_kernel.dylib`madvise (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (45 samples, 0.09%)libsystem_malloc.dylib`nanov2_madvise_block (13 samples, 0.03%)libsystem_malloc.dylib`nanov2_madvise_block_locked (13 samples, 0.03%)libsystem_kernel.dylib`madvise (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::vec::Vec<T,A> as core::ops::drop::Drop>::drop (133 samples, 0.28%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::TableFactor> (89 samples, 0.19%)libsystem_platform.dylib`_platform_memset (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (9 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (11 samples, 0.02%)libsystem_malloc.dylib`_free (6 samples, 0.01%)libsystem_malloc.dylib`free_tiny (25 samples, 0.05%)libsystem_malloc.dylib`tiny_free_no_lock (15 samples, 0.03%)libsystem_malloc.dylib`tiny_free_list_add_ptr (9 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (56 samples, 0.12%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`_szone_free (7 samples, 0.01%)libsystem_malloc.dylib`small_free_list_add_ptr (14 samples, 0.03%)libsystem_malloc.dylib`free_small (28 samples, 0.06%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Function> (63 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::FunctionArgumentList> (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::SetExpr> (478 samples, 1.00%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::query::Query>> (753 samples, 1.58%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::query::Query> (737 samples, 1.55%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (142 samples, 0.30%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Whitespace> (32 samples, 0.07%)libsystem_malloc.dylib`_nanov2_free (121 samples, 0.25%)libsystem_malloc.dylib`free_medium (45 samples, 0.09%)libsystem_kernel.dylib`madvise (45 samples, 0.09%)libsystem_malloc.dylib`free_small (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_madvise_block (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_madvise_block_locked (6 samples, 0.01%)libsystem_kernel.dylib`madvise (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (95 samples, 0.20%)libsystem_platform.dylib`_platform_memset (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (10 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (27 samples, 0.06%)libsystem_malloc.dylib`small_free_list_add_ptr (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (80 samples, 0.17%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (58 samples, 0.12%)libsystem_malloc.dylib`szone_malloc_should_clear (36 samples, 0.08%)libsystem_malloc.dylib`small_malloc_should_clear (30 samples, 0.06%)libsystem_malloc.dylib`small_malloc_from_free_list (25 samples, 0.05%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (14 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (54 samples, 0.11%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (8 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`szone_malloc_should_clear (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (138 samples, 0.29%)libsystem_malloc.dylib`small_free_list_add_ptr (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::alloc::exchange_malloc (35 samples, 0.07%)libsystem_malloc.dylib`szone_malloc_should_clear (32 samples, 0.07%)libsystem_malloc.dylib`small_malloc_should_clear (24 samples, 0.05%)libsystem_malloc.dylib`small_malloc_from_free_list (23 samples, 0.05%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (11 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_token (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (19 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::ops::function::FnOnce::call_once (6 samples, 0.01%)libsystem_malloc.dylib`szone_malloc_should_clear (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keywords (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_keyword (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (13 samples, 0.03%)libsystem_malloc.dylib`szone_malloc_should_clear (5 samples, 0.01%)libsystem_malloc.dylib`small_malloc_should_clear (5 samples, 0.01%)libsystem_malloc.dylib`small_malloc_from_free_list (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_alias (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (10 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (7 samples, 0.01%)libsystem_malloc.dylib`_free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_platform.dylib`_platform_memset (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (13 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_realloc (20 samples, 0.04%)libsystem_malloc.dylib`_realloc (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (67 samples, 0.14%)sqlparser_bench-959bc5267970ca34`core::fmt::write (66 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::write (58 samples, 0.12%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (14 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (96 samples, 0.20%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_type_modifiers (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (99 samples, 0.21%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_compound_field_access (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (7 samples, 0.01%)libsystem_malloc.dylib`szone_malloc_should_clear (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (10 samples, 0.02%)libsystem_malloc.dylib`nanov2_realloc (7 samples, 0.01%)libsystem_malloc.dylib`_realloc (13 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_realloc (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`core::fmt::write (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_named_arg_operator (70 samples, 0.15%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (13 samples, 0.03%)libsystem_malloc.dylib`_realloc (13 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_realloc (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::fmt::write (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (93 samples, 0.20%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_args (239 samples, 0.50%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_expr (136 samples, 0.29%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (120 samples, 0.25%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_argument_list (315 samples, 0.66%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (274 samples, 0.58%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_call (334 samples, 0.70%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (364 samples, 0.76%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (614 samples, 1.29%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::try_parse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select_item (740 samples, 1.55%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_expr (698 samples, 1.47%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (669 samples, 1.40%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_factor (58 samples, 0.12%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_tokens (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (865 samples, 1.82%)s..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_and_joins (82 samples, 0.17%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (6 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_realloc (13 samples, 0.03%)libsystem_malloc.dylib`_realloc (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::fmt::write (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_group_by (93 samples, 0.20%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (76 samples, 0.16%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (68 samples, 0.14%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_query (1,096 samples, 2.30%)s..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_query_body (1,055 samples, 2.22%)s..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select (1,000 samples, 2.10%)s..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_cte (1,138 samples, 2.39%)sq..libsystem_platform.dylib`_platform_memmove (79 samples, 0.17%)libsystem_platform.dylib`_platform_memmove (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (11 samples, 0.02%)libsystem_malloc.dylib`_realloc (11 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (11 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (13 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (10 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (20 samples, 0.04%)libsystem_malloc.dylib`_realloc (42 samples, 0.09%)libsystem_malloc.dylib`_malloc_zone_realloc (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (54 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (84 samples, 0.18%)sqlparser_bench-959bc5267970ca34`core::fmt::write (81 samples, 0.17%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (99 samples, 0.21%)sqlparser_bench-959bc5267970ca34`core::fmt::write (93 samples, 0.20%)libsystem_malloc.dylib`_nanov2_free (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (16 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (65 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (269 samples, 0.56%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (377 samples, 0.79%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_order_by_expr (348 samples, 0.73%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (323 samples, 0.68%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_order_by (462 samples, 0.97%)libsystem_malloc.dylib`small_free_list_add_ptr (6 samples, 0.01%)libsystem_malloc.dylib`szone_malloc_should_clear (36 samples, 0.08%)libsystem_malloc.dylib`small_malloc_should_clear (23 samples, 0.05%)libsystem_malloc.dylib`small_malloc_from_free_list (20 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (139 samples, 0.29%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (9 samples, 0.02%)libsystem_malloc.dylib`small_free_list_remove_ptr (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::FnOnce::call_once (40 samples, 0.08%)libsystem_malloc.dylib`szone_malloc_should_clear (34 samples, 0.07%)libsystem_malloc.dylib`small_malloc_should_clear (25 samples, 0.05%)libsystem_malloc.dylib`small_malloc_from_free_list (22 samples, 0.05%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keywords (13 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (95 samples, 0.20%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (6 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_keyword (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::is_parse_comma_separated_end_with_trailing_commas (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_all_or_distinct (12 samples, 0.03%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (191 samples, 0.40%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (8 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_malloc (16 samples, 0.03%)libsystem_malloc.dylib`_realloc (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_realloc (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_malloc.dylib`rack_get_thread_index (5 samples, 0.01%)libsystem_malloc.dylib`small_free_list_add_ptr (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (104 samples, 0.22%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (72 samples, 0.15%)libsystem_malloc.dylib`szone_malloc_should_clear (47 samples, 0.10%)libsystem_malloc.dylib`small_malloc_should_clear (32 samples, 0.07%)libsystem_malloc.dylib`small_malloc_from_free_list (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::is_parse_comma_separated_end_with_trailing_commas (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::cmp::PartialEq>::eq (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_select_item_exclude (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_select_item_rename (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_additional_options (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_select_item_replace (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::cmp::PartialEq>::eq (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (12 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (13 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (17 samples, 0.04%)libsystem_platform.dylib`_platform_memset (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_realloc (18 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_realloc (30 samples, 0.06%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_malloc.dylib`_realloc (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (79 samples, 0.17%)sqlparser_bench-959bc5267970ca34`core::fmt::write (78 samples, 0.16%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (88 samples, 0.18%)sqlparser_bench-959bc5267970ca34`core::fmt::write (84 samples, 0.18%)libsystem_malloc.dylib`_nanov2_free (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (9 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (13 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_type_modifiers (53 samples, 0.11%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (130 samples, 0.27%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (131 samples, 0.28%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_compound_field_access (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::expect_token (19 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (14 samples, 0.03%)libsystem_malloc.dylib`small_free_list_add_ptr (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (26 samples, 0.05%)libsystem_malloc.dylib`szone_malloc_should_clear (22 samples, 0.05%)libsystem_malloc.dylib`small_malloc_should_clear (22 samples, 0.05%)libsystem_malloc.dylib`small_malloc_from_free_list (18 samples, 0.04%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (5 samples, 0.01%)libsystem_malloc.dylib`_free (8 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_realloc (10 samples, 0.02%)libsystem_malloc.dylib`_realloc (20 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_realloc (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::write (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::fmt::Display>::fmt (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (115 samples, 0.24%)sqlparser_bench-959bc5267970ca34`core::fmt::write (96 samples, 0.20%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_named_arg_operator (131 samples, 0.28%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (32 samples, 0.07%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (16 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (15 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_realloc (29 samples, 0.06%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)libsystem_malloc.dylib`_realloc (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (39 samples, 0.08%)libsystem_malloc.dylib`nanov2_size (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (70 samples, 0.15%)sqlparser_bench-959bc5267970ca34`core::fmt::write (68 samples, 0.14%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (80 samples, 0.17%)sqlparser_bench-959bc5267970ca34`core::fmt::write (74 samples, 0.16%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (11 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (200 samples, 0.42%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (559 samples, 1.17%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_args (485 samples, 1.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_expr (277 samples, 0.58%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (243 samples, 0.51%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_argument_list (663 samples, 1.39%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_listagg_on_overflow (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_json_null_clause (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_function_call (723 samples, 1.52%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_listagg_on_overflow (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (766 samples, 1.61%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (1,085 samples, 2.28%)s..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select_item (1,285 samples, 2.70%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_wildcard_expr (1,190 samples, 2.50%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (1,144 samples, 2.40%)sq..libsystem_malloc.dylib`_free (15 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (15 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<T as core::any::Any>::type_id (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (8 samples, 0.02%)libsystem_malloc.dylib`_realloc (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_realloc (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_join_constraint (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_parenthesized_column_list (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keywords (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_one_of_keywords (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<T as core::any::Any>::type_id (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::cmp::PartialEq>::eq (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::maybe_parse_table_sample (6 samples, 0.01%)libsystem_malloc.dylib`_free (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (26 samples, 0.05%)libsystem_malloc.dylib`nanov2_malloc_type (25 samples, 0.05%)libsystem_malloc.dylib`nanov2_allocate_outlined (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (12 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (23 samples, 0.05%)libsystem_malloc.dylib`nanov2_malloc_type (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (23 samples, 0.05%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (231 samples, 0.49%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (147 samples, 0.31%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (13 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_table_alias (100 samples, 0.21%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_optional_alias (82 samples, 0.17%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (37 samples, 0.08%)libsystem_malloc.dylib`_malloc_zone_malloc (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (15 samples, 0.03%)libsystem_malloc.dylib`nanov2_malloc_type (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (33 samples, 0.07%)libsystem_malloc.dylib`_malloc_zone_malloc (15 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (19 samples, 0.04%)libsystem_malloc.dylib`nanov2_malloc_type (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_factor (701 samples, 1.47%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_tokens (177 samples, 0.37%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (65 samples, 0.14%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_version (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_table_and_joins (991 samples, 2.08%)s..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_comma_separated_with_trailing_commas (2,665 samples, 5.60%)sqlpars..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_infix (6 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (13 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (85 samples, 0.18%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (8 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (14 samples, 0.03%)libsystem_malloc.dylib`rack_get_thread_index (8 samples, 0.02%)libsystem_malloc.dylib`tiny_malloc_from_free_list (8 samples, 0.02%)libsystem_malloc.dylib`set_tiny_meta_header_in_use (11 samples, 0.02%)libsystem_malloc.dylib`_tiny_check_and_zero_inline_meta_from_freelist (5 samples, 0.01%)libsystem_malloc.dylib`szone_malloc_should_clear (114 samples, 0.24%)libsystem_malloc.dylib`tiny_malloc_should_clear (81 samples, 0.17%)libsystem_malloc.dylib`tiny_malloc_from_free_list (53 samples, 0.11%)libsystem_malloc.dylib`tiny_free_list_add_ptr (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (23 samples, 0.05%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::tokenizer::Token> (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (12 samples, 0.03%)libsystem_malloc.dylib`set_tiny_meta_header_in_use (8 samples, 0.02%)libsystem_malloc.dylib`szone_malloc_should_clear (37 samples, 0.08%)libsystem_malloc.dylib`tiny_malloc_should_clear (36 samples, 0.08%)libsystem_malloc.dylib`tiny_malloc_from_free_list (25 samples, 0.05%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (19 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::dialect::Dialect::get_next_precedence_default (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::peek_token (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (13 samples, 0.03%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_platform.dylib`_platform_memset (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (13 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (9 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (18 samples, 0.04%)libsystem_malloc.dylib`_realloc (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (22 samples, 0.05%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`core::fmt::write (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::fmt::Display>::fmt (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::write (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (157 samples, 0.33%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (155 samples, 0.33%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (136 samples, 0.29%)sqlparser_bench-959bc5267970ca34`core::fmt::write (121 samples, 0.25%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (223 samples, 0.47%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_value (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_infix (338 samples, 0.71%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (291 samples, 0.61%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (7 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (24 samples, 0.05%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_platform.dylib`_platform_memset (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_pointer_size (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (10 samples, 0.02%)libsystem_malloc.dylib`nanov2_realloc (18 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_realloc (38 samples, 0.08%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)libsystem_malloc.dylib`_realloc (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (55 samples, 0.12%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (85 samples, 0.18%)sqlparser_bench-959bc5267970ca34`core::fmt::write (84 samples, 0.18%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (96 samples, 0.20%)sqlparser_bench-959bc5267970ca34`core::fmt::write (89 samples, 0.19%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::data_type::DataType> (11 samples, 0.02%)libsystem_platform.dylib`_platform_memset (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (15 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_pointer_size (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (21 samples, 0.04%)libsystem_malloc.dylib`_realloc (46 samples, 0.10%)libsystem_malloc.dylib`_malloc_zone_realloc (39 samples, 0.08%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (63 samples, 0.13%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (57 samples, 0.12%)libsystem_malloc.dylib`nanov2_size (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (73 samples, 0.15%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral::write_prefix (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (121 samples, 0.25%)sqlparser_bench-959bc5267970ca34`core::fmt::write (120 samples, 0.25%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::fmt::Display>::fmt (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (260 samples, 0.55%)sqlparser_bench-959bc5267970ca34`core::fmt::write (214 samples, 0.45%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (6 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (8 samples, 0.02%)libsystem_platform.dylib`_platform_memmove (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_object_name (63 samples, 0.13%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_identifier (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (352 samples, 0.74%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (365 samples, 0.77%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_expr_prefix_by_unreserved_word (11 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (10 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (6 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_value (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (622 samples, 1.31%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_infix (1,318 samples, 2.77%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (1,147 samples, 2.41%)sq..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_keyword (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (10 samples, 0.02%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (9 samples, 0.02%)libsystem_platform.dylib`_platform_memset (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (23 samples, 0.05%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (20 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc (7 samples, 0.01%)libsystem_malloc.dylib`nanov2_realloc (21 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_realloc (34 samples, 0.07%)libsystem_platform.dylib`_platform_memmove (8 samples, 0.02%)libsystem_malloc.dylib`_realloc (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (58 samples, 0.12%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (48 samples, 0.10%)libsystem_malloc.dylib`nanov2_size (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral (14 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad_integral::write_prefix (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Location as core::fmt::Display>::fmt (126 samples, 0.26%)sqlparser_bench-959bc5267970ca34`core::fmt::write (123 samples, 0.26%)sqlparser_bench-959bc5267970ca34`core::fmt::num::imp::_<impl core::fmt::Display for u64>::fmt (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (5 samples, 0.01%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::fmt::Display>::fmt (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::fmt::format::format_inner (245 samples, 0.51%)sqlparser_bench-959bc5267970ca34`core::fmt::write (224 samples, 0.47%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type (281 samples, 0.59%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_data_type_helper (270 samples, 0.57%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_value (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<sqlparser::tokenizer::Token as core::clone::Clone>::clone (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::clone::Clone>::clone (9 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_prefix (365 samples, 0.77%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_subexpr (1,893 samples, 3.98%)sqlp..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_query_body (5,198 samples, 10.92%)sqlparser_bench-..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_select (4,878 samples, 10.24%)sqlparser_bench..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_query (7,112 samples, 14.94%)sqlparser_bench-959bc52..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_statements (7,412 samples, 15.57%)sqlparser_bench-959bc526..sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_statement (7,260 samples, 15.25%)sqlparser_bench-959bc52..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::next_token (136 samples, 0.29%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_identifier_or_keyword (26 samples, 0.05%)libsystem_malloc.dylib`_free (121 samples, 0.25%)libsystem_malloc.dylib`_nanov2_free (33 samples, 0.07%)libsystem_platform.dylib`_platform_memmove (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::iter::traits::collect::FromIterator<char>>::from_iter (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::dialect::generic::GenericDialect as sqlparser::dialect::Dialect>::is_delimited_identifier_start (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::dialect::mssql::MsSqlDialect as sqlparser::dialect::Dialect>::is_identifier_start (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`__rdl_dealloc (5 samples, 0.01%)libsystem_malloc.dylib`_malloc_zone_malloc (11 samples, 0.02%)libsystem_malloc.dylib`_szone_free (5 samples, 0.01%)libsystem_malloc.dylib`free_medium (5 samples, 0.01%)libsystem_kernel.dylib`madvise (5 samples, 0.01%)libsystem_malloc.dylib`tiny_free_list_add_ptr (5 samples, 0.01%)libsystem_malloc.dylib`free_tiny (33 samples, 0.07%)libsystem_malloc.dylib`tiny_free_no_lock (27 samples, 0.06%)libsystem_malloc.dylib`tiny_free_list_remove_ptr (8 samples, 0.02%)libsystem_malloc.dylib`small_try_realloc_in_place (12 samples, 0.03%)libsystem_malloc.dylib`small_free_list_add_ptr (5 samples, 0.01%)libsystem_malloc.dylib`szone_malloc_should_clear (38 samples, 0.08%)libsystem_malloc.dylib`small_malloc_should_clear (27 samples, 0.06%)libsystem_malloc.dylib`small_malloc_from_free_list (23 samples, 0.05%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (8 samples, 0.02%)libsystem_malloc.dylib`szone_size (9 samples, 0.02%)libsystem_malloc.dylib`tiny_size (8 samples, 0.02%)libsystem_malloc.dylib`tiny_try_realloc_in_place (24 samples, 0.05%)libsystem_malloc.dylib`szone_realloc (166 samples, 0.35%)libsystem_platform.dylib`_platform_memset (10 samples, 0.02%)libsystem_malloc.dylib`_malloc_zone_realloc (223 samples, 0.47%)libsystem_platform.dylib`_platform_memmove (36 samples, 0.08%)libsystem_malloc.dylib`nanov2_realloc (6 samples, 0.01%)libsystem_malloc.dylib`szone_realloc (10 samples, 0.02%)libsystem_malloc.dylib`_realloc (297 samples, 0.62%)libsystem_malloc.dylib`szone_size (34 samples, 0.07%)libsystem_malloc.dylib`tiny_size (33 samples, 0.07%)libsystem_malloc.dylib`szone_malloc_should_clear (39 samples, 0.08%)libsystem_malloc.dylib`tiny_malloc_should_clear (31 samples, 0.07%)libsystem_malloc.dylib`tiny_malloc_from_free_list (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (401 samples, 0.84%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (358 samples, 0.75%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::State::next (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::State::peek (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::consume_and_return (18 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_malloc (7 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (21 samples, 0.04%)libsystem_platform.dylib`_platform_memcmp (42 samples, 0.09%)libsystem_platform.dylib`_platform_memmove (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::dialect::mssql::MsSqlDialect as sqlparser::dialect::Dialect>::is_identifier_part (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcmp (15 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_malloc (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (10 samples, 0.02%)libsystem_malloc.dylib`nanov2_malloc_type (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Token::make_word (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::str::_<impl str>::to_uppercase (6 samples, 0.01%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`nanov2_malloc_type (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::next_token (641 samples, 1.35%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_word (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (7 samples, 0.01%)libsystem_malloc.dylib`_free (37 samples, 0.08%)libsystem_malloc.dylib`_malloc_zone_malloc (32 samples, 0.07%)libsystem_malloc.dylib`_nanov2_free (214 samples, 0.45%)libsystem_platform.dylib`_platform_memcmp (527 samples, 1.11%)libsystem_platform.dylib`_platform_memmove (89 samples, 0.19%)libsystem_malloc.dylib`_malloc_zone_malloc (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (136 samples, 0.29%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (63 samples, 0.13%)libsystem_malloc.dylib`nanov2_malloc_type (48 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::iter::traits::collect::FromIterator<char>>::from_iter (201 samples, 0.42%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<sqlparser::dialect::mssql::MsSqlDialect as sqlparser::dialect::Dialect>::is_identifier_part (105 samples, 0.22%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$free (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$malloc (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcmp (176 samples, 0.37%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`alloc::str::_<impl str>::to_uppercase (27 samples, 0.06%)libsystem_malloc.dylib`_malloc_zone_malloc (37 samples, 0.08%)libsystem_malloc.dylib`nanov2_malloc_type (57 samples, 0.12%)libsystem_malloc.dylib`nanov2_allocate_outlined (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$malloc (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Token::make_word (300 samples, 0.63%)sqlparser_bench-959bc5267970ca34`alloc::str::_<impl str>::to_uppercase (111 samples, 0.23%)libsystem_malloc.dylib`nanov2_malloc_type (57 samples, 0.12%)libsystem_malloc.dylib`_nanov2_free (68 samples, 0.14%)libsystem_malloc.dylib`_malloc_zone_malloc (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$malloc (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`__rdl_alloc (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVec<T,A>::grow_one (149 samples, 0.31%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (72 samples, 0.15%)libsystem_malloc.dylib`nanov2_malloc_type (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_identifier_or_keyword (2,345 samples, 4.92%)sqlpar..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_word (492 samples, 1.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_with_location (4,225 samples, 8.87%)sqlparser_ben..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_with_location_into_buf (4,059 samples, 8.52%)sqlparser_be..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_word (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::try_with_sql (4,258 samples, 8.94%)sqlparser_ben..sqlparser_bench-959bc5267970ca34`sqlparser::tokenizer::Tokenizer::tokenize_with_location_into_buf (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_sql (12,017 samples, 25.24%)sqlparser_bench-959bc5267970ca34`sqlpars..sqlparser_bench-959bc5267970ca34`criterion::bencher::Bencher<M>::iter (16,292 samples, 34.21%)sqlparser_bench-959bc5267970ca34`criterion::bencher::Be..sqlparser_bench-959bc5267970ca34`criterion::benchmark_group::BenchmarkGroup<M>::bench_function (35,307 samples, 74.15%)sqlparser_bench-959bc5267970ca34`criterion::benchmark_group::BenchmarkGroup<M>::bench_functionsqlparser_bench-959bc5267970ca34`criterion::analysis::common (35,307 samples, 74.15%)sqlparser_bench-959bc5267970ca34`criterion::analysis::commonsqlparser_bench-959bc5267970ca34`criterion::routine::Routine::sample (35,254 samples, 74.04%)sqlparser_bench-959bc5267970ca34`criterion::routine::Routine::samplesqlparser_bench-959bc5267970ca34`<criterion::routine::Function<M,F,T> as criterion::routine::Routine<M,T>>::warm_up (16,308 samples, 34.25%)sqlparser_bench-959bc5267970ca34`<criterion::routine::Fu..dyld`start (35,315 samples, 74.16%)dyld`startsqlparser_bench-959bc5267970ca34`main (35,314 samples, 74.16%)sqlparser_bench-959bc5267970ca34`mainsqlparser_bench-959bc5267970ca34`std::rt::lang_start_internal (35,314 samples, 74.16%)sqlparser_bench-959bc5267970ca34`std::rt::lang_start_internalsqlparser_bench-959bc5267970ca34`std::rt::lang_start::_{{closure}} (35,314 samples, 74.16%)sqlparser_bench-959bc5267970ca34`std::rt::lang_start::_{{closure}}sqlparser_bench-959bc5267970ca34`std::sys::backtrace::__rust_begin_short_backtrace (35,314 samples, 74.16%)sqlparser_bench-959bc5267970ca34`std::sys::backtrace::__rust_begin_short_backtracesqlparser_bench-959bc5267970ca34`sqlparser_bench::main (35,314 samples, 74.16%)sqlparser_bench-959bc5267970ca34`sqlparser_bench::mainsqlparser_bench-959bc5267970ca34`sqlparser::parser::Parser::parse_sql (5 samples, 0.01%)libsystem_kernel.dylib`swtch_pri (133 samples, 0.28%)libsystem_m.dylib`exp (23 samples, 0.05%)libsystem_m.dylib`exp (43 samples, 0.09%)libsystem_m.dylib`exp (47 samples, 0.10%)libsystem_m.dylib`exp (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (8 samples, 0.02%)libsystem_m.dylib`exp (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (6 samples, 0.01%)libsystem_m.dylib`exp (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (5 samples, 0.01%)libsystem_m.dylib`exp (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::resamples::Resamples<A>::next (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (108 samples, 0.23%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (71 samples, 0.15%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::bivariate::resamples::Resamples<X,Y>::next (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (224 samples, 0.47%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (283 samples, 0.59%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (12 samples, 0.03%)libsystem_m.dylib`exp (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (134 samples, 0.28%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (121 samples, 0.25%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (492 samples, 1.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (440 samples, 0.92%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)libsystem_m.dylib`exp (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (11 samples, 0.02%)libsystem_m.dylib`exp (70 samples, 0.15%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (126 samples, 0.26%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (9 samples, 0.02%)libsystem_m.dylib`exp (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (137 samples, 0.29%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (125 samples, 0.26%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (311 samples, 0.65%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (309 samples, 0.65%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (284 samples, 0.60%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (943 samples, 1.98%)s..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (860 samples, 1.81%)s..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (6 samples, 0.01%)libsystem_m.dylib`exp (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (6 samples, 0.01%)libsystem_m.dylib`exp (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (7 samples, 0.01%)libsystem_m.dylib`exp (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (105 samples, 0.22%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (8 samples, 0.02%)libsystem_m.dylib`exp (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (115 samples, 0.24%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (106 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (278 samples, 0.58%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (229 samples, 0.48%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (6 samples, 0.01%)libsystem_m.dylib`exp (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (6 samples, 0.01%)libsystem_m.dylib`exp (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (103 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (6 samples, 0.01%)libsystem_m.dylib`exp (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (104 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (96 samples, 0.20%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (276 samples, 0.58%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (270 samples, 0.57%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (228 samples, 0.48%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (653 samples, 1.37%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (647 samples, 1.36%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (592 samples, 1.24%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::bivariate::resamples::Resamples<X,Y>::next (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (104 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (100 samples, 0.21%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (1,836 samples, 3.86%)sqlp..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (1,775 samples, 3.73%)sqlp..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (169 samples, 0.35%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (168 samples, 0.35%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (168 samples, 0.35%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (166 samples, 0.35%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (5 samples, 0.01%)libsystem_m.dylib`exp (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (13 samples, 0.03%)libsystem_m.dylib`exp (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (7 samples, 0.01%)libsystem_m.dylib`exp (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (6 samples, 0.01%)libsystem_m.dylib`exp (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (7 samples, 0.01%)libsystem_m.dylib`exp (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (159 samples, 0.33%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (123 samples, 0.26%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (54 samples, 0.11%)libsystem_m.dylib`exp (30 samples, 0.06%)libsystem_m.dylib`exp (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (60 samples, 0.13%)libsystem_m.dylib`exp (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (54 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (169 samples, 0.35%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (167 samples, 0.35%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (129 samples, 0.27%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (408 samples, 0.86%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (364 samples, 0.76%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (8 samples, 0.02%)libsystem_m.dylib`exp (29 samples, 0.06%)libsystem_m.dylib`exp (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (13 samples, 0.03%)libsystem_m.dylib`exp (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (46 samples, 0.10%)libsystem_m.dylib`exp (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (129 samples, 0.27%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (88 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (5 samples, 0.01%)libsystem_m.dylib`exp (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (6 samples, 0.01%)libsystem_m.dylib`exp (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (41 samples, 0.09%)libsystem_m.dylib`exp (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (142 samples, 0.30%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (136 samples, 0.29%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (96 samples, 0.20%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (375 samples, 0.79%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (367 samples, 0.77%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (332 samples, 0.70%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (73 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (72 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (147 samples, 0.31%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (147 samples, 0.31%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (143 samples, 0.30%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (1,005 samples, 2.11%)s..sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (998 samples, 2.10%)s..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (945 samples, 1.98%)s..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (148 samples, 0.31%)libsystem_m.dylib`exp (9 samples, 0.02%)libsystem_m.dylib`exp (6 samples, 0.01%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (58 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (114 samples, 0.24%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (107 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (185 samples, 0.39%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (178 samples, 0.37%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (75 samples, 0.16%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (75 samples, 0.16%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (71 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (48 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (3,267 samples, 6.86%)sqlparser..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (3,209 samples, 6.74%)sqlparser..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (364 samples, 0.76%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (362 samples, 0.76%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (360 samples, 0.76%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (348 samples, 0.73%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (87 samples, 0.18%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (87 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (86 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (82 samples, 0.17%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (24 samples, 0.05%)libsystem_m.dylib`exp (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (5 samples, 0.01%)libsystem_m.dylib`exp (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)libsystem_m.dylib`exp (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (9 samples, 0.02%)libsystem_m.dylib`exp (15 samples, 0.03%)libsystem_m.dylib`exp (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (8 samples, 0.02%)libsystem_m.dylib`exp (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (61 samples, 0.13%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (5 samples, 0.01%)libsystem_m.dylib`exp (14 samples, 0.03%)libsystem_m.dylib`exp (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (13 samples, 0.03%)libsystem_m.dylib`exp (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (177 samples, 0.37%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (142 samples, 0.30%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (79 samples, 0.17%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (74 samples, 0.16%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (11 samples, 0.02%)libsystem_m.dylib`exp (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)libsystem_m.dylib`exp (19 samples, 0.04%)libsystem_m.dylib`exp (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (8 samples, 0.02%)libsystem_m.dylib`exp (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (74 samples, 0.16%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)libsystem_m.dylib`exp (15 samples, 0.03%)libsystem_m.dylib`exp (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (73 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (69 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (223 samples, 0.47%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (219 samples, 0.46%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (183 samples, 0.38%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (466 samples, 0.98%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (409 samples, 0.86%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)libsystem_m.dylib`exp (52 samples, 0.11%)libsystem_m.dylib`exp (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (9 samples, 0.02%)libsystem_m.dylib`exp (19 samples, 0.04%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (11 samples, 0.02%)libsystem_m.dylib`exp (12 samples, 0.03%)libsystem_m.dylib`exp (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (12 samples, 0.03%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (150 samples, 0.32%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (105 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (7 samples, 0.01%)libsystem_m.dylib`exp (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)libsystem_m.dylib`exp (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (8 samples, 0.02%)libsystem_m.dylib`exp (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (145 samples, 0.30%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (141 samples, 0.30%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (113 samples, 0.24%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (71 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (71 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (70 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (435 samples, 0.91%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (431 samples, 0.91%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (370 samples, 0.78%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (72 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (88 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (81 samples, 0.17%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (1,110 samples, 2.33%)s..sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (1,106 samples, 2.32%)s..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (1,064 samples, 2.23%)s..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (156 samples, 0.33%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (155 samples, 0.33%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (155 samples, 0.33%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (153 samples, 0.32%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)libsystem_kernel.dylib`swtch_pri (5 samples, 0.01%)libsystem_m.dylib`exp (12 samples, 0.03%)libsystem_m.dylib`exp (11 samples, 0.02%)libsystem_m.dylib`exp (14 samples, 0.03%)libsystem_m.dylib`exp (10 samples, 0.02%)libsystem_m.dylib`exp (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (103 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (91 samples, 0.19%)libsystem_m.dylib`exp (6 samples, 0.01%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (54 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (194 samples, 0.41%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (178 samples, 0.37%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)libsystem_m.dylib`exp (10 samples, 0.02%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (129 samples, 0.27%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (128 samples, 0.27%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (117 samples, 0.25%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (385 samples, 0.81%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (369 samples, 0.77%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (136 samples, 0.29%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (136 samples, 0.29%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (132 samples, 0.28%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (56 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (70 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (64 samples, 0.13%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (142 samples, 0.30%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (140 samples, 0.29%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (133 samples, 0.28%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (684 samples, 1.44%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (681 samples, 1.43%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (667 samples, 1.40%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (145 samples, 0.30%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (690 samples, 1.45%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5,126 samples, 10.77%)sqlparser_bench-..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5,072 samples, 10.65%)sqlparser_bench-..libsystem_m.dylib`exp (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (7 samples, 0.01%)libsystem_m.dylib`exp (18 samples, 0.04%)libsystem_m.dylib`exp (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)libsystem_m.dylib`exp (6 samples, 0.01%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)libsystem_m.dylib`exp (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (124 samples, 0.26%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (95 samples, 0.20%)libsystem_m.dylib`exp (18 samples, 0.04%)libsystem_m.dylib`exp (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (112 samples, 0.24%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (109 samples, 0.23%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (86 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (285 samples, 0.60%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (260 samples, 0.55%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (20 samples, 0.04%)libsystem_m.dylib`exp (23 samples, 0.05%)libsystem_m.dylib`exp (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)libsystem_m.dylib`exp (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (75 samples, 0.16%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)libsystem_m.dylib`exp (7 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (72 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (69 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (60 samples, 0.13%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (59 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (276 samples, 0.58%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (272 samples, 0.57%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (248 samples, 0.52%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (99 samples, 0.21%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (99 samples, 0.21%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (99 samples, 0.21%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (99 samples, 0.21%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (86 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (84 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (159 samples, 0.33%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (155 samples, 0.33%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (880 samples, 1.85%)s..sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (878 samples, 1.84%)s..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (847 samples, 1.78%)s..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (279 samples, 0.59%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (278 samples, 0.58%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (278 samples, 0.58%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (269 samples, 0.56%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (72 samples, 0.15%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (72 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (71 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (69 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)libsystem_kernel.dylib`swtch_pri (12 samples, 0.03%)libsystem_m.dylib`exp (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)libsystem_m.dylib`exp (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::Producer::fold_with (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::compare::estimates::stats (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)libsystem_m.dylib`exp (17 samples, 0.04%)libsystem_m.dylib`exp (17 samples, 0.04%)libsystem_m.dylib`exp (20 samples, 0.04%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ops::function::impls::_<impl core::ops::function::Fn<A> for &F>::call (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`oorandom::Rand64::rand_range (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (43 samples, 0.09%)libsystem_m.dylib`exp (5 samples, 0.01%)libsystem_m.dylib`exp (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (5 samples, 0.01%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (112 samples, 0.24%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (88 samples, 0.18%)libsystem_m.dylib`exp (15 samples, 0.03%)libsystem_m.dylib`exp (6 samples, 0.01%)libsystem_m.dylib`exp (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (5 samples, 0.01%)libsystem_m.dylib`exp (9 samples, 0.02%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (107 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (104 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (83 samples, 0.17%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (241 samples, 0.51%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (221 samples, 0.46%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$exp (5 samples, 0.01%)libsystem_m.dylib`exp (15 samples, 0.03%)libsystem_m.dylib`exp (13 samples, 0.03%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)libsystem_m.dylib`exp (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (66 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (50 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (6 samples, 0.01%)libsystem_m.dylib`exp (10 samples, 0.02%)libsystem_m.dylib`exp (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (188 samples, 0.39%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (183 samples, 0.38%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (164 samples, 0.34%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (485 samples, 1.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (463 samples, 0.97%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)libsystem_m.dylib`exp (14 samples, 0.03%)libsystem_m.dylib`exp (12 samples, 0.03%)libsystem_m.dylib`exp (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)libsystem_m.dylib`exp (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (84 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (70 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (26 samples, 0.05%)libsystem_m.dylib`exp (13 samples, 0.03%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (24 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (72 samples, 0.15%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (69 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (55 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (231 samples, 0.49%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (227 samples, 0.48%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (211 samples, 0.44%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (53 samples, 0.11%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (53 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)libsystem_m.dylib`exp (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (48 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (90 samples, 0.19%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (86 samples, 0.18%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (922 samples, 1.94%)s..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (896 samples, 1.88%)s..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (178 samples, 0.37%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (176 samples, 0.37%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (176 samples, 0.37%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (169 samples, 0.35%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (46 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)libsystem_m.dylib`exp (7 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (69 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (58 samples, 0.12%)libsystem_m.dylib`exp (6 samples, 0.01%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (51 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (220 samples, 0.46%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (220 samples, 0.46%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (204 samples, 0.43%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (79 samples, 0.17%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (79 samples, 0.17%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (78 samples, 0.16%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (77 samples, 0.16%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (19 samples, 0.04%)libsystem_kernel.dylib`swtch_pri (5 samples, 0.01%)libsystem_m.dylib`exp (9 samples, 0.02%)libsystem_m.dylib`exp (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (43 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (40 samples, 0.08%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (107 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (94 samples, 0.20%)libsystem_m.dylib`exp (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (22 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (68 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (68 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (215 samples, 0.45%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (210 samples, 0.44%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (33 samples, 0.07%)libsystem_m.dylib`exp (5 samples, 0.01%)libsystem_m.dylib`exp (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (41 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (42 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (101 samples, 0.21%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (100 samples, 0.21%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (97 samples, 0.20%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (31 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (1,554 samples, 3.26%)sql..sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (1,554 samples, 3.26%)sql..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (1,525 samples, 3.20%)sql..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (380 samples, 0.80%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (375 samples, 0.79%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (374 samples, 0.79%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (363 samples, 0.76%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (103 samples, 0.22%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7,610 samples, 15.98%)sqlparser_bench-959bc5267..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7,579 samples, 15.92%)sqlparser_bench-959bc526..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (1,568 samples, 3.29%)sql..sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (64 samples, 0.13%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (22 samples, 0.05%)libsystem_m.dylib`exp (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::median_abs_dev (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`criterion::analysis::estimates::stats (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`criterion::stats::univariate::sample::Sample<A>::percentiles (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (71 samples, 0.15%)libsystem_m.dylib`exp (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (30 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (23 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (67 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (67 samples, 0.14%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (28 samples, 0.06%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (52 samples, 0.11%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (49 samples, 0.10%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (133 samples, 0.28%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (132 samples, 0.28%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (193 samples, 0.41%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (193 samples, 0.41%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (193 samples, 0.41%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (192 samples, 0.40%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (298 samples, 0.63%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (298 samples, 0.63%)libsystem_m.dylib`exp (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (38 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon::iter::fold::FoldFolder<C,ID,F> as rayon::iter::plumbing::Folder<T>>::consume_iter (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (26 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (59 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (16 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (10 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (116 samples, 0.24%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (116 samples, 0.24%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (116 samples, 0.24%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (116 samples, 0.24%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (40 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (39 samples, 0.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (453 samples, 0.95%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (453 samples, 0.95%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (21 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (19 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::job::StackJob<L,F,R>::run_inline (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (509 samples, 1.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (509 samples, 1.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (35 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (33 samples, 0.07%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (514 samples, 1.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (514 samples, 1.08%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (514 samples, 1.08%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (514 samples, 1.08%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::iter::plumbing::bridge_producer_consumer::helper (517 samples, 1.09%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (515 samples, 1.08%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`<rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute (8,206 samples, 17.23%)sqlparser_bench-959bc526797..sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (525 samples, 1.10%)sqlparser_bench-959bc5267970ca34`rayon::slice::quicksort::recurse (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`rayon_core::join::join_context::_{{closure}} (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<core::iter::adapters::chain::Chain<A,B> as core::iter::traits::iterator::Iterator>::try_fold (13 samples, 0.03%)sqlparser_bench-959bc5267970ca34`crossbeam_deque::deque::Stealer<T>::steal (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`crossbeam_epoch::default::with_handle (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::find_work (14 samples, 0.03%)libsystem_kernel.dylib`__psynch_cvwait (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`rayon_core::sleep::Sleep::sleep (8 samples, 0.02%)libsystem_pthread.dylib`_pthread_mutex_firstfit_lock_slow (10 samples, 0.02%)libsystem_kernel.dylib`__psynch_mutexwait (10 samples, 0.02%)libsystem_pthread.dylib`thread_start (8,379 samples, 17.60%)libsystem_pthread.dylib`thr..libsystem_pthread.dylib`_pthread_start (8,379 samples, 17.60%)libsystem_pthread.dylib`_pt..sqlparser_bench-959bc5267970ca34`std::sys::pal::unix::thread::Thread::new::thread_start (8,379 samples, 17.60%)sqlparser_bench-959bc526797..sqlparser_bench-959bc5267970ca34`core::ops::function::FnOnce::call_once{{vtable.shim}} (8,379 samples, 17.60%)sqlparser_bench-959bc526797..sqlparser_bench-959bc5267970ca34`std::sys::backtrace::__rust_begin_short_backtrace (8,379 samples, 17.60%)sqlparser_bench-959bc526797..sqlparser_bench-959bc5267970ca34`rayon_core::registry::ThreadBuilder::run (8,379 samples, 17.60%)sqlparser_bench-959bc526797..sqlparser_bench-959bc5267970ca34`rayon_core::registry::WorkerThread::wait_until_cold (8,379 samples, 17.60%)sqlparser_bench-959bc526797..sqlparser_bench-959bc5267970ca34`rayon_core::sleep::Sleep::wake_any_threads (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`rayon_core::sleep::Sleep::wake_specific_thread (17 samples, 0.04%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`alloc::raw_vec::finish_grow (12 samples, 0.03%)libsystem_malloc.dylib`_realloc (12 samples, 0.03%)libsystem_malloc.dylib`_malloc_zone_realloc (12 samples, 0.03%)libsystem_platform.dylib`_platform_memmove (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (72 samples, 0.15%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (133 samples, 0.28%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (96 samples, 0.20%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::operator::BinaryOperator as core::fmt::Display>::fmt (58 samples, 0.12%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::value::Value as core::fmt::Display>::fmt (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_str (12 samples, 0.03%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,141 samples, 2.40%)sq..sqlparser_bench-959bc5267970ca34`core::fmt::write (713 samples, 1.50%)libdyld.dylib`tlv_get_addr (127 samples, 0.27%)sqlparser_bench-959bc5267970ca34`psm::stack_pointer (44 samples, 0.09%)sqlparser_bench-959bc5267970ca34`rust_psm_stack_pointer (124 samples, 0.26%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt (408 samples, 0.86%)sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (113 samples, 0.24%)sqlparser_bench-959bc5267970ca34`<&T as core::fmt::Display>::fmt (27 samples, 0.06%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (68 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (20 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (65 samples, 0.14%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,858 samples, 3.90%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,846 samples, 3.88%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,846 samples, 3.88%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,846 samples, 3.88%)sqlp..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,846 samples, 3.88%)sqlp..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::value::Value as core::fmt::Display>::fmt (297 samples, 0.62%)sqlparser_bench-959bc5267970ca34`core::fmt::write (117 samples, 0.25%)libsystem_platform.dylib`_platform_memmove (253 samples, 0.53%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (108 samples, 0.23%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt (62 samples, 0.13%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (372 samples, 0.78%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Ident as core::fmt::Display>::fmt (45 samples, 0.09%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::value::Value as core::fmt::Display>::fmt (57 samples, 0.12%)sqlparser_bench-959bc5267970ca34`DYLD-STUB$$memcpy (71 samples, 0.15%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_fmt (32 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::write_str (34 samples, 0.07%)sqlparser_bench-959bc5267970ca34`core::fmt::write (127 samples, 0.27%)sqlparser_bench-959bc5267970ca34`recursive::get_minimum_stack_size (101 samples, 0.21%)sqlparser_bench-959bc5267970ca34`recursive::get_stack_allocation_size (63 samples, 0.13%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,396 samples, 2.93%)sq..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,396 samples, 2.93%)sq..sqlparser_bench-959bc5267970ca34`stacker::remaining_stack (71 samples, 0.15%)libsystem_platform.dylib`_platform_memmove (84 samples, 0.18%)sqlparser_bench-959bc5267970ca34`<alloc::string::String as core::fmt::Write>::write_str (47 samples, 0.10%)sqlparser_bench-959bc5267970ca34`<str as core::fmt::Display>::fmt (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,590 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::Expr as core::fmt::Display>::fmt::_{{closure}} (1,589 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`core::fmt::write (1,589 samples, 3.34%)sql..sqlparser_bench-959bc5267970ca34`<sqlparser::ast::value::Value as core::fmt::Display>::fmt (193 samples, 0.41%)sqlparser_bench-959bc5267970ca34`core::fmt::write (193 samples, 0.41%)sqlparser_bench-959bc5267970ca34`core::fmt::Formatter::pad (36 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::fmt::write (1,598 samples, 3.36%)sql..libsystem_malloc.dylib`free_tiny (6 samples, 0.01%)libsystem_malloc.dylib`tiny_free_no_lock (6 samples, 0.01%)libsystem_malloc.dylib`tiny_free_list_add_ptr (7 samples, 0.01%)libsystem_malloc.dylib`tiny_free_scan_madvise_free (6 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (29 samples, 0.06%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (20 samples, 0.04%)libsystem_malloc.dylib`free_tiny (20 samples, 0.04%)libsystem_malloc.dylib`tiny_free_no_lock (20 samples, 0.04%)libsystem_malloc.dylib`tiny_free_scan_madvise_free (5 samples, 0.01%)libsystem_kernel.dylib`madvise (5 samples, 0.01%)libsystem_malloc.dylib`free_tiny (9 samples, 0.02%)libsystem_malloc.dylib`tiny_free_no_lock (9 samples, 0.02%)libsystem_malloc.dylib`tiny_free_list_add_ptr (8 samples, 0.02%)libsystem_malloc.dylib`tiny_free_list_remove_ptr (7 samples, 0.01%)libsystem_malloc.dylib`free_tiny (20 samples, 0.04%)libsystem_malloc.dylib`tiny_free_no_lock (20 samples, 0.04%)libsystem_malloc.dylib`free_tiny (11 samples, 0.02%)libsystem_malloc.dylib`tiny_free_no_lock (9 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (7 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (10 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (5 samples, 0.01%)libsystem_malloc.dylib`free_tiny (11 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (54 samples, 0.11%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (33 samples, 0.07%)libsystem_malloc.dylib`free_tiny (16 samples, 0.03%)libsystem_malloc.dylib`tiny_free_no_lock (12 samples, 0.03%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_malloc.dylib`_szone_free (8 samples, 0.02%)libsystem_malloc.dylib`free_tiny (13 samples, 0.03%)libsystem_platform.dylib`_platform_memset (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (94 samples, 0.20%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (267 samples, 0.56%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (238 samples, 0.50%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (229 samples, 0.48%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (155 samples, 0.33%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (139 samples, 0.29%)libsystem_malloc.dylib`tiny_free_scan_madvise_free (11 samples, 0.02%)libsystem_kernel.dylib`madvise (11 samples, 0.02%)libsystem_malloc.dylib`free_tiny (15 samples, 0.03%)libsystem_malloc.dylib`tiny_free_no_lock (15 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (18 samples, 0.04%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (18 samples, 0.04%)libsystem_kernel.dylib`madvise (5 samples, 0.01%)libsystem_malloc.dylib`free_tiny (8 samples, 0.02%)libsystem_malloc.dylib`tiny_free_no_lock (8 samples, 0.02%)libsystem_malloc.dylib`tiny_madvise_free_range_no_lock (6 samples, 0.01%)libsystem_malloc.dylib`tiny_free_list_remove_ptr (6 samples, 0.01%)libsystem_malloc.dylib`free_tiny (12 samples, 0.03%)libsystem_malloc.dylib`tiny_free_no_lock (12 samples, 0.03%)libsystem_malloc.dylib`free_tiny (11 samples, 0.02%)libsystem_malloc.dylib`tiny_free_no_lock (9 samples, 0.02%)libsystem_malloc.dylib`_nanov2_free (8 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (37 samples, 0.08%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (25 samples, 0.05%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (14 samples, 0.03%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (6 samples, 0.01%)libsystem_malloc.dylib`free_tiny (16 samples, 0.03%)libsystem_malloc.dylib`tiny_free_no_lock (16 samples, 0.03%)libsystem_malloc.dylib`tiny_free_no_lock (12 samples, 0.03%)libsystem_malloc.dylib`free_tiny (16 samples, 0.03%)libsystem_malloc.dylib`_free (7 samples, 0.01%)libsystem_malloc.dylib`_szone_free (5 samples, 0.01%)libsystem_malloc.dylib`free_tiny (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (36 samples, 0.08%)libsystem_malloc.dylib`free_tiny (5 samples, 0.01%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (9 samples, 0.02%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (165 samples, 0.35%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (147 samples, 0.31%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (147 samples, 0.31%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (102 samples, 0.21%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<alloc::boxed::Box<sqlparser::ast::Expr>> (86 samples, 0.18%)sqlparser_bench-959bc5267970ca34`core::ptr::drop_in_place<sqlparser::ast::Expr> (34 samples, 0.07%)all (47,617 samples, 100%) \ No newline at end of file From 94ea20628fbc9950e87d165e3b4f904419ec03f0 Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Thu, 2 Jan 2025 04:54:58 +0800 Subject: [PATCH 664/806] Add support for MYSQL's `RENAME TABLE` (#1616) Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 26 +++++++++++++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 18 ++++++++++++ tests/sqlparser_common.rs | 59 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d99f68886..756774353 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3413,6 +3413,13 @@ pub enum Statement { partitioned: Option>, table_format: Option, }, + /// ```sql + /// Rename TABLE tbl_name TO new_tbl_name[, tbl_name2 TO new_tbl_name2] ... + /// ``` + /// Renames one or more tables + /// + /// See Mysql + RenameTable(Vec), } impl fmt::Display for Statement { @@ -4970,6 +4977,9 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::RenameTable(rename_tables) => { + write!(f, "RENAME TABLE {}", display_comma_separated(rename_tables)) + } } } } @@ -7672,6 +7682,22 @@ impl Display for JsonNullClause { } } +/// rename object definition +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct RenameTable { + pub old_name: ObjectName, + pub new_name: ObjectName, +} + +impl fmt::Display for RenameTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} TO {}", self.old_name, self.new_name)?; + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 574830ef5..dad0c5379 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -492,6 +492,7 @@ impl Spanned for Statement { Statement::NOTIFY { .. } => Span::empty(), Statement::LoadData { .. } => Span::empty(), Statement::UNLISTEN { .. } => Span::empty(), + Statement::RenameTable { .. } => Span::empty(), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 012314b42..47d4d6f0d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -595,6 +595,7 @@ impl<'a> Parser<'a> { // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html Keyword::PRAGMA => self.parse_pragma(), Keyword::UNLOAD => self.parse_unload(), + Keyword::RENAME => self.parse_rename(), // `INSTALL` is duckdb specific https://duckdb.org/docs/extensions/overview Keyword::INSTALL if dialect_of!(self is DuckDbDialect | GenericDialect) => { self.parse_install() @@ -1085,6 +1086,23 @@ impl<'a> Parser<'a> { Ok(Statement::NOTIFY { channel, payload }) } + /// Parses a `RENAME TABLE` statement. See [Statement::RenameTable] + pub fn parse_rename(&mut self) -> Result { + if self.peek_keyword(Keyword::TABLE) { + self.expect_keyword(Keyword::TABLE)?; + let rename_tables = self.parse_comma_separated(|parser| { + let old_name = parser.parse_object_name(false)?; + parser.expect_keyword(Keyword::TO)?; + let new_name = parser.parse_object_name(false)?; + + Ok(RenameTable { old_name, new_name }) + })?; + Ok(Statement::RenameTable(rename_tables)) + } else { + self.expected("KEYWORD `TABLE` after RENAME", self.peek_token()) + } + } + // Tries to parse an expression by matching the specified word to known keywords that have a special meaning in the dialect. // Returns `None if no match is found. fn parse_expr_prefix_by_reserved_word( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3b21160b9..3c2e0899f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4133,6 +4133,65 @@ fn parse_alter_table() { } } +#[test] +fn parse_rename_table() { + match verified_stmt("RENAME TABLE test.test1 TO test_db.test2") { + Statement::RenameTable(rename_tables) => { + assert_eq!( + vec![RenameTable { + old_name: ObjectName(vec![ + Ident::new("test".to_string()), + Ident::new("test1".to_string()), + ]), + new_name: ObjectName(vec![ + Ident::new("test_db".to_string()), + Ident::new("test2".to_string()), + ]), + }], + rename_tables + ); + } + _ => unreachable!(), + }; + + match verified_stmt( + "RENAME TABLE old_table1 TO new_table1, old_table2 TO new_table2, old_table3 TO new_table3", + ) { + Statement::RenameTable(rename_tables) => { + assert_eq!( + vec![ + RenameTable { + old_name: ObjectName(vec![Ident::new("old_table1".to_string())]), + new_name: ObjectName(vec![Ident::new("new_table1".to_string())]), + }, + RenameTable { + old_name: ObjectName(vec![Ident::new("old_table2".to_string())]), + new_name: ObjectName(vec![Ident::new("new_table2".to_string())]), + }, + RenameTable { + old_name: ObjectName(vec![Ident::new("old_table3".to_string())]), + new_name: ObjectName(vec![Ident::new("new_table3".to_string())]), + } + ], + rename_tables + ); + } + _ => unreachable!(), + }; + + assert_eq!( + parse_sql_statements("RENAME TABLE old_table TO new_table a").unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: a".to_string()) + ); + + assert_eq!( + parse_sql_statements("RENAME TABLE1 old_table TO new_table a").unwrap_err(), + ParserError::ParserError( + "Expected: KEYWORD `TABLE` after RENAME, found: TABLE1".to_string() + ) + ); +} + #[test] fn test_alter_table_with_on_cluster() { match all_dialects() From 8bc63f0e4a01b3b8a2e694e42a99dc3665a06b8e Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 5 Jan 2025 15:37:34 +0100 Subject: [PATCH 665/806] Correctly tokenize nested comments (#1629) --- src/dialect/generic.rs | 4 ++ src/dialect/mod.rs | 6 +++ src/dialect/postgresql.rs | 4 ++ src/tokenizer.rs | 111 ++++++++++++++++++++++++++++++++------ 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index f852152a0..e2a73de8a 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -131,4 +131,8 @@ impl Dialect for GenericDialect { fn supports_empty_projections(&self) -> bool { true } + + fn supports_nested_comments(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 1343efca6..9ffbd8ed8 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -682,6 +682,12 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports nested comments + /// e.g. `/* /* nested */ */` + fn supports_nested_comments(&self) -> bool { + false + } + /// Returns true if this dialect supports treating the equals operator `=` within a `SelectItem` /// as an alias assignment operator, rather than a boolean expression. /// For example: the following statements are equivalent for such a dialect: diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 6a13a386a..170b0a7c9 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -241,6 +241,10 @@ impl Dialect for PostgreSqlDialect { fn supports_empty_projections(&self) -> bool { true } + + fn supports_nested_comments(&self) -> bool { + true + } } pub fn parse_create(parser: &mut Parser) -> Option> { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index da61303b4..38bd33d60 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1855,28 +1855,33 @@ impl<'a> Tokenizer<'a> { ) -> Result, TokenizerError> { let mut s = String::new(); let mut nested = 1; - let mut last_ch = ' '; + let supports_nested_comments = self.dialect.supports_nested_comments(); loop { match chars.next() { - Some(ch) => { - if last_ch == '/' && ch == '*' { - nested += 1; - } else if last_ch == '*' && ch == '/' { - nested -= 1; - if nested == 0 { - s.pop(); - break Ok(Some(Token::Whitespace(Whitespace::MultiLineComment(s)))); - } + Some('/') if matches!(chars.peek(), Some('*')) && supports_nested_comments => { + chars.next(); // consume the '*' + s.push('/'); + s.push('*'); + nested += 1; + } + Some('*') if matches!(chars.peek(), Some('/')) => { + chars.next(); // consume the '/' + nested -= 1; + if nested == 0 { + break Ok(Some(Token::Whitespace(Whitespace::MultiLineComment(s)))); } + s.push('*'); + s.push('/'); + } + Some(ch) => { s.push(ch); - last_ch = ch; } None => { break self.tokenizer_error( chars.location(), "Unexpected EOF while in a multi-line comment", - ) + ); } } } @@ -2718,18 +2723,90 @@ mod tests { #[test] fn tokenize_nested_multiline_comment() { - let sql = String::from("0/*multi-line\n* \n/* comment \n /*comment*/*/ */ /comment*/1"); + let dialect = GenericDialect {}; + let test_cases = vec![ + ( + "0/*multi-line\n* \n/* comment \n /*comment*/*/ */ /comment*/1", + vec![ + Token::Number("0".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment( + "multi-line\n* \n/* comment \n /*comment*/*/ ".into(), + )), + Token::Whitespace(Whitespace::Space), + Token::Div, + Token::Word(Word { + value: "comment".to_string(), + quote_style: None, + keyword: Keyword::COMMENT, + }), + Token::Mul, + Token::Div, + Token::Number("1".to_string(), false), + ], + ), + ( + "0/*multi-line\n* \n/* comment \n /*comment/**/ */ /comment*/*/1", + vec![ + Token::Number("0".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment( + "multi-line\n* \n/* comment \n /*comment/**/ */ /comment*/".into(), + )), + Token::Number("1".to_string(), false), + ], + ), + ( + "SELECT 1/* a /* b */ c */0", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number("1".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment(" a /* b */ c ".to_string())), + Token::Number("0".to_string(), false), + ], + ), + ]; + + for (sql, expected) in test_cases { + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + compare(expected, tokens); + } + } + + #[test] + fn tokenize_nested_multiline_comment_empty() { + let sql = "select 1/*/**/*/0"; let dialect = GenericDialect {}; - let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); let expected = vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::Number("1".to_string(), false), + Token::Whitespace(Whitespace::MultiLineComment("/**/".to_string())), Token::Number("0".to_string(), false), + ]; + + compare(expected, tokens); + } + + #[test] + fn tokenize_nested_comments_if_not_supported() { + let dialect = SQLiteDialect {}; + let sql = "SELECT 1/*/* nested comment */*/0"; + let tokens = Tokenizer::new(&dialect, sql).tokenize(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number("1".to_string(), false), Token::Whitespace(Whitespace::MultiLineComment( - "multi-line\n* \n/* comment \n /*comment*/*/ */ /comment".to_string(), + "/* nested comment ".to_string(), )), - Token::Number("1".to_string(), false), + Token::Mul, + Token::Div, + Token::Number("0".to_string(), false), ]; - compare(expected, tokens); + + compare(expected, tokens.unwrap()); } #[test] From 02d60cc0fc5bd9d5876214bc5a3d6a76343fac18 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 5 Jan 2025 19:30:06 +0100 Subject: [PATCH 666/806] Add support for USE SECONDARY ROLE (vs. ROLES) (#1637) --- src/parser/mod.rs | 2 +- tests/sqlparser_snowflake.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 47d4d6f0d..0a245d8df 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10529,7 +10529,7 @@ impl<'a> Parser<'a> { } fn parse_secondary_roles(&mut self) -> Result { - self.expect_keyword_is(Keyword::ROLES)?; + self.expect_one_of_keywords(&[Keyword::ROLES, Keyword::ROLE])?; if self.parse_keyword(Keyword::NONE) { Ok(Use::SecondaryRoles(SecondaryRoles::None)) } else if self.parse_keyword(Keyword::ALL) { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 9fe14783c..b5add6116 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2782,6 +2782,14 @@ fn parse_use() { snowflake().verified_stmt("USE SECONDARY ROLES ALL"); snowflake().verified_stmt("USE SECONDARY ROLES NONE"); snowflake().verified_stmt("USE SECONDARY ROLES r1, r2, r3"); + + // The following is not documented by Snowflake but still works: + snowflake().one_statement_parses_to("USE SECONDARY ROLE ALL", "USE SECONDARY ROLES ALL"); + snowflake().one_statement_parses_to("USE SECONDARY ROLE NONE", "USE SECONDARY ROLES NONE"); + snowflake().one_statement_parses_to( + "USE SECONDARY ROLE r1, r2, r3", + "USE SECONDARY ROLES r1, r2, r3", + ); } #[test] From e23877cb2d558019621eb772c27a3cb090124d8c Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 5 Jan 2025 19:31:51 +0100 Subject: [PATCH 667/806] Add support for various Snowflake grantees (#1640) --- src/ast/mod.rs | 62 ++++++++++++++++++++++++++++++++++++++- src/keywords.rs | 1 + src/parser/mod.rs | 58 +++++++++++++++++++++++++++++++++++- tests/sqlparser_common.rs | 9 ++++++ 4 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 756774353..867ae25b6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3159,7 +3159,7 @@ pub enum Statement { Grant { privileges: Privileges, objects: GrantObjects, - grantees: Vec, + grantees: Vec, with_grant_option: bool, granted_by: Option, }, @@ -5366,6 +5366,66 @@ impl fmt::Display for Action { } } +/// The principal that receives the privileges +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Grantee { + pub grantee_type: GranteesType, + pub name: Option, +} + +impl fmt::Display for Grantee { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.grantee_type { + GranteesType::Role => { + write!(f, "ROLE ")?; + } + GranteesType::Share => { + write!(f, "SHARE ")?; + } + GranteesType::User => { + write!(f, "USER ")?; + } + GranteesType::Group => { + write!(f, "GROUP ")?; + } + GranteesType::Public => { + write!(f, "PUBLIC ")?; + } + GranteesType::DatabaseRole => { + write!(f, "DATABASE ROLE ")?; + } + GranteesType::Application => { + write!(f, "APPLICATION ")?; + } + GranteesType::ApplicationRole => { + write!(f, "APPLICATION ROLE ")?; + } + GranteesType::None => (), + } + if let Some(ref name) = self.name { + write!(f, "{}", name)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum GranteesType { + Role, + Share, + User, + Group, + Public, + DatabaseRole, + Application, + ApplicationRole, + None, +} + /// Objects on which privileges are granted in a GRANT statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/keywords.rs b/src/keywords.rs index 43abc2b03..3fed882c3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -615,6 +615,7 @@ define_keywords!( PROCEDURE, PROGRAM, PROJECTION, + PUBLIC, PURGE, QUALIFY, QUARTER, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0a245d8df..170f34392 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11611,7 +11611,7 @@ impl<'a> Parser<'a> { let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::TO)?; - let grantees = self.parse_comma_separated(|p| p.parse_identifier())?; + let grantees = self.parse_grantees()?; let with_grant_option = self.parse_keywords(&[Keyword::WITH, Keyword::GRANT, Keyword::OPTION]); @@ -11629,6 +11629,62 @@ impl<'a> Parser<'a> { }) } + fn parse_grantees(&mut self) -> Result, ParserError> { + let mut values = vec![]; + let mut grantee_type = GranteesType::None; + loop { + grantee_type = if self.parse_keyword(Keyword::ROLE) { + GranteesType::Role + } else if self.parse_keyword(Keyword::USER) { + GranteesType::User + } else if self.parse_keyword(Keyword::SHARE) { + GranteesType::Share + } else if self.parse_keyword(Keyword::GROUP) { + GranteesType::Group + } else if self.parse_keyword(Keyword::PUBLIC) { + GranteesType::Public + } else if self.parse_keywords(&[Keyword::DATABASE, Keyword::ROLE]) { + GranteesType::DatabaseRole + } else if self.parse_keywords(&[Keyword::APPLICATION, Keyword::ROLE]) { + GranteesType::ApplicationRole + } else if self.parse_keyword(Keyword::APPLICATION) { + GranteesType::Application + } else { + grantee_type // keep from previous iteraton, if not specified + }; + + let grantee = if grantee_type == GranteesType::Public { + Grantee { + grantee_type: grantee_type.clone(), + name: None, + } + } else { + let mut name = self.parse_object_name(false)?; + if self.consume_token(&Token::Colon) { + // Redshift supports namespace prefix for extenrnal users and groups: + // : or : + // https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-native-idp.html + let ident = self.parse_identifier()?; + if let Some(n) = name.0.first() { + name = ObjectName(vec![Ident::new(format!("{}:{}", n.value, ident.value))]); + }; + } + Grantee { + grantee_type: grantee_type.clone(), + name: Some(name), + } + }; + + values.push(grantee); + + if !self.consume_token(&Token::Comma) { + break; + } + } + + Ok(values) + } + pub fn parse_grant_revoke_privileges_objects( &mut self, ) -> Result<(Privileges, GrantObjects), ParserError> { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3c2e0899f..4b307e8d7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8497,6 +8497,15 @@ fn parse_grant() { }, _ => unreachable!(), } + + verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO ROLE role1"); + verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO ROLE role1 WITH GRANT OPTION"); + verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO DATABASE ROLE role1"); + 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 USAGE ON SCHEMA sc1 TO a:b"); + verified_stmt("GRANT USAGE ON SCHEMA sc1 TO GROUP group1"); } #[test] From 17e22f0a60788c68952630024e2cb77db5be3c56 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:35:24 +0100 Subject: [PATCH 668/806] Add support for the SQL OVERLAPS predicate (#1638) --- src/ast/operator.rs | 6 ++++++ src/dialect/mod.rs | 1 + src/parser/mod.rs | 1 + tests/sqlparser_common.rs | 5 +++++ 4 files changed, 13 insertions(+) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index e44ea2bf4..1f9a6b82b 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -248,6 +248,11 @@ pub enum BinaryOperator { /// See [CREATE OPERATOR](https://www.postgresql.org/docs/current/sql-createoperator.html) /// for more information. PGCustomBinaryOperator(Vec), + /// The `OVERLAPS` operator + /// + /// Specifies a test for an overlap between two datetime periods: + /// + Overlaps, } impl fmt::Display for BinaryOperator { @@ -304,6 +309,7 @@ impl fmt::Display for BinaryOperator { BinaryOperator::PGCustomBinaryOperator(idents) => { write!(f, "OPERATOR({})", display_separated(idents, ".")) } + BinaryOperator::Overlaps => f.write_str("OVERLAPS"), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 9ffbd8ed8..025b5b356 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -512,6 +512,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)), Token::Word(w) if w.keyword == Keyword::IN => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(p!(Between)), + Token::Word(w) if w.keyword == Keyword::OVERLAPS => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::LIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::RLIKE => Ok(p!(Like)), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 170f34392..7776c66aa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3060,6 +3060,7 @@ impl<'a> Parser<'a> { Keyword::AND => Some(BinaryOperator::And), Keyword::OR => Some(BinaryOperator::Or), Keyword::XOR => Some(BinaryOperator::Xor), + Keyword::OVERLAPS => Some(BinaryOperator::Overlaps), Keyword::OPERATOR if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { self.expect_token(&Token::LParen)?; // there are special rules for operator names in diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4b307e8d7..791fa38c0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12805,3 +12805,8 @@ fn parse_update_from_before_select() { parse_sql_statements(query).unwrap_err() ); } + +#[test] +fn parse_overlaps() { + verified_stmt("SELECT (DATE '2016-01-10', DATE '2016-02-01') OVERLAPS (DATE '2016-01-20', DATE '2016-02-10')"); +} From 4c6af0ae4f7b2242e3f6fc92ad536d7c5f89a3b8 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:41:09 +0100 Subject: [PATCH 669/806] Add support for Snowflake LIST and REMOVE (#1639) --- src/ast/helpers/stmt_data_loading.rs | 21 ++++++++++++++++- src/ast/mod.rs | 10 +++++++- src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 35 +++++++++++++++++++++++++--- src/keywords.rs | 4 ++++ tests/sqlparser_snowflake.rs | 31 ++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index cda6c6ea4..42e1df06b 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -29,7 +29,7 @@ use core::fmt::Formatter; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::ast::Ident; +use crate::ast::{Ident, ObjectName}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; @@ -156,3 +156,22 @@ impl fmt::Display for StageLoadSelectItem { 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 FileStagingCommand { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub stage: ObjectName, + pub pattern: Option, +} + +impl fmt::Display for FileStagingCommand { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.stage)?; + if let Some(pattern) = self.pattern.as_ref() { + write!(f, " PATTERN='{pattern}'")?; + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 867ae25b6..f46438b3e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -23,7 +23,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use helpers::attached_token::AttachedToken; +use helpers::{attached_token::AttachedToken, stmt_data_loading::FileStagingCommand}; use core::ops::Deref; use core::{ @@ -3420,6 +3420,12 @@ pub enum Statement { /// /// See Mysql RenameTable(Vec), + /// Snowflake `LIST` + /// See: + List(FileStagingCommand), + /// Snowflake `REMOVE` + /// See: + Remove(FileStagingCommand), } impl fmt::Display for Statement { @@ -4980,6 +4986,8 @@ impl fmt::Display for Statement { Statement::RenameTable(rename_tables) => { write!(f, "RENAME TABLE {}", display_comma_separated(rename_tables)) } + Statement::List(command) => write!(f, "LIST {command}"), + Statement::Remove(command) => write!(f, "REMOVE {command}"), } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index dad0c5379..1dd9118f5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -493,6 +493,7 @@ impl Spanned for Statement { Statement::LoadData { .. } => Span::empty(), Statement::UNLISTEN { .. } => Span::empty(), Statement::RenameTable { .. } => Span::empty(), + Statement::List(..) | Statement::Remove(..) => Span::empty(), } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 249241d73..55343da18 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -19,8 +19,8 @@ use crate::alloc::string::ToString; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ - DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageLoadSelectItem, - StageParamsObject, + DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, FileStagingCommand, + StageLoadSelectItem, StageParamsObject, }; use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, Ident, IdentityParameters, IdentityProperty, @@ -165,6 +165,15 @@ impl Dialect for SnowflakeDialect { return Some(parse_copy_into(parser)); } + if let Some(kw) = parser.parse_one_of_keywords(&[ + Keyword::LIST, + Keyword::LS, + Keyword::REMOVE, + Keyword::RM, + ]) { + return Some(parse_file_staging_command(kw, parser)); + } + None } @@ -240,6 +249,26 @@ impl Dialect for SnowflakeDialect { } } +fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { + let stage = parse_snowflake_stage_name(parser)?; + let pattern = if parser.parse_keyword(Keyword::PATTERN) { + parser.expect_token(&Token::Eq)?; + Some(parser.parse_literal_string()?) + } else { + None + }; + + match kw { + Keyword::LIST | Keyword::LS => Ok(Statement::List(FileStagingCommand { stage, pattern })), + Keyword::REMOVE | Keyword::RM => { + Ok(Statement::Remove(FileStagingCommand { stage, pattern })) + } + _ => Err(ParserError::ParserError( + "unexpected stage command, expecting LIST, LS, REMOVE or RM".to_string(), + )), + } +} + /// Parse snowflake create table statement. /// pub fn parse_create_table( @@ -501,7 +530,7 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result ident.push('~'), Token::Mod => ident.push('%'), Token::Div => ident.push('/'), - Token::Word(w) => ident.push_str(&w.value), + Token::Word(w) => ident.push_str(&w.to_string()), _ => return parser.expected("stage name identifier", parser.peek_token()), } } diff --git a/src/keywords.rs b/src/keywords.rs index 3fed882c3..b7ff39e04 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -451,6 +451,7 @@ define_keywords!( LIKE_REGEX, LIMIT, LINES, + LIST, LISTEN, LN, LOAD, @@ -467,6 +468,7 @@ define_keywords!( LOWCARDINALITY, LOWER, LOW_PRIORITY, + LS, MACRO, MANAGEDLOCATION, MAP, @@ -649,6 +651,7 @@ define_keywords!( RELAY, RELEASE, REMOTE, + REMOVE, RENAME, REORG, REPAIR, @@ -672,6 +675,7 @@ define_keywords!( REVOKE, RIGHT, RLIKE, + RM, ROLE, ROLES, ROLLBACK, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b5add6116..112aa5264 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2991,3 +2991,34 @@ fn test_table_sample() { snowflake_and_generic().verified_stmt("SELECT id FROM mytable TABLESAMPLE (10) REPEATABLE (1)"); snowflake_and_generic().verified_stmt("SELECT id FROM mytable TABLESAMPLE (10) SEED (1)"); } + +#[test] +fn parse_ls_and_rm() { + snowflake().one_statement_parses_to("LS @~", "LIST @~"); + snowflake().one_statement_parses_to("RM @~", "REMOVE @~"); + + let statement = snowflake() + .verified_stmt("LIST @SNOWFLAKE_KAFKA_CONNECTOR_externalDataLakeSnowflakeConnector_STAGE_call_tracker_stream/"); + match statement { + Statement::List(command) => { + assert_eq!(command.stage, ObjectName(vec!["@SNOWFLAKE_KAFKA_CONNECTOR_externalDataLakeSnowflakeConnector_STAGE_call_tracker_stream/".into()])); + assert!(command.pattern.is_none()); + } + _ => unreachable!(), + }; + + let statement = + snowflake().verified_stmt("REMOVE @my_csv_stage/analysis/ PATTERN='.*data_0.*'"); + match statement { + Statement::Remove(command) => { + assert_eq!( + command.stage, + ObjectName(vec!["@my_csv_stage/analysis/".into()]) + ); + assert_eq!(command.pattern, Some(".*data_0.*".to_string())); + } + _ => unreachable!(), + }; + + snowflake().verified_stmt(r#"LIST @"STAGE_WITH_QUOTES""#); +} From 8cfc46277fdfce4d7bd71efdce5cc3e669c34afc Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:13:38 +0100 Subject: [PATCH 670/806] Add support for MySQL's INSERT INTO ... SET syntax (#1641) --- src/ast/dml.rs | 14 +++++++++----- src/ast/spans.rs | 2 ++ src/dialect/mod.rs | 7 +++++++ src/dialect/mysql.rs | 7 ++++++- src/parser/mod.rs | 24 ++++++++++-------------- tests/sqlparser_common.rs | 6 ++++++ tests/sqlparser_postgres.rs | 3 +++ 7 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 22309c8f8..f64818e61 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -32,8 +32,8 @@ use sqlparser_derive::{Visit, VisitMut}; pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ - display_comma_separated, display_separated, ClusteredBy, CommentDef, Expr, FileFormat, - FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, + display_comma_separated, display_separated, Assignment, ClusteredBy, CommentDef, Expr, + FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, TableWithJoins, Tag, WrappedCollection, @@ -480,6 +480,9 @@ pub struct Insert { pub overwrite: bool, /// A SQL query that specifies what to insert pub source: Option>, + /// MySQL `INSERT INTO ... SET` + /// See: + pub assignments: Vec, /// partitioned insert (Hive) pub partitioned: Option>, /// Columns defined after PARTITION @@ -545,9 +548,10 @@ impl Display for Insert { if let Some(source) = &self.source { write!(f, "{source}")?; - } - - if self.source.is_none() && self.columns.is_empty() { + } else if !self.assignments.is_empty() { + write!(f, "SET ")?; + write!(f, "{}", display_comma_separated(&self.assignments))?; + } else if self.source.is_none() && self.columns.is_empty() { write!(f, "DEFAULT VALUES")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1dd9118f5..2ca659147 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1153,6 +1153,7 @@ impl Spanned for Insert { replace_into: _, // bool priority: _, // todo, mysql specific insert_alias: _, // todo, mysql specific + assignments, } = self; union_spans( @@ -1160,6 +1161,7 @@ impl Spanned for Insert { .chain(table_alias.as_ref().map(|i| i.span)) .chain(columns.iter().map(|i| i.span)) .chain(source.as_ref().map(|q| q.span())) + .chain(assignments.iter().map(|i| i.span())) .chain(partitioned.iter().flat_map(|i| i.iter().map(|k| k.span()))) .chain(after_columns.iter().map(|i| i.span)) .chain(on.as_ref().map(|i| i.span())) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 025b5b356..7b14f2db5 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -775,6 +775,13 @@ pub trait Dialect: Debug + Any { fn supports_table_sample_before_alias(&self) -> bool { false } + + /// Returns true if this dialect supports the `INSERT INTO ... SET col1 = 1, ...` syntax. + /// + /// MySQL: + fn supports_insert_set(&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 1ede59f5a..3c3f2ee85 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -98,10 +98,15 @@ impl Dialect for MySqlDialect { true } - /// see + /// See: fn supports_create_table_select(&self) -> bool { true } + + /// See: + fn supports_insert_set(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7776c66aa..85ae66399 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11899,9 +11899,9 @@ impl<'a> Parser<'a> { let is_mysql = dialect_of!(self is MySqlDialect); - let (columns, partitioned, after_columns, source) = + let (columns, partitioned, after_columns, source, assignments) = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES]) { - (vec![], None, vec![], None) + (vec![], None, vec![], None, vec![]) } else { let (columns, partitioned, after_columns) = if !self.peek_subquery_start() { let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?; @@ -11918,9 +11918,14 @@ impl<'a> Parser<'a> { Default::default() }; - let source = Some(self.parse_query()?); + let (source, assignments) = + if self.dialect.supports_insert_set() && self.parse_keyword(Keyword::SET) { + (None, self.parse_comma_separated(Parser::parse_assignment)?) + } else { + (Some(self.parse_query()?), vec![]) + }; - (columns, partitioned, after_columns, source) + (columns, partitioned, after_columns, source, assignments) }; let insert_alias = if dialect_of!(self is MySqlDialect | GenericDialect) @@ -12000,6 +12005,7 @@ impl<'a> Parser<'a> { columns, after_columns, source, + assignments, table, on, returning, @@ -14228,16 +14234,6 @@ mod tests { assert!(Parser::parse_sql(&GenericDialect {}, sql).is_err()); } - #[test] - fn test_replace_into_set() { - // NOTE: This is actually valid MySQL syntax, REPLACE and INSERT, - // but the parser does not yet support it. - // https://dev.mysql.com/doc/refman/8.3/en/insert.html - let sql = "REPLACE INTO t SET a='1'"; - - assert!(Parser::parse_sql(&MySqlDialect {}, sql).is_err()); - } - #[test] fn test_replace_into_set_placeholder() { let sql = "REPLACE INTO t SET ?"; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 791fa38c0..7c8fd05a8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -119,6 +119,12 @@ fn parse_insert_values() { verified_stmt("INSERT INTO customer WITH foo AS (SELECT 1) SELECT * FROM foo UNION VALUES (1)"); } +#[test] +fn parse_insert_set() { + let dialects = all_dialects_where(|d| d.supports_insert_set()); + dialects.verified_stmt("INSERT INTO tbl1 SET col1 = 1, col2 = 'abc', col3 = current_date()"); +} + #[test] fn parse_replace_into() { let dialect = PostgreSqlDialect {}; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fd520d507..1a621ee74 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4423,6 +4423,7 @@ fn test_simple_postgres_insert_with_alias() { settings: None, format_clause: None, })), + assignments: vec![], partitioned: None, after_columns: vec![], table: false, @@ -4493,6 +4494,7 @@ fn test_simple_postgres_insert_with_alias() { settings: None, format_clause: None, })), + assignments: vec![], partitioned: None, after_columns: vec![], table: false, @@ -4559,6 +4561,7 @@ fn test_simple_insert_with_quoted_alias() { settings: None, format_clause: None, })), + assignments: vec![], partitioned: None, after_columns: vec![], table: false, From 0cd49fb6999f7945d5e64a2c93f34f4c25a4a962 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Tue, 7 Jan 2025 18:35:03 +0100 Subject: [PATCH 671/806] Start new line if \r in Postgres dialect (#1647) --- src/tokenizer.rs | 63 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 38bd33d60..b517ed661 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1621,11 +1621,17 @@ impl<'a> Tokenizer<'a> { // Consume characters until newline fn tokenize_single_line_comment(&self, chars: &mut State) -> String { - let mut comment = peeking_take_while(chars, |ch| ch != '\n'); + let mut comment = peeking_take_while(chars, |ch| match ch { + '\n' => false, // Always stop at \n + '\r' if dialect_of!(self is PostgreSqlDialect) => false, // Stop at \r for Postgres + _ => true, // Keep consuming for other characters + }); + if let Some(ch) = chars.next() { - assert_eq!(ch, '\n'); + assert!(ch == '\n' || ch == '\r'); comment.push(ch); } + comment } @@ -2677,17 +2683,62 @@ mod tests { #[test] fn tokenize_comment() { - let sql = String::from("0--this is a comment\n1"); + let test_cases = vec![ + ( + String::from("0--this is a comment\n1"), + vec![ + Token::Number("0".to_string(), false), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: "this is a comment\n".to_string(), + }), + Token::Number("1".to_string(), false), + ], + ), + ( + String::from("0--this is a comment\r1"), + vec![ + Token::Number("0".to_string(), false), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: "this is a comment\r1".to_string(), + }), + ], + ), + ( + String::from("0--this is a comment\r\n1"), + vec![ + Token::Number("0".to_string(), false), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: "this is a comment\r\n".to_string(), + }), + Token::Number("1".to_string(), false), + ], + ), + ]; let dialect = GenericDialect {}; + + for (sql, expected) in test_cases { + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + compare(expected, tokens); + } + } + + #[test] + fn tokenize_comment_postgres() { + let sql = String::from("1--\r0"); + + let dialect = PostgreSqlDialect {}; let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); let expected = vec![ - Token::Number("0".to_string(), false), + Token::Number("1".to_string(), false), Token::Whitespace(Whitespace::SingleLineComment { prefix: "--".to_string(), - comment: "this is a comment\n".to_string(), + comment: "\r".to_string(), }), - Token::Number("1".to_string(), false), + Token::Number("0".to_string(), false), ]; compare(expected, tokens); } From 62bcaa1c98ae1c063623a709c114d73cf729dedc Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Thu, 9 Jan 2025 01:28:20 +0800 Subject: [PATCH 672/806] Support pluralized time units (#1630) --- src/ast/value.rs | 14 +++++ src/keywords.rs | 6 +++ src/parser/mod.rs | 14 +++++ tests/sqlparser_common.rs | 106 +++++++++++++++++++++++++++++++------- 4 files changed, 122 insertions(+), 18 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index 28bf89ba8..45cc06a07 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -155,7 +155,9 @@ impl fmt::Display for DollarQuotedString { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum DateTimeField { Year, + Years, Month, + Months, /// Week optionally followed by a WEEKDAY. /// /// ```sql @@ -164,14 +166,19 @@ pub enum DateTimeField { /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/date_functions#extract) Week(Option), + Weeks, Day, DayOfWeek, DayOfYear, + Days, Date, Datetime, Hour, + Hours, Minute, + Minutes, Second, + Seconds, Century, Decade, Dow, @@ -210,7 +217,9 @@ impl fmt::Display for DateTimeField { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { DateTimeField::Year => write!(f, "YEAR"), + DateTimeField::Years => write!(f, "YEARS"), DateTimeField::Month => write!(f, "MONTH"), + DateTimeField::Months => write!(f, "MONTHS"), DateTimeField::Week(week_day) => { write!(f, "WEEK")?; if let Some(week_day) = week_day { @@ -218,14 +227,19 @@ impl fmt::Display for DateTimeField { } Ok(()) } + DateTimeField::Weeks => write!(f, "WEEKS"), DateTimeField::Day => write!(f, "DAY"), DateTimeField::DayOfWeek => write!(f, "DAYOFWEEK"), DateTimeField::DayOfYear => write!(f, "DAYOFYEAR"), + DateTimeField::Days => write!(f, "DAYS"), DateTimeField::Date => write!(f, "DATE"), DateTimeField::Datetime => write!(f, "DATETIME"), DateTimeField::Hour => write!(f, "HOUR"), + DateTimeField::Hours => write!(f, "HOURS"), DateTimeField::Minute => write!(f, "MINUTE"), + DateTimeField::Minutes => write!(f, "MINUTES"), DateTimeField::Second => write!(f, "SECOND"), + DateTimeField::Seconds => write!(f, "SECONDS"), DateTimeField::Century => write!(f, "CENTURY"), DateTimeField::Decade => write!(f, "DECADE"), DateTimeField::Dow => write!(f, "DOW"), diff --git a/src/keywords.rs b/src/keywords.rs index b7ff39e04..c8f3cba12 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -234,6 +234,7 @@ define_keywords!( DAY, DAYOFWEEK, DAYOFYEAR, + DAYS, DEALLOCATE, DEC, DECADE, @@ -499,6 +500,7 @@ define_keywords!( MILLISECONDS, MIN, MINUTE, + MINUTES, MINVALUE, MOD, MODE, @@ -506,6 +508,7 @@ define_keywords!( MODIFY, MODULE, MONTH, + MONTHS, MSCK, MULTISET, MUTATION, @@ -698,6 +701,7 @@ define_keywords!( SEARCH, SECOND, SECONDARY, + SECONDS, SECRET, SECURITY, SEED, @@ -866,6 +870,7 @@ define_keywords!( VOLATILE, WAREHOUSE, WEEK, + WEEKS, WHEN, WHENEVER, WHERE, @@ -880,6 +885,7 @@ define_keywords!( XML, XOR, YEAR, + YEARS, ZONE, ZORDER ); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 85ae66399..c127a7c42 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2358,7 +2358,9 @@ impl<'a> Parser<'a> { match &next_token.token { Token::Word(w) => match w.keyword { Keyword::YEAR => Ok(DateTimeField::Year), + Keyword::YEARS => Ok(DateTimeField::Years), Keyword::MONTH => Ok(DateTimeField::Month), + Keyword::MONTHS => Ok(DateTimeField::Months), Keyword::WEEK => { let week_day = if dialect_of!(self is BigQueryDialect | GenericDialect) && self.consume_token(&Token::LParen) @@ -2371,14 +2373,19 @@ impl<'a> Parser<'a> { }; Ok(DateTimeField::Week(week_day)) } + Keyword::WEEKS => Ok(DateTimeField::Weeks), Keyword::DAY => Ok(DateTimeField::Day), Keyword::DAYOFWEEK => Ok(DateTimeField::DayOfWeek), Keyword::DAYOFYEAR => Ok(DateTimeField::DayOfYear), + Keyword::DAYS => Ok(DateTimeField::Days), Keyword::DATE => Ok(DateTimeField::Date), Keyword::DATETIME => Ok(DateTimeField::Datetime), Keyword::HOUR => Ok(DateTimeField::Hour), + Keyword::HOURS => Ok(DateTimeField::Hours), Keyword::MINUTE => Ok(DateTimeField::Minute), + Keyword::MINUTES => Ok(DateTimeField::Minutes), Keyword::SECOND => Ok(DateTimeField::Second), + Keyword::SECONDS => Ok(DateTimeField::Seconds), Keyword::CENTURY => Ok(DateTimeField::Century), Keyword::DECADE => Ok(DateTimeField::Decade), Keyword::DOY => Ok(DateTimeField::Doy), @@ -2605,12 +2612,19 @@ impl<'a> Parser<'a> { matches!( word.keyword, Keyword::YEAR + | Keyword::YEARS | Keyword::MONTH + | Keyword::MONTHS | Keyword::WEEK + | Keyword::WEEKS | Keyword::DAY + | Keyword::DAYS | Keyword::HOUR + | Keyword::HOURS | Keyword::MINUTE + | Keyword::MINUTES | Keyword::SECOND + | Keyword::SECONDS | Keyword::CENTURY | Keyword::DECADE | Keyword::DOW diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7c8fd05a8..bbc3fcbd7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -50,6 +50,7 @@ mod test_utils; #[cfg(test)] use pretty_assertions::assert_eq; use sqlparser::ast::ColumnOption::Comment; +use sqlparser::ast::DateTimeField::Seconds; use sqlparser::ast::Expr::{Identifier, UnaryOp}; use sqlparser::ast::Value::Number; use sqlparser::test_utils::all_dialects_except; @@ -5399,6 +5400,19 @@ fn parse_interval_all() { expr_from_projection(only(&select.projection)), ); + let sql = "SELECT INTERVAL 5 DAYS"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::Interval(Interval { + value: Box::new(Expr::Value(number("5"))), + leading_field: Some(DateTimeField::Days), + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + }), + expr_from_projection(only(&select.projection)), + ); + let sql = "SELECT INTERVAL '10' HOUR (1)"; let select = verified_only_select(sql); assert_eq!( @@ -5426,10 +5440,18 @@ fn parse_interval_all() { verified_only_select("SELECT INTERVAL '1' YEAR"); verified_only_select("SELECT INTERVAL '1' MONTH"); + verified_only_select("SELECT INTERVAL '1' WEEK"); verified_only_select("SELECT INTERVAL '1' DAY"); verified_only_select("SELECT INTERVAL '1' HOUR"); verified_only_select("SELECT INTERVAL '1' MINUTE"); verified_only_select("SELECT INTERVAL '1' SECOND"); + verified_only_select("SELECT INTERVAL '1' YEARS"); + verified_only_select("SELECT INTERVAL '1' MONTHS"); + verified_only_select("SELECT INTERVAL '1' WEEKS"); + verified_only_select("SELECT INTERVAL '1' DAYS"); + verified_only_select("SELECT INTERVAL '1' HOURS"); + verified_only_select("SELECT INTERVAL '1' MINUTES"); + verified_only_select("SELECT INTERVAL '1' SECONDS"); verified_only_select("SELECT INTERVAL '1' YEAR TO MONTH"); verified_only_select("SELECT INTERVAL '1' DAY TO HOUR"); verified_only_select("SELECT INTERVAL '1' DAY TO MINUTE"); @@ -5439,10 +5461,21 @@ fn parse_interval_all() { verified_only_select("SELECT INTERVAL '1' MINUTE TO SECOND"); verified_only_select("SELECT INTERVAL 1 YEAR"); verified_only_select("SELECT INTERVAL 1 MONTH"); + verified_only_select("SELECT INTERVAL 1 WEEK"); verified_only_select("SELECT INTERVAL 1 DAY"); verified_only_select("SELECT INTERVAL 1 HOUR"); verified_only_select("SELECT INTERVAL 1 MINUTE"); verified_only_select("SELECT INTERVAL 1 SECOND"); + verified_only_select("SELECT INTERVAL 1 YEARS"); + verified_only_select("SELECT INTERVAL 1 MONTHS"); + verified_only_select("SELECT INTERVAL 1 WEEKS"); + verified_only_select("SELECT INTERVAL 1 DAYS"); + verified_only_select("SELECT INTERVAL 1 HOURS"); + verified_only_select("SELECT INTERVAL 1 MINUTES"); + verified_only_select("SELECT INTERVAL 1 SECONDS"); + verified_only_select( + "SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::INTERVAL", + ); } #[test] @@ -11356,16 +11389,12 @@ fn test_group_by_nothing() { #[test] fn test_extract_seconds_ok() { let dialects = all_dialects_where(|d| d.allow_extract_custom()); - let stmt = dialects.verified_expr("EXTRACT(seconds FROM '2 seconds'::INTERVAL)"); + let stmt = dialects.verified_expr("EXTRACT(SECONDS FROM '2 seconds'::INTERVAL)"); assert_eq!( stmt, Expr::Extract { - field: DateTimeField::Custom(Ident { - value: "seconds".to_string(), - quote_style: None, - span: Span::empty(), - }), + field: Seconds, syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { kind: CastKind::DoubleColon, @@ -11376,7 +11405,59 @@ fn test_extract_seconds_ok() { format: None, }), } - ) + ); + + let actual_ast = dialects + .parse_sql_statements("SELECT EXTRACT(seconds FROM '2 seconds'::INTERVAL)") + .unwrap(); + + let expected_ast = vec![Statement::Query(Box::new(Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![UnnamedExpr(Expr::Extract { + field: Seconds, + syntax: ExtractSyntax::From, + expr: Box::new(Expr::Cast { + kind: CastKind::DoubleColon, + expr: Box::new(Expr::Value(Value::SingleQuotedString( + "2 seconds".to_string(), + ))), + data_type: DataType::Interval, + format: None, + }), + })], + 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![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: None, + }))), + order_by: None, + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + }))]; + + assert_eq!(actual_ast, expected_ast); } #[test] @@ -11405,17 +11486,6 @@ fn test_extract_seconds_single_quote_ok() { ) } -#[test] -fn test_extract_seconds_err() { - let sql = "SELECT EXTRACT(seconds FROM '2 seconds'::INTERVAL)"; - let dialects = all_dialects_except(|d| d.allow_extract_custom()); - let err = dialects.parse_sql_statements(sql).unwrap_err(); - assert_eq!( - err.to_string(), - "sql parser error: Expected: date/time field, found: seconds" - ); -} - #[test] fn test_extract_seconds_single_quote_err() { let sql = r#"SELECT EXTRACT('seconds' FROM '2 seconds'::INTERVAL)"#; From 397bceb241f3d8b8802ab81ea6908200bd18593d Mon Sep 17 00:00:00 2001 From: Stepan Koltsov Date: Wed, 8 Jan 2025 18:27:25 +0000 Subject: [PATCH 673/806] Replace `ReferentialAction` enum in `DROP` statements (#1648) --- src/ast/ddl.rs | 20 ++++++++++++++++++++ src/ast/mod.rs | 32 ++++++++++++++++---------------- src/parser/mod.rs | 18 +++++++++--------- tests/sqlparser_common.rs | 4 ++-- tests/sqlparser_postgres.rs | 12 ++++++------ 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c87960679..f31fbf293 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1786,6 +1786,26 @@ impl fmt::Display for ReferentialAction { } } +/// ` ::= CASCADE | RESTRICT`. +/// +/// Used in `DROP` statements. +#[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 DropBehavior { + Restrict, + Cascade, +} + +impl fmt::Display for DropBehavior { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + DropBehavior::Restrict => "RESTRICT", + DropBehavior::Cascade => "CASCADE", + }) + } +} + /// SQL user defined type definition #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f46438b3e..6d0ef423c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -49,12 +49,12 @@ pub use self::dcl::{ pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterPolicyOperation, AlterTableOperation, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateFunction, Deduplicate, DeferrableInitial, GeneratedAs, - GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, - IdentityPropertyKind, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, - NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, TableConstraint, - TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, - ViewColumnDef, + ConstraintCharacteristics, 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, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -2700,7 +2700,7 @@ pub enum Statement { /// One or more function to drop func_desc: Vec, /// `CASCADE` or `RESTRICT` - option: Option, + drop_behavior: Option, }, /// ```sql /// DROP PROCEDURE @@ -2710,7 +2710,7 @@ pub enum Statement { /// One or more function to drop proc_desc: Vec, /// `CASCADE` or `RESTRICT` - option: Option, + drop_behavior: Option, }, /// ```sql /// DROP SECRET @@ -2729,7 +2729,7 @@ pub enum Statement { if_exists: bool, name: Ident, table_name: ObjectName, - option: Option, + drop_behavior: Option, }, /// ```sql /// DECLARE @@ -4317,7 +4317,7 @@ impl fmt::Display for Statement { Statement::DropFunction { if_exists, func_desc, - option, + drop_behavior, } => { write!( f, @@ -4325,7 +4325,7 @@ impl fmt::Display for Statement { if *if_exists { " IF EXISTS" } else { "" }, display_comma_separated(func_desc), )?; - if let Some(op) = option { + if let Some(op) = drop_behavior { write!(f, " {op}")?; } Ok(()) @@ -4333,7 +4333,7 @@ impl fmt::Display for Statement { Statement::DropProcedure { if_exists, proc_desc, - option, + drop_behavior, } => { write!( f, @@ -4341,7 +4341,7 @@ impl fmt::Display for Statement { if *if_exists { " IF EXISTS" } else { "" }, display_comma_separated(proc_desc), )?; - if let Some(op) = option { + if let Some(op) = drop_behavior { write!(f, " {op}")?; } Ok(()) @@ -4370,15 +4370,15 @@ impl fmt::Display for Statement { if_exists, name, table_name, - option, + drop_behavior, } => { write!(f, "DROP POLICY")?; if *if_exists { write!(f, " IF EXISTS")?; } write!(f, " {name} ON {table_name}")?; - if let Some(option) = option { - write!(f, " {option}")?; + if let Some(drop_behavior) = drop_behavior { + write!(f, " {drop_behavior}")?; } Ok(()) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c127a7c42..95413a8b3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5514,10 +5514,10 @@ impl<'a> Parser<'a> { }) } - fn parse_optional_referential_action(&mut self) -> Option { + fn parse_optional_drop_behavior(&mut self) -> Option { match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { - Some(Keyword::CASCADE) => Some(ReferentialAction::Cascade), - Some(Keyword::RESTRICT) => Some(ReferentialAction::Restrict), + Some(Keyword::CASCADE) => Some(DropBehavior::Cascade), + Some(Keyword::RESTRICT) => Some(DropBehavior::Restrict), _ => None, } } @@ -5529,11 +5529,11 @@ impl<'a> Parser<'a> { fn parse_drop_function(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let func_desc = self.parse_comma_separated(Parser::parse_function_desc)?; - let option = self.parse_optional_referential_action(); + let drop_behavior = self.parse_optional_drop_behavior(); Ok(Statement::DropFunction { if_exists, func_desc, - option, + drop_behavior, }) } @@ -5547,12 +5547,12 @@ impl<'a> Parser<'a> { let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::ON)?; let table_name = self.parse_object_name(false)?; - let option = self.parse_optional_referential_action(); + let drop_behavior = self.parse_optional_drop_behavior(); Ok(Statement::DropPolicy { if_exists, name, table_name, - option, + drop_behavior, }) } @@ -5563,11 +5563,11 @@ impl<'a> Parser<'a> { fn parse_drop_procedure(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let proc_desc = self.parse_comma_separated(Parser::parse_function_desc)?; - let option = self.parse_optional_referential_action(); + let drop_behavior = self.parse_optional_drop_behavior(); Ok(Statement::DropProcedure { if_exists, proc_desc, - option, + drop_behavior, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bbc3fcbd7..b47159dd5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11760,12 +11760,12 @@ fn test_drop_policy() { if_exists, name, table_name, - option, + drop_behavior, } => { assert_eq!(if_exists, true); assert_eq!(name.to_string(), "my_policy"); assert_eq!(table_name.to_string(), "my_table"); - assert_eq!(option, Some(ReferentialAction::Restrict)); + assert_eq!(drop_behavior, Some(DropBehavior::Restrict)); } _ => unreachable!(), } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1a621ee74..68fc010c0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3805,7 +3805,7 @@ fn parse_drop_function() { }]), args: None }], - option: None + drop_behavior: None } ); @@ -3830,7 +3830,7 @@ fn parse_drop_function() { } ]), }], - option: None + drop_behavior: None } ); @@ -3879,7 +3879,7 @@ fn parse_drop_function() { ]), } ], - option: None + drop_behavior: None } ); } @@ -3899,7 +3899,7 @@ fn parse_drop_procedure() { }]), args: None }], - option: None + drop_behavior: None } ); @@ -3924,7 +3924,7 @@ fn parse_drop_procedure() { } ]), }], - option: None + drop_behavior: None } ); @@ -3973,7 +3973,7 @@ fn parse_drop_procedure() { ]), } ], - option: None + drop_behavior: None } ); From 687ce2d5f4790cd8e36a0f34ab98cdb0572feb96 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:31:24 +0100 Subject: [PATCH 674/806] Add support for MS-SQL BEGIN/END TRY/CATCH (#1649) --- src/ast/helpers/stmt_create_table.rs | 6 +++- src/ast/mod.rs | 41 ++++++++++++++++++++++---- src/dialect/mod.rs | 7 ++++- src/dialect/mssql.rs | 7 +++++ src/keywords.rs | 2 ++ src/parser/mod.rs | 17 +++++++++++ tests/sqlparser_common.rs | 43 ++++++++++++++++++++++------ tests/sqlparser_custom_dialect.rs | 6 +++- tests/sqlparser_sqlite.rs | 8 +----- 9 files changed, 112 insertions(+), 25 deletions(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 364969c40..a3be57986 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -548,7 +548,11 @@ mod tests { #[test] pub fn test_from_invalid_statement() { - let stmt = Statement::Commit { chain: false }; + let stmt = Statement::Commit { + chain: false, + end: false, + modifier: None, + }; assert_eq!( CreateTableBuilder::try_from(stmt).unwrap_err(), diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6d0ef423c..248bdcba3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2958,7 +2958,6 @@ pub enum Statement { modes: Vec, begin: bool, transaction: Option, - /// Only for SQLite modifier: Option, }, /// ```sql @@ -2985,7 +2984,17 @@ pub enum Statement { /// ```sql /// COMMIT [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] /// ``` - Commit { chain: bool }, + /// If `end` is false + /// + /// ```sql + /// END [ TRY | CATCH ] + /// ``` + /// If `end` is true + Commit { + chain: bool, + end: bool, + modifier: Option, + }, /// ```sql /// ROLLBACK [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] [ TO [ SAVEPOINT ] savepoint_name ] /// ``` @@ -4614,8 +4623,23 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Commit { chain } => { - write!(f, "COMMIT{}", if *chain { " AND CHAIN" } else { "" },) + Statement::Commit { + chain, + end: end_syntax, + modifier, + } => { + if *end_syntax { + write!(f, "END")?; + if let Some(modifier) = *modifier { + write!(f, " {}", modifier)?; + } + if *chain { + write!(f, " AND CHAIN")?; + } + } else { + write!(f, "COMMIT{}", if *chain { " AND CHAIN" } else { "" })?; + } + Ok(()) } Statement::Rollback { chain, savepoint } => { write!(f, "ROLLBACK")?; @@ -6388,9 +6412,10 @@ impl fmt::Display for TransactionIsolationLevel { } } -/// SQLite specific syntax +/// Modifier for the transaction in the `BEGIN` syntax /// -/// +/// SQLite: +/// MS-SQL: #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -6398,6 +6423,8 @@ pub enum TransactionModifier { Deferred, Immediate, Exclusive, + Try, + Catch, } impl fmt::Display for TransactionModifier { @@ -6407,6 +6434,8 @@ impl fmt::Display for TransactionModifier { Deferred => "DEFERRED", Immediate => "IMMEDIATE", Exclusive => "EXCLUSIVE", + Try => "TRY", + Catch => "CATCH", }) } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 7b14f2db5..4c3f0b4b2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -260,11 +260,16 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if the dialect supports `BEGIN {DEFERRED | IMMEDIATE | EXCLUSIVE} [TRANSACTION]` statements + /// Returns true if the dialect supports `BEGIN {DEFERRED | IMMEDIATE | EXCLUSIVE | TRY | CATCH} [TRANSACTION]` statements fn supports_start_transaction_modifier(&self) -> bool { false } + /// Returns true if the dialect supports `END {TRY | CATCH}` statements + fn supports_end_transaction_modifier(&self) -> bool { + false + } + /// Returns true if the dialect supports named arguments of the form `FUN(a = '1', b = '2')`. fn supports_named_fn_args_with_eq_operator(&self) -> bool { false diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 2d0ef027f..fa77bdc1e 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -78,4 +78,11 @@ impl Dialect for MsSqlDialect { fn supports_named_fn_args_with_rarrow_operator(&self) -> bool { false } + + fn supports_start_transaction_modifier(&self) -> bool { + true + } + fn supports_end_transaction_modifier(&self) -> bool { + true + } } diff --git a/src/keywords.rs b/src/keywords.rs index c8f3cba12..c50c2bd40 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -151,6 +151,7 @@ define_keywords!( CASE, CAST, CATALOG, + CATCH, CEIL, CEILING, CENTURY, @@ -812,6 +813,7 @@ define_keywords!( TRIM_ARRAY, TRUE, TRUNCATE, + TRY, TRY_CAST, TRY_CONVERT, TUPLE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 95413a8b3..3ffd78d02 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12800,6 +12800,10 @@ impl<'a> Parser<'a> { Some(TransactionModifier::Immediate) } else if self.parse_keyword(Keyword::EXCLUSIVE) { Some(TransactionModifier::Exclusive) + } else if self.parse_keyword(Keyword::TRY) { + Some(TransactionModifier::Try) + } else if self.parse_keyword(Keyword::CATCH) { + Some(TransactionModifier::Catch) } else { None }; @@ -12817,8 +12821,19 @@ impl<'a> Parser<'a> { } pub fn parse_end(&mut self) -> Result { + let modifier = if !self.dialect.supports_end_transaction_modifier() { + None + } else if self.parse_keyword(Keyword::TRY) { + Some(TransactionModifier::Try) + } else if self.parse_keyword(Keyword::CATCH) { + Some(TransactionModifier::Catch) + } else { + None + }; Ok(Statement::Commit { chain: self.parse_commit_rollback_chain()?, + end: true, + modifier, }) } @@ -12861,6 +12876,8 @@ impl<'a> Parser<'a> { pub fn parse_commit(&mut self) -> Result { Ok(Statement::Commit { chain: self.parse_commit_rollback_chain()?, + end: false, + modifier: None, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b47159dd5..899194eb1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7887,6 +7887,27 @@ fn parse_start_transaction() { ParserError::ParserError("Expected: transaction mode, found: EOF".to_string()), res.unwrap_err() ); + + // MS-SQL syntax + let dialects = all_dialects_where(|d| d.supports_start_transaction_modifier()); + dialects.verified_stmt("BEGIN TRY"); + dialects.verified_stmt("BEGIN CATCH"); + + let dialects = all_dialects_where(|d| { + d.supports_start_transaction_modifier() && d.supports_end_transaction_modifier() + }); + dialects + .parse_sql_statements( + r#" + BEGIN TRY; + SELECT 1/0; + END TRY; + BEGIN CATCH; + EXECUTE foo; + END CATCH; + "#, + ) + .unwrap(); } #[test] @@ -8102,12 +8123,12 @@ fn parse_set_time_zone_alias() { #[test] fn parse_commit() { match verified_stmt("COMMIT") { - Statement::Commit { chain: false } => (), + Statement::Commit { chain: false, .. } => (), _ => unreachable!(), } match verified_stmt("COMMIT AND CHAIN") { - Statement::Commit { chain: true } => (), + Statement::Commit { chain: true, .. } => (), _ => unreachable!(), } @@ -8122,13 +8143,17 @@ fn parse_commit() { #[test] fn parse_end() { - one_statement_parses_to("END AND NO CHAIN", "COMMIT"); - one_statement_parses_to("END WORK AND NO CHAIN", "COMMIT"); - one_statement_parses_to("END TRANSACTION AND NO CHAIN", "COMMIT"); - one_statement_parses_to("END WORK AND CHAIN", "COMMIT AND CHAIN"); - one_statement_parses_to("END TRANSACTION AND CHAIN", "COMMIT AND CHAIN"); - one_statement_parses_to("END WORK", "COMMIT"); - one_statement_parses_to("END TRANSACTION", "COMMIT"); + one_statement_parses_to("END AND NO CHAIN", "END"); + one_statement_parses_to("END WORK AND NO CHAIN", "END"); + one_statement_parses_to("END TRANSACTION AND NO CHAIN", "END"); + one_statement_parses_to("END WORK AND CHAIN", "END AND CHAIN"); + one_statement_parses_to("END TRANSACTION AND CHAIN", "END AND CHAIN"); + one_statement_parses_to("END WORK", "END"); + one_statement_parses_to("END TRANSACTION", "END"); + // MS-SQL syntax + let dialects = all_dialects_where(|d| d.supports_end_transaction_modifier()); + dialects.verified_stmt("END TRY"); + dialects.verified_stmt("END CATCH"); } #[test] diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index e9ca82aba..61874fc27 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -115,7 +115,11 @@ fn custom_statement_parser() -> Result<(), ParserError> { for _ in 0..3 { let _ = parser.next_token(); } - Some(Ok(Statement::Commit { chain: false })) + Some(Ok(Statement::Commit { + chain: false, + end: false, + modifier: None, + })) } else { None } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 0adf7f755..edd1365f4 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -523,13 +523,7 @@ fn parse_start_transaction_with_modifier() { sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE"); sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE"); - let unsupported_dialects = TestedDialects::new( - all_dialects() - .dialects - .into_iter() - .filter(|x| !(x.is::() || x.is::())) - .collect(), - ); + let unsupported_dialects = all_dialects_except(|d| d.supports_start_transaction_modifier()); let res = unsupported_dialects.parse_sql_statements("BEGIN DEFERRED"); assert_eq!( ParserError::ParserError("Expected: end of statement, found: DEFERRED".to_string()), From 4fdf5e1b30820526d54bbcdc32d9d021e1947722 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Thu, 9 Jan 2025 15:31:06 -0800 Subject: [PATCH 675/806] Fix MySQL parsing of GRANT, REVOKE, and CREATE VIEW (#1538) --- src/ast/mod.rs | 138 +++++++++++++++++++++--- src/ast/spans.rs | 1 + src/dialect/generic.rs | 4 + src/dialect/mod.rs | 5 + src/dialect/mysql.rs | 4 + src/keywords.rs | 5 + src/parser/mod.rs | 168 +++++++++++++++++++++++------ src/tokenizer.rs | 66 +++++++++++- tests/sqlparser_common.rs | 58 +++++++++-- tests/sqlparser_mysql.rs | 203 ++++++++++++++++++++++++++++++++++-- tests/sqlparser_postgres.rs | 4 +- 11 files changed, 589 insertions(+), 67 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 248bdcba3..83c182672 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2368,7 +2368,7 @@ pub enum Statement { identity: Option, /// Postgres-specific option /// [ CASCADE | RESTRICT ] - cascade: Option, + cascade: Option, /// ClickHouse-specific option /// [ ON CLUSTER cluster_name ] /// @@ -2509,6 +2509,8 @@ pub enum Statement { /// if not None, has Clickhouse `TO` clause, specify the table into which to insert results /// to: Option, + /// MySQL: Optional parameters for the view algorithm, definer, and security context + params: Option, }, /// ```sql /// CREATE TABLE @@ -3178,9 +3180,9 @@ pub enum Statement { Revoke { privileges: Privileges, objects: GrantObjects, - grantees: Vec, + grantees: Vec, granted_by: Option, - cascade: bool, + cascade: Option, }, /// ```sql /// DEALLOCATE [ PREPARE ] { name | ALL } @@ -3616,8 +3618,8 @@ impl fmt::Display for Statement { } if let Some(cascade) = cascade { match cascade { - TruncateCascadeOption::Cascade => write!(f, " CASCADE")?, - TruncateCascadeOption::Restrict => write!(f, " RESTRICT")?, + CascadeOption::Cascade => write!(f, " CASCADE")?, + CascadeOption::Restrict => write!(f, " RESTRICT")?, } } @@ -3946,11 +3948,19 @@ impl fmt::Display for Statement { if_not_exists, temporary, to, + params, } => { write!( f, - "CREATE {or_replace}{materialized}{temporary}VIEW {if_not_exists}{name}{to}", + "CREATE {or_replace}", or_replace = if *or_replace { "OR REPLACE " } else { "" }, + )?; + if let Some(params) = params { + params.fmt(f)?; + } + write!( + f, + "{materialized}{temporary}VIEW {if_not_exists}{name}{to}", materialized = if *materialized { "MATERIALIZED " } else { "" }, name = name, temporary = if *temporary { "TEMPORARY " } else { "" }, @@ -4701,7 +4711,9 @@ impl fmt::Display for Statement { if let Some(grantor) = granted_by { write!(f, " GRANTED BY {grantor}")?; } - write!(f, " {}", if *cascade { "CASCADE" } else { "RESTRICT" })?; + if let Some(cascade) = cascade { + write!(f, " {}", cascade)?; + } Ok(()) } Statement::Deallocate { name, prepare } => write!( @@ -5103,16 +5115,25 @@ pub enum TruncateIdentityOption { Continue, } -/// PostgreSQL cascade option for TRUNCATE table +/// Cascade/restrict option for Postgres TRUNCATE table, MySQL GRANT/REVOKE, etc. /// [ CASCADE | RESTRICT ] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum TruncateCascadeOption { +pub enum CascadeOption { Cascade, Restrict, } +impl Display for CascadeOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CascadeOption::Cascade => write!(f, "CASCADE"), + CascadeOption::Restrict => write!(f, "RESTRICT"), + } + } +} + /// Transaction started with [ TRANSACTION | WORK ] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -5404,7 +5425,7 @@ impl fmt::Display for Action { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Grantee { pub grantee_type: GranteesType, - pub name: Option, + pub name: Option, } impl fmt::Display for Grantee { @@ -5437,7 +5458,7 @@ impl fmt::Display for Grantee { GranteesType::None => (), } if let Some(ref name) = self.name { - write!(f, "{}", name)?; + name.fmt(f)?; } Ok(()) } @@ -5458,6 +5479,28 @@ pub enum GranteesType { None, } +/// Users/roles designated in a GRANT/REVOKE +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum GranteeName { + /// A bare identifier + ObjectName(ObjectName), + /// A MySQL user/host pair such as 'root'@'%' + UserHost { user: Ident, host: Ident }, +} + +impl fmt::Display for GranteeName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GranteeName::ObjectName(name) => name.fmt(f), + GranteeName::UserHost { user, host } => { + write!(f, "{}@{}", user, host) + } + } + } +} + /// Objects on which privileges are granted in a GRANT statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -7460,15 +7503,84 @@ pub enum MySQLColumnPosition { impl Display for MySQLColumnPosition { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - MySQLColumnPosition::First => Ok(write!(f, "FIRST")?), + MySQLColumnPosition::First => write!(f, "FIRST"), MySQLColumnPosition::After(ident) => { let column_name = &ident.value; - Ok(write!(f, "AFTER {column_name}")?) + write!(f, "AFTER {column_name}") } } } } +/// MySQL `CREATE VIEW` algorithm parameter: [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateViewAlgorithm { + Undefined, + Merge, + TempTable, +} + +impl Display for CreateViewAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreateViewAlgorithm::Undefined => write!(f, "UNDEFINED"), + CreateViewAlgorithm::Merge => write!(f, "MERGE"), + CreateViewAlgorithm::TempTable => write!(f, "TEMPTABLE"), + } + } +} +/// MySQL `CREATE VIEW` security parameter: [SQL SECURITY { DEFINER | INVOKER }] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateViewSecurity { + Definer, + Invoker, +} + +impl Display for CreateViewSecurity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreateViewSecurity::Definer => write!(f, "DEFINER"), + CreateViewSecurity::Invoker => write!(f, "INVOKER"), + } + } +} + +/// [MySQL] `CREATE VIEW` additional parameters +/// +/// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/create-view.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 CreateViewParams { + pub algorithm: Option, + pub definer: Option, + pub security: Option, +} + +impl Display for CreateViewParams { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let CreateViewParams { + algorithm, + definer, + security, + } = self; + if let Some(algorithm) = algorithm { + write!(f, "ALGORITHM = {algorithm} ")?; + } + if let Some(definers) = definer { + write!(f, "DEFINER = {definers} ")?; + } + if let Some(security) = security { + write!(f, "SQL SECURITY {security} ")?; + } + Ok(()) + } +} + /// Engine of DB. Some warehouse has parameters of engine, e.g. [clickhouse] /// /// [clickhouse]: https://clickhouse.com/docs/en/engines/table-engines diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 2ca659147..c61c584d6 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -373,6 +373,7 @@ impl Spanned for Statement { if_not_exists: _, temporary: _, to, + params: _, } => union_spans( core::iter::once(name.span()) .chain(columns.iter().map(|i| i.span())) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index e2a73de8a..d696861b5 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -135,4 +135,8 @@ impl Dialect for GenericDialect { fn supports_nested_comments(&self) -> bool { true } + + fn supports_user_host_grantee(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 4c3f0b4b2..4b1558ba5 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -434,6 +434,11 @@ pub trait Dialect: Debug + Any { false } + /// Does the dialect support MySQL-style `'user'@'host'` grantee syntax? + fn supports_user_host_grantee(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 3c3f2ee85..535b4298a 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -107,6 +107,10 @@ impl Dialect for MySqlDialect { fn supports_insert_set(&self) -> bool { true } + + fn supports_user_host_grantee(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/keywords.rs b/src/keywords.rs index c50c2bd40..897e5b5cc 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -84,6 +84,7 @@ define_keywords!( AFTER, AGAINST, AGGREGATION, + ALGORITHM, ALIAS, ALL, ALLOCATE, @@ -248,6 +249,7 @@ define_keywords!( DEFERRED, DEFINE, DEFINED, + DEFINER, DELAYED, DELETE, DELIMITED, @@ -423,6 +425,7 @@ define_keywords!( INTERSECTION, INTERVAL, INTO, + INVOKER, IS, ISODOW, ISOLATION, @@ -780,6 +783,7 @@ define_keywords!( TBLPROPERTIES, TEMP, TEMPORARY, + TEMPTABLE, TERMINATED, TERSE, TEXT, @@ -828,6 +832,7 @@ define_keywords!( UNBOUNDED, UNCACHE, UNCOMMITTED, + UNDEFINED, UNFREEZE, UNION, UNIQUE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3ffd78d02..70868335f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -805,13 +805,7 @@ impl<'a> Parser<'a> { None }; - cascade = if self.parse_keyword(Keyword::CASCADE) { - Some(TruncateCascadeOption::Cascade) - } else if self.parse_keyword(Keyword::RESTRICT) { - Some(TruncateCascadeOption::Restrict) - } else { - None - }; + cascade = self.parse_cascade_option(); }; let on_cluster = self.parse_optional_on_cluster()?; @@ -827,6 +821,16 @@ impl<'a> Parser<'a> { }) } + fn parse_cascade_option(&mut self) -> Option { + if self.parse_keyword(Keyword::CASCADE) { + Some(CascadeOption::Cascade) + } else if self.parse_keyword(Keyword::RESTRICT) { + Some(CascadeOption::Restrict) + } else { + None + } + } + pub fn parse_attach_duckdb_database_options( &mut self, ) -> Result, ParserError> { @@ -4147,11 +4151,12 @@ impl<'a> Parser<'a> { .is_some(); let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); + let create_view_params = self.parse_create_view_params()?; if self.parse_keyword(Keyword::TABLE) { 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) + self.parse_create_view(or_replace, temporary, create_view_params) } else if self.parse_keyword(Keyword::POLICY) { self.parse_create_policy() } else if self.parse_keyword(Keyword::EXTERNAL) { @@ -5039,6 +5044,7 @@ impl<'a> Parser<'a> { &mut self, or_replace: bool, temporary: bool, + create_view_params: Option, ) -> Result { let materialized = self.parse_keyword(Keyword::MATERIALIZED); self.expect_keyword_is(Keyword::VIEW)?; @@ -5116,9 +5122,68 @@ impl<'a> Parser<'a> { if_not_exists, temporary, to, + params: create_view_params, }) } + /// Parse optional parameters for the `CREATE VIEW` statement supported by [MySQL]. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/create-view.html + fn parse_create_view_params(&mut self) -> Result, ParserError> { + let algorithm = if self.parse_keyword(Keyword::ALGORITHM) { + self.expect_token(&Token::Eq)?; + Some( + match self.expect_one_of_keywords(&[ + Keyword::UNDEFINED, + Keyword::MERGE, + Keyword::TEMPTABLE, + ])? { + Keyword::UNDEFINED => CreateViewAlgorithm::Undefined, + Keyword::MERGE => CreateViewAlgorithm::Merge, + Keyword::TEMPTABLE => CreateViewAlgorithm::TempTable, + _ => { + self.prev_token(); + let found = self.next_token(); + return self + .expected("UNDEFINED or MERGE or TEMPTABLE after ALGORITHM =", found); + } + }, + ) + } else { + None + }; + let definer = if self.parse_keyword(Keyword::DEFINER) { + self.expect_token(&Token::Eq)?; + Some(self.parse_grantee_name()?) + } else { + None + }; + let security = if self.parse_keywords(&[Keyword::SQL, Keyword::SECURITY]) { + Some( + match self.expect_one_of_keywords(&[Keyword::DEFINER, Keyword::INVOKER])? { + Keyword::DEFINER => CreateViewSecurity::Definer, + Keyword::INVOKER => CreateViewSecurity::Invoker, + _ => { + self.prev_token(); + let found = self.next_token(); + return self.expected("DEFINER or INVOKER after SQL SECURITY", found); + } + }, + ) + } else { + None + }; + if algorithm.is_some() || definer.is_some() || security.is_some() { + Ok(Some(CreateViewParams { + algorithm, + definer, + security, + })) + } else { + Ok(None) + } + } + pub fn parse_create_role(&mut self) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let names = self.parse_comma_separated(|p| p.parse_object_name(false))?; @@ -8872,13 +8937,13 @@ impl<'a> Parser<'a> { } } - /// 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 { + /// 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 { @@ -8891,19 +8956,41 @@ impl<'a> Parser<'a> { } } else { loop { - 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("")); - } - idents.push(self.parse_identifier()?); + 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(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 { + let ObjectName(mut idents) = + self.parse_object_name_with_wildcards(in_table_clause, false)?; // BigQuery accepts any number of quoted identifiers of a table name. // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#quoted_identifiers @@ -11674,14 +11761,17 @@ impl<'a> Parser<'a> { name: None, } } else { - let mut name = self.parse_object_name(false)?; + let mut name = self.parse_grantee_name()?; if self.consume_token(&Token::Colon) { // Redshift supports namespace prefix for extenrnal users and groups: // : or : // https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-native-idp.html let ident = self.parse_identifier()?; - if let Some(n) = name.0.first() { - name = ObjectName(vec![Ident::new(format!("{}:{}", n.value, ident.value))]); + if let GranteeName::ObjectName(namespace) = name { + name = GranteeName::ObjectName(ObjectName(vec![Ident::new(format!( + "{}:{}", + namespace, ident + ))])); }; } Grantee { @@ -11764,7 +11854,8 @@ impl<'a> Parser<'a> { } else { let object_type = self.parse_one_of_keywords(&[Keyword::SEQUENCE, Keyword::SCHEMA, Keyword::TABLE]); - let objects = self.parse_comma_separated(|p| p.parse_object_name(false)); + let objects = + self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); match object_type { Some(Keyword::SCHEMA) => GrantObjects::Schemas(objects?), Some(Keyword::SEQUENCE) => GrantObjects::Sequences(objects?), @@ -11808,23 +11899,32 @@ impl<'a> Parser<'a> { } } + pub fn parse_grantee_name(&mut self) -> Result { + let mut name = self.parse_object_name(false)?; + if self.dialect.supports_user_host_grantee() + && name.0.len() == 1 + && self.consume_token(&Token::AtSign) + { + let user = name.0.pop().unwrap(); + let host = self.parse_identifier()?; + Ok(GranteeName::UserHost { user, host }) + } else { + Ok(GranteeName::ObjectName(name)) + } + } + /// Parse a REVOKE statement pub fn parse_revoke(&mut self) -> Result { let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; self.expect_keyword_is(Keyword::FROM)?; - let grantees = self.parse_comma_separated(|p| p.parse_identifier())?; + let grantees = self.parse_grantees()?; let granted_by = self .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) .then(|| self.parse_identifier().unwrap()); - let loc = self.peek_token().span.start; - let cascade = self.parse_keyword(Keyword::CASCADE); - let restrict = self.parse_keyword(Keyword::RESTRICT); - if cascade && restrict { - return parser_err!("Cannot specify both CASCADE and RESTRICT in REVOKE", loc); - } + let cascade = self.parse_cascade_option(); Ok(Statement::Revoke { privileges, diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b517ed661..15b131222 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1432,6 +1432,18 @@ impl<'a> Tokenizer<'a> { } } Some(' ') => Ok(Some(Token::AtSign)), + // We break on quotes here, because no dialect allows identifiers starting + // with @ and containing quotation marks (e.g. `@'foo'`) unless they are + // quoted, which is tokenized as a quoted string, not here (e.g. + // `"@'foo'"`). Further, at least two dialects parse `@` followed by a + // quoted string as two separate tokens, which this allows. For example, + // Postgres parses `@'1'` as the absolute value of '1' which is implicitly + // cast to a numeric type. And when parsing MySQL-style grantees (e.g. + // `GRANT ALL ON *.* to 'root'@'localhost'`), we also want separate tokens + // for the user, the `@`, and the host. + Some('\'') => Ok(Some(Token::AtSign)), + Some('\"') => Ok(Some(Token::AtSign)), + Some('`') => Ok(Some(Token::AtSign)), Some(sch) if self.dialect.is_identifier_start('@') => { self.tokenize_identifier_or_keyword([ch, *sch], chars) } @@ -1459,7 +1471,7 @@ impl<'a> Tokenizer<'a> { } '$' => Ok(Some(self.tokenize_dollar_preceded_value(chars)?)), - //whitespace check (including unicode chars) should be last as it covers some of the chars above + // whitespace check (including unicode chars) should be last as it covers some of the chars above ch if ch.is_whitespace() => { self.consume_and_return(chars, Token::Whitespace(Whitespace::Space)) } @@ -3396,4 +3408,56 @@ mod tests { let expected = vec![Token::SingleQuotedString("''".to_string())]; compare(expected, tokens); } + + #[test] + fn test_mysql_users_grantees() { + let dialect = MySqlDialect {}; + + let sql = "CREATE USER `root`@`%`"; + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("CREATE"), + Token::Whitespace(Whitespace::Space), + Token::make_keyword("USER"), + Token::Whitespace(Whitespace::Space), + Token::make_word("root", Some('`')), + Token::AtSign, + Token::make_word("%", Some('`')), + ]; + compare(expected, tokens); + } + + #[test] + fn test_postgres_abs_without_space_and_string_literal() { + let dialect = MySqlDialect {}; + + let sql = "SELECT @'1'"; + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::AtSign, + Token::SingleQuotedString("1".to_string()), + ]; + compare(expected, tokens); + } + + #[test] + fn test_postgres_abs_without_space_and_quoted_column() { + let dialect = MySqlDialect {}; + + let sql = r#"SELECT @"bar" FROM foo"#; + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::AtSign, + Token::DoubleQuotedString("bar".to_string()), + Token::Whitespace(Whitespace::Space), + Token::make_keyword("FROM"), + Token::Whitespace(Whitespace::Space), + Token::make_word("foo", None), + ]; + compare(expected, tokens); + } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 899194eb1..df39c2216 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7192,6 +7192,7 @@ fn parse_create_view() { if_not_exists, temporary, to, + params, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -7204,7 +7205,8 @@ fn parse_create_view() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); - assert!(to.is_none()) + assert!(to.is_none()); + assert!(params.is_none()); } _ => unreachable!(), } @@ -7252,6 +7254,7 @@ fn parse_create_view_with_columns() { if_not_exists, temporary, to, + params, } => { assert_eq!("v", name.to_string()); assert_eq!( @@ -7274,7 +7277,8 @@ fn parse_create_view_with_columns() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); - assert!(to.is_none()) + assert!(to.is_none()); + assert!(params.is_none()); } _ => unreachable!(), } @@ -7297,6 +7301,7 @@ fn parse_create_view_temporary() { if_not_exists, temporary, to, + params, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -7309,7 +7314,8 @@ fn parse_create_view_temporary() { assert!(!late_binding); assert!(!if_not_exists); assert!(temporary); - assert!(to.is_none()) + assert!(to.is_none()); + assert!(params.is_none()); } _ => unreachable!(), } @@ -7332,6 +7338,7 @@ fn parse_create_or_replace_view() { if_not_exists, temporary, to, + params, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -7344,7 +7351,8 @@ fn parse_create_or_replace_view() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); - assert!(to.is_none()) + assert!(to.is_none()); + assert!(params.is_none()); } _ => unreachable!(), } @@ -7371,6 +7379,7 @@ fn parse_create_or_replace_materialized_view() { if_not_exists, temporary, to, + params, } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -7383,7 +7392,8 @@ fn parse_create_or_replace_materialized_view() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); - assert!(to.is_none()) + assert!(to.is_none()); + assert!(params.is_none()); } _ => unreachable!(), } @@ -7406,6 +7416,7 @@ fn parse_create_materialized_view() { if_not_exists, temporary, to, + params, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -7418,7 +7429,8 @@ fn parse_create_materialized_view() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); - assert!(to.is_none()) + assert!(to.is_none()); + assert!(params.is_none()); } _ => unreachable!(), } @@ -7441,6 +7453,7 @@ fn parse_create_materialized_view_with_cluster_by() { if_not_exists, temporary, to, + params, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -7453,7 +7466,8 @@ fn parse_create_materialized_view_with_cluster_by() { assert!(!late_binding); assert!(!if_not_exists); assert!(!temporary); - assert!(to.is_none()) + assert!(to.is_none()); + assert!(params.is_none()); } _ => unreachable!(), } @@ -8574,14 +8588,40 @@ fn parse_grant() { #[test] fn test_revoke() { - let sql = "REVOKE ALL PRIVILEGES ON users, auth FROM analyst CASCADE"; + let sql = "REVOKE ALL PRIVILEGES ON users, auth FROM analyst"; match verified_stmt(sql) { Statement::Revoke { privileges, objects: GrantObjects::Tables(tables), grantees, + granted_by, cascade, + } => { + assert_eq!( + Privileges::All { + with_privileges_keyword: true + }, + privileges + ); + assert_eq_vec(&["users", "auth"], &tables); + assert_eq_vec(&["analyst"], &grantees); + assert_eq!(cascade, None); + assert_eq!(None, granted_by); + } + _ => unreachable!(), + } +} + +#[test] +fn test_revoke_with_cascade() { + let sql = "REVOKE ALL PRIVILEGES ON users, auth FROM analyst CASCADE"; + match all_dialects_except(|d| d.is::()).verified_stmt(sql) { + Statement::Revoke { + privileges, + objects: GrantObjects::Tables(tables), + grantees, granted_by, + cascade, } => { assert_eq!( Privileges::All { @@ -8591,7 +8631,7 @@ fn test_revoke() { ); assert_eq_vec(&["users", "auth"], &tables); assert_eq_vec(&["analyst"], &grantees); - assert!(cascade); + assert_eq!(cascade, Some(CascadeOption::Cascade)); assert_eq!(None, granted_by); } _ => unreachable!(), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 4a4e79611..62884afc0 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -33,6 +33,14 @@ use test_utils::*; #[macro_use] mod test_utils; +fn mysql() -> TestedDialects { + TestedDialects::new(vec![Box::new(MySqlDialect {})]) +} + +fn mysql_and_generic() -> TestedDialects { + TestedDialects::new(vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})]) +} + #[test] fn parse_identifiers() { mysql().verified_stmt("SELECT $a$, àà"); @@ -2732,14 +2740,6 @@ fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name mysql_and_generic().verified_stmt("CREATE TABLE tb (c1 INT, CONSTRAINT cons FULLTEXT (c1))"); } -fn mysql() -> TestedDialects { - TestedDialects::new(vec![Box::new(MySqlDialect {})]) -} - -fn mysql_and_generic() -> TestedDialects { - TestedDialects::new(vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})]) -} - #[test] fn parse_values() { mysql().verified_stmt("VALUES ROW(1, true, 'a')"); @@ -3001,6 +3001,193 @@ fn parse_bitstring_literal() { ); } +#[test] +fn parse_grant() { + let sql = "GRANT ALL ON *.* TO 'jeffrey'@'%'"; + let stmt = mysql().verified_stmt(sql); + if let Statement::Grant { + privileges, + objects, + grantees, + with_grant_option, + granted_by, + } = stmt + { + assert_eq!( + privileges, + Privileges::All { + with_privileges_keyword: false + } + ); + assert_eq!( + objects, + GrantObjects::Tables(vec![ObjectName(vec!["*".into(), "*".into()])]) + ); + assert!(!with_grant_option); + assert!(granted_by.is_none()); + if let [Grantee { + grantee_type: GranteesType::None, + name: Some(GranteeName::UserHost { user, host }), + }] = grantees.as_slice() + { + assert_eq!(user.value, "jeffrey"); + assert_eq!(user.quote_style, Some('\'')); + assert_eq!(host.value, "%"); + assert_eq!(host.quote_style, Some('\'')); + } else { + unreachable!() + } + } else { + unreachable!() + } +} + +#[test] +fn parse_revoke() { + let sql = "REVOKE ALL ON db1.* FROM 'jeffrey'@'%'"; + let stmt = mysql_and_generic().verified_stmt(sql); + if let Statement::Revoke { + privileges, + objects, + grantees, + granted_by, + cascade, + } = stmt + { + assert_eq!( + privileges, + Privileges::All { + with_privileges_keyword: false + } + ); + assert_eq!( + objects, + GrantObjects::Tables(vec![ObjectName(vec!["db1".into(), "*".into()])]) + ); + if let [Grantee { + grantee_type: GranteesType::None, + name: Some(GranteeName::UserHost { user, host }), + }] = grantees.as_slice() + { + assert_eq!(user.value, "jeffrey"); + assert_eq!(user.quote_style, Some('\'')); + assert_eq!(host.value, "%"); + assert_eq!(host.quote_style, Some('\'')); + } else { + unreachable!() + } + assert!(granted_by.is_none()); + assert!(cascade.is_none()); + } else { + unreachable!() + } +} + +#[test] +fn parse_create_view_algorithm_param() { + let sql = "CREATE ALGORITHM = MERGE VIEW foo AS SELECT 1"; + let stmt = mysql().verified_stmt(sql); + if let Statement::CreateView { + params: + Some(CreateViewParams { + algorithm, + definer, + security, + }), + .. + } = stmt + { + assert_eq!(algorithm, Some(CreateViewAlgorithm::Merge)); + assert!(definer.is_none()); + assert!(security.is_none()); + } else { + unreachable!() + } + mysql().verified_stmt("CREATE ALGORITHM = UNDEFINED VIEW foo AS SELECT 1"); + mysql().verified_stmt("CREATE ALGORITHM = TEMPTABLE VIEW foo AS SELECT 1"); +} + +#[test] +fn parse_create_view_definer_param() { + let sql = "CREATE DEFINER = 'jeffrey'@'localhost' VIEW foo AS SELECT 1"; + let stmt = mysql().verified_stmt(sql); + if let Statement::CreateView { + params: + Some(CreateViewParams { + algorithm, + definer, + security, + }), + .. + } = stmt + { + assert!(algorithm.is_none()); + if let Some(GranteeName::UserHost { user, host }) = definer { + assert_eq!(user.value, "jeffrey"); + assert_eq!(user.quote_style, Some('\'')); + assert_eq!(host.value, "localhost"); + assert_eq!(host.quote_style, Some('\'')); + } else { + unreachable!() + } + assert!(security.is_none()); + } else { + unreachable!() + } +} + +#[test] +fn parse_create_view_security_param() { + let sql = "CREATE SQL SECURITY DEFINER VIEW foo AS SELECT 1"; + let stmt = mysql().verified_stmt(sql); + if let Statement::CreateView { + params: + Some(CreateViewParams { + algorithm, + definer, + security, + }), + .. + } = stmt + { + assert!(algorithm.is_none()); + assert!(definer.is_none()); + assert_eq!(security, Some(CreateViewSecurity::Definer)); + } else { + unreachable!() + } + mysql().verified_stmt("CREATE SQL SECURITY INVOKER VIEW foo AS SELECT 1"); +} + +#[test] +fn parse_create_view_multiple_params() { + let sql = "CREATE ALGORITHM = UNDEFINED DEFINER = `root`@`%` SQL SECURITY INVOKER VIEW foo AS SELECT 1"; + let stmt = mysql().verified_stmt(sql); + if let Statement::CreateView { + params: + Some(CreateViewParams { + algorithm, + definer, + security, + }), + .. + } = stmt + { + assert_eq!(algorithm, Some(CreateViewAlgorithm::Undefined)); + if let Some(GranteeName::UserHost { user, host }) = definer { + assert_eq!(user.value, "root"); + assert_eq!(user.quote_style, Some('`')); + assert_eq!(host.value, "%"); + assert_eq!(host.quote_style, Some('`')); + } else { + unreachable!() + } + assert_eq!(security, Some(CreateViewSecurity::Invoker)); + } else { + unreachable!() + } +} + #[test] fn parse_longblob_type() { let sql = "CREATE TABLE foo (bar LONGBLOB)"; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 68fc010c0..6f6cf8618 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4171,7 +4171,7 @@ fn parse_truncate_with_options() { table: true, only: true, identity: Some(TruncateIdentityOption::Restart), - cascade: Some(TruncateCascadeOption::Cascade), + cascade: Some(CascadeOption::Cascade), on_cluster: None, }, truncate @@ -4203,7 +4203,7 @@ fn parse_truncate_with_table_list() { table: true, only: false, identity: Some(TruncateIdentityOption::Restart), - cascade: Some(TruncateCascadeOption::Cascade), + cascade: Some(CascadeOption::Cascade), on_cluster: None, }, truncate From 5a761dd6dbb6945607b4eaf82891aa5a8d241171 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Fri, 10 Jan 2025 00:52:09 +0100 Subject: [PATCH 676/806] Add support for the Snowflake MINUS set operator (#1652) --- src/ast/query.rs | 2 ++ src/keywords.rs | 3 +++ src/parser/mod.rs | 12 ++++++++++-- tests/sqlparser_common.rs | 5 ++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 9e4e9e2ef..2f0663a5f 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -208,6 +208,7 @@ pub enum SetOperator { Union, Except, Intersect, + Minus, } impl fmt::Display for SetOperator { @@ -216,6 +217,7 @@ impl fmt::Display for SetOperator { SetOperator::Union => "UNION", SetOperator::Except => "EXCEPT", SetOperator::Intersect => "INTERSECT", + SetOperator::Minus => "MINUS", }) } } diff --git a/src/keywords.rs b/src/keywords.rs index 897e5b5cc..bd538ec69 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -503,6 +503,7 @@ define_keywords!( MILLISECOND, MILLISECONDS, MIN, + MINUS, MINUTE, MINUTES, MINVALUE, @@ -921,6 +922,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::UNION, Keyword::EXCEPT, Keyword::INTERSECT, + Keyword::MINUS, // Reserved only as a table alias in the `FROM`/`JOIN` clauses: Keyword::ON, Keyword::JOIN, @@ -984,6 +986,7 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::UNION, Keyword::EXCEPT, Keyword::INTERSECT, + Keyword::MINUS, Keyword::CLUSTER, Keyword::DISTRIBUTE, Keyword::RETURNING, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 70868335f..397ff37bc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9942,7 +9942,9 @@ impl<'a> Parser<'a> { let op = self.parse_set_operator(&self.peek_token().token); let next_precedence = match op { // UNION and EXCEPT have the same binding power and evaluate left-to-right - Some(SetOperator::Union) | Some(SetOperator::Except) => 10, + Some(SetOperator::Union) | Some(SetOperator::Except) | Some(SetOperator::Minus) => { + 10 + } // INTERSECT has higher precedence than UNION/EXCEPT Some(SetOperator::Intersect) => 20, // Unexpected token or EOF => stop parsing the query body @@ -9969,13 +9971,19 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::UNION => Some(SetOperator::Union), Token::Word(w) if w.keyword == Keyword::EXCEPT => Some(SetOperator::Except), Token::Word(w) if w.keyword == Keyword::INTERSECT => Some(SetOperator::Intersect), + Token::Word(w) if w.keyword == Keyword::MINUS => Some(SetOperator::Minus), _ => None, } } pub fn parse_set_quantifier(&mut self, op: &Option) -> SetQuantifier { match op { - Some(SetOperator::Except | SetOperator::Intersect | SetOperator::Union) => { + Some( + SetOperator::Except + | SetOperator::Intersect + | SetOperator::Union + | SetOperator::Minus, + ) => { if self.parse_keywords(&[Keyword::DISTINCT, Keyword::BY, Keyword::NAME]) { SetQuantifier::DistinctByName } else if self.parse_keywords(&[Keyword::BY, Keyword::NAME]) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index df39c2216..ba2f5309b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6860,7 +6860,7 @@ fn parse_derived_tables() { } #[test] -fn parse_union_except_intersect() { +fn parse_union_except_intersect_minus() { // TODO: add assertions verified_stmt("SELECT 1 UNION SELECT 2"); verified_stmt("SELECT 1 UNION ALL SELECT 2"); @@ -6868,6 +6868,9 @@ fn parse_union_except_intersect() { verified_stmt("SELECT 1 EXCEPT SELECT 2"); verified_stmt("SELECT 1 EXCEPT ALL SELECT 2"); verified_stmt("SELECT 1 EXCEPT DISTINCT SELECT 1"); + verified_stmt("SELECT 1 MINUS SELECT 2"); + verified_stmt("SELECT 1 MINUS ALL SELECT 2"); + verified_stmt("SELECT 1 MINUS DISTINCT SELECT 1"); verified_stmt("SELECT 1 INTERSECT SELECT 2"); verified_stmt("SELECT 1 INTERSECT ALL SELECT 2"); verified_stmt("SELECT 1 INTERSECT DISTINCT SELECT 1"); From c54ba4dc41c613510c8a2df4d93913171a204777 Mon Sep 17 00:00:00 2001 From: Stepan Koltsov Date: Fri, 10 Jan 2025 00:29:34 +0000 Subject: [PATCH 677/806] ALTER TABLE DROP {COLUMN|CONSTRAINT} RESTRICT (#1651) --- src/ast/ddl.rs | 20 ++++++++++---- src/ast/spans.rs | 4 +-- src/parser/mod.rs | 8 +++--- tests/sqlparser_common.rs | 58 +++++++++++++++++++-------------------- 4 files changed, 48 insertions(+), 42 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f31fbf293..1b5ccda26 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -115,13 +115,13 @@ pub enum AlterTableOperation { DropConstraint { if_exists: bool, name: Ident, - cascade: bool, + drop_behavior: Option, }, /// `DROP [ COLUMN ] [ IF EXISTS ] [ CASCADE ]` DropColumn { column_name: Ident, if_exists: bool, - cascade: bool, + drop_behavior: Option, }, /// `ATTACH PART|PARTITION ` /// Note: this is a ClickHouse-specific operation, please refer to @@ -451,27 +451,35 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::DropConstraint { if_exists, name, - cascade, + drop_behavior, } => { write!( f, "DROP CONSTRAINT {}{}{}", if *if_exists { "IF EXISTS " } else { "" }, name, - if *cascade { " CASCADE" } else { "" }, + match drop_behavior { + None => "", + Some(DropBehavior::Restrict) => " RESTRICT", + Some(DropBehavior::Cascade) => " CASCADE", + } ) } AlterTableOperation::DropPrimaryKey => write!(f, "DROP PRIMARY KEY"), AlterTableOperation::DropColumn { column_name, if_exists, - cascade, + drop_behavior, } => write!( f, "DROP COLUMN {}{}{}", if *if_exists { "IF EXISTS " } else { "" }, column_name, - if *cascade { " CASCADE" } else { "" } + match drop_behavior { + None => "", + Some(DropBehavior::Restrict) => " RESTRICT", + Some(DropBehavior::Cascade) => " CASCADE", + } ), AlterTableOperation::AttachPartition { partition } => { write!(f, "ATTACH {partition}") diff --git a/src/ast/spans.rs b/src/ast/spans.rs index c61c584d6..be0db9528 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -961,12 +961,12 @@ impl Spanned for AlterTableOperation { AlterTableOperation::DropConstraint { if_exists: _, name, - cascade: _, + drop_behavior: _, } => name.span, AlterTableOperation::DropColumn { column_name, if_exists: _, - cascade: _, + drop_behavior: _, } => column_name.span, AlterTableOperation::AttachPartition { partition } => partition.span(), AlterTableOperation::DetachPartition { partition } => partition.span(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 397ff37bc..bfa4590c9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7680,11 +7680,11 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::CONSTRAINT) { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let name = self.parse_identifier()?; - let cascade = self.parse_keyword(Keyword::CASCADE); + let drop_behavior = self.parse_optional_drop_behavior(); AlterTableOperation::DropConstraint { if_exists, name, - cascade, + drop_behavior, } } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) && dialect_of!(self is MySqlDialect | GenericDialect) @@ -7702,11 +7702,11 @@ impl<'a> Parser<'a> { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let column_name = self.parse_identifier()?; - let cascade = self.parse_keyword(Keyword::CASCADE); + let drop_behavior = self.parse_optional_drop_behavior(); AlterTableOperation::DropColumn { column_name, if_exists, - cascade, + drop_behavior, } } } else if self.parse_keyword(Keyword::PARTITION) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ba2f5309b..c7b555771 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4388,7 +4388,9 @@ fn parse_alter_table_constraints() { #[test] fn parse_alter_table_drop_column() { + check_one("DROP COLUMN IF EXISTS is_active"); 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", @@ -4403,11 +4405,15 @@ fn parse_alter_table_drop_column() { AlterTableOperation::DropColumn { column_name, if_exists, - cascade, + drop_behavior, } => { assert_eq!("is_active", column_name.to_string()); assert!(if_exists); - assert!(cascade); + match drop_behavior { + None => assert!(constraint_text.ends_with(" is_active")), + Some(DropBehavior::Restrict) => assert!(constraint_text.ends_with(" RESTRICT")), + Some(DropBehavior::Cascade) => assert!(constraint_text.ends_with(" CASCADE")), + } } _ => unreachable!(), } @@ -4497,37 +4503,29 @@ fn parse_alter_table_alter_column_type() { #[test] fn parse_alter_table_drop_constraint() { - let alter_stmt = "ALTER TABLE tab"; - match alter_table_op(verified_stmt( - "ALTER TABLE tab DROP CONSTRAINT constraint_name CASCADE", - )) { - AlterTableOperation::DropConstraint { - name: constr_name, - if_exists, - cascade, - } => { - assert_eq!("constraint_name", constr_name.to_string()); - assert!(!if_exists); - assert!(cascade); - } - _ => unreachable!(), - } - match alter_table_op(verified_stmt( - "ALTER TABLE tab DROP CONSTRAINT IF EXISTS constraint_name", - )) { - AlterTableOperation::DropConstraint { - name: constr_name, - if_exists, - cascade, - } => { - assert_eq!("constraint_name", constr_name.to_string()); - assert!(if_exists); - assert!(!cascade); + check_one("DROP CONSTRAINT IF EXISTS constraint_name"); + check_one("DROP CONSTRAINT IF EXISTS constraint_name RESTRICT"); + check_one("DROP CONSTRAINT IF EXISTS constraint_name CASCADE"); + fn check_one(constraint_text: &str) { + match alter_table_op(verified_stmt(&format!("ALTER TABLE tab {constraint_text}"))) { + AlterTableOperation::DropConstraint { + name: constr_name, + if_exists, + drop_behavior, + } => { + assert_eq!("constraint_name", constr_name.to_string()); + assert!(if_exists); + match drop_behavior { + None => assert!(constraint_text.ends_with(" constraint_name")), + Some(DropBehavior::Restrict) => assert!(constraint_text.ends_with(" RESTRICT")), + Some(DropBehavior::Cascade) => assert!(constraint_text.ends_with(" CASCADE")), + } + } + _ => unreachable!(), } - _ => unreachable!(), } - let res = parse_sql_statements(&format!("{alter_stmt} DROP CONSTRAINT is_active TEXT")); + let res = parse_sql_statements("ALTER TABLE tab DROP CONSTRAINT is_active TEXT"); assert_eq!( ParserError::ParserError("Expected: end of statement, found: TEXT".to_string()), res.unwrap_err() From b09514e49261708f0511f080c69fc831fdfed3a7 Mon Sep 17 00:00:00 2001 From: cjw Date: Fri, 10 Jan 2025 22:23:56 +0800 Subject: [PATCH 678/806] feat: support `INSERT INTO [TABLE] FUNCTION` of Clickhouse (#1633) Co-authored-by: Kermit Co-authored-by: Ifeanyi Ubah --- src/ast/dml.rs | 13 ++++---- src/ast/mod.rs | 30 ++++++++++++++++++ src/ast/spans.rs | 22 ++++++++++--- src/dialect/clickhouse.rs | 4 +++ src/dialect/mod.rs | 5 +++ src/parser/mod.rs | 18 +++++++++-- src/test_utils.rs | 1 - tests/sqlparser_clickhouse.rs | 6 ++++ tests/sqlparser_common.rs | 23 +++++++++----- tests/sqlparser_mysql.rs | 58 +++++++++++++++++++++++------------ tests/sqlparser_postgres.rs | 20 ++++++------ 11 files changed, 147 insertions(+), 53 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index f64818e61..d68a2277e 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -36,7 +36,7 @@ use super::{ FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, - TableWithJoins, Tag, WrappedCollection, + TableObject, TableWithJoins, Tag, WrappedCollection, }; /// CREATE INDEX statement. @@ -470,8 +470,7 @@ pub struct Insert { /// INTO - optional keyword pub into: bool, /// TABLE - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - pub table_name: ObjectName, + pub table: TableObject, /// table_name as foo (for PostgreSQL) pub table_alias: Option, /// COLUMNS @@ -488,7 +487,7 @@ pub struct Insert { /// Columns defined after PARTITION pub after_columns: Vec, /// whether the insert has the table keyword (Hive) - pub table: bool, + pub has_table_keyword: bool, pub on: Option, /// RETURNING pub returning: Option>, @@ -503,9 +502,9 @@ pub struct Insert { impl Display for Insert { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let table_name = if let Some(alias) = &self.table_alias { - format!("{0} AS {alias}", self.table_name) + format!("{0} AS {alias}", self.table) } else { - self.table_name.to_string() + self.table.to_string() }; if let Some(on_conflict) = self.or { @@ -531,7 +530,7 @@ impl Display for Insert { ignore = if self.ignore { " IGNORE" } else { "" }, over = if self.overwrite { " OVERWRITE" } else { "" }, int = if self.into { " INTO" } else { "" }, - tbl = if self.table { " TABLE" } else { "" }, + tbl = if self.has_table_keyword { " TABLE" } else { "" }, )?; } if !self.columns.is_empty() { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 83c182672..5ab2fc939 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7907,6 +7907,36 @@ impl fmt::Display for RenameTable { } } +/// Represents the referenced table in an `INSERT INTO` 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 TableObject { + /// Table specified by name. + /// Example: + /// ```sql + /// INSERT INTO my_table + /// ``` + TableName(#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] ObjectName), + + /// Table specified as a function. + /// Example: + /// ```sql + /// INSERT INTO TABLE FUNCTION remote('localhost', default.simple_table) + /// ``` + /// [Clickhouse](https://clickhouse.com/docs/en/sql-reference/table-functions) + TableFunction(Function), +} + +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), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index be0db9528..8a27c4ac1 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -32,8 +32,9 @@ use super::{ OrderBy, OrderByExpr, Partition, PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, - TableConstraint, TableFactor, TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, - Value, Values, ViewColumnDef, WildcardAdditionalOptions, With, WithFill, + 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. @@ -1141,14 +1142,14 @@ impl Spanned for Insert { or: _, // enum, sqlite specific ignore: _, // bool into: _, // bool - table_name, + table, table_alias, columns, overwrite: _, // bool source, partitioned, after_columns, - table: _, // bool + has_table_keyword: _, // bool on, returning, replace_into: _, // bool @@ -1158,7 +1159,7 @@ impl Spanned for Insert { } = self; union_spans( - core::iter::once(table_name.span()) + core::iter::once(table.span()) .chain(table_alias.as_ref().map(|i| i.span)) .chain(columns.iter().map(|i| i.span)) .chain(source.as_ref().map(|q| q.span())) @@ -2121,6 +2122,17 @@ impl Spanned for UpdateTableFromKind { } } +impl Spanned for TableObject { + fn span(&self) -> Span { + match self { + TableObject::TableName(ObjectName(segments)) => { + union_spans(segments.iter().map(|i| i.span)) + } + TableObject::TableFunction(func) => func.span(), + } + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 0c8f08040..267f766f7 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -50,4 +50,8 @@ impl Dialect for ClickHouseDialect { fn supports_limit_comma(&self) -> bool { true } + + fn supports_insert_table_function(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 4b1558ba5..a682e4f63 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -792,6 +792,11 @@ pub trait Dialect: Debug + Any { fn supports_insert_set(&self) -> bool { false } + + /// Does the dialect support table function in insertion? + fn supports_insert_table_function(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bfa4590c9..b6e3fd1c4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8937,6 +8937,18 @@ impl<'a> Parser<'a> { } } + /// Parse a table object for insetion + /// e.g. `some_database.some_table` or `FUNCTION some_table_func(...)` + pub fn parse_table_object(&mut self) -> Result { + if self.dialect.supports_insert_table_function() && self.parse_keyword(Keyword::FUNCTION) { + let fn_name = self.parse_object_name(false)?; + self.parse_function_call(fn_name) + .map(TableObject::TableFunction) + } else { + self.parse_object_name(false).map(TableObject::TableName) + } + } + /// Parse a possibly qualified, possibly quoted identifier, optionally allowing for wildcards, /// e.g. *, *.*, `foo`.*, or "foo"."bar" fn parse_object_name_with_wildcards( @@ -12010,7 +12022,7 @@ impl<'a> Parser<'a> { } else { // Hive lets you put table here regardless let table = self.parse_keyword(Keyword::TABLE); - let table_name = self.parse_object_name(false)?; + let table_object = self.parse_table_object()?; let table_alias = if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::AS) { @@ -12118,7 +12130,7 @@ impl<'a> Parser<'a> { Ok(Statement::Insert(Insert { or, - table_name, + table: table_object, table_alias, ignore, into, @@ -12128,7 +12140,7 @@ impl<'a> Parser<'a> { after_columns, source, assignments, - table, + has_table_keyword: table, on, returning, replace_into, diff --git a/src/test_utils.rs b/src/test_utils.rs index e76cdb87a..914be7d9f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -154,7 +154,6 @@ impl TestedDialects { 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); - if !canonical.is_empty() && sql != canonical { assert_eq!(self.parse_sql_statements(canonical).unwrap(), statements); } diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 2f1b043b6..4fa657baa 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -222,6 +222,12 @@ fn parse_create_table() { ); } +#[test] +fn parse_insert_into_function() { + clickhouse().verified_stmt(r#"INSERT INTO TABLE FUNCTION remote('localhost', default.simple_table) VALUES (100, 'inserted via remote()')"#); + clickhouse().verified_stmt(r#"INSERT INTO FUNCTION remote('localhost', default.simple_table) VALUES (100, 'inserted via remote()')"#); +} + #[test] fn parse_alter_table_attach_and_detach_partition() { for operation in &["ATTACH", "DETACH"] { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c7b555771..ab69b48ae 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -96,7 +96,7 @@ fn parse_insert_values() { ) { match verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source: Some(source), .. @@ -149,7 +149,7 @@ fn parse_insert_default_values() { partitioned, returning, source, - table_name, + table: table_name, .. }) => { assert_eq!(columns, vec![]); @@ -158,7 +158,10 @@ fn parse_insert_default_values() { assert_eq!(partitioned, None); assert_eq!(returning, None); assert_eq!(source, None); - assert_eq!(table_name, ObjectName(vec!["test_table".into()])); + assert_eq!( + table_name, + TableObject::TableName(ObjectName(vec!["test_table".into()])) + ); } _ => unreachable!(), } @@ -174,7 +177,7 @@ fn parse_insert_default_values() { partitioned, returning, source, - table_name, + table: table_name, .. }) => { assert_eq!(after_columns, vec![]); @@ -183,7 +186,10 @@ fn parse_insert_default_values() { assert_eq!(partitioned, None); assert!(returning.is_some()); assert_eq!(source, None); - assert_eq!(table_name, ObjectName(vec!["test_table".into()])); + assert_eq!( + table_name, + TableObject::TableName(ObjectName(vec!["test_table".into()])) + ); } _ => unreachable!(), } @@ -199,7 +205,7 @@ fn parse_insert_default_values() { partitioned, returning, source, - table_name, + table: table_name, .. }) => { assert_eq!(after_columns, vec![]); @@ -208,7 +214,10 @@ fn parse_insert_default_values() { assert_eq!(partitioned, None); assert_eq!(returning, None); assert_eq!(source, None); - assert_eq!(table_name, ObjectName(vec!["test_table".into()])); + assert_eq!( + table_name, + TableObject::TableName(ObjectName(vec!["test_table".into()])) + ); } _ => unreachable!(), } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 62884afc0..dcf3f57fe 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1406,13 +1406,16 @@ fn parse_simple_insert() { match mysql().verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, on, .. }) => { - assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!( + TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + table_name + ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); assert_eq!( @@ -1460,14 +1463,17 @@ fn parse_ignore_insert() { match mysql_and_generic().verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, on, ignore, .. }) => { - assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!( + TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + table_name + ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); assert!(ignore); @@ -1504,14 +1510,17 @@ fn parse_priority_insert() { match mysql_and_generic().verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, on, priority, .. }) => { - assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!( + TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + table_name + ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); assert_eq!(priority, Some(HighPriority)); @@ -1545,14 +1554,17 @@ fn parse_priority_insert() { match mysql().verified_stmt(sql2) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, on, priority, .. }) => { - assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!( + TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + table_name + ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); assert_eq!(priority, Some(LowPriority)); @@ -1588,14 +1600,14 @@ fn parse_insert_as() { let sql = r"INSERT INTO `table` (`date`) VALUES ('2024-01-01') AS `alias`"; match mysql_and_generic().verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, insert_alias, .. }) => { assert_eq!( - ObjectName(vec![Ident::with_quote('`', "table")]), + TableObject::TableName(ObjectName(vec![Ident::with_quote('`', "table")])), table_name ); assert_eq!(vec![Ident::with_quote('`', "date")], columns); @@ -1640,14 +1652,14 @@ fn parse_insert_as() { let sql = r"INSERT INTO `table` (`id`, `date`) VALUES (1, '2024-01-01') AS `alias` (`mek_id`, `mek_date`)"; match mysql_and_generic().verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, insert_alias, .. }) => { assert_eq!( - ObjectName(vec![Ident::with_quote('`', "table")]), + TableObject::TableName(ObjectName(vec![Ident::with_quote('`', "table")])), table_name ); assert_eq!( @@ -1698,7 +1710,7 @@ fn parse_replace_insert() { let sql = r"REPLACE DELAYED INTO tasks (title, priority) VALUES ('Test Some Inserts', 1)"; match mysql().verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, on, @@ -1706,7 +1718,10 @@ fn parse_replace_insert() { priority, .. }) => { - assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); + assert_eq!( + TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + table_name + ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); assert!(on.is_none()); assert!(replace_into); @@ -1744,13 +1759,16 @@ fn parse_empty_row_insert() { match mysql().one_statement_parses_to(sql, "INSERT INTO tb VALUES (), ()") { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, on, .. }) => { - assert_eq!(ObjectName(vec![Ident::new("tb")]), table_name); + assert_eq!( + TableObject::TableName(ObjectName(vec![Ident::new("tb")])), + table_name + ); assert!(columns.is_empty()); assert!(on.is_none()); assert_eq!( @@ -1783,14 +1801,14 @@ fn parse_insert_with_on_duplicate_update() { match mysql().verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, source, on, .. }) => { assert_eq!( - ObjectName(vec![Ident::new("permission_groups")]), + TableObject::TableName(ObjectName(vec![Ident::new("permission_groups")])), table_name ); assert_eq!( @@ -1974,12 +1992,12 @@ fn parse_insert_with_numeric_prefix_column_name() { let sql = "INSERT INTO s1.t1 (123col_$@length123) VALUES (67.654)"; match mysql().verified_stmt(sql) { Statement::Insert(Insert { - table_name, + table: table_name, columns, .. }) => { assert_eq!( - ObjectName(vec![Ident::new("s1"), Ident::new("t1")]), + TableObject::TableName(ObjectName(vec![Ident::new("s1"), Ident::new("t1")])), table_name ); assert_eq!(vec![Ident::new("123col_$@length123")], columns); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6f6cf8618..ce31a0628 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1725,7 +1725,7 @@ fn parse_prepare() { }; match sub_stmt.as_ref() { Statement::Insert(Insert { - table_name, + table: table_name, columns, source: Some(source), .. @@ -4381,11 +4381,11 @@ fn test_simple_postgres_insert_with_alias() { or: None, ignore: false, into: true, - table_name: ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), - }]), + }])), table_alias: Some(Ident { value: "test_table".to_string(), quote_style: None, @@ -4426,7 +4426,7 @@ fn test_simple_postgres_insert_with_alias() { assignments: vec![], partitioned: None, after_columns: vec![], - table: false, + has_table_keyword: false, on: None, returning: None, replace_into: false, @@ -4449,11 +4449,11 @@ fn test_simple_postgres_insert_with_alias() { or: None, ignore: false, into: true, - table_name: ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), - }]), + }])), table_alias: Some(Ident { value: "test_table".to_string(), quote_style: None, @@ -4497,7 +4497,7 @@ fn test_simple_postgres_insert_with_alias() { assignments: vec![], partitioned: None, after_columns: vec![], - table: false, + has_table_keyword: false, on: None, returning: None, replace_into: false, @@ -4519,11 +4519,11 @@ fn test_simple_insert_with_quoted_alias() { or: None, ignore: false, into: true, - table_name: ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), - }]), + }])), table_alias: Some(Ident { value: "Test_Table".to_string(), quote_style: Some('"'), @@ -4564,7 +4564,7 @@ fn test_simple_insert_with_quoted_alias() { assignments: vec![], partitioned: None, after_columns: vec![], - table: false, + has_table_keyword: false, on: None, returning: None, replace_into: false, From 0c3b6c09740389064f28fd4db548da3085034269 Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Fri, 10 Jan 2025 18:17:28 +0100 Subject: [PATCH 679/806] Add support for ClickHouse `FORMAT` on `INSERT` (#1628) --- src/ast/dml.rs | 31 +++++++++++--- src/ast/mod.rs | 8 ++-- src/ast/query.rs | 23 +++++++++++ src/ast/spans.rs | 2 + src/dialect/clickhouse.rs | 12 ++++++ src/dialect/mod.rs | 5 +++ src/keywords.rs | 2 - src/parser/mod.rs | 78 +++++++++++++++++++++++++---------- tests/sqlparser_clickhouse.rs | 20 +++++++++ tests/sqlparser_postgres.rs | 10 ++++- 10 files changed, 155 insertions(+), 36 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index d68a2277e..de555c109 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -32,11 +32,11 @@ use sqlparser_derive::{Visit, VisitMut}; pub use super::ddl::{ColumnDef, TableConstraint}; use super::{ - display_comma_separated, display_separated, Assignment, ClusteredBy, CommentDef, Expr, - FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, - InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, - OrderByExpr, Query, RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, - TableObject, TableWithJoins, Tag, WrappedCollection, + 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, TableEngine, TableObject, TableWithJoins, Tag, WrappedCollection, }; /// CREATE INDEX statement. @@ -497,6 +497,19 @@ pub struct Insert { pub priority: Option, /// Only for mysql pub insert_alias: Option, + /// Settings used for ClickHouse. + /// + /// ClickHouse syntax: `INSERT INTO tbl SETTINGS format_template_resultset = '/some/path/resultset.format'` + /// + /// [ClickHouse `INSERT INTO`](https://clickhouse.com/docs/en/sql-reference/statements/insert-into) + pub settings: Option>, + /// Format for `INSERT` statement when not using standard SQL format. Can be e.g. `CSV`, + /// `JSON`, `JSONAsString`, `LineAsString` and more. + /// + /// ClickHouse syntax: `INSERT INTO tbl FORMAT JSONEachRow {"foo": 1, "bar": 2}, {"foo": 3}` + /// + /// [ClickHouse formats JSON insert](https://clickhouse.com/docs/en/interfaces/formats#json-inserting-data) + pub format_clause: Option, } impl Display for Insert { @@ -545,12 +558,18 @@ impl Display for Insert { write!(f, "({}) ", display_comma_separated(&self.after_columns))?; } + if let Some(settings) = &self.settings { + write!(f, "SETTINGS {} ", display_comma_separated(settings))?; + } + if let Some(source) = &self.source { write!(f, "{source}")?; } else if !self.assignments.is_empty() { write!(f, "SET ")?; write!(f, "{}", display_comma_separated(&self.assignments))?; - } else if self.source.is_none() && self.columns.is_empty() { + } else if let Some(format_clause) = &self.format_clause { + write!(f, "{format_clause}")?; + } else if self.columns.is_empty() { write!(f, "DEFAULT VALUES")?; } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5ab2fc939..1f8df3529 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -61,10 +61,10 @@ 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, Interpolate, - InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, - JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, LateralView, - LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, + 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, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, diff --git a/src/ast/query.rs b/src/ast/query.rs index 2f0663a5f..e7020ae23 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2480,6 +2480,29 @@ impl fmt::Display for FormatClause { } } +/// FORMAT identifier in input context, specific to ClickHouse. +/// +/// [ClickHouse]: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct InputFormatClause { + pub ident: Ident, + pub values: Vec, +} + +impl fmt::Display for InputFormatClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "FORMAT {}", self.ident)?; + + if !self.values.is_empty() { + write!(f, " {}", display_comma_separated(self.values.as_slice()))?; + } + + Ok(()) + } +} + /// FOR XML or FOR JSON clause, specific to MSSQL /// (formats the output of a query as XML or JSON) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8a27c4ac1..19f6074b3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1156,6 +1156,8 @@ impl Spanned for Insert { priority: _, // todo, mysql specific insert_alias: _, // todo, mysql specific assignments, + settings: _, // todo, clickhouse specific + format_clause: _, // todo, clickhouse specific } = self; union_spans( diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 267f766f7..884dfcbcb 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -54,4 +54,16 @@ impl Dialect for ClickHouseDialect { fn supports_insert_table_function(&self) -> bool { true } + + fn supports_insert_format(&self) -> bool { + true + } + + // ClickHouse uses this for some FORMAT expressions in `INSERT` context, e.g. when inserting + // with FORMAT JSONEachRow a raw JSON key-value expression is valid and expected. + // + // [ClickHouse formats](https://clickhouse.com/docs/en/interfaces/formats) + fn supports_dictionary_syntax(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a682e4f63..32b0ed482 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -797,6 +797,11 @@ pub trait Dialect: Debug + Any { fn supports_insert_table_function(&self) -> bool { false } + + /// Does the dialect support insert formats, e.g. `INSERT INTO ... FORMAT ` + fn supports_insert_format(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/keywords.rs b/src/keywords.rs index bd538ec69..8c8860e12 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -949,9 +949,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::PARTITION, // for Clickhouse PREWHERE Keyword::PREWHERE, - // for ClickHouse SELECT * FROM t SETTINGS ... Keyword::SETTINGS, - // for ClickHouse SELECT * FROM t FORMAT... Keyword::FORMAT, // for Snowflake START WITH .. CONNECT BY Keyword::START, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b6e3fd1c4..c17402515 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12033,35 +12033,55 @@ impl<'a> Parser<'a> { let is_mysql = dialect_of!(self is MySqlDialect); - let (columns, partitioned, after_columns, source, assignments) = - if self.parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES]) { - (vec![], None, vec![], None, vec![]) - } else { - let (columns, partitioned, after_columns) = if !self.peek_subquery_start() { - let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?; + let (columns, partitioned, after_columns, source, assignments) = if self + .parse_keywords(&[Keyword::DEFAULT, Keyword::VALUES]) + { + (vec![], None, vec![], None, vec![]) + } else { + let (columns, partitioned, after_columns) = if !self.peek_subquery_start() { + let columns = self.parse_parenthesized_column_list(Optional, is_mysql)?; - let partitioned = self.parse_insert_partition()?; - // Hive allows you to specify columns after partitions as well if you want. - let after_columns = if dialect_of!(self is HiveDialect) { - self.parse_parenthesized_column_list(Optional, false)? - } else { - vec![] - }; - (columns, partitioned, after_columns) + let partitioned = self.parse_insert_partition()?; + // Hive allows you to specify columns after partitions as well if you want. + let after_columns = if dialect_of!(self is HiveDialect) { + self.parse_parenthesized_column_list(Optional, false)? } else { - Default::default() + vec![] }; + (columns, partitioned, after_columns) + } else { + Default::default() + }; - let (source, assignments) = - if self.dialect.supports_insert_set() && self.parse_keyword(Keyword::SET) { - (None, self.parse_comma_separated(Parser::parse_assignment)?) - } else { - (Some(self.parse_query()?), vec![]) - }; + let (source, assignments) = if self.peek_keyword(Keyword::FORMAT) + || self.peek_keyword(Keyword::SETTINGS) + { + (None, vec![]) + } else if self.dialect.supports_insert_set() && self.parse_keyword(Keyword::SET) { + (None, self.parse_comma_separated(Parser::parse_assignment)?) + } else { + (Some(self.parse_query()?), vec![]) + }; + + (columns, partitioned, after_columns, source, assignments) + }; - (columns, partitioned, after_columns, source, assignments) + let (format_clause, settings) = if self.dialect.supports_insert_format() { + // Settings always comes before `FORMAT` for ClickHouse: + // + let settings = self.parse_settings()?; + + let format = if self.parse_keyword(Keyword::FORMAT) { + Some(self.parse_input_format_clause()?) + } else { + None }; + (format, settings) + } else { + Default::default() + }; + let insert_alias = if dialect_of!(self is MySqlDialect | GenericDialect) && self.parse_keyword(Keyword::AS) { @@ -12146,10 +12166,24 @@ impl<'a> Parser<'a> { replace_into, priority, insert_alias, + settings, + format_clause, })) } } + // Parses input format clause used for [ClickHouse]. + // + // + pub fn parse_input_format_clause(&mut self) -> Result { + let ident = self.parse_identifier()?; + let values = self + .maybe_parse(|p| p.parse_comma_separated(|p| p.parse_expr()))? + .unwrap_or_default(); + + Ok(InputFormatClause { ident, values }) + } + /// Returns true if the immediate tokens look like the /// beginning of a subquery. `(SELECT ...` fn peek_subquery_start(&mut self) -> bool { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 4fa657baa..fed4308fc 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1404,6 +1404,26 @@ fn test_query_with_format_clause() { } } +#[test] +fn test_insert_query_with_format_clause() { + let cases = [ + r#"INSERT INTO tbl FORMAT JSONEachRow {"id": 1, "value": "foo"}, {"id": 2, "value": "bar"}"#, + r#"INSERT INTO tbl FORMAT JSONEachRow ["first", "second", "third"]"#, + r#"INSERT INTO tbl FORMAT JSONEachRow [{"first": 1}]"#, + r#"INSERT INTO tbl (foo) FORMAT JSONAsObject {"foo": {"bar": {"x": "y"}, "baz": 1}}"#, + r#"INSERT INTO tbl (foo, bar) FORMAT JSON {"foo": 1, "bar": 2}"#, + r#"INSERT INTO tbl FORMAT CSV col1, col2, col3"#, + r#"INSERT INTO tbl FORMAT LineAsString "I love apple", "I love banana", "I love orange""#, + r#"INSERT INTO tbl (foo) SETTINGS input_format_json_read_bools_as_numbers = true FORMAT JSONEachRow {"id": 1, "value": "foo"}"#, + r#"INSERT INTO tbl SETTINGS format_template_resultset = '/some/path/resultset.format', format_template_row = '/some/path/row.format' FORMAT Template"#, + r#"INSERT INTO tbl SETTINGS input_format_json_read_bools_as_numbers = true FORMAT JSONEachRow {"id": 1, "value": "foo"}"#, + ]; + + for sql in &cases { + clickhouse().verified_stmt(sql); + } +} + #[test] fn parse_create_table_on_commit_and_as_query() { let sql = r#"CREATE LOCAL TEMPORARY TABLE test ON COMMIT PRESERVE ROWS AS SELECT 1"#; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ce31a0628..864fb5eb3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4431,7 +4431,9 @@ fn test_simple_postgres_insert_with_alias() { returning: None, replace_into: false, priority: None, - insert_alias: None + insert_alias: None, + settings: None, + format_clause: None, }) ) } @@ -4502,7 +4504,9 @@ fn test_simple_postgres_insert_with_alias() { returning: None, replace_into: false, priority: None, - insert_alias: None + insert_alias: None, + settings: None, + format_clause: None, }) ) } @@ -4570,6 +4574,8 @@ fn test_simple_insert_with_quoted_alias() { replace_into: false, priority: None, insert_alias: None, + settings: None, + format_clause: None, }) ) } From 3b4dc0f2272b636e741cdd23512e766659670a75 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 11 Jan 2025 10:51:01 +0100 Subject: [PATCH 680/806] MsSQL SET for session params (#1646) --- src/ast/mod.rs | 127 +++++++++++++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/mod.rs | 6 ++ src/dialect/mssql.rs | 5 ++ src/keywords.rs | 5 ++ src/parser/mod.rs | 66 ++++++++++++++++++++ tests/sqlparser_mssql.rs | 61 +++++++++++++++++++ 7 files changed, 271 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1f8df3529..2d79f7d6b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3437,6 +3437,10 @@ pub enum Statement { /// Snowflake `REMOVE` /// See: Remove(FileStagingCommand), + /// MS-SQL session + /// + /// See + SetSessionParam(SetSessionParamKind), } impl fmt::Display for Statement { @@ -5024,6 +5028,7 @@ 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}"), } } } @@ -6441,6 +6446,7 @@ pub enum TransactionIsolationLevel { ReadCommitted, RepeatableRead, Serializable, + Snapshot, } impl fmt::Display for TransactionIsolationLevel { @@ -6451,6 +6457,7 @@ impl fmt::Display for TransactionIsolationLevel { ReadCommitted => "READ COMMITTED", RepeatableRead => "REPEATABLE READ", Serializable => "SERIALIZABLE", + Snapshot => "SNAPSHOT", }) } } @@ -7937,6 +7944,126 @@ impl fmt::Display for TableObject { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SetSessionParamKind { + Generic(SetSessionParamGeneric), + IdentityInsert(SetSessionParamIdentityInsert), + Offsets(SetSessionParamOffsets), + Statistics(SetSessionParamStatistics), +} + +impl fmt::Display for SetSessionParamKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SetSessionParamKind::Generic(x) => write!(f, "{x}"), + SetSessionParamKind::IdentityInsert(x) => write!(f, "{x}"), + SetSessionParamKind::Offsets(x) => write!(f, "{x}"), + SetSessionParamKind::Statistics(x) => write!(f, "{x}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamGeneric { + pub names: Vec, + pub value: String, +} + +impl fmt::Display for SetSessionParamGeneric { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", display_comma_separated(&self.names), self.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 SetSessionParamIdentityInsert { + pub obj: ObjectName, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamIdentityInsert { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "IDENTITY_INSERT {} {}", self.obj, self.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 SetSessionParamOffsets { + pub keywords: Vec, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamOffsets { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "OFFSETS {} {}", + display_comma_separated(&self.keywords), + self.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 SetSessionParamStatistics { + pub topic: SessionParamStatsTopic, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "STATISTICS {} {}", self.topic, self.value) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SessionParamStatsTopic { + IO, + Profile, + Time, + Xml, +} + +impl fmt::Display for SessionParamStatsTopic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SessionParamStatsTopic::IO => write!(f, "IO"), + SessionParamStatsTopic::Profile => write!(f, "PROFILE"), + SessionParamStatsTopic::Time => write!(f, "TIME"), + SessionParamStatsTopic::Xml => write!(f, "XML"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SessionParamValue { + On, + Off, +} + +impl fmt::Display for SessionParamValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SessionParamValue::On => write!(f, "ON"), + SessionParamValue::Off => write!(f, "OFF"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 19f6074b3..183bebf8c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -496,6 +496,7 @@ impl Spanned for Statement { Statement::UNLISTEN { .. } => Span::empty(), Statement::RenameTable { .. } => 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 32b0ed482..c66982d1f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -802,6 +802,12 @@ pub trait Dialect: Debug + Any { fn supports_insert_format(&self) -> bool { false } + + /// Returns true if this dialect supports `SET` statements without an explicit + /// assignment operator such as `=`. For example: `SET SHOWPLAN_XML ON`. + fn supports_set_stmt_without_operator(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index fa77bdc1e..67a648944 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -85,4 +85,9 @@ impl Dialect for MsSqlDialect { fn supports_end_transaction_modifier(&self) -> bool { true } + + /// See: + fn supports_set_stmt_without_operator(&self) -> bool { + true + } } diff --git a/src/keywords.rs b/src/keywords.rs index 8c8860e12..8c8077f51 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -388,6 +388,7 @@ define_keywords!( HOURS, ID, IDENTITY, + IDENTITY_INSERT, IF, IGNORE, ILIKE, @@ -426,6 +427,7 @@ define_keywords!( INTERVAL, INTO, INVOKER, + IO, IS, ISODOW, ISOLATION, @@ -557,7 +559,9 @@ define_keywords!( OCTETS, OCTET_LENGTH, OF, + OFF, OFFSET, + OFFSETS, OLD, OMIT, ON, @@ -623,6 +627,7 @@ define_keywords!( PRIOR, PRIVILEGES, PROCEDURE, + PROFILE, PROGRAM, PROJECTION, PUBLIC, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c17402515..3cf3c585e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10428,11 +10428,75 @@ impl<'a> Parser<'a> { 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()) } } + pub fn parse_set_session_params(&mut self) -> Result { + if self.parse_keyword(Keyword::STATISTICS) { + let topic = match self.parse_one_of_keywords(&[ + Keyword::IO, + Keyword::PROFILE, + Keyword::TIME, + Keyword::XML, + ]) { + Some(Keyword::IO) => SessionParamStatsTopic::IO, + Some(Keyword::PROFILE) => SessionParamStatsTopic::Profile, + Some(Keyword::TIME) => SessionParamStatsTopic::Time, + Some(Keyword::XML) => SessionParamStatsTopic::Xml, + _ => 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 }, + ))) + } 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 }), + )) + } else if self.parse_keyword(Keyword::OFFSETS) { + let keywords = self.parse_comma_separated(|parser| { + let next_token = parser.next_token(); + match &next_token.token { + Token::Word(w) => Ok(w.to_string()), + _ => parser.expected("SQL keyword", next_token), + } + })?; + let value = self.parse_session_param_value()?; + Ok(Statement::SetSessionParam(SetSessionParamKind::Offsets( + SetSessionParamOffsets { keywords, value }, + ))) + } else { + let names = self.parse_comma_separated(|parser| { + let next_token = parser.next_token(); + match next_token.token { + Token::Word(w) => Ok(w.to_string()), + _ => parser.expected("Session param name", next_token), + } + })?; + let value = self.parse_expr()?.to_string(); + Ok(Statement::SetSessionParam(SetSessionParamKind::Generic( + SetSessionParamGeneric { names, value }, + ))) + } + } + + fn parse_session_param_value(&mut self) -> Result { + if self.parse_keyword(Keyword::ON) { + Ok(SessionParamValue::On) + } else if self.parse_keyword(Keyword::OFF) { + Ok(SessionParamValue::Off) + } else { + self.expected("ON or OFF", self.peek_token()) + } + } + pub fn parse_show(&mut self) -> Result { let terse = self.parse_keyword(Keyword::TERSE); let extended = self.parse_keyword(Keyword::EXTENDED); @@ -13004,6 +13068,8 @@ impl<'a> Parser<'a> { TransactionIsolationLevel::RepeatableRead } else if self.parse_keyword(Keyword::SERIALIZABLE) { TransactionIsolationLevel::Serializable + } else if self.parse_keyword(Keyword::SNAPSHOT) { + TransactionIsolationLevel::Snapshot } else { self.expected("isolation level", self.peek_token())? }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ecc874af8..567cd5382 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1679,6 +1679,67 @@ fn parse_true_false_as_identifiers() { ); } +#[test] +fn parse_mssql_set_session_value() { + ms().verified_stmt( + "SET OFFSETS SELECT, FROM, ORDER, TABLE, PROCEDURE, STATEMENT, PARAM, EXECUTE ON", + ); + ms().verified_stmt("SET IDENTITY_INSERT dbo.Tool ON"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL READ COMMITTED"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL SNAPSHOT"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); + ms().verified_stmt("SET STATISTICS IO ON"); + ms().verified_stmt("SET STATISTICS XML ON"); + ms().verified_stmt("SET STATISTICS PROFILE ON"); + ms().verified_stmt("SET STATISTICS TIME ON"); + ms().verified_stmt("SET DATEFIRST 7"); + ms().verified_stmt("SET DATEFIRST @xxx"); + ms().verified_stmt("SET DATEFIRST @@xxx"); + ms().verified_stmt("SET DATEFORMAT dmy"); + ms().verified_stmt("SET DATEFORMAT @datevar"); + ms().verified_stmt("SET DATEFORMAT @@datevar"); + ms().verified_stmt("SET DEADLOCK_PRIORITY 'LOW'"); + ms().verified_stmt("SET DEADLOCK_PRIORITY LOW"); + ms().verified_stmt("SET DEADLOCK_PRIORITY 8"); + ms().verified_stmt("SET DEADLOCK_PRIORITY -8"); + ms().verified_stmt("SET DEADLOCK_PRIORITY @xxx"); + ms().verified_stmt("SET DEADLOCK_PRIORITY @@xxx"); + ms().verified_stmt("SET LOCK_TIMEOUT 1800"); + ms().verified_stmt("SET CONCAT_NULL_YIELDS_NULL ON"); + ms().verified_stmt("SET CURSOR_CLOSE_ON_COMMIT ON"); + ms().verified_stmt("SET FIPS_FLAGGER 'level'"); + ms().verified_stmt("SET FIPS_FLAGGER OFF"); + ms().verified_stmt("SET LANGUAGE Italian"); + ms().verified_stmt("SET QUOTED_IDENTIFIER ON"); + ms().verified_stmt("SET ARITHABORT ON"); + ms().verified_stmt("SET ARITHIGNORE OFF"); + ms().verified_stmt("SET FMTONLY ON"); + ms().verified_stmt("SET NOCOUNT OFF"); + ms().verified_stmt("SET NOEXEC ON"); + ms().verified_stmt("SET NUMERIC_ROUNDABORT ON"); + ms().verified_stmt("SET QUERY_GOVERNOR_COST_LIMIT 11"); + ms().verified_stmt("SET ROWCOUNT 4"); + ms().verified_stmt("SET ROWCOUNT @xxx"); + ms().verified_stmt("SET ROWCOUNT @@xxx"); + ms().verified_stmt("SET TEXTSIZE 11"); + ms().verified_stmt("SET ANSI_DEFAULTS ON"); + ms().verified_stmt("SET ANSI_NULL_DFLT_OFF ON"); + ms().verified_stmt("SET ANSI_NULL_DFLT_ON ON"); + ms().verified_stmt("SET ANSI_NULLS ON"); + ms().verified_stmt("SET ANSI_PADDING ON"); + ms().verified_stmt("SET ANSI_WARNINGS ON"); + ms().verified_stmt("SET FORCEPLAN ON"); + ms().verified_stmt("SET SHOWPLAN_ALL ON"); + ms().verified_stmt("SET SHOWPLAN_TEXT ON"); + ms().verified_stmt("SET SHOWPLAN_XML ON"); + ms().verified_stmt("SET IMPLICIT_TRANSACTIONS ON"); + ms().verified_stmt("SET REMOTE_PROC_TRANSACTIONS ON"); + ms().verified_stmt("SET XACT_ABORT ON"); + ms().verified_stmt("SET ANSI_NULLS, ANSI_PADDING ON"); +} + fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } From c808c4e4fdc0131396b5967e489c2c0bcfac9e5b Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 12 Jan 2025 21:34:09 +0100 Subject: [PATCH 681/806] Correctly look for end delimiter dollar quoted string (#1650) --- src/tokenizer.rs | 176 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 129 insertions(+), 47 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 15b131222..5f9c0f98f 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1566,46 +1566,33 @@ impl<'a> Tokenizer<'a> { if matches!(chars.peek(), Some('$')) && !self.dialect.supports_dollar_placeholder() { chars.next(); - 'searching_for_end: loop { - s.push_str(&peeking_take_while(chars, |ch| ch != '$')); - match chars.peek() { - Some('$') => { - chars.next(); - let mut maybe_s = String::from("$"); - for c in value.chars() { - if let Some(next_char) = chars.next() { - maybe_s.push(next_char); - if next_char != c { - // This doesn't match the dollar quote delimiter so this - // is not the end of the string. - s.push_str(&maybe_s); - continue 'searching_for_end; - } - } else { - return self.tokenizer_error( - chars.location(), - "Unterminated dollar-quoted, expected $", - ); + let mut temp = String::new(); + let end_delimiter = format!("${}$", value); + + loop { + match chars.next() { + Some(ch) => { + temp.push(ch); + + if temp.ends_with(&end_delimiter) { + if let Some(temp) = temp.strip_suffix(&end_delimiter) { + s.push_str(temp); } - } - if chars.peek() == Some(&'$') { - chars.next(); - maybe_s.push('$'); - // maybe_s matches the end delimiter - break 'searching_for_end; - } else { - // This also doesn't match the dollar quote delimiter as there are - // more characters before the second dollar so this is not the end - // of the string. - s.push_str(&maybe_s); - continue 'searching_for_end; + break; } } - _ => { + None => { + if temp.ends_with(&end_delimiter) { + if let Some(temp) = temp.strip_suffix(&end_delimiter) { + s.push_str(temp); + } + break; + } + return self.tokenizer_error( chars.location(), "Unterminated dollar-quoted, expected $", - ) + ); } } } @@ -2569,20 +2556,67 @@ mod tests { #[test] fn tokenize_dollar_quoted_string_tagged() { - let sql = String::from( - "SELECT $tag$dollar '$' quoted strings have $tags like this$ or like this $$$tag$", - ); - let dialect = GenericDialect {}; - let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); - let expected = vec![ - Token::make_keyword("SELECT"), - Token::Whitespace(Whitespace::Space), - Token::DollarQuotedString(DollarQuotedString { - value: "dollar '$' quoted strings have $tags like this$ or like this $$".into(), - tag: Some("tag".into()), - }), + let test_cases = vec![ + ( + String::from("SELECT $tag$dollar '$' quoted strings have $tags like this$ or like this $$$tag$"), + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::DollarQuotedString(DollarQuotedString { + value: "dollar '$' quoted strings have $tags like this$ or like this $$".into(), + tag: Some("tag".into()), + }) + ] + ), + ( + String::from("SELECT $abc$x$ab$abc$"), + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::DollarQuotedString(DollarQuotedString { + value: "x$ab".into(), + tag: Some("abc".into()), + }) + ] + ), + ( + String::from("SELECT $abc$$abc$"), + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::DollarQuotedString(DollarQuotedString { + value: "".into(), + tag: Some("abc".into()), + }) + ] + ), + ( + String::from("0$abc$$abc$1"), + vec![ + Token::Number("0".into(), false), + Token::DollarQuotedString(DollarQuotedString { + value: "".into(), + tag: Some("abc".into()), + }), + Token::Number("1".into(), false), + ] + ), + ( + String::from("$function$abc$q$data$q$$function$"), + vec![ + Token::DollarQuotedString(DollarQuotedString { + value: "abc$q$data$q$".into(), + tag: Some("function".into()), + }), + ] + ), ]; - compare(expected, tokens); + + let dialect = GenericDialect {}; + for (sql, expected) in test_cases { + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + compare(expected, tokens); + } } #[test] @@ -2601,6 +2635,22 @@ mod tests { ); } + #[test] + fn tokenize_dollar_quoted_string_tagged_unterminated_mirror() { + let sql = String::from("SELECT $abc$abc$"); + let dialect = GenericDialect {}; + assert_eq!( + Tokenizer::new(&dialect, &sql).tokenize(), + Err(TokenizerError { + message: "Unterminated dollar-quoted, expected $".into(), + location: Location { + line: 1, + column: 17 + } + }) + ); + } + #[test] fn tokenize_dollar_placeholder() { let sql = String::from("SELECT $$, $$ABC$$, $ABC$, $ABC"); @@ -2625,6 +2675,38 @@ mod tests { ); } + #[test] + fn tokenize_nested_dollar_quoted_strings() { + let sql = String::from("SELECT $tag$dollar $nested$ string$tag$"); + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::DollarQuotedString(DollarQuotedString { + value: "dollar $nested$ string".into(), + tag: Some("tag".into()), + }), + ]; + compare(expected, tokens); + } + + #[test] + fn tokenize_dollar_quoted_string_untagged_empty() { + let sql = String::from("SELECT $$$$"); + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::DollarQuotedString(DollarQuotedString { + value: "".into(), + tag: None, + }), + ]; + compare(expected, tokens); + } + #[test] fn tokenize_dollar_quoted_string_untagged() { let sql = From 65074846978d52358e0acf46b4bcef3f7160e58a Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Tue, 14 Jan 2025 00:57:11 +0800 Subject: [PATCH 682/806] Support single line comments starting with '#' for Hive (#1654) --- src/tokenizer.rs | 5 +++-- tests/sqlparser_common.rs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 5f9c0f98f..39ca84c9f 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -40,13 +40,13 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::ast::DollarQuotedString; use crate::dialect::Dialect; use crate::dialect::{ BigQueryDialect, DuckDbDialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SnowflakeDialect, }; use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX}; +use crate::{ast::DollarQuotedString, dialect::HiveDialect}; /// SQL Token enumeration #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -1372,7 +1372,8 @@ impl<'a> Tokenizer<'a> { } '{' => self.consume_and_return(chars, Token::LBrace), '}' => self.consume_and_return(chars, Token::RBrace), - '#' if dialect_of!(self is SnowflakeDialect | BigQueryDialect | MySqlDialect) => { + '#' if dialect_of!(self is SnowflakeDialect | BigQueryDialect | MySqlDialect | HiveDialect) => + { chars.next(); // consume the '#', starting a snowflake single-line comment let comment = self.tokenize_single_line_comment(chars); Ok(Some(Token::Whitespace(Whitespace::SingleLineComment { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ab69b48ae..b5b12891f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10423,6 +10423,7 @@ fn test_comment_hash_syntax() { Box::new(BigQueryDialect {}), Box::new(SnowflakeDialect {}), Box::new(MySqlDialect {}), + Box::new(HiveDialect {}), ]); let sql = r#" # comment From 36db1766577c3ec63a90d6c057fa76abe4a0397d Mon Sep 17 00:00:00 2001 From: bar sela Date: Tue, 14 Jan 2025 16:38:24 +0200 Subject: [PATCH 683/806] Support trailing commas in `FROM` clause (#1645) Co-authored-by: Ifeanyi Ubah --- src/dialect/mod.rs | 12 +++++++++++ src/dialect/snowflake.rs | 4 ++++ src/keywords.rs | 10 ++++++++++ src/parser/mod.rs | 42 +++++++++++++++++++++++++++++---------- tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++++- 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c66982d1f..64dbc4b1b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -404,6 +404,12 @@ pub trait Dialect: Debug + Any { self.supports_trailing_commas() } + /// Returns true if the dialect supports trailing commas in the `FROM` clause of a `SELECT` statement. + /// /// Example: `SELECT 1 FROM T, U, LIMIT 1` + fn supports_from_trailing_commas(&self) -> bool { + false + } + /// Returns true if the dialect supports double dot notation for object names /// /// Example @@ -775,6 +781,12 @@ pub trait Dialect: Debug + Any { keywords::RESERVED_FOR_IDENTIFIER.contains(&kw) } + // Returns reserved keywords when looking to parse a [TableFactor]. + /// See [Self::supports_from_trailing_commas] + fn get_reserved_keywords_for_table_factor(&self) -> &[Keyword] { + keywords::RESERVED_FOR_TABLE_FACTOR + } + /// 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 55343da18..6b8380d63 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -54,6 +54,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_from_trailing_commas(&self) -> bool { + true + } + // Snowflake supports double-dot notation when the schema name is not specified // In this case the default PUBLIC schema is used // diff --git a/src/keywords.rs b/src/keywords.rs index 8c8077f51..eb9e3ea6f 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -999,6 +999,16 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::END, ]; +// Global list of reserved keywords alloweed 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] = &[ + Keyword::INTO, + Keyword::LIMIT, + Keyword::HAVING, + Keyword::WHERE, +]; + /// Global list of reserved keywords that cannot be parsed as identifiers /// without special handling like quoting. Parser should call `Dialect::is_reserved_for_identifier` /// to allow for each dialect to customize the list. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3cf3c585e..ac764a535 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3940,7 +3940,11 @@ impl<'a> Parser<'a> { let trailing_commas = self.options.trailing_commas | self.dialect.supports_projection_trailing_commas(); - self.parse_comma_separated_with_trailing_commas(|p| p.parse_select_item(), trailing_commas) + self.parse_comma_separated_with_trailing_commas( + |p| p.parse_select_item(), + trailing_commas, + None, + ) } pub fn parse_actions_list(&mut self) -> Result, ParserError> { @@ -3966,20 +3970,32 @@ impl<'a> Parser<'a> { Ok(values) } + /// Parse a list of [TableWithJoins] + fn parse_table_with_joins(&mut self) -> Result, ParserError> { + let trailing_commas = self.dialect.supports_from_trailing_commas(); + + self.parse_comma_separated_with_trailing_commas( + Parser::parse_table_and_joins, + trailing_commas, + Some(self.dialect.get_reserved_keywords_for_table_factor()), + ) + } + /// Parse the comma of a comma-separated syntax element. /// Allows for control over trailing commas /// Returns true if there is a next element - fn is_parse_comma_separated_end_with_trailing_commas(&mut self, trailing_commas: bool) -> bool { + fn is_parse_comma_separated_end_with_trailing_commas( + &mut self, + trailing_commas: bool, + reserved_keywords: Option<&[Keyword]>, + ) -> bool { + let reserved_keywords = reserved_keywords.unwrap_or(keywords::RESERVED_FOR_COLUMN_ALIAS); if !self.consume_token(&Token::Comma) { true } else if trailing_commas { let token = self.peek_token().token; match token { - Token::Word(ref kw) - if keywords::RESERVED_FOR_COLUMN_ALIAS.contains(&kw.keyword) => - { - true - } + Token::Word(ref kw) if reserved_keywords.contains(&kw.keyword) => true, Token::RParen | Token::SemiColon | Token::EOF | Token::RBracket | Token::RBrace => { true } @@ -3993,7 +4009,7 @@ impl<'a> Parser<'a> { /// Parse the comma of a comma-separated syntax element. /// Returns true if there is a next element fn is_parse_comma_separated_end(&mut self) -> bool { - self.is_parse_comma_separated_end_with_trailing_commas(self.options.trailing_commas) + self.is_parse_comma_separated_end_with_trailing_commas(self.options.trailing_commas, None) } /// Parse a comma-separated list of 1+ items accepted by `F` @@ -4001,7 +4017,7 @@ impl<'a> Parser<'a> { where F: FnMut(&mut Parser<'a>) -> Result, { - self.parse_comma_separated_with_trailing_commas(f, self.options.trailing_commas) + self.parse_comma_separated_with_trailing_commas(f, self.options.trailing_commas, None) } /// Parse a comma-separated list of 1+ items accepted by `F` @@ -4010,6 +4026,7 @@ impl<'a> Parser<'a> { &mut self, mut f: F, trailing_commas: bool, + reserved_keywords: Option<&[Keyword]>, ) -> Result, ParserError> where F: FnMut(&mut Parser<'a>) -> Result, @@ -4017,7 +4034,10 @@ impl<'a> Parser<'a> { let mut values = vec![]; loop { values.push(f(self)?); - if self.is_parse_comma_separated_end_with_trailing_commas(trailing_commas) { + if self.is_parse_comma_separated_end_with_trailing_commas( + trailing_commas, + reserved_keywords, + ) { break; } } @@ -10073,7 +10093,7 @@ impl<'a> Parser<'a> { // or `from`. let from = if self.parse_keyword(Keyword::FROM) { - self.parse_comma_separated(Parser::parse_table_and_joins)? + self.parse_table_with_joins()? } else { vec![] }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b5b12891f..07a30bc08 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12957,8 +12957,38 @@ fn parse_update_from_before_select() { parse_sql_statements(query).unwrap_err() ); } - #[test] fn parse_overlaps() { verified_stmt("SELECT (DATE '2016-01-10', DATE '2016-02-01') OVERLAPS (DATE '2016-01-20', DATE '2016-02-10')"); } + +#[test] +fn test_trailing_commas_in_from() { + let dialects = all_dialects_where(|d| d.supports_from_trailing_commas()); + dialects.verified_only_select_with_canonical("SELECT 1, 2 FROM t,", "SELECT 1, 2 FROM t"); + + dialects + .verified_only_select_with_canonical("SELECT 1, 2 FROM t1, t2,", "SELECT 1, 2 FROM t1, t2"); + + let sql = "SELECT a, FROM b, LIMIT 1"; + let _ = dialects.parse_sql_statements(sql).unwrap(); + + let sql = "INSERT INTO a SELECT b FROM c,"; + let _ = dialects.parse_sql_statements(sql).unwrap(); + + let sql = "SELECT a FROM b, HAVING COUNT(*) > 1"; + let _ = dialects.parse_sql_statements(sql).unwrap(); + + let sql = "SELECT a FROM b, WHERE c = 1"; + let _ = dialects.parse_sql_statements(sql).unwrap(); + + // nasted + let sql = "SELECT 1, 2 FROM (SELECT * FROM t,),"; + let _ = dialects.parse_sql_statements(sql).unwrap(); + + // multiple_subqueries + dialects.verified_only_select_with_canonical( + "SELECT 1, 2 FROM (SELECT * FROM t1), (SELECT * FROM t2),", + "SELECT 1, 2 FROM (SELECT * FROM t1), (SELECT * FROM t2)", + ); +} From 9105cae261bb29d4235b47e86c99c11909a4a3fb Mon Sep 17 00:00:00 2001 From: Martin Abelson Sahlen Date: Thu, 16 Jan 2025 10:27:26 +0200 Subject: [PATCH 684/806] Allow empty options for BigQuery (#1657) Co-authored-by: Martin Abelson Sahlen --- src/parser/mod.rs | 2 +- tests/sqlparser_bigquery.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ac764a535..861a392d4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7341,7 +7341,7 @@ impl<'a> Parser<'a> { pub fn parse_options(&mut self, keyword: Keyword) -> Result, ParserError> { if self.parse_keyword(keyword) { self.expect_token(&Token::LParen)?; - let options = self.parse_comma_separated(Parser::parse_sql_option)?; + let options = self.parse_comma_separated0(Parser::parse_sql_option, Token::RParen)?; self.expect_token(&Token::RParen)?; Ok(options) } else { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 9dfabc014..a173a6cc9 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -473,6 +473,12 @@ fn parse_create_table_with_options() { r#"description = "table option description")"# ); bigquery().verified_stmt(sql); + + let sql = "CREATE TABLE foo (x INT64) OPTIONS()"; + bigquery().verified_stmt(sql); + + let sql = "CREATE TABLE db.schema.test (x INT64 OPTIONS(description = 'An optional INTEGER field')) OPTIONS()"; + bigquery().verified_stmt(sql); } #[test] From 474150006fc8dd299bfa212321206263de0f4290 Mon Sep 17 00:00:00 2001 From: AvivDavid-Satori <107786696+AvivDavid-Satori@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:09:53 +0200 Subject: [PATCH 685/806] Add support for parsing RAISERROR (#1656) --- src/ast/mod.rs | 50 ++++++++++++++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 3 +++ src/parser/mod.rs | 41 ++++++++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 33 ++++++++++++++++++++++++++ 5 files changed, 128 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2d79f7d6b..e6499e14a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3441,6 +3441,38 @@ pub enum Statement { /// /// See SetSessionParam(SetSessionParamKind), + /// RaiseError (MSSQL) + /// RAISERROR ( { msg_id | msg_str | @local_variable } + /// { , severity , state } + /// [ , argument [ , ...n ] ] ) + /// [ WITH option [ , ...n ] ] + /// See + RaisError { + message: Box, + severity: Box, + state: Box, + arguments: Vec, + options: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RaisErrorOption { + Log, + NoWait, + SetError, +} + +impl fmt::Display for RaisErrorOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RaisErrorOption::Log => write!(f, "LOG"), + RaisErrorOption::NoWait => write!(f, "NOWAIT"), + RaisErrorOption::SetError => write!(f, "SETERROR"), + } + } } impl fmt::Display for Statement { @@ -5026,6 +5058,24 @@ impl fmt::Display for Statement { Statement::RenameTable(rename_tables) => { write!(f, "RENAME TABLE {}", display_comma_separated(rename_tables)) } + Statement::RaisError { + message, + severity, + state, + arguments, + options, + } => { + write!(f, "RAISERROR({message}, {severity}, {state}")?; + if !arguments.is_empty() { + write!(f, ", {}", display_comma_separated(arguments))?; + } + write!(f, ")")?; + if !options.is_empty() { + write!(f, " WITH {}", display_comma_separated(options))?; + } + Ok(()) + } + Statement::List(command) => write!(f, "LIST {command}"), Statement::Remove(command) => write!(f, "REMOVE {command}"), Statement::SetSessionParam(kind) => write!(f, "SET {kind}"), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 183bebf8c..6f89cd0db 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -495,6 +495,7 @@ impl Spanned for Statement { Statement::LoadData { .. } => Span::empty(), Statement::UNLISTEN { .. } => Span::empty(), Statement::RenameTable { .. } => Span::empty(), + Statement::RaisError { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), Statement::SetSessionParam { .. } => Span::empty(), } diff --git a/src/keywords.rs b/src/keywords.rs index eb9e3ea6f..6b09a877a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -468,6 +468,7 @@ define_keywords!( LOCATION, LOCK, LOCKED, + LOG, LOGIN, LOGS, LONGBLOB, @@ -636,6 +637,7 @@ define_keywords!( QUARTER, QUERY, QUOTE, + RAISERROR, RANGE, RANK, RAW, @@ -728,6 +730,7 @@ define_keywords!( SESSION, SESSION_USER, SET, + SETERROR, SETS, SETTINGS, SHARE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 861a392d4..f443b6e42 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -579,6 +579,7 @@ impl<'a> Parser<'a> { Keyword::SAVEPOINT => self.parse_savepoint(), Keyword::RELEASE => self.parse_release(), Keyword::COMMIT => self.parse_commit(), + Keyword::RAISERROR => Ok(self.parse_raiserror()?), Keyword::ROLLBACK => self.parse_rollback(), Keyword::ASSERT => self.parse_assert(), // `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific @@ -13150,6 +13151,46 @@ impl<'a> Parser<'a> { } } + /// Parse a 'RAISERROR' statement + pub fn parse_raiserror(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let message = Box::new(self.parse_expr()?); + self.expect_token(&Token::Comma)?; + let severity = Box::new(self.parse_expr()?); + self.expect_token(&Token::Comma)?; + let state = Box::new(self.parse_expr()?); + let arguments = if self.consume_token(&Token::Comma) { + self.parse_comma_separated(Parser::parse_expr)? + } else { + vec![] + }; + self.expect_token(&Token::RParen)?; + let options = if self.parse_keyword(Keyword::WITH) { + self.parse_comma_separated(Parser::parse_raiserror_option)? + } else { + vec![] + }; + Ok(Statement::RaisError { + message, + severity, + state, + arguments, + options, + }) + } + + pub fn parse_raiserror_option(&mut self) -> Result { + match self.expect_one_of_keywords(&[Keyword::LOG, Keyword::NOWAIT, Keyword::SETERROR])? { + Keyword::LOG => Ok(RaisErrorOption::Log), + Keyword::NOWAIT => Ok(RaisErrorOption::NoWait), + Keyword::SETERROR => Ok(RaisErrorOption::SetError), + _ => self.expected( + "LOG, NOWAIT OR SETERROR raiserror option", + self.peek_token(), + ), + } + } + pub fn parse_deallocate(&mut self) -> Result { let prepare = self.parse_keyword(Keyword::PREPARE); let name = self.parse_identifier()?; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 567cd5382..a0ac8a4d8 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1250,6 +1250,39 @@ fn parse_mssql_declare() { ); } +#[test] +fn test_parse_raiserror() { + let sql = r#"RAISERROR('This is a test', 16, 1)"#; + let s = ms().verified_stmt(sql); + assert_eq!( + s, + Statement::RaisError { + message: Box::new(Expr::Value(Value::SingleQuotedString( + "This is a test".to_string() + ))), + severity: Box::new(Expr::Value(Value::Number("16".parse().unwrap(), false))), + state: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + arguments: vec![], + options: vec![], + } + ); + + let sql = r#"RAISERROR('This is a test', 16, 1) WITH NOWAIT"#; + let _ = ms().verified_stmt(sql); + + let sql = r#"RAISERROR('This is a test', 16, 1, 'ARG') WITH SETERROR, LOG"#; + let _ = ms().verified_stmt(sql); + + let sql = r#"RAISERROR(N'This is message %s %d.', 10, 1, N'number', 5)"#; + let _ = ms().verified_stmt(sql); + + let sql = r#"RAISERROR(N'<<%*.*s>>', 10, 1, 7, 3, N'abcde')"#; + let _ = ms().verified_stmt(sql); + + let sql = r#"RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState)"#; + let _ = ms().verified_stmt(sql); +} + #[test] fn parse_use() { let valid_object_names = [ From b4b5576dd4bcf5b386665f9010b6c3e94465eeaf Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:50:30 +0200 Subject: [PATCH 686/806] Add support for Snowflake column aliases that use SQL keywords (#1632) --- src/dialect/mod.rs | 14 ++++ src/dialect/snowflake.rs | 45 ++++++++++++ src/parser/mod.rs | 131 ++++++++++++++++++++--------------- tests/sqlparser_snowflake.rs | 29 ++++++++ 4 files changed, 163 insertions(+), 56 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 64dbc4b1b..c69253b76 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -820,6 +820,20 @@ pub trait Dialect: Debug + Any { fn supports_set_stmt_without_operator(&self) -> bool { false } + + /// Returns true if the specified keyword should be parsed as a select item alias. + /// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided + /// to enable looking ahead if needed. + fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { + explicit || !keywords::RESERVED_FOR_COLUMN_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) + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 6b8380d63..f6e9c9eba 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -251,6 +251,51 @@ impl Dialect for SnowflakeDialect { fn supports_partiql(&self) -> bool { true } + + fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + explicit + || 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` + Keyword::EXCEPT + // e.g. `SELECT 1 LIMIT 5` + | Keyword::LIMIT + // e.g. `SELECT 1 OFFSET 5 ROWS` + | Keyword::OFFSET + // e.g. `INSERT INTO t SELECT 1 RETURNING *` + | Keyword::RETURNING if !matches!(parser.peek_token_ref().token, Token::Comma | 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 meanins, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias + Keyword::FETCH + if parser.peek_keyword(Keyword::FIRST) || parser.peek_keyword(Keyword::NEXT) => + { + false + } + + // Reserved keywords by the Snowflake dialect, which seem to be less strictive + // than what is listed in `keywords::RESERVED_FOR_COLUMN_ALIAS`. The following + // keywords were tested with the this statement: `SELECT 1 `. + Keyword::FROM + | Keyword::GROUP + | Keyword::HAVING + | Keyword::INTERSECT + | Keyword::INTO + | Keyword::MINUS + | Keyword::ORDER + | Keyword::SELECT + | Keyword::UNION + | Keyword::WHERE + | Keyword::WITH => false, + + // Any other word is considered an alias + _ => 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 f443b6e42..f34a5d742 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8838,38 +8838,76 @@ impl<'a> Parser<'a> { Ok(IdentWithAlias { ident, alias }) } - /// Parse `AS identifier` (or simply `identifier` if it's not a reserved keyword) - /// Some examples with aliases: `SELECT 1 foo`, `SELECT COUNT(*) AS cnt`, - /// `SELECT ... FROM t1 foo, t2 bar`, `SELECT ... FROM (...) AS bar` + /// 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 { + parser.dialect.is_select_item_alias(explicit, kw, parser) + } + self.parse_optional_alias_inner(None, validator) + } + + /// Optionally parses an alias for a table like in `... FROM generate_series(1, 10) AS t (col)`. + /// In this case, the alias is allowed to optionally name the columns in the table, in + /// addition to the table itself. + pub fn maybe_parse_table_alias(&mut self) -> Result, ParserError> { + fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + parser.dialect.is_table_factor_alias(explicit, kw, parser) + } + match self.parse_optional_alias_inner(None, validator)? { + Some(name) => { + let columns = self.parse_table_alias_column_defs()?; + Ok(Some(TableAlias { name, columns })) + } + None => Ok(None), + } + } + + /// Wrapper for parse_optional_alias_inner, left for backwards-compatibility + /// but new flows should use the context-specific methods such as `maybe_parse_select_item_alias` + /// and `maybe_parse_table_alias`. pub fn parse_optional_alias( &mut self, reserved_kwds: &[Keyword], ) -> Result, ParserError> { + fn validator(_explicit: bool, _kw: &Keyword, _parser: &mut Parser) -> bool { + false + } + self.parse_optional_alias_inner(Some(reserved_kwds), validator) + } + + /// Parses an optional alias after a SQL element such as a select list item + /// or a table name. + /// + /// This method accepts an optional list of reserved keywords or a function + /// to call to validate if a keyword should be parsed as an alias, to allow + /// callers to customize the parsing logic based on their context. + fn parse_optional_alias_inner( + &mut self, + reserved_kwds: Option<&[Keyword]>, + validator: F, + ) -> Result, ParserError> + where + F: Fn(bool, &Keyword, &mut Parser) -> bool, + { let after_as = self.parse_keyword(Keyword::AS); + let next_token = self.next_token(); match next_token.token { - // Accept any identifier after `AS` (though many dialects have restrictions on - // keywords that may appear here). If there's no `AS`: don't parse keywords, - // which may start a construct allowed in this position, to be parsed as aliases. - // (For example, in `FROM t1 JOIN` the `JOIN` will always be parsed as a keyword, - // not an alias.) - Token::Word(w) if after_as || !reserved_kwds.contains(&w.keyword) => { + // By default, if a word is located after the `AS` keyword we consider it an alias + // as long as it's not reserved. + Token::Word(w) + if after_as || reserved_kwds.is_some_and(|x| !x.contains(&w.keyword)) => + { Ok(Some(w.into_ident(next_token.span))) } - // MSSQL supports single-quoted strings as aliases for columns - // We accept them as table aliases too, although MSSQL does not. - // - // Note, that this conflicts with an obscure rule from the SQL - // standard, which we don't implement: - // https://crate.io/docs/sql-99/en/latest/chapters/07.html#character-string-literal-s - // "[Obscure Rule] SQL allows you to break a long up into two or more smaller s, split by a that includes a newline - // character. When it sees such a , your DBMS will - // ignore the and treat the multiple strings as - // a single ." + // This pattern allows for customizing the acceptance of words as aliases based on the caller's + // context, such as to what SQL element this word is a potential alias of (select item alias, table name + // alias, etc.) or dialect-specific logic that goes beyond a simple list of reserved keywords. + Token::Word(w) if validator(after_as, &w.keyword, self) => { + Ok(Some(w.into_ident(next_token.span))) + } + // For backwards-compatibility, we accept quoted strings as aliases regardless of the context. Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))), - // Support for MySql dialect double-quoted string, `AS "HOUR"` for example Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))), _ => { if after_as { @@ -8881,23 +8919,6 @@ impl<'a> Parser<'a> { } } - /// Parse `AS identifier` when the AS is describing a table-valued object, - /// like in `... FROM generate_series(1, 10) AS t (col)`. In this case - /// the alias is allowed to optionally name the columns in the table, in - /// addition to the table itself. - pub fn parse_optional_table_alias( - &mut self, - reserved_kwds: &[Keyword], - ) -> Result, ParserError> { - match self.parse_optional_alias(reserved_kwds)? { - Some(name) => { - let columns = self.parse_table_alias_column_defs()?; - Ok(Some(TableAlias { name, columns })) - } - None => Ok(None), - } - } - pub fn parse_optional_group_by(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { let expressions = if self.parse_keyword(Keyword::ALL) { @@ -10899,7 +10920,7 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(false)?; self.expect_token(&Token::LParen)?; let args = self.parse_optional_args()?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Function { lateral: true, name, @@ -10912,7 +10933,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let expr = self.parse_expr()?; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::TableFunction { expr, alias }) } else if self.consume_token(&Token::LParen) { // A left paren introduces either a derived table (i.e., a subquery) @@ -10961,7 +10982,7 @@ impl<'a> Parser<'a> { #[allow(clippy::if_same_then_else)] if !table_and_joins.joins.is_empty() { self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::NestedJoin { table_with_joins: Box::new(table_and_joins), alias, @@ -10974,7 +10995,7 @@ impl<'a> Parser<'a> { // (B): `table_and_joins` (what we found inside the parentheses) // is a nested join `(foo JOIN bar)`, not followed by other joins. self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::NestedJoin { table_with_joins: Box::new(table_and_joins), alias, @@ -10988,9 +11009,7 @@ impl<'a> Parser<'a> { // [AS alias])`) as well. self.expect_token(&Token::RParen)?; - if let Some(outer_alias) = - self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)? - { + if let Some(outer_alias) = self.maybe_parse_table_alias()? { // Snowflake also allows specifying an alias *after* parens // e.g. `FROM (mytable) AS alias` match &mut table_and_joins.relation { @@ -11043,7 +11062,7 @@ impl<'a> Parser<'a> { // SELECT * FROM VALUES (1, 'a'), (2, 'b') AS t (col1, col2) // where there are no parentheses around the VALUES clause. let values = SetExpr::Values(self.parse_values(false)?); - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Derived { lateral: false, subquery: Box::new(Query { @@ -11069,7 +11088,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); - let alias = match self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS) { + let alias = match self.maybe_parse_table_alias() { Ok(Some(alias)) => Some(alias), Ok(None) => None, Err(e) => return Err(e), @@ -11106,7 +11125,7 @@ impl<'a> Parser<'a> { let columns = self.parse_comma_separated(Parser::parse_json_table_column_def)?; self.expect_token(&Token::RParen)?; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::JsonTable { json_expr, json_path, @@ -11151,7 +11170,7 @@ impl<'a> Parser<'a> { } } - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; // MSSQL-specific table hints: let mut with_hints = vec![]; @@ -11329,7 +11348,7 @@ impl<'a> Parser<'a> { } else { Vec::new() }; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::OpenJsonTable { json_expr, json_path, @@ -11428,7 +11447,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::MatchRecognize { table: Box::new(table), @@ -11672,7 +11691,7 @@ impl<'a> Parser<'a> { ) -> Result { let subquery = self.parse_query()?; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Derived { lateral: match lateral { Lateral => true, @@ -11766,7 +11785,7 @@ impl<'a> Parser<'a> { }; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Pivot { table: Box::new(table), aggregate_functions, @@ -11788,7 +11807,7 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::IN)?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; self.expect_token(&Token::RParen)?; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Unpivot { table: Box::new(table), value, @@ -12614,7 +12633,7 @@ impl<'a> Parser<'a> { }) } expr => self - .parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) + .maybe_parse_select_item_alias() .map(|alias| match alias { Some(alias) => SelectItem::ExprWithAlias { expr, alias }, None => SelectItem::UnnamedExpr(expr), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 112aa5264..fe6439b36 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3022,3 +3022,32 @@ fn parse_ls_and_rm() { snowflake().verified_stmt(r#"LIST @"STAGE_WITH_QUOTES""#); } + +#[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"]; + for kw in unreserved_kws { + snowflake() + .one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {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()); + } +} From 3eeb9160eadbe01e10c011cf144ef0fbd3f578d6 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Fri, 17 Jan 2025 08:13:12 +0100 Subject: [PATCH 687/806] fix parsing of `INSERT INTO ... SELECT ... RETURNING ` (#1661) --- src/keywords.rs | 1 + tests/sqlparser_common.rs | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/keywords.rs b/src/keywords.rs index 6b09a877a..cc56329f4 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -946,6 +946,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::GLOBAL, Keyword::ANTI, Keyword::SEMI, + Keyword::RETURNING, // for MSSQL-specific OUTER APPLY (seems reserved in most dialects) Keyword::OUTER, Keyword::SET, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 07a30bc08..0c44fb849 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -265,6 +265,27 @@ fn parse_insert_select_returning() { } } +#[test] +fn parse_insert_select_from_returning() { + let sql = "INSERT INTO table1 SELECT * FROM table2 RETURNING id"; + match verified_stmt(sql) { + Statement::Insert(Insert { + table: TableObject::TableName(table_name), + source: Some(source), + returning: Some(returning), + .. + }) => { + assert_eq!("table1", table_name.to_string()); + assert!(matches!(*source.body, SetExpr::Select(_))); + assert_eq!( + returning, + vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("id"))),] + ); + } + bad_stmt => unreachable!("Expected valid insert, got {:?}", bad_stmt), + } +} + #[test] fn parse_returning_as_column_alias() { verified_stmt("SELECT 1 AS RETURNING"); From e9498d538aea161283767acc441d70d612dddc00 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Fri, 17 Jan 2025 13:59:47 +0400 Subject: [PATCH 688/806] Add support for `IS [NOT] [form] NORMALIZED` (#1655) Co-authored-by: Alexander Beedie --- src/ast/mod.rs | 30 ++++++++++++-- src/ast/query.rs | 4 +- src/ast/spans.rs | 7 +++- src/ast/value.rs | 29 ++++++++++++++ src/keywords.rs | 5 +++ src/parser/mod.rs | 41 ++++++++++++++++--- tests/sqlparser_common.rs | 84 +++++++++++++++++++++++++++++++++++++-- tests/sqlparser_mysql.rs | 2 +- 8 files changed, 185 insertions(+), 17 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e6499e14a..7992596e3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -83,7 +83,7 @@ pub use self::trigger::{ pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, - TrimWhereField, Value, + NormalizationForm, TrimWhereField, Value, }; use crate::ast::helpers::stmt_data_loading::{ @@ -653,6 +653,12 @@ pub enum Expr { IsDistinctFrom(Box, Box), /// `IS NOT DISTINCT FROM` operator IsNotDistinctFrom(Box, Box), + /// ` IS [ NOT ] [ form ] NORMALIZED` + IsNormalized { + expr: Box, + form: Option, + negated: bool, + }, /// `[ NOT ] IN (val1, val2, ...)` InList { expr: Box, @@ -1118,7 +1124,7 @@ impl fmt::Display for LambdaFunction { /// `OneOrManyWithParens` implements `Deref` and `IntoIterator`, /// so you can call slice methods on it and iterate over items /// # Examples -/// Acessing as a slice: +/// Accessing as a slice: /// ``` /// # use sqlparser::ast::OneOrManyWithParens; /// let one = OneOrManyWithParens::One("a"); @@ -1419,6 +1425,24 @@ impl fmt::Display for Expr { if *regexp { "REGEXP" } else { "RLIKE" }, pattern ), + Expr::IsNormalized { + expr, + form, + negated, + } => { + let not_ = if *negated { "NOT " } else { "" }; + if form.is_none() { + write!(f, "{} IS {}NORMALIZED", expr, not_) + } else { + write!( + f, + "{} IS {}{} NORMALIZED", + expr, + not_, + form.as_ref().unwrap() + ) + } + } Expr::SimilarTo { negated, expr, @@ -7799,7 +7823,7 @@ where /// ```sql /// EXPLAIN (ANALYZE, VERBOSE TRUE, FORMAT TEXT) SELECT * FROM my_table; /// -/// VACCUM (VERBOSE, ANALYZE ON, PARALLEL 10) my_table; +/// VACUUM (VERBOSE, ANALYZE ON, PARALLEL 10) my_table; /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/query.rs b/src/ast/query.rs index e7020ae23..9bcdc2e74 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2821,10 +2821,10 @@ impl fmt::Display for ValueTableMode { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum UpdateTableFromKind { - /// Update Statment where the 'FROM' clause is before the 'SET' keyword (Supported by Snowflake) + /// Update Statement where the 'FROM' clause is before the 'SET' keyword (Supported by Snowflake) /// For Example: `UPDATE FROM t1 SET t1.name='aaa'` BeforeSet(TableWithJoins), - /// Update Statment where the 'FROM' clause is after the 'SET' keyword (Which is the standard way) + /// Update Statement where the 'FROM' clause is after the 'SET' keyword (Which is the standard way) /// For Example: `UPDATE SET t1.name='aaa' FROM t1` AfterSet(TableWithJoins), } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 6f89cd0db..2a5a75b49 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1325,6 +1325,12 @@ impl Spanned for Expr { escape_char: _, any: _, } => expr.span().union(&pattern.span()), + Expr::RLike { .. } => Span::empty(), + Expr::IsNormalized { + expr, + form: _, + negated: _, + } => expr.span(), Expr::SimilarTo { negated: _, expr, @@ -1360,7 +1366,6 @@ impl Spanned for Expr { Expr::Array(array) => array.span(), Expr::MatchAgainst { .. } => Span::empty(), Expr::JsonAccess { value, path } => value.span().union(&path.span()), - Expr::RLike { .. } => Span::empty(), Expr::AnyOp { left, compare_op: _, diff --git a/src/ast/value.rs b/src/ast/value.rs index 45cc06a07..1b16646be 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -270,6 +270,35 @@ impl fmt::Display for DateTimeField { } } +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// The Unicode Standard defines four normalization forms, which are intended to eliminate +/// certain distinctions between visually or functionally identical characters. +/// +/// See [Unicode Normalization Forms](https://unicode.org/reports/tr15/) for details. +pub enum NormalizationForm { + /// Canonical Decomposition, followed by Canonical Composition. + NFC, + /// Canonical Decomposition. + NFD, + /// Compatibility Decomposition, followed by Canonical Composition. + NFKC, + /// Compatibility Decomposition. + NFKD, +} + +impl fmt::Display for NormalizationForm { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NormalizationForm::NFC => write!(f, "NFC"), + NormalizationForm::NFD => write!(f, "NFD"), + NormalizationForm::NFKC => write!(f, "NFKC"), + NormalizationForm::NFKD => write!(f, "NFKD"), + } + } +} + pub struct EscapeQuotedString<'a> { string: &'a str, quote: char, diff --git a/src/keywords.rs b/src/keywords.rs index cc56329f4..0d1bab9e5 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -530,6 +530,10 @@ define_keywords!( NESTED, NEW, NEXT, + NFC, + NFD, + NFKC, + NFKD, NO, NOBYPASSRLS, NOCREATEDB, @@ -540,6 +544,7 @@ define_keywords!( NOORDER, NOREPLICATION, NORMALIZE, + NORMALIZED, NOSCAN, NOSUPERUSER, NOT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f34a5d742..5cacfda98 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3184,9 +3184,11 @@ impl<'a> Parser<'a> { { let expr2 = self.parse_expr()?; Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2))) + } else if let Ok(is_normalized) = self.parse_unicode_is_normalized(expr) { + Ok(is_normalized) } else { self.expected( - "[NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS", + "[NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS", self.peek_token(), ) } @@ -3851,7 +3853,7 @@ impl<'a> Parser<'a> { /// If the current token is the `expected` keyword, consume the token. /// Otherwise, return an error. /// - // todo deprecate infavor of expected_keyword_is + // todo deprecate in favor of expected_keyword_is pub fn expect_keyword(&mut self, expected: Keyword) -> Result { if self.parse_keyword(expected) { Ok(self.get_current_token().clone()) @@ -8453,6 +8455,33 @@ impl<'a> Parser<'a> { } } + /// Parse a literal unicode normalization clause + pub fn parse_unicode_is_normalized(&mut self, expr: Expr) -> Result { + let neg = self.parse_keyword(Keyword::NOT); + let normalized_form = self.maybe_parse(|parser| { + match parser.parse_one_of_keywords(&[ + Keyword::NFC, + Keyword::NFD, + Keyword::NFKC, + Keyword::NFKD, + ]) { + Some(Keyword::NFC) => Ok(NormalizationForm::NFC), + Some(Keyword::NFD) => Ok(NormalizationForm::NFD), + Some(Keyword::NFKC) => Ok(NormalizationForm::NFKC), + Some(Keyword::NFKD) => Ok(NormalizationForm::NFKD), + _ => parser.expected("unicode normalization form", parser.peek_token()), + } + })?; + if self.parse_keyword(Keyword::NORMALIZED) { + return Ok(Expr::IsNormalized { + expr: Box::new(expr), + form: normalized_form, + negated: neg, + }); + } + self.expected("unicode normalization form", self.peek_token()) + } + pub fn parse_enum_values(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let values = self.parse_comma_separated(|parser| { @@ -8979,7 +9008,7 @@ impl<'a> Parser<'a> { } } - /// Parse a table object for insetion + /// 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 { if self.dialect.supports_insert_table_function() && self.parse_keyword(Keyword::FUNCTION) { @@ -11887,7 +11916,7 @@ impl<'a> Parser<'a> { } else { let mut name = self.parse_grantee_name()?; if self.consume_token(&Token::Colon) { - // Redshift supports namespace prefix for extenrnal users and groups: + // Redshift supports namespace prefix for external users and groups: // : or : // https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-native-idp.html let ident = self.parse_identifier()?; @@ -12883,7 +12912,7 @@ impl<'a> Parser<'a> { Ok(WithFill { from, to, step }) } - // Parse a set of comma seperated INTERPOLATE expressions (ClickHouse dialect) + // Parse a set of comma separated INTERPOLATE expressions (ClickHouse dialect) // that follow the INTERPOLATE keyword in an ORDER BY clause with the WITH FILL modifier pub fn parse_interpolations(&mut self) -> Result, ParserError> { if !self.parse_keyword(Keyword::INTERPOLATE) { @@ -14432,7 +14461,7 @@ mod tests { assert_eq!( ast, Err(ParserError::ParserError( - "Expected: [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: a at Line: 1, Column: 16" + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: a at Line: 1, Column: 16" .to_string() )) ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0c44fb849..49588a581 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4600,7 +4600,7 @@ fn run_explain_analyze( expected_verbose: bool, expected_analyze: bool, expected_format: Option, - exepcted_options: Option>, + expected_options: Option>, ) { match dialect.verified_stmt(query) { Statement::Explain { @@ -4616,7 +4616,7 @@ fn run_explain_analyze( assert_eq!(verbose, expected_verbose); assert_eq!(analyze, expected_analyze); assert_eq!(format, expected_format); - assert_eq!(options, exepcted_options); + assert_eq!(options, expected_options); assert!(!query_plan); assert!(!estimate); assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); @@ -9317,6 +9317,46 @@ fn parse_is_boolean() { verified_expr(sql) ); + let sql = "a IS NORMALIZED"; + assert_eq!( + IsNormalized { + expr: Box::new(Identifier(Ident::new("a"))), + form: None, + negated: false, + }, + verified_expr(sql) + ); + + let sql = "a IS NOT NORMALIZED"; + assert_eq!( + IsNormalized { + expr: Box::new(Identifier(Ident::new("a"))), + form: None, + negated: true, + }, + verified_expr(sql) + ); + + let sql = "a IS NFKC NORMALIZED"; + assert_eq!( + IsNormalized { + expr: Box::new(Identifier(Ident::new("a"))), + form: Some(NormalizationForm::NFKC), + negated: false, + }, + verified_expr(sql) + ); + + let sql = "a IS NOT NFKD NORMALIZED"; + assert_eq!( + IsNormalized { + expr: Box::new(Identifier(Ident::new("a"))), + form: Some(NormalizationForm::NFKD), + negated: true, + }, + verified_expr(sql) + ); + let sql = "a IS UNKNOWN"; assert_eq!( IsUnknown(Box::new(Identifier(Ident::new("a")))), @@ -9335,6 +9375,12 @@ fn parse_is_boolean() { verified_stmt("SELECT f FROM foo WHERE field IS FALSE"); verified_stmt("SELECT f FROM foo WHERE field IS NOT FALSE"); + verified_stmt("SELECT f FROM foo WHERE field IS NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS NFC NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS NFD NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS NOT NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS NOT NFKC NORMALIZED"); + verified_stmt("SELECT f FROM foo WHERE field IS UNKNOWN"); verified_stmt("SELECT f FROM foo WHERE field IS NOT UNKNOWN"); @@ -9342,7 +9388,37 @@ fn parse_is_boolean() { let res = parse_sql_statements(sql); assert_eq!( ParserError::ParserError( - "Expected: [NOT] NULL or TRUE|FALSE or [NOT] DISTINCT FROM after IS, found: 0" + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: 0" + .to_string() + ), + res.unwrap_err() + ); + + let sql = "SELECT s, s IS XYZ NORMALIZED FROM foo"; + let res = parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: XYZ" + .to_string() + ), + res.unwrap_err() + ); + + let sql = "SELECT s, s IS NFKC FROM foo"; + let res = parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: FROM" + .to_string() + ), + res.unwrap_err() + ); + + let sql = "SELECT s, s IS TRIM(' NFKC ') FROM foo"; + let res = parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: TRIM" .to_string() ), res.unwrap_err() @@ -13003,7 +13079,7 @@ fn test_trailing_commas_in_from() { let sql = "SELECT a FROM b, WHERE c = 1"; let _ = dialects.parse_sql_statements(sql).unwrap(); - // nasted + // nested let sql = "SELECT 1, 2 FROM (SELECT * FROM t,),"; let _ = dialects.parse_sql_statements(sql).unwrap(); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index dcf3f57fe..e93ac5695 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2572,7 +2572,7 @@ fn parse_kill() { } #[test] -fn parse_table_colum_option_on_update() { +fn parse_table_column_option_on_update() { let sql1 = "CREATE TABLE foo (`modification_time` DATETIME ON UPDATE CURRENT_TIMESTAMP())"; match mysql().verified_stmt(sql1) { Statement::CreateTable(CreateTable { name, columns, .. }) => { From 44df6d6f92e8bba1cbc6d4b57c99f8f126b786fd Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:43:45 +0100 Subject: [PATCH 689/806] Add support for qualified column names in JOIN ... USING (#1663) --- src/ast/query.rs | 2 +- src/ast/spans.rs | 2 +- src/parser/mod.rs | 35 +++++++++++++++++++++++++++++++---- tests/sqlparser_common.rs | 3 ++- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 9bcdc2e74..66c70b466 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2050,7 +2050,7 @@ pub enum JoinOperator { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum JoinConstraint { On(Expr), - Using(Vec), + Using(Vec), Natural, None, } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 2a5a75b49..1ddd47d7f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2008,7 +2008,7 @@ impl Spanned for JoinConstraint { fn span(&self) -> Span { match self { JoinConstraint::On(expr) => expr.span(), - JoinConstraint::Using(vec) => union_spans(vec.iter().map(|i| i.span)), + JoinConstraint::Using(vec) => union_spans(vec.iter().map(|i| i.span())), JoinConstraint::Natural => Span::empty(), JoinConstraint::None => Span::empty(), } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5cacfda98..a3adb0235 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9336,18 +9336,45 @@ impl<'a> Parser<'a> { }) } - /// Parse a parenthesized comma-separated list of unqualified, possibly quoted identifiers + /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. + /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( &mut self, optional: IsOptional, allow_empty: bool, ) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| p.parse_identifier()) + } + + /// 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( + &mut self, + optional: IsOptional, + allow_empty: bool, + ) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { + p.parse_object_name(true) + }) + } + + /// Parses a parenthesized comma-separated list of columns using + /// the provided function to parse each element. + fn parse_parenthesized_column_list_inner( + &mut self, + optional: IsOptional, + allow_empty: bool, + mut f: F, + ) -> Result, ParserError> + where + F: FnMut(&mut Parser) -> Result, + { if self.consume_token(&Token::LParen) { if allow_empty && self.peek_token().token == Token::RParen { self.next_token(); Ok(vec![]) } else { - let cols = self.parse_comma_separated(|p| p.parse_identifier())?; + let cols = self.parse_comma_separated(|p| f(p))?; self.expect_token(&Token::RParen)?; Ok(cols) } @@ -9358,7 +9385,7 @@ impl<'a> Parser<'a> { } } - /// Parse a parenthesized comma-separated list of table alias column definitions. + /// Parses a parenthesized comma-separated list of table alias column definitions. fn parse_table_alias_column_defs(&mut self) -> Result, ParserError> { if self.consume_token(&Token::LParen) { let cols = self.parse_comma_separated(|p| { @@ -11853,7 +11880,7 @@ impl<'a> Parser<'a> { let constraint = self.parse_expr()?; Ok(JoinConstraint::On(constraint)) } else if self.parse_keyword(Keyword::USING) { - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_qualified_column_list(Mandatory, false)?; Ok(JoinConstraint::Using(columns)) } else { Ok(JoinConstraint::None) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 49588a581..7271d6375 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6541,7 +6541,7 @@ fn parse_joins_using() { sample: None, }, global: false, - join_operator: f(JoinConstraint::Using(vec!["c1".into()])), + join_operator: f(JoinConstraint::Using(vec![ObjectName(vec!["c1".into()])])), } } // Test parsing of aliases @@ -6598,6 +6598,7 @@ fn parse_joins_using() { only(&verified_only_select("SELECT * FROM t1 FULL JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::FullOuter)] ); + verified_stmt("SELECT * FROM tbl1 AS t1 JOIN tbl2 AS t2 USING(t2.col1)"); } #[test] From 5da702fc19f9dc73559d9a6f0408729f1121444a Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 19 Jan 2025 12:08:45 +0100 Subject: [PATCH 690/806] Add support for Snowflake AT/BEFORE (#1667) --- src/ast/query.rs | 6 ++++++ src/dialect/bigquery.rs | 5 +++++ src/dialect/mod.rs | 6 ++++++ src/dialect/mssql.rs | 5 +++++ src/dialect/snowflake.rs | 5 +++++ src/parser/mod.rs | 26 ++++++++++++++------------ tests/sqlparser_snowflake.rs | 7 +++++++ 7 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 66c70b466..4053dd239 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1873,13 +1873,19 @@ impl fmt::Display for TableAliasColumnDef { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TableVersion { + /// When the table version is defined using `FOR SYSTEM_TIME AS OF`. + /// For example: `SELECT * FROM tbl FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)` ForSystemTimeAsOf(Expr), + /// When the table version is defined using a function. + /// For example: `SELECT * FROM tbl AT(TIMESTAMP => '2020-08-14 09:30:00')` + Function(Expr), } impl Display for TableVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { TableVersion::ForSystemTimeAsOf(e) => write!(f, " FOR SYSTEM_TIME AS OF {e}")?, + TableVersion::Function(func) => write!(f, " {func}")?, } Ok(()) } diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 66d7d2061..e92169a35 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -77,4 +77,9 @@ impl Dialect for BigQueryDialect { fn supports_struct_literal(&self) -> bool { true } + + // See + fn supports_timestamp_versioning(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c69253b76..119bb3cf7 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -834,6 +834,12 @@ pub trait Dialect: Debug + Any { fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { explicit || !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) } + + /// Returns true if this dialect supports querying historical table data + /// by specifying which version of the data to query. + fn supports_timestamp_versioning(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 67a648944..7d8611cb0 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -90,4 +90,9 @@ impl Dialect for MsSqlDialect { fn supports_set_stmt_without_operator(&self) -> bool { true } + + /// See: + fn supports_timestamp_versioning(&self) -> bool { + true + } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index f6e9c9eba..78237acd0 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -296,6 +296,11 @@ impl Dialect for SnowflakeDialect { _ => true, } } + + /// See: + fn supports_timestamp_versioning(&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 a3adb0235..51bbcfabd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11208,7 +11208,7 @@ impl<'a> Parser<'a> { }; // Parse potential version qualifier - let version = self.parse_table_version()?; + let version = self.maybe_parse_table_version()?; // Postgres, MSSQL, ClickHouse: table-valued functions: let args = if self.consume_token(&Token::LParen) { @@ -11639,18 +11639,20 @@ impl<'a> Parser<'a> { } } - /// Parse a given table version specifier. - /// - /// For now it only supports timestamp versioning for BigQuery and MSSQL dialects. - pub fn parse_table_version(&mut self) -> Result, ParserError> { - if dialect_of!(self is BigQueryDialect | MsSqlDialect) - && self.parse_keywords(&[Keyword::FOR, Keyword::SYSTEM_TIME, Keyword::AS, Keyword::OF]) - { - let expr = self.parse_expr()?; - Ok(Some(TableVersion::ForSystemTimeAsOf(expr))) - } else { - Ok(None) + /// Parses a the timestamp version specifier (i.e. query historical data) + pub fn maybe_parse_table_version(&mut self) -> Result, ParserError> { + if self.dialect.supports_timestamp_versioning() { + if self.parse_keywords(&[Keyword::FOR, Keyword::SYSTEM_TIME, Keyword::AS, Keyword::OF]) + { + let expr = self.parse_expr()?; + return Ok(Some(TableVersion::ForSystemTimeAsOf(expr))); + } else if self.peek_keyword(Keyword::AT) || self.peek_keyword(Keyword::BEFORE) { + let func_name = self.parse_object_name(true)?; + let func = self.parse_function(func_name)?; + return Ok(Some(TableVersion::Function(func))); + } } + Ok(None) } /// Parses MySQL's JSON_TABLE column definition. diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index fe6439b36..0c4bdf149 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3051,3 +3051,10 @@ fn test_sql_keywords_as_select_item_aliases() { .is_err()); } } + +#[test] +fn test_timetravel_at_before() { + snowflake().verified_only_select("SELECT * FROM tbl AT(TIMESTAMP => '2024-12-15 00:00:00')"); + snowflake() + .verified_only_select("SELECT * FROM tbl BEFORE(TIMESTAMP => '2024-12-15 00:00:00')"); +} From 17d5610f098c085e5e89d8edc71df1949defe76d Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 20 Jan 2025 11:32:12 -0500 Subject: [PATCH 691/806] Update verson to 0.54.0 and update changelog (#1668) --- CHANGELOG.md | 6 ++- Cargo.toml | 2 +- changelog/0.54.0.md | 118 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 changelog/0.54.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ec74bf633..d1c55b285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,13 +18,17 @@ --> # Changelog -All notable changes to this project will be documented in this file. +All notable changes to this project will be documented in one of the linked +files. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + Given that the parser produces a typed AST, any changes to the AST will 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.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) - `0.52.0`: [changelog/0.52.0.md](changelog/0.52.0.md) - `0.51.0` and earlier: [changelog/0.51.0-pre.md](changelog/0.51.0-pre.md) diff --git a/Cargo.toml b/Cargo.toml index 8ff0ceb55..06fed2c68 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.53.0" +version = "0.54.0" authors = ["Apache DataFusion "] homepage = "https://github.com/apache/datafusion-sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/changelog/0.54.0.md b/changelog/0.54.0.md new file mode 100644 index 000000000..c0a63ae45 --- /dev/null +++ b/changelog/0.54.0.md @@ -0,0 +1,118 @@ + + +# sqlparser-rs 0.54.0 Changelog + +This release consists of 57 commits from 24 contributors. See credits at the end of this changelog for more information. + +**Implemented enhancements:** + +- feat: support `INSERT INTO [TABLE] FUNCTION` of Clickhouse [#1633](https://github.com/apache/datafusion-sqlparser-rs/pull/1633) (byte-sourcerer) + +**Other:** + +- Run cargo fmt on `derive` crate [#1595](https://github.com/apache/datafusion-sqlparser-rs/pull/1595) (alamb) +- Add Apache license header to spans.rs [#1594](https://github.com/apache/datafusion-sqlparser-rs/pull/1594) (alamb) +- Add support for BigQuery `ANY TYPE` data type [#1602](https://github.com/apache/datafusion-sqlparser-rs/pull/1602) (MartinSahlen) +- Add support for TABLESAMPLE [#1580](https://github.com/apache/datafusion-sqlparser-rs/pull/1580) (yoavcloud) +- Redshift: Fix parsing for quoted numbered columns [#1576](https://github.com/apache/datafusion-sqlparser-rs/pull/1576) (7phs) +- Add the alter table ON COMMIT option to Snowflake [#1606](https://github.com/apache/datafusion-sqlparser-rs/pull/1606) (yoavcloud) +- Support parsing `EXPLAIN ESTIMATE` of Clickhouse [#1605](https://github.com/apache/datafusion-sqlparser-rs/pull/1605) (byte-sourcerer) +- Fix BigQuery hyphenated ObjectName with numbers [#1598](https://github.com/apache/datafusion-sqlparser-rs/pull/1598) (ayman-sigma) +- Fix test compilation issue [#1609](https://github.com/apache/datafusion-sqlparser-rs/pull/1609) (iffyio) +- Allow foreign table constraint without columns [#1608](https://github.com/apache/datafusion-sqlparser-rs/pull/1608) (ramnivas) +- Support optional table for `ANALYZE` statement [#1599](https://github.com/apache/datafusion-sqlparser-rs/pull/1599) (yuyang-ok) +- Support DOUBLE data types with precision for Mysql [#1611](https://github.com/apache/datafusion-sqlparser-rs/pull/1611) (artorias1024) +- Add `#[recursive]` [#1522](https://github.com/apache/datafusion-sqlparser-rs/pull/1522) (blaginin) +- Support arbitrary composite access expressions [#1600](https://github.com/apache/datafusion-sqlparser-rs/pull/1600) (ayman-sigma) +- Consolidate `MapAccess`, and `Subscript` into `CompoundExpr` to handle the complex field access chain [#1551](https://github.com/apache/datafusion-sqlparser-rs/pull/1551) (goldmedal) +- Handle empty projection in Postgres SELECT statements [#1613](https://github.com/apache/datafusion-sqlparser-rs/pull/1613) (tobyhede) +- Merge composite and compound expr test cases [#1615](https://github.com/apache/datafusion-sqlparser-rs/pull/1615) (iffyio) +- Support Snowflake Update-From-Select [#1604](https://github.com/apache/datafusion-sqlparser-rs/pull/1604) (yuval-illumex) +- Improve parsing performance by reducing token cloning [#1587](https://github.com/apache/datafusion-sqlparser-rs/pull/1587) (davisp) +- Improve Parser documentation [#1617](https://github.com/apache/datafusion-sqlparser-rs/pull/1617) (alamb) +- Add support for DROP EXTENSION [#1610](https://github.com/apache/datafusion-sqlparser-rs/pull/1610) (ramnivas) +- Refactor advancing token to avoid duplication, avoid borrow checker issues [#1618](https://github.com/apache/datafusion-sqlparser-rs/pull/1618) (alamb) +- Fix the parsing result for the special double number [#1621](https://github.com/apache/datafusion-sqlparser-rs/pull/1621) (goldmedal) +- SQLite: Allow dollar signs in placeholder names [#1620](https://github.com/apache/datafusion-sqlparser-rs/pull/1620) (hansott) +- Improve error for an unexpected token after DROP [#1623](https://github.com/apache/datafusion-sqlparser-rs/pull/1623) (ramnivas) +- Fix `sqlparser_bench` benchmark compilation [#1625](https://github.com/apache/datafusion-sqlparser-rs/pull/1625) (alamb) +- Improve parsing speed by avoiding some clones in parse_identifier [#1624](https://github.com/apache/datafusion-sqlparser-rs/pull/1624) (alamb) +- Simplify `parse_keyword_apis` more [#1626](https://github.com/apache/datafusion-sqlparser-rs/pull/1626) (alamb) +- Test benchmarks and Improve benchmark README.md [#1627](https://github.com/apache/datafusion-sqlparser-rs/pull/1627) (alamb) +- Add support for MYSQL's `RENAME TABLE` [#1616](https://github.com/apache/datafusion-sqlparser-rs/pull/1616) (wugeer) +- Correctly tokenize nested comments [#1629](https://github.com/apache/datafusion-sqlparser-rs/pull/1629) (hansott) +- Add support for USE SECONDARY ROLE (vs. ROLES) [#1637](https://github.com/apache/datafusion-sqlparser-rs/pull/1637) (yoavcloud) +- Add support for various Snowflake grantees [#1640](https://github.com/apache/datafusion-sqlparser-rs/pull/1640) (yoavcloud) +- Add support for the SQL OVERLAPS predicate [#1638](https://github.com/apache/datafusion-sqlparser-rs/pull/1638) (yoavcloud) +- Add support for Snowflake LIST and REMOVE [#1639](https://github.com/apache/datafusion-sqlparser-rs/pull/1639) (yoavcloud) +- Add support for MySQL's INSERT INTO ... SET syntax [#1641](https://github.com/apache/datafusion-sqlparser-rs/pull/1641) (yoavcloud) +- Start new line if \r in Postgres dialect [#1647](https://github.com/apache/datafusion-sqlparser-rs/pull/1647) (hansott) +- Support pluralized time units [#1630](https://github.com/apache/datafusion-sqlparser-rs/pull/1630) (wugeer) +- Replace `ReferentialAction` enum in `DROP` statements [#1648](https://github.com/apache/datafusion-sqlparser-rs/pull/1648) (stepancheg) +- Add support for MS-SQL BEGIN/END TRY/CATCH [#1649](https://github.com/apache/datafusion-sqlparser-rs/pull/1649) (yoavcloud) +- Fix MySQL parsing of GRANT, REVOKE, and CREATE VIEW [#1538](https://github.com/apache/datafusion-sqlparser-rs/pull/1538) (mvzink) +- Add support for the Snowflake MINUS set operator [#1652](https://github.com/apache/datafusion-sqlparser-rs/pull/1652) (yoavcloud) +- ALTER TABLE DROP {COLUMN|CONSTRAINT} RESTRICT [#1651](https://github.com/apache/datafusion-sqlparser-rs/pull/1651) (stepancheg) +- Add support for ClickHouse `FORMAT` on `INSERT` [#1628](https://github.com/apache/datafusion-sqlparser-rs/pull/1628) (bombsimon) +- MsSQL SET for session params [#1646](https://github.com/apache/datafusion-sqlparser-rs/pull/1646) (yoavcloud) +- Correctly look for end delimiter dollar quoted string [#1650](https://github.com/apache/datafusion-sqlparser-rs/pull/1650) (hansott) +- Support single line comments starting with '#' for Hive [#1654](https://github.com/apache/datafusion-sqlparser-rs/pull/1654) (wugeer) +- Support trailing commas in `FROM` clause [#1645](https://github.com/apache/datafusion-sqlparser-rs/pull/1645) (barsela1) +- Allow empty options for BigQuery [#1657](https://github.com/apache/datafusion-sqlparser-rs/pull/1657) (MartinSahlen) +- Add support for parsing RAISERROR [#1656](https://github.com/apache/datafusion-sqlparser-rs/pull/1656) (AvivDavid-Satori) +- Add support for Snowflake column aliases that use SQL keywords [#1632](https://github.com/apache/datafusion-sqlparser-rs/pull/1632) (yoavcloud) +- fix parsing of `INSERT INTO ... SELECT ... RETURNING ` [#1661](https://github.com/apache/datafusion-sqlparser-rs/pull/1661) (lovasoa) +- Add support for `IS [NOT] [form] NORMALIZED` [#1655](https://github.com/apache/datafusion-sqlparser-rs/pull/1655) (alexander-beedie) +- Add support for qualified column names in JOIN ... USING [#1663](https://github.com/apache/datafusion-sqlparser-rs/pull/1663) (yoavcloud) +- Add support for Snowflake AT/BEFORE [#1667](https://github.com/apache/datafusion-sqlparser-rs/pull/1667) (yoavcloud) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 13 Yoav Cohen + 9 Andrew Lamb + 4 Hans Ott + 3 Ramnivas Laddad + 3 wugeer + 2 Ayman Elkfrawy + 2 Ifeanyi Ubah + 2 Jax Liu + 2 Martin Abelson Sahlen + 2 Stepan Koltsov + 2 cjw + 1 Aleksei Piianin + 1 Alexander Beedie + 1 AvivDavid-Satori + 1 Dmitrii Blaginin + 1 Michael Victor Zink + 1 Ophir LOJKINE + 1 Paul J. Davis + 1 Simon Sawert + 1 Toby Hede + 1 Yuval Shkolar + 1 artorias1024 + 1 bar sela + 1 yuyang +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. + From e5bc3dfad879ea15dcbe28b85d92697193298f08 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 20 Jan 2025 14:19:14 -0500 Subject: [PATCH 692/806] Update rat_exclude_file.txt (#1670) --- dev/release/rat_exclude_files.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index a567eda9c..562eec2f1 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -3,4 +3,5 @@ .tool-versions dev/release/rat_exclude_files.txt fuzz/.gitignore +sqlparser_bench/img/flamegraph.svg From 183274e274dc0f98f4946e0eb55d5d4dcf63cadb Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 20 Jan 2025 20:20:41 +0100 Subject: [PATCH 693/806] Add support for Snowflake account privileges (#1666) --- src/ast/mod.rs | 274 +++++++++++++++++++++++++++++++- src/keywords.rs | 41 +++++ src/parser/mod.rs | 296 +++++++++++++++++++++++++++-------- tests/sqlparser_common.rs | 5 +- tests/sqlparser_snowflake.rs | 106 +++++++++++++ 5 files changed, 651 insertions(+), 71 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7992596e3..5c06d7196 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5453,29 +5453,107 @@ impl fmt::Display for FetchDirection { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum Action { + AddSearchOptimization, + Apply { + apply_type: ActionApplyType, + }, + ApplyBudget, + AttachListing, + AttachPolicy, + Audit, + BindServiceEndpoint, Connect, - Create, + Create { + obj_type: Option, + }, Delete, - Execute, - Insert { columns: Option> }, - References { columns: Option> }, - Select { columns: Option> }, + EvolveSchema, + Execute { + obj_type: Option, + }, + Failover, + ImportedPrivileges, + ImportShare, + Insert { + columns: Option>, + }, + Manage { + manage_type: ActionManageType, + }, + ManageReleases, + ManageVersions, + Modify { + modify_type: ActionModifyType, + }, + Monitor { + monitor_type: ActionMonitorType, + }, + Operate, + OverrideShareRestrictions, + Ownership, + PurchaseDataExchangeListing, + Read, + ReadSession, + References { + columns: Option>, + }, + Replicate, + ResolveAll, + Select { + columns: Option>, + }, Temporary, Trigger, Truncate, - Update { columns: Option> }, + Update { + columns: Option>, + }, Usage, } impl fmt::Display for Action { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 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::AttachListing => f.write_str("ATTACH LISTING")?, + Action::AttachPolicy => f.write_str("ATTACH POLICY")?, + Action::Audit => f.write_str("AUDIT")?, + Action::BindServiceEndpoint => f.write_str("BIND SERVICE ENDPOINT")?, Action::Connect => f.write_str("CONNECT")?, - Action::Create => f.write_str("CREATE")?, + Action::Create { obj_type } => { + f.write_str("CREATE")?; + if let Some(obj_type) = obj_type { + write!(f, " {obj_type}")? + } + } Action::Delete => f.write_str("DELETE")?, - Action::Execute => f.write_str("EXECUTE")?, + Action::EvolveSchema => f.write_str("EVOLVE SCHEMA")?, + Action::Execute { obj_type } => { + f.write_str("EXECUTE")?; + if let Some(obj_type) = obj_type { + write!(f, " {obj_type}")? + } + } + Action::Failover => f.write_str("FAILOVER")?, + Action::ImportedPrivileges => f.write_str("IMPORTED PRIVILEGES")?, + Action::ImportShare => f.write_str("IMPORT SHARE")?, Action::Insert { .. } => f.write_str("INSERT")?, + 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::Operate => f.write_str("OPERATE")?, + Action::OverrideShareRestrictions => f.write_str("OVERRIDE SHARE RESTRICTIONS")?, + Action::Ownership => f.write_str("OWNERSHIP")?, + Action::PurchaseDataExchangeListing => f.write_str("PURCHASE DATA EXCHANGE LISTING")?, + Action::Read => f.write_str("READ")?, + Action::ReadSession => f.write_str("READ SESSION")?, Action::References { .. } => f.write_str("REFERENCES")?, + Action::Replicate => f.write_str("REPLICATE")?, + Action::ResolveAll => f.write_str("RESOLVE ALL")?, Action::Select { .. } => f.write_str("SELECT")?, Action::Temporary => f.write_str("TEMPORARY")?, Action::Trigger => f.write_str("TRIGGER")?, @@ -5498,6 +5576,186 @@ impl fmt::Display for Action { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `CREATE` privilege. +pub enum ActionCreateObjectType { + Account, + Application, + ApplicationPackage, + ComputePool, + DataExchangeListing, + Database, + ExternalVolume, + FailoverGroup, + Integration, + NetworkPolicy, + OrganiationListing, + ReplicationGroup, + Role, + Share, + User, + Warehouse, +} + +impl fmt::Display for ActionCreateObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionCreateObjectType::Account => write!(f, "ACCOUNT"), + ActionCreateObjectType::Application => write!(f, "APPLICATION"), + ActionCreateObjectType::ApplicationPackage => write!(f, "APPLICATION PACKAGE"), + ActionCreateObjectType::ComputePool => write!(f, "COMPUTE POOL"), + ActionCreateObjectType::DataExchangeListing => write!(f, "DATA EXCHANGE LISTING"), + ActionCreateObjectType::Database => write!(f, "DATABASE"), + ActionCreateObjectType::ExternalVolume => write!(f, "EXTERNAL VOLUME"), + ActionCreateObjectType::FailoverGroup => write!(f, "FAILOVER GROUP"), + ActionCreateObjectType::Integration => write!(f, "INTEGRATION"), + ActionCreateObjectType::NetworkPolicy => write!(f, "NETWORK POLICY"), + ActionCreateObjectType::OrganiationListing => write!(f, "ORGANIZATION LISTING"), + ActionCreateObjectType::ReplicationGroup => write!(f, "REPLICATION GROUP"), + ActionCreateObjectType::Role => write!(f, "ROLE"), + ActionCreateObjectType::Share => write!(f, "SHARE"), + ActionCreateObjectType::User => write!(f, "USER"), + ActionCreateObjectType::Warehouse => write!(f, "WAREHOUSE"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `APPLY` privilege. +pub enum ActionApplyType { + AggregationPolicy, + AuthenticationPolicy, + JoinPolicy, + MaskingPolicy, + PackagesPolicy, + PasswordPolicy, + ProjectionPolicy, + RowAccessPolicy, + SessionPolicy, + Tag, +} + +impl fmt::Display for ActionApplyType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionApplyType::AggregationPolicy => write!(f, "AGGREGATION POLICY"), + ActionApplyType::AuthenticationPolicy => write!(f, "AUTHENTICATION POLICY"), + ActionApplyType::JoinPolicy => write!(f, "JOIN POLICY"), + ActionApplyType::MaskingPolicy => write!(f, "MASKING POLICY"), + ActionApplyType::PackagesPolicy => write!(f, "PACKAGES POLICY"), + ActionApplyType::PasswordPolicy => write!(f, "PASSWORD POLICY"), + ActionApplyType::ProjectionPolicy => write!(f, "PROJECTION POLICY"), + ActionApplyType::RowAccessPolicy => write!(f, "ROW ACCESS POLICY"), + ActionApplyType::SessionPolicy => write!(f, "SESSION POLICY"), + ActionApplyType::Tag => write!(f, "TAG"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `EXECUTE` privilege. +pub enum ActionExecuteObjectType { + Alert, + DataMetricFunction, + ManagedAlert, + ManagedTask, + Task, +} + +impl fmt::Display for ActionExecuteObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionExecuteObjectType::Alert => write!(f, "ALERT"), + ActionExecuteObjectType::DataMetricFunction => write!(f, "DATA METRIC FUNCTION"), + ActionExecuteObjectType::ManagedAlert => write!(f, "MANAGED ALERT"), + ActionExecuteObjectType::ManagedTask => write!(f, "MANAGED TASK"), + ActionExecuteObjectType::Task => write!(f, "TASK"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `MANAGE` privilege. +pub enum ActionManageType { + AccountSupportCases, + EventSharing, + Grants, + ListingAutoFulfillment, + OrganizationSupportCases, + UserSupportCases, + Warehouses, +} + +impl fmt::Display for ActionManageType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionManageType::AccountSupportCases => write!(f, "ACCOUNT SUPPORT CASES"), + ActionManageType::EventSharing => write!(f, "EVENT SHARING"), + ActionManageType::Grants => write!(f, "GRANTS"), + ActionManageType::ListingAutoFulfillment => write!(f, "LISTING AUTO FULFILLMENT"), + ActionManageType::OrganizationSupportCases => write!(f, "ORGANIZATION SUPPORT CASES"), + ActionManageType::UserSupportCases => write!(f, "USER SUPPORT CASES"), + ActionManageType::Warehouses => write!(f, "WAREHOUSES"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `MODIFY` privilege. +pub enum ActionModifyType { + LogLevel, + TraceLevel, + SessionLogLevel, + SessionTraceLevel, +} + +impl fmt::Display for ActionModifyType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionModifyType::LogLevel => write!(f, "LOG LEVEL"), + ActionModifyType::TraceLevel => write!(f, "TRACE LEVEL"), + ActionModifyType::SessionLogLevel => write!(f, "SESSION LOG LEVEL"), + ActionModifyType::SessionTraceLevel => write!(f, "SESSION TRACE LEVEL"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// See +/// under `globalPrivileges` in the `MONITOR` privilege. +pub enum ActionMonitorType { + Execution, + Security, + Usage, +} + +impl fmt::Display for ActionMonitorType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionMonitorType::Execution => write!(f, "EXECUTION"), + ActionMonitorType::Security => write!(f, "SECURITY"), + ActionMonitorType::Usage => write!(f, "USAGE"), + } + } +} + /// The principal that receives the privileges #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/keywords.rs b/src/keywords.rs index 0d1bab9e5..68b040c08 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -84,6 +84,7 @@ define_keywords!( AFTER, AGAINST, AGGREGATION, + ALERT, ALGORITHM, ALIAS, ALL, @@ -96,6 +97,7 @@ define_keywords!( ANY, APPLICATION, APPLY, + APPLYBUDGET, ARCHIVE, ARE, ARRAY, @@ -109,6 +111,8 @@ define_keywords!( AT, ATOMIC, ATTACH, + AUDIT, + AUTHENTICATION, AUTHORIZATION, AUTO, AUTOINCREMENT, @@ -127,6 +131,7 @@ define_keywords!( BIGINT, BIGNUMERIC, BINARY, + BIND, BINDING, BIT, BLOB, @@ -150,6 +155,7 @@ define_keywords!( CASCADE, CASCADED, CASE, + CASES, CAST, CATALOG, CATCH, @@ -305,12 +311,15 @@ define_keywords!( ESTIMATE, EVENT, EVERY, + EVOLVE, EXCEPT, EXCEPTION, + EXCHANGE, EXCLUDE, EXCLUSIVE, EXEC, EXECUTE, + EXECUTION, EXISTS, EXP, EXPANSION, @@ -322,6 +331,7 @@ define_keywords!( EXTERNAL, EXTRACT, FAIL, + FAILOVER, FALSE, FETCH, FIELDS, @@ -357,6 +367,7 @@ define_keywords!( FREEZE, FROM, FSCK, + FULFILLMENT, FULL, FULLTEXT, FUNCTION, @@ -394,6 +405,8 @@ define_keywords!( ILIKE, IMMEDIATE, IMMUTABLE, + IMPORT, + IMPORTED, IN, INCLUDE, INCLUDE_NULL_VALUES, @@ -421,6 +434,7 @@ define_keywords!( INT64, INT8, INTEGER, + INTEGRATION, INTERPOLATE, INTERSECT, INTERSECTION, @@ -460,6 +474,7 @@ define_keywords!( LINES, LIST, LISTEN, + LISTING, LN, LOAD, LOCAL, @@ -478,6 +493,8 @@ define_keywords!( LOW_PRIORITY, LS, MACRO, + MANAGE, + MANAGED, MANAGEDLOCATION, MAP, MASKING, @@ -499,6 +516,7 @@ define_keywords!( MERGE, METADATA, METHOD, + METRIC, MICROSECOND, MICROSECONDS, MILLENIUM, @@ -515,6 +533,7 @@ define_keywords!( MODIFIES, MODIFY, MODULE, + MONITOR, MONTH, MONTHS, MSCK, @@ -528,6 +547,7 @@ define_keywords!( NCHAR, NCLOB, NESTED, + NETWORK, NEW, NEXT, NFC, @@ -575,7 +595,9 @@ define_keywords!( ONLY, OPEN, OPENJSON, + OPERATE, OPERATOR, + OPTIMIZATION, OPTIMIZE, OPTIMIZER_COSTS, OPTION, @@ -584,6 +606,7 @@ define_keywords!( ORC, ORDER, ORDINALITY, + ORGANIZATION, OUT, OUTER, OUTPUTFORMAT, @@ -591,9 +614,13 @@ define_keywords!( OVERFLOW, OVERLAPS, OVERLAY, + OVERRIDE, OVERWRITE, OWNED, OWNER, + OWNERSHIP, + PACKAGE, + PACKAGES, PARALLEL, PARAMETER, PARQUET, @@ -618,6 +645,7 @@ define_keywords!( PLAN, PLANS, POLICY, + POOL, PORTION, POSITION, POSITION_REGEX, @@ -637,6 +665,7 @@ define_keywords!( PROGRAM, PROJECTION, PUBLIC, + PURCHASE, PURGE, QUALIFY, QUARTER, @@ -670,6 +699,7 @@ define_keywords!( RELATIVE, RELAY, RELEASE, + RELEASES, REMOTE, REMOVE, RENAME, @@ -678,12 +708,15 @@ define_keywords!( REPEATABLE, REPLACE, REPLICA, + REPLICATE, REPLICATION, RESET, + RESOLVE, RESPECT, RESTART, RESTRICT, RESTRICTED, + RESTRICTIONS, RESTRICTIVE, RESULT, RESULTSET, @@ -732,6 +765,7 @@ define_keywords!( SERDE, SERDEPROPERTIES, SERIALIZABLE, + SERVICE, SESSION, SESSION_USER, SET, @@ -739,6 +773,7 @@ define_keywords!( SETS, SETTINGS, SHARE, + SHARING, SHOW, SIMILAR, SKIP, @@ -782,6 +817,7 @@ define_keywords!( SUM, SUPER, SUPERUSER, + SUPPORT, SUSPEND, SWAP, SYMMETRIC, @@ -794,6 +830,7 @@ define_keywords!( TABLESAMPLE, TAG, TARGET, + TASK, TBLPROPERTIES, TEMP, TEMPORARY, @@ -819,6 +856,7 @@ define_keywords!( TO, TOP, TOTALS, + TRACE, TRAILING, TRANSACTION, TRANSIENT, @@ -885,11 +923,14 @@ define_keywords!( VERBOSE, VERSION, VERSIONING, + VERSIONS, VIEW, VIEWS, VIRTUAL, VOLATILE, + VOLUME, WAREHOUSE, + WAREHOUSES, WEEK, WEEKS, WHEN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 51bbcfabd..355e520ab 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -48,9 +48,6 @@ pub enum ParserError { RecursionLimitExceeded, } -// avoid clippy type_complexity warnings -type ParsedAction = (Keyword, Option>); - // Use `Parser::expected` instead, if possible macro_rules! parser_err { ($MSG:expr, $loc:expr) => { @@ -3950,7 +3947,7 @@ impl<'a> Parser<'a> { ) } - pub fn parse_actions_list(&mut self) -> Result, ParserError> { + pub fn parse_actions_list(&mut self) -> Result, ParserError> { let mut values = vec![]; loop { values.push(self.parse_grant_permission()?); @@ -11980,37 +11977,8 @@ impl<'a> Parser<'a> { with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), } } else { - let (actions, err): (Vec<_>, Vec<_>) = self - .parse_actions_list()? - .into_iter() - .map(|(kw, columns)| match kw { - Keyword::DELETE => Ok(Action::Delete), - Keyword::INSERT => Ok(Action::Insert { columns }), - Keyword::REFERENCES => Ok(Action::References { columns }), - Keyword::SELECT => Ok(Action::Select { columns }), - Keyword::TRIGGER => Ok(Action::Trigger), - Keyword::TRUNCATE => Ok(Action::Truncate), - Keyword::UPDATE => Ok(Action::Update { columns }), - Keyword::USAGE => Ok(Action::Usage), - Keyword::CONNECT => Ok(Action::Connect), - Keyword::CREATE => Ok(Action::Create), - Keyword::EXECUTE => Ok(Action::Execute), - Keyword::TEMPORARY => Ok(Action::Temporary), - // This will cover all future added keywords to - // parse_grant_permission and unhandled in this - // match - _ => Err(kw), - }) - .partition(Result::is_ok); - - if !err.is_empty() { - let errors: Vec = err.into_iter().filter_map(|x| x.err()).collect(); - return Err(ParserError::ParserError(format!( - "INTERNAL ERROR: GRANT/REVOKE unexpected keyword(s) - {errors:?}" - ))); - } - let act = actions.into_iter().filter_map(|x| x.ok()).collect(); - Privileges::Actions(act) + let actions = self.parse_actions_list()?; + Privileges::Actions(actions) }; self.expect_keyword_is(Keyword::ON)?; @@ -12049,38 +12017,244 @@ impl<'a> Parser<'a> { Ok((privileges, objects)) } - pub fn parse_grant_permission(&mut self) -> Result { - if let Some(kw) = self.parse_one_of_keywords(&[ - Keyword::CONNECT, - Keyword::CREATE, - Keyword::DELETE, - Keyword::EXECUTE, - Keyword::INSERT, - Keyword::REFERENCES, - Keyword::SELECT, - Keyword::TEMPORARY, - Keyword::TRIGGER, - Keyword::TRUNCATE, - Keyword::UPDATE, - Keyword::USAGE, + 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)?; + if columns.is_empty() { + Ok(None) + } else { + Ok(Some(columns)) + } + } + + // Multi-word privileges + if self.parse_keywords(&[Keyword::IMPORTED, Keyword::PRIVILEGES]) { + Ok(Action::ImportedPrivileges) + } else if self.parse_keywords(&[Keyword::ADD, Keyword::SEARCH, Keyword::OPTIMIZATION]) { + Ok(Action::AddSearchOptimization) + } else if self.parse_keywords(&[Keyword::ATTACH, Keyword::LISTING]) { + Ok(Action::AttachListing) + } else if self.parse_keywords(&[Keyword::ATTACH, Keyword::POLICY]) { + Ok(Action::AttachPolicy) + } else if self.parse_keywords(&[Keyword::BIND, Keyword::SERVICE, Keyword::ENDPOINT]) { + Ok(Action::BindServiceEndpoint) + } else if self.parse_keywords(&[Keyword::EVOLVE, Keyword::SCHEMA]) { + Ok(Action::EvolveSchema) + } else if self.parse_keywords(&[Keyword::IMPORT, Keyword::SHARE]) { + Ok(Action::ImportShare) + } else if self.parse_keywords(&[Keyword::MANAGE, Keyword::VERSIONS]) { + Ok(Action::ManageVersions) + } else if self.parse_keywords(&[Keyword::MANAGE, Keyword::RELEASES]) { + Ok(Action::ManageReleases) + } else if self.parse_keywords(&[Keyword::OVERRIDE, Keyword::SHARE, Keyword::RESTRICTIONS]) { + Ok(Action::OverrideShareRestrictions) + } else if self.parse_keywords(&[ + Keyword::PURCHASE, + Keyword::DATA, + Keyword::EXCHANGE, + Keyword::LISTING, ]) { - let columns = match kw { - Keyword::INSERT | Keyword::REFERENCES | Keyword::SELECT | Keyword::UPDATE => { - let columns = self.parse_parenthesized_column_list(Optional, false)?; - if columns.is_empty() { - None - } else { - Some(columns) - } - } - _ => None, - }; - Ok((kw, columns)) + Ok(Action::PurchaseDataExchangeListing) + } else if self.parse_keywords(&[Keyword::RESOLVE, Keyword::ALL]) { + Ok(Action::ResolveAll) + } else if self.parse_keywords(&[Keyword::READ, Keyword::SESSION]) { + Ok(Action::ReadSession) + + // Single-word privileges + } else if self.parse_keyword(Keyword::APPLY) { + let apply_type = self.parse_action_apply_type()?; + Ok(Action::Apply { apply_type }) + } else if self.parse_keyword(Keyword::APPLYBUDGET) { + Ok(Action::ApplyBudget) + } else if self.parse_keyword(Keyword::AUDIT) { + Ok(Action::Audit) + } else if self.parse_keyword(Keyword::CONNECT) { + Ok(Action::Connect) + } else if self.parse_keyword(Keyword::CREATE) { + let obj_type = self.maybe_parse_action_create_object_type(); + Ok(Action::Create { obj_type }) + } else if self.parse_keyword(Keyword::DELETE) { + Ok(Action::Delete) + } else if self.parse_keyword(Keyword::EXECUTE) { + let obj_type = self.maybe_parse_action_execute_obj_type(); + Ok(Action::Execute { obj_type }) + } else if self.parse_keyword(Keyword::FAILOVER) { + Ok(Action::Failover) + } else if self.parse_keyword(Keyword::INSERT) { + Ok(Action::Insert { + columns: parse_columns(self)?, + }) + } else if self.parse_keyword(Keyword::MANAGE) { + 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()?; + Ok(Action::Modify { modify_type }) + } else if self.parse_keyword(Keyword::MONITOR) { + let monitor_type = self.parse_action_monitor_type()?; + Ok(Action::Monitor { monitor_type }) + } else if self.parse_keyword(Keyword::OPERATE) { + Ok(Action::Operate) + } else if self.parse_keyword(Keyword::REFERENCES) { + Ok(Action::References { + columns: parse_columns(self)?, + }) + } else if self.parse_keyword(Keyword::READ) { + Ok(Action::Read) + } else if self.parse_keyword(Keyword::REPLICATE) { + Ok(Action::Replicate) + } else if self.parse_keyword(Keyword::SELECT) { + Ok(Action::Select { + columns: parse_columns(self)?, + }) + } else if self.parse_keyword(Keyword::TEMPORARY) { + Ok(Action::Temporary) + } else if self.parse_keyword(Keyword::TRIGGER) { + Ok(Action::Trigger) + } else if self.parse_keyword(Keyword::TRUNCATE) { + Ok(Action::Truncate) + } else if self.parse_keyword(Keyword::UPDATE) { + Ok(Action::Update { + columns: parse_columns(self)?, + }) + } else if self.parse_keyword(Keyword::USAGE) { + Ok(Action::Usage) + } else if self.parse_keyword(Keyword::OWNERSHIP) { + Ok(Action::Ownership) } else { self.expected("a privilege keyword", self.peek_token())? } } + fn maybe_parse_action_create_object_type(&mut self) -> Option { + // Multi-word object types + if self.parse_keywords(&[Keyword::APPLICATION, Keyword::PACKAGE]) { + Some(ActionCreateObjectType::ApplicationPackage) + } else if self.parse_keywords(&[Keyword::COMPUTE, Keyword::POOL]) { + Some(ActionCreateObjectType::ComputePool) + } else if self.parse_keywords(&[Keyword::DATA, Keyword::EXCHANGE, Keyword::LISTING]) { + Some(ActionCreateObjectType::DataExchangeListing) + } else if self.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) { + Some(ActionCreateObjectType::ExternalVolume) + } else if self.parse_keywords(&[Keyword::FAILOVER, Keyword::GROUP]) { + Some(ActionCreateObjectType::FailoverGroup) + } else if self.parse_keywords(&[Keyword::NETWORK, Keyword::POLICY]) { + Some(ActionCreateObjectType::NetworkPolicy) + } else if self.parse_keywords(&[Keyword::ORGANIZATION, Keyword::LISTING]) { + Some(ActionCreateObjectType::OrganiationListing) + } else if self.parse_keywords(&[Keyword::REPLICATION, Keyword::GROUP]) { + Some(ActionCreateObjectType::ReplicationGroup) + } + // Single-word object types + else if self.parse_keyword(Keyword::ACCOUNT) { + Some(ActionCreateObjectType::Account) + } else if self.parse_keyword(Keyword::APPLICATION) { + Some(ActionCreateObjectType::Application) + } else if self.parse_keyword(Keyword::DATABASE) { + Some(ActionCreateObjectType::Database) + } else if self.parse_keyword(Keyword::INTEGRATION) { + Some(ActionCreateObjectType::Integration) + } else if self.parse_keyword(Keyword::ROLE) { + Some(ActionCreateObjectType::Role) + } else if self.parse_keyword(Keyword::SHARE) { + Some(ActionCreateObjectType::Share) + } else if self.parse_keyword(Keyword::USER) { + Some(ActionCreateObjectType::User) + } else if self.parse_keyword(Keyword::WAREHOUSE) { + Some(ActionCreateObjectType::Warehouse) + } else { + None + } + } + + fn parse_action_apply_type(&mut self) -> Result { + if self.parse_keywords(&[Keyword::AGGREGATION, Keyword::POLICY]) { + Ok(ActionApplyType::AggregationPolicy) + } else if self.parse_keywords(&[Keyword::AUTHENTICATION, Keyword::POLICY]) { + Ok(ActionApplyType::AuthenticationPolicy) + } else if self.parse_keywords(&[Keyword::JOIN, Keyword::POLICY]) { + Ok(ActionApplyType::JoinPolicy) + } else if self.parse_keywords(&[Keyword::MASKING, Keyword::POLICY]) { + Ok(ActionApplyType::MaskingPolicy) + } else if self.parse_keywords(&[Keyword::PACKAGES, Keyword::POLICY]) { + Ok(ActionApplyType::PackagesPolicy) + } else if self.parse_keywords(&[Keyword::PASSWORD, Keyword::POLICY]) { + Ok(ActionApplyType::PasswordPolicy) + } else if self.parse_keywords(&[Keyword::PROJECTION, Keyword::POLICY]) { + Ok(ActionApplyType::ProjectionPolicy) + } else if self.parse_keywords(&[Keyword::ROW, Keyword::ACCESS, Keyword::POLICY]) { + Ok(ActionApplyType::RowAccessPolicy) + } else if self.parse_keywords(&[Keyword::SESSION, Keyword::POLICY]) { + Ok(ActionApplyType::SessionPolicy) + } else if self.parse_keyword(Keyword::TAG) { + Ok(ActionApplyType::Tag) + } else { + self.expected("GRANT APPLY type", self.peek_token()) + } + } + + fn maybe_parse_action_execute_obj_type(&mut self) -> Option { + if self.parse_keywords(&[Keyword::DATA, Keyword::METRIC, Keyword::FUNCTION]) { + Some(ActionExecuteObjectType::DataMetricFunction) + } else if self.parse_keywords(&[Keyword::MANAGED, Keyword::ALERT]) { + Some(ActionExecuteObjectType::ManagedAlert) + } else if self.parse_keywords(&[Keyword::MANAGED, Keyword::TASK]) { + Some(ActionExecuteObjectType::ManagedTask) + } else if self.parse_keyword(Keyword::ALERT) { + Some(ActionExecuteObjectType::Alert) + } else if self.parse_keyword(Keyword::TASK) { + Some(ActionExecuteObjectType::Task) + } else { + None + } + } + + fn parse_action_manage_type(&mut self) -> Result { + if self.parse_keywords(&[Keyword::ACCOUNT, Keyword::SUPPORT, Keyword::CASES]) { + Ok(ActionManageType::AccountSupportCases) + } else if self.parse_keywords(&[Keyword::EVENT, Keyword::SHARING]) { + Ok(ActionManageType::EventSharing) + } else if self.parse_keywords(&[Keyword::LISTING, Keyword::AUTO, Keyword::FULFILLMENT]) { + Ok(ActionManageType::ListingAutoFulfillment) + } else if self.parse_keywords(&[Keyword::ORGANIZATION, Keyword::SUPPORT, Keyword::CASES]) { + Ok(ActionManageType::OrganizationSupportCases) + } else if self.parse_keywords(&[Keyword::USER, Keyword::SUPPORT, Keyword::CASES]) { + Ok(ActionManageType::UserSupportCases) + } else if self.parse_keyword(Keyword::GRANTS) { + Ok(ActionManageType::Grants) + } else if self.parse_keyword(Keyword::WAREHOUSES) { + Ok(ActionManageType::Warehouses) + } else { + self.expected("GRANT MANAGE type", self.peek_token()) + } + } + + fn parse_action_modify_type(&mut self) -> Result { + if self.parse_keywords(&[Keyword::LOG, Keyword::LEVEL]) { + Ok(ActionModifyType::LogLevel) + } else if self.parse_keywords(&[Keyword::TRACE, Keyword::LEVEL]) { + Ok(ActionModifyType::TraceLevel) + } else if self.parse_keywords(&[Keyword::SESSION, Keyword::LOG, Keyword::LEVEL]) { + Ok(ActionModifyType::SessionLogLevel) + } else if self.parse_keywords(&[Keyword::SESSION, Keyword::TRACE, Keyword::LEVEL]) { + Ok(ActionModifyType::SessionTraceLevel) + } else { + self.expected("GRANT MODIFY type", self.peek_token()) + } + } + + fn parse_action_monitor_type(&mut self) -> Result { + if self.parse_keyword(Keyword::EXECUTION) { + Ok(ActionMonitorType::Execution) + } else if self.parse_keyword(Keyword::SECURITY) { + Ok(ActionMonitorType::Security) + } else if self.parse_keyword(Keyword::USAGE) { + Ok(ActionMonitorType::Usage) + } else { + self.expected("GRANT MONITOR type", self.peek_token()) + } + } + pub fn parse_grantee_name(&mut self) -> Result { let mut name = self.parse_object_name(false)?; if self.dialect.supports_user_host_grantee() diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7271d6375..ebd6bef03 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8500,8 +8500,8 @@ fn parse_grant() { Action::References { columns: None }, Action::Trigger, Action::Connect, - Action::Create, - Action::Execute, + Action::Create { obj_type: None }, + Action::Execute { obj_type: None }, Action::Temporary, ], actions @@ -8616,6 +8616,7 @@ fn parse_grant() { verified_stmt("GRANT SELECT ON ALL TABLES IN SCHEMA db1.sc1 TO SHARE share1"); 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"); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 0c4bdf149..324c45e86 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3058,3 +3058,109 @@ fn test_timetravel_at_before() { snowflake() .verified_only_select("SELECT * FROM tbl BEFORE(TIMESTAMP => '2024-12-15 00:00:00')"); } + +#[test] +fn test_grant_account_privileges() { + let privileges = vec![ + "ALL", + "ALL PRIVILEGES", + "ATTACH POLICY", + "AUDIT", + "BIND SERVICE ENDPOINT", + "IMPORT SHARE", + "OVERRIDE SHARE RESTRICTIONS", + "PURCHASE DATA EXCHANGE LISTING", + "RESOLVE ALL", + "READ SESSION", + ]; + let with_grant_options = vec!["", " WITH GRANT OPTION"]; + + for p in &privileges { + for wgo in &with_grant_options { + let sql = format!("GRANT {p} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let create_object_types = vec![ + "ACCOUNT", + "APPLICATION", + "APPLICATION PACKAGE", + "COMPUTE POOL", + "DATA EXCHANGE LISTING", + "DATABASE", + "EXTERNAL VOLUME", + "FAILOVER GROUP", + "INTEGRATION", + "NETWORK POLICY", + "ORGANIZATION LISTING", + "REPLICATION GROUP", + "ROLE", + "SHARE", + "USER", + "WAREHOUSE", + ]; + for t in &create_object_types { + for wgo in &with_grant_options { + let sql = format!("GRANT CREATE {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let apply_types = vec![ + "AGGREGATION POLICY", + "AUTHENTICATION POLICY", + "JOIN POLICY", + "MASKING POLICY", + "PACKAGES POLICY", + "PASSWORD POLICY", + "PROJECTION POLICY", + "ROW ACCESS POLICY", + "SESSION POLICY", + "TAG", + ]; + for t in &apply_types { + for wgo in &with_grant_options { + let sql = format!("GRANT APPLY {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let execute_types = vec![ + "ALERT", + "DATA METRIC FUNCTION", + "MANAGED ALERT", + "MANAGED TASK", + "TASK", + ]; + for t in &execute_types { + for wgo in &with_grant_options { + let sql = format!("GRANT EXECUTE {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let manage_types = vec![ + "ACCOUNT SUPPORT CASES", + "EVENT SHARING", + "GRANTS", + "LISTING AUTO FULFILLMENT", + "ORGANIZATION SUPPORT CASES", + "USER SUPPORT CASES", + "WAREHOUSES", + ]; + for t in &manage_types { + for wgo in &with_grant_options { + let sql = format!("GRANT MANAGE {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } + + let monitor_types = vec!["EXECUTION", "SECURITY", "USAGE"]; + for t in &monitor_types { + for wgo in &with_grant_options { + let sql = format!("GRANT MONITOR {t} ON ACCOUNT TO ROLE role1{wgo}"); + snowflake_and_generic().verified_stmt(&sql); + } + } +} From c7c0de6551e09d897d42d86a7324b4b2051feb2f Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Mon, 20 Jan 2025 22:39:44 +0200 Subject: [PATCH 694/806] Add support for Create Iceberg Table statement for Snowflake parser (#1664) --- src/ast/dml.rs | 48 +++++++++++++++++- src/ast/helpers/stmt_create_table.rs | 65 +++++++++++++++++++++++- src/ast/mod.rs | 23 +++++++++ src/ast/spans.rs | 6 +++ src/dialect/snowflake.rs | 51 ++++++++++++++++++- src/keywords.rs | 7 +++ tests/sqlparser_duckdb.rs | 8 ++- tests/sqlparser_mssql.rs | 12 +++++ tests/sqlparser_postgres.rs | 6 +++ tests/sqlparser_snowflake.rs | 75 ++++++++++++++++++++++++++++ 10 files changed, 296 insertions(+), 5 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index de555c109..8cfc67414 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -36,7 +36,8 @@ use super::{ CommentDef, Expr, FileFormat, FromTable, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, RowAccessPolicy, SelectItem, Setting, SqlOption, - SqliteOnConflict, TableEngine, TableObject, TableWithJoins, Tag, WrappedCollection, + SqliteOnConflict, StorageSerializationPolicy, TableEngine, TableObject, TableWithJoins, Tag, + WrappedCollection, }; /// CREATE INDEX statement. @@ -117,6 +118,7 @@ pub struct CreateTable { pub if_not_exists: bool, pub transient: bool, pub volatile: bool, + pub iceberg: bool, /// Table name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub name: ObjectName, @@ -192,6 +194,21 @@ pub struct CreateTable { /// Snowflake "WITH TAG" clause /// pub with_tags: Option>, + /// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables + /// + pub external_volume: Option, + /// Snowflake "BASE_LOCATION" clause for Iceberg tables + /// + pub base_location: Option, + /// Snowflake "CATALOG" clause for Iceberg tables + /// + pub catalog: Option, + /// Snowflake "CATALOG_SYNC" clause for Iceberg tables + /// + pub catalog_sync: Option, + /// Snowflake "STORAGE_SERIALIZATION_POLICY" clause for Iceberg tables + /// + pub storage_serialization_policy: Option, } impl Display for CreateTable { @@ -205,7 +222,7 @@ impl Display for CreateTable { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}{iceberg}TABLE {if_not_exists}{name}", or_replace = if self.or_replace { "OR REPLACE " } else { "" }, external = if self.external { "EXTERNAL " } else { "" }, global = self.global @@ -221,6 +238,8 @@ impl Display for CreateTable { temporary = if self.temporary { "TEMPORARY " } else { "" }, transient = if self.transient { "TRANSIENT " } else { "" }, volatile = if self.volatile { "VOLATILE " } else { "" }, + // Only for Snowflake + iceberg = if self.iceberg { "ICEBERG " } else { "" }, name = self.name, )?; if let Some(on_cluster) = &self.on_cluster { @@ -382,6 +401,31 @@ impl Display for CreateTable { )?; } + if let Some(external_volume) = self.external_volume.as_ref() { + write!(f, " EXTERNAL_VOLUME = '{external_volume}'")?; + } + + if let Some(catalog) = self.catalog.as_ref() { + write!(f, " CATALOG = '{catalog}'")?; + } + + if self.iceberg { + if let Some(base_location) = self.base_location.as_ref() { + write!(f, " BASE_LOCATION = '{base_location}'")?; + } + } + + if let Some(catalog_sync) = self.catalog_sync.as_ref() { + write!(f, " CATALOG_SYNC = '{catalog_sync}'")?; + } + + if let Some(storage_serialization_policy) = self.storage_serialization_policy.as_ref() { + write!( + f, + " STORAGE_SERIALIZATION_POLICY = {storage_serialization_policy}" + )?; + } + if self.copy_grants { write!(f, " COPY GRANTS")?; } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index a3be57986..e7090cb86 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -28,7 +28,7 @@ use super::super::dml::CreateTable; use crate::ast::{ ClusteredBy, ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, - TableConstraint, TableEngine, Tag, WrappedCollection, + StorageSerializationPolicy, TableConstraint, TableEngine, Tag, WrappedCollection, }; use crate::parser::ParserError; @@ -71,6 +71,7 @@ pub struct CreateTableBuilder { pub if_not_exists: bool, pub transient: bool, pub volatile: bool, + pub iceberg: bool, pub name: ObjectName, pub columns: Vec, pub constraints: Vec, @@ -107,6 +108,11 @@ pub struct CreateTableBuilder { pub with_aggregation_policy: Option, pub with_row_access_policy: Option, pub with_tags: Option>, + pub base_location: Option, + pub external_volume: Option, + pub catalog: Option, + pub catalog_sync: Option, + pub storage_serialization_policy: Option, } impl CreateTableBuilder { @@ -119,6 +125,7 @@ impl CreateTableBuilder { if_not_exists: false, transient: false, volatile: false, + iceberg: false, name, columns: vec![], constraints: vec![], @@ -155,6 +162,11 @@ impl CreateTableBuilder { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, + base_location: None, + external_volume: None, + catalog: None, + catalog_sync: None, + storage_serialization_policy: None, } } pub fn or_replace(mut self, or_replace: bool) -> Self { @@ -192,6 +204,11 @@ impl CreateTableBuilder { self } + pub fn iceberg(mut self, iceberg: bool) -> Self { + self.iceberg = iceberg; + self + } + pub fn columns(mut self, columns: Vec) -> Self { self.columns = columns; self @@ -371,6 +388,34 @@ impl CreateTableBuilder { self } + pub fn base_location(mut self, base_location: Option) -> Self { + self.base_location = base_location; + self + } + + pub fn external_volume(mut self, external_volume: Option) -> Self { + self.external_volume = external_volume; + self + } + + pub fn catalog(mut self, catalog: Option) -> Self { + self.catalog = catalog; + self + } + + pub fn catalog_sync(mut self, catalog_sync: Option) -> Self { + self.catalog_sync = catalog_sync; + self + } + + pub fn storage_serialization_policy( + mut self, + storage_serialization_policy: Option, + ) -> Self { + self.storage_serialization_policy = storage_serialization_policy; + self + } + pub fn build(self) -> Statement { Statement::CreateTable(CreateTable { or_replace: self.or_replace, @@ -380,6 +425,7 @@ impl CreateTableBuilder { if_not_exists: self.if_not_exists, transient: self.transient, volatile: self.volatile, + iceberg: self.iceberg, name: self.name, columns: self.columns, constraints: self.constraints, @@ -416,6 +462,11 @@ impl CreateTableBuilder { with_aggregation_policy: self.with_aggregation_policy, with_row_access_policy: self.with_row_access_policy, with_tags: self.with_tags, + base_location: self.base_location, + external_volume: self.external_volume, + catalog: self.catalog, + catalog_sync: self.catalog_sync, + storage_serialization_policy: self.storage_serialization_policy, }) } } @@ -435,6 +486,7 @@ impl TryFrom for CreateTableBuilder { if_not_exists, transient, volatile, + iceberg, name, columns, constraints, @@ -471,6 +523,11 @@ impl TryFrom for CreateTableBuilder { with_aggregation_policy, with_row_access_policy, with_tags, + base_location, + external_volume, + catalog, + catalog_sync, + storage_serialization_policy, }) => Ok(Self { or_replace, temporary, @@ -505,6 +562,7 @@ impl TryFrom for CreateTableBuilder { clustered_by, options, strict, + iceberg, copy_grants, enable_schema_evolution, change_tracking, @@ -515,6 +573,11 @@ impl TryFrom for CreateTableBuilder { with_row_access_policy, with_tags, volatile, + base_location, + external_volume, + catalog, + catalog_sync, + storage_serialization_policy, }), _ => Err(ParserError::ParserError(format!( "Expected create table statement, but received: {stmt}" diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5c06d7196..2fc89e29b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8396,6 +8396,29 @@ impl fmt::Display for SessionParamValue { } } +/// Snowflake StorageSerializationPolicy for Iceberg Tables +/// ```sql +/// [ STORAGE_SERIALIZATION_POLICY = { COMPATIBLE | OPTIMIZED } ] +/// ``` +/// +/// +#[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 StorageSerializationPolicy { + Compatible, + Optimized, +} + +impl Display for StorageSerializationPolicy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StorageSerializationPolicy::Compatible => write!(f, "COMPATIBLE"), + StorageSerializationPolicy::Optimized => write!(f, "OPTIMIZED"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1ddd47d7f..acd3987da 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -532,6 +532,7 @@ impl Spanned for CreateTable { if_not_exists: _, // bool transient: _, // bool volatile: _, // bool + iceberg: _, // bool, Snowflake specific name, columns, constraints, @@ -568,6 +569,11 @@ impl Spanned for CreateTable { with_aggregation_policy: _, // todo, Snowflake specific with_row_access_policy: _, // todo, Snowflake specific with_tags: _, // todo, Snowflake specific + external_volume: _, // todo, Snowflake specific + base_location: _, // todo, Snowflake specific + catalog: _, // todo, Snowflake specific + catalog_sync: _, // todo, Snowflake specific + storage_serialization_policy: _, // todo, Snowflake specific } = self; union_spans( diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 78237acd0..88e54016d 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -37,6 +37,7 @@ use alloc::string::String; use alloc::vec::Vec; #[cfg(not(feature = "std"))] use alloc::{format, vec}; +use sqlparser::ast::StorageSerializationPolicy; use super::keywords::RESERVED_FOR_IDENTIFIER; @@ -130,16 +131,19 @@ impl Dialect for SnowflakeDialect { let mut temporary = false; let mut volatile = false; let mut transient = false; + let mut iceberg = false; match parser.parse_one_of_keywords(&[ Keyword::TEMP, Keyword::TEMPORARY, Keyword::VOLATILE, Keyword::TRANSIENT, + Keyword::ICEBERG, ]) { Some(Keyword::TEMP | Keyword::TEMPORARY) => temporary = true, Some(Keyword::VOLATILE) => volatile = true, Some(Keyword::TRANSIENT) => transient = true, + Some(Keyword::ICEBERG) => iceberg = true, _ => {} } @@ -148,7 +152,7 @@ impl Dialect for SnowflakeDialect { return Some(parse_create_stage(or_replace, temporary, parser)); } else if parser.parse_keyword(Keyword::TABLE) { return Some(parse_create_table( - or_replace, global, temporary, volatile, transient, parser, + or_replace, global, temporary, volatile, transient, iceberg, parser, )); } else { // need to go back with the cursor @@ -325,12 +329,14 @@ fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result +/// pub fn parse_create_table( or_replace: bool, global: Option, temporary: bool, volatile: bool, transient: bool, + iceberg: bool, parser: &mut Parser, ) -> Result { let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); @@ -342,6 +348,7 @@ pub fn parse_create_table( .temporary(temporary) .transient(transient) .volatile(volatile) + .iceberg(iceberg) .global(global) .hive_formats(Some(Default::default())); @@ -468,6 +475,28 @@ pub fn parse_create_table( let on_commit = Some(parser.parse_create_table_on_commit()?); builder = builder.on_commit(on_commit); } + Keyword::EXTERNAL_VOLUME => { + parser.expect_token(&Token::Eq)?; + builder.external_volume = Some(parser.parse_literal_string()?); + } + Keyword::CATALOG => { + parser.expect_token(&Token::Eq)?; + builder.catalog = Some(parser.parse_literal_string()?); + } + Keyword::BASE_LOCATION => { + parser.expect_token(&Token::Eq)?; + builder.base_location = Some(parser.parse_literal_string()?); + } + Keyword::CATALOG_SYNC => { + parser.expect_token(&Token::Eq)?; + builder.catalog_sync = Some(parser.parse_literal_string()?); + } + Keyword::STORAGE_SERIALIZATION_POLICY => { + parser.expect_token(&Token::Eq)?; + + builder.storage_serialization_policy = + Some(parse_storage_serialization_policy(parser)?); + } _ => { return parser.expected("end of statement", next_token); } @@ -502,9 +531,29 @@ pub fn parse_create_table( } } + if iceberg && builder.base_location.is_none() { + return Err(ParserError::ParserError( + "BASE_LOCATION is required for ICEBERG tables".to_string(), + )); + } + Ok(builder.build()) } +pub fn parse_storage_serialization_policy( + parser: &mut Parser, +) -> Result { + let next_token = parser.next_token(); + match &next_token.token { + Token::Word(w) => match w.keyword { + Keyword::COMPATIBLE => Ok(StorageSerializationPolicy::Compatible), + Keyword::OPTIMIZED => Ok(StorageSerializationPolicy::Optimized), + _ => parser.expected("storage_serialization_policy", next_token), + }, + _ => parser.expected("storage_serialization_policy", next_token), + } +} + pub fn parse_create_stage( or_replace: bool, temporary: bool, diff --git a/src/keywords.rs b/src/keywords.rs index 68b040c08..02ce04988 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -121,6 +121,7 @@ define_keywords!( AVRO, BACKWARD, BASE64, + BASE_LOCATION, BEFORE, BEGIN, BEGIN_FRAME, @@ -158,6 +159,7 @@ define_keywords!( CASES, CAST, CATALOG, + CATALOG_SYNC, CATCH, CEIL, CEILING, @@ -191,6 +193,7 @@ define_keywords!( COMMENT, COMMIT, COMMITTED, + COMPATIBLE, COMPRESSION, COMPUTE, CONCURRENTLY, @@ -329,6 +332,7 @@ define_keywords!( EXTENDED, EXTENSION, EXTERNAL, + EXTERNAL_VOLUME, EXTRACT, FAIL, FAILOVER, @@ -397,6 +401,7 @@ define_keywords!( HOSTS, HOUR, HOURS, + ICEBERG, ID, IDENTITY, IDENTITY_INSERT, @@ -599,6 +604,7 @@ define_keywords!( OPERATOR, OPTIMIZATION, OPTIMIZE, + OPTIMIZED, OPTIMIZER_COSTS, OPTION, OPTIONS, @@ -806,6 +812,7 @@ define_keywords!( STDOUT, STEP, STORAGE_INTEGRATION, + STORAGE_SERIALIZATION_POLICY, STORED, STRICT, STRING, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index db4ffb6f6..ca7f926a9 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -660,6 +660,7 @@ fn test_duckdb_union_datatype() { if_not_exists: Default::default(), transient: Default::default(), volatile: Default::default(), + iceberg: Default::default(), name: ObjectName(vec!["tbl1".into()]), columns: vec![ ColumnDef { @@ -737,7 +738,12 @@ fn test_duckdb_union_datatype() { default_ddl_collation: Default::default(), with_aggregation_policy: Default::default(), with_row_access_policy: Default::default(), - with_tags: Default::default() + with_tags: Default::default(), + base_location: Default::default(), + external_volume: Default::default(), + catalog: Default::default(), + catalog_sync: Default::default(), + storage_serialization_policy: Default::default(), }), stmt ); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index a0ac8a4d8..da2b6160e 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1539,6 +1539,7 @@ fn parse_create_table_with_valid_options() { clustered_by: None, options: None, strict: false, + iceberg: false, copy_grants: false, enable_schema_evolution: None, change_tracking: None, @@ -1548,6 +1549,11 @@ fn parse_create_table_with_valid_options() { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, + base_location: None, + external_volume: None, + catalog: None, + catalog_sync: None, + storage_serialization_policy: None, }) ); } @@ -1641,6 +1647,7 @@ fn parse_create_table_with_identity_column() { if_not_exists: false, transient: false, volatile: false, + iceberg: false, name: ObjectName(vec![Ident { value: "mytable".to_string(), quote_style: None, @@ -1695,6 +1702,11 @@ fn parse_create_table_with_identity_column() { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, + base_location: None, + external_volume: None, + catalog: None, + catalog_sync: None, + storage_serialization_policy: None, }), ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 864fb5eb3..0fca4cec1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5043,6 +5043,7 @@ fn parse_trigger_related_functions() { if_not_exists: false, transient: false, volatile: false, + iceberg: false, name: ObjectName(vec![Ident::new("emp")]), columns: vec![ ColumnDef { @@ -5109,6 +5110,11 @@ fn parse_trigger_related_functions() { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, + base_location: None, + external_volume: None, + catalog: None, + catalog_sync: None, + storage_serialization_policy: None, } ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 324c45e86..3320400e9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -849,6 +849,81 @@ fn test_snowflake_create_table_with_several_column_options() { } } +#[test] +fn test_snowflake_create_iceberg_table_all_options() { + match snowflake().verified_stmt("CREATE ICEBERG TABLE my_table (a INT, b INT) \ + CLUSTER BY (a, b) EXTERNAL_VOLUME = 'volume' CATALOG = 'SNOWFLAKE' BASE_LOCATION = 'relative/path' CATALOG_SYNC = 'OPEN_CATALOG' \ + STORAGE_SERIALIZATION_POLICY = COMPATIBLE COPY GRANTS CHANGE_TRACKING=TRUE DATA_RETENTION_TIME_IN_DAYS=5 MAX_DATA_EXTENSION_TIME_IN_DAYS=10 \ + WITH AGGREGATION POLICY policy_name WITH ROW ACCESS POLICY policy_name ON (a) WITH TAG (A='TAG A', B='TAG B')") { + Statement::CreateTable(CreateTable { + name, cluster_by, base_location, + external_volume, catalog, catalog_sync, + storage_serialization_policy, change_tracking, + copy_grants, data_retention_time_in_days, + max_data_extension_time_in_days, with_aggregation_policy, + with_row_access_policy, with_tags, .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!( + Some(WrappedCollection::Parentheses(vec![ + Ident::new("a"), + Ident::new("b"), + ])), + cluster_by + ); + assert_eq!("relative/path", base_location.unwrap()); + assert_eq!("volume", external_volume.unwrap()); + assert_eq!("SNOWFLAKE", catalog.unwrap()); + assert_eq!("OPEN_CATALOG", catalog_sync.unwrap()); + assert_eq!(StorageSerializationPolicy::Compatible, storage_serialization_policy.unwrap()); + assert!(change_tracking.unwrap()); + assert!(copy_grants); + assert_eq!(Some(5), data_retention_time_in_days); + assert_eq!(Some(10), max_data_extension_time_in_days); + assert_eq!( + Some("WITH ROW ACCESS POLICY policy_name ON (a)".to_string()), + with_row_access_policy.map(|policy| policy.to_string()) + ); + assert_eq!( + Some("policy_name".to_string()), + 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()), + ]), with_tags); + + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_iceberg_table() { + match snowflake() + .verified_stmt("CREATE ICEBERG TABLE my_table (a INT) BASE_LOCATION = 'relative_path'") + { + Statement::CreateTable(CreateTable { + name, + base_location, + .. + }) => { + assert_eq!("my_table", name.to_string()); + assert_eq!("relative_path", base_location.unwrap()); + } + _ => unreachable!(), + } +} + +#[test] +fn test_snowflake_create_iceberg_table_without_location() { + let res = snowflake().parse_sql_statements("CREATE ICEBERG TABLE my_table (a INT)"); + assert_eq!( + ParserError::ParserError("BASE_LOCATION is required for ICEBERG tables".to_string()), + res.unwrap_err() + ); +} + #[test] fn parse_sf_create_or_replace_view_with_comment_missing_equal() { assert!(snowflake_and_generic() From 4f7154288e0d6f3b324aee4a262babe5872191fc Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Thu, 23 Jan 2025 17:16:53 +0100 Subject: [PATCH 695/806] National strings: check if dialect supports backslash escape (#1672) --- src/test_utils.rs | 14 +++++++++++++- src/tokenizer.rs | 32 +++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 914be7d9f..51e4fd748 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -33,7 +33,7 @@ use core::fmt::Debug; use crate::dialect::*; use crate::parser::{Parser, ParserError}; -use crate::tokenizer::Tokenizer; +use crate::tokenizer::{Token, Tokenizer}; use crate::{ast::*, parser::ParserOptions}; #[cfg(test)] @@ -237,6 +237,18 @@ impl TestedDialects { pub fn verified_expr(&self, sql: &str) -> Expr { self.expr_parses_to(sql, sql) } + + /// Check that the tokenizer returns the expected tokens for the given SQL. + pub fn tokenizes_to(&self, sql: &str, expected: Vec) { + self.dialects.iter().for_each(|dialect| { + let mut tokenizer = Tokenizer::new(&**dialect, sql); + if let Some(options) = &self.options { + tokenizer = tokenizer.with_unescape(options.unescape); + } + let tokens = tokenizer.tokenize().unwrap(); + assert_eq!(expected, tokens); + }); + } } /// Returns all available dialects. diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 39ca84c9f..08e233b66 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -971,7 +971,10 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('\'') => { // N'...' - a - let s = self.tokenize_single_quoted_string(chars, '\'', true)?; + let backslash_escape = + self.dialect.supports_string_literal_backslash_escape(); + let s = + self.tokenize_single_quoted_string(chars, '\'', backslash_escape)?; Ok(Some(Token::NationalStringLiteral(s))) } _ => { @@ -2155,6 +2158,7 @@ mod tests { use crate::dialect::{ BigQueryDialect, ClickHouseDialect, HiveDialect, MsSqlDialect, MySqlDialect, SQLiteDialect, }; + use crate::test_utils::all_dialects_where; use core::fmt::Debug; #[test] @@ -3543,4 +3547,30 @@ mod tests { ]; compare(expected, tokens); } + + #[test] + fn test_national_strings_backslash_escape_not_supported() { + all_dialects_where(|dialect| !dialect.supports_string_literal_backslash_escape()) + .tokenizes_to( + "select n'''''\\'", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::NationalStringLiteral("''\\".to_string()), + ], + ); + } + + #[test] + fn test_national_strings_backslash_escape_supported() { + all_dialects_where(|dialect| dialect.supports_string_literal_backslash_escape()) + .tokenizes_to( + "select n'''''\\''", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::NationalStringLiteral("'''".to_string()), + ], + ); + } } From ef072be9e1b1ecbf8032bd2040131a9d5b00de5d Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 24 Jan 2025 09:02:53 +0100 Subject: [PATCH 696/806] Only support escape literals for Postgres, Redshift and generic dialect (#1674) --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 7 ++++++ src/dialect/postgresql.rs | 4 ++++ src/dialect/redshift.rs | 4 ++++ src/test_utils.rs | 6 ++++- src/tokenizer.rs | 46 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index d696861b5..4021b5753 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -139,4 +139,8 @@ impl Dialect for GenericDialect { fn supports_user_host_grantee(&self) -> bool { true } + + fn supports_string_escape_constant(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 119bb3cf7..79260326b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -840,6 +840,13 @@ pub trait Dialect: Debug + Any { fn supports_timestamp_versioning(&self) -> bool { false } + + /// Returns true if this dialect supports the E'...' syntax for string literals + /// + /// Postgres: + fn supports_string_escape_constant(&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 170b0a7c9..d4f2a032e 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -245,6 +245,10 @@ impl Dialect for PostgreSqlDialect { fn supports_nested_comments(&self) -> bool { true } + + fn supports_string_escape_constant(&self) -> bool { + true + } } pub fn parse_create(parser: &mut Parser) -> Option> { diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 55405ba53..a4522bbf8 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -109,4 +109,8 @@ impl Dialect for RedshiftSqlDialect { fn supports_partiql(&self) -> bool { true } + + fn supports_string_escape_constant(&self) -> bool { + true + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 51e4fd748..1c322f654 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -240,13 +240,17 @@ impl TestedDialects { /// Check that the tokenizer returns the expected tokens for the given SQL. pub fn tokenizes_to(&self, sql: &str, expected: Vec) { + if self.dialects.is_empty() { + panic!("No dialects to test"); + } + self.dialects.iter().for_each(|dialect| { let mut tokenizer = Tokenizer::new(&**dialect, sql); if let Some(options) = &self.options { tokenizer = tokenizer.with_unescape(options.unescape); } let tokens = tokenizer.tokenize().unwrap(); - assert_eq!(expected, tokens); + assert_eq!(expected, tokens, "Tokenized differently for {:?}", dialect); }); } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 08e233b66..309f09d81 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -985,7 +985,7 @@ impl<'a> Tokenizer<'a> { } } // PostgreSQL accepts "escape" string constants, which are an extension to the SQL standard. - x @ 'e' | x @ 'E' => { + x @ 'e' | x @ 'E' if self.dialect.supports_string_escape_constant() => { let starting_loc = chars.location(); chars.next(); // consume, to check the next char match chars.peek() { @@ -3573,4 +3573,48 @@ mod tests { ], ); } + + #[test] + fn test_string_escape_constant_not_supported() { + all_dialects_where(|dialect| !dialect.supports_string_escape_constant()).tokenizes_to( + "select e'...'", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::make_word("e", None), + Token::SingleQuotedString("...".to_string()), + ], + ); + + all_dialects_where(|dialect| !dialect.supports_string_escape_constant()).tokenizes_to( + "select E'...'", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::make_word("E", None), + Token::SingleQuotedString("...".to_string()), + ], + ); + } + + #[test] + fn test_string_escape_constant_supported() { + all_dialects_where(|dialect| dialect.supports_string_escape_constant()).tokenizes_to( + "select e'\\''", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::EscapedStringLiteral("'".to_string()), + ], + ); + + all_dialects_where(|dialect| dialect.supports_string_escape_constant()).tokenizes_to( + "select E'\\''", + vec![ + Token::make_keyword("select"), + Token::Whitespace(Whitespace::Space), + Token::EscapedStringLiteral("'".to_string()), + ], + ); + } } From fd6c98e9332cfe80f44acbe3a994309065c70946 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Sat, 25 Jan 2025 16:01:33 +0100 Subject: [PATCH 697/806] BigQuery: Support trailing commas in column definitions list (#1682) --- src/dialect/bigquery.rs | 5 ++++ src/dialect/mod.rs | 9 +++++- src/parser/mod.rs | 12 ++++++-- tests/sqlparser_common.rs | 59 ++++++++++++++++++++++++++++----------- 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index e92169a35..716174391 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -31,6 +31,11 @@ impl Dialect for BigQueryDialect { true } + /// See + fn supports_column_definition_trailing_commas(&self) -> bool { + true + } + fn is_identifier_start(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 79260326b..9fc16cd56 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -405,11 +405,18 @@ pub trait Dialect: Debug + Any { } /// Returns true if the dialect supports trailing commas in the `FROM` clause of a `SELECT` statement. - /// /// Example: `SELECT 1 FROM T, U, LIMIT 1` + /// Example: `SELECT 1 FROM T, U, LIMIT 1` fn supports_from_trailing_commas(&self) -> bool { false } + /// Returns true if the dialect supports trailing commas in the + /// column definitions list of a `CREATE` statement. + /// Example: `CREATE TABLE T (x INT, y TEXT,)` + fn supports_column_definition_trailing_commas(&self) -> bool { + false + } + /// Returns true if the dialect supports double dot notation for object names /// /// Example diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 355e520ab..c5b222acf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6718,7 +6718,11 @@ impl<'a> Parser<'a> { return self.expected("',' or ')' after column definition", self.peek_token()); }; - if rparen && (!comma || self.options.trailing_commas) { + if rparen + && (!comma + || self.dialect.supports_column_definition_trailing_commas() + || self.options.trailing_commas) + { let _ = self.consume_token(&Token::RParen); break; } @@ -9298,7 +9302,11 @@ impl<'a> Parser<'a> { self.next_token(); Ok(vec![]) } else { - let cols = self.parse_comma_separated(Parser::parse_view_column)?; + let cols = self.parse_comma_separated_with_trailing_commas( + Parser::parse_view_column, + self.dialect.supports_column_definition_trailing_commas(), + None, + )?; self.expect_token(&Token::RParen)?; Ok(cols) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ebd6bef03..e1ef2f909 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10189,15 +10189,19 @@ fn parse_trailing_comma() { "Expected: column name or constraint definition, found: )".to_string() ) ); + + let unsupported_dialects = all_dialects_where(|d| !d.supports_trailing_commas()); + assert_eq!( + unsupported_dialects + .parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,") + .unwrap_err(), + ParserError::ParserError("Expected: an expression, found: EOF".to_string()) + ); } #[test] fn parse_projection_trailing_comma() { - // Some dialects allow trailing commas only in the projection - let trailing_commas = TestedDialects::new(vec![ - Box::new(SnowflakeDialect {}), - Box::new(BigQueryDialect {}), - ]); + let trailing_commas = all_dialects_where(|d| d.supports_projection_trailing_commas()); trailing_commas.one_statement_parses_to( "SELECT album_id, name, FROM track", @@ -10210,20 +10214,14 @@ fn parse_projection_trailing_comma() { trailing_commas.verified_stmt("SELECT DISTINCT ON (album_id) name FROM track"); + let unsupported_dialects = all_dialects_where(|d| { + !d.supports_projection_trailing_commas() && !d.supports_trailing_commas() + }); assert_eq!( - trailing_commas - .parse_sql_statements("SELECT * FROM track ORDER BY milliseconds,") + unsupported_dialects + .parse_sql_statements("SELECT album_id, name, FROM track") .unwrap_err(), - ParserError::ParserError("Expected: an expression, found: EOF".to_string()) - ); - - assert_eq!( - trailing_commas - .parse_sql_statements("CREATE TABLE employees (name text, age int,)") - .unwrap_err(), - ParserError::ParserError( - "Expected: column name or constraint definition, found: )".to_string() - ), + ParserError::ParserError("Expected an expression, found: FROM".to_string()) ); } @@ -13061,6 +13059,33 @@ fn parse_overlaps() { verified_stmt("SELECT (DATE '2016-01-10', DATE '2016-02-01') OVERLAPS (DATE '2016-01-20', DATE '2016-02-10')"); } +#[test] +fn parse_column_definition_trailing_commas() { + let dialects = all_dialects_where(|d| d.supports_column_definition_trailing_commas()); + + dialects.one_statement_parses_to("CREATE TABLE T (x INT64,)", "CREATE TABLE T (x INT64)"); + dialects.one_statement_parses_to( + "CREATE TABLE T (x INT64, y INT64, )", + "CREATE TABLE T (x INT64, y INT64)", + ); + dialects.one_statement_parses_to( + "CREATE VIEW T (x, y, ) AS SELECT 1", + "CREATE VIEW T (x, y) AS SELECT 1", + ); + + let unsupported_dialects = all_dialects_where(|d| { + !d.supports_projection_trailing_commas() && !d.supports_trailing_commas() + }); + assert_eq!( + unsupported_dialects + .parse_sql_statements("CREATE TABLE employees (name text, age int,)") + .unwrap_err(), + ParserError::ParserError( + "Expected: column name or constraint definition, found: )".to_string() + ), + ); +} + #[test] fn test_trailing_commas_in_from() { let dialects = all_dialects_where(|d| d.supports_from_trailing_commas()); From 211b15e790328272b0cf4ffd01f477f75fae7d42 Mon Sep 17 00:00:00 2001 From: Ayman Elkfrawy <120422207+ayman-sigma@users.noreply.github.com> Date: Sun, 26 Jan 2025 06:13:51 -0800 Subject: [PATCH 698/806] Enhance object name path segments (#1539) --- README.md | 2 +- src/ast/helpers/stmt_create_table.rs | 4 +- src/ast/mod.rs | 32 ++- src/ast/spans.rs | 40 ++-- src/ast/visitor.rs | 6 +- src/dialect/snowflake.rs | 2 +- src/parser/mod.rs | 93 +++++---- src/test_utils.rs | 6 +- tests/sqlparser_bigquery.rs | 31 +-- tests/sqlparser_clickhouse.rs | 39 ++-- tests/sqlparser_common.rs | 293 ++++++++++++++------------- tests/sqlparser_databricks.rs | 24 ++- tests/sqlparser_duckdb.rs | 24 +-- tests/sqlparser_hive.rs | 13 +- tests/sqlparser_mssql.rs | 44 ++-- tests/sqlparser_mysql.rs | 103 +++++----- tests/sqlparser_postgres.rs | 199 ++++++++++-------- tests/sqlparser_redshift.rs | 17 +- tests/sqlparser_snowflake.rs | 70 ++++--- tests/sqlparser_sqlite.rs | 8 +- 20 files changed, 584 insertions(+), 466 deletions(-) diff --git a/README.md b/README.md index 41a44d3d7..d18a76b50 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ println!("AST: {:?}", ast); This outputs ```rust -AST: [Query(Query { ctes: [], body: Select(Select { distinct: false, projection: [UnnamedExpr(Identifier("a")), UnnamedExpr(Identifier("b")), UnnamedExpr(Value(Long(123))), UnnamedExpr(Function(Function { name: ObjectName(["myfunc"]), args: [Identifier("b")], filter: None, over: None, distinct: false }))], from: [TableWithJoins { relation: Table { name: ObjectName(["table_1"]), alias: None, args: [], with_hints: [] }, joins: [] }], selection: Some(BinaryOp { left: BinaryOp { left: Identifier("a"), op: Gt, right: Identifier("b") }, op: And, right: BinaryOp { left: Identifier("b"), op: Lt, right: Value(Long(100)) } }), group_by: [], having: None }), order_by: [OrderByExpr { expr: Identifier("a"), asc: Some(false) }, OrderByExpr { expr: Identifier("b"), asc: None }], limit: None, offset: None, fetch: None })] +AST: [Query(Query { ctes: [], body: Select(Select { distinct: false, projection: [UnnamedExpr(Identifier("a")), UnnamedExpr(Identifier("b")), UnnamedExpr(Value(Long(123))), UnnamedExpr(Function(Function { name:ObjectName([Identifier(Ident { value: "myfunc", quote_style: None })]), args: [Identifier("b")], filter: None, over: None, distinct: false }))], from: [TableWithJoins { relation: Table { name: ObjectName([Identifier(Ident { value: "table_1", quote_style: None })]), alias: None, args: [], with_hints: [] }, joins: [] }], selection: Some(BinaryOp { left: BinaryOp { left: Identifier("a"), op: Gt, right: Identifier("b") }, op: And, right: BinaryOp { left: Identifier("b"), op: Lt, right: Value(Long(100)) } }), group_by: [], having: None }), order_by: [OrderByExpr { expr: Identifier("a"), asc: Some(false) }, OrderByExpr { expr: Identifier("b"), asc: None }], limit: None, offset: None, fetch: None })] ``` diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index e7090cb86..2a44cef3e 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -42,7 +42,7 @@ use crate::parser::ParserError; /// ```rust /// use sqlparser::ast::helpers::stmt_create_table::CreateTableBuilder; /// use sqlparser::ast::{ColumnDef, DataType, Ident, ObjectName}; -/// let builder = CreateTableBuilder::new(ObjectName(vec![Ident::new("table_name")])) +/// let builder = CreateTableBuilder::new(ObjectName::from(vec![Ident::new("table_name")])) /// .if_not_exists(true) /// .columns(vec![ColumnDef { /// name: Ident::new("c1"), @@ -602,7 +602,7 @@ mod tests { #[test] pub fn test_from_valid_statement() { - let builder = CreateTableBuilder::new(ObjectName(vec![Ident::new("table_name")])); + let builder = CreateTableBuilder::new(ObjectName::from(vec![Ident::new("table_name")])); let stmt = builder.clone().build(); diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2fc89e29b..b473dc11f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -267,7 +267,13 @@ impl fmt::Display for Ident { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct ObjectName(pub Vec); +pub struct ObjectName(pub Vec); + +impl From> for ObjectName { + fn from(idents: Vec) -> Self { + ObjectName(idents.into_iter().map(ObjectNamePart::Identifier).collect()) + } +} impl fmt::Display for ObjectName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -275,6 +281,30 @@ impl fmt::Display for ObjectName { } } +/// A single part of an ObjectName +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ObjectNamePart { + Identifier(Ident), +} + +impl ObjectNamePart { + pub fn as_ident(&self) -> Option<&Ident> { + match self { + ObjectNamePart::Identifier(ident) => Some(ident), + } + } +} + +impl fmt::Display for ObjectNamePart { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ObjectNamePart::Identifier(ident) => write!(f, "{}", ident), + } + } +} + /// 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 acd3987da..5316bfbda 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -28,13 +28,13 @@ use super::{ FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, MatchRecognizePattern, - Measure, NamedWindowDefinition, ObjectName, Offset, OnConflict, OnConflictAction, OnInsert, - OrderBy, OrderByExpr, 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, + Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, + OnConflictAction, OnInsert, OrderBy, OrderByExpr, 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. @@ -1358,7 +1358,7 @@ impl Spanned for Expr { .union_opt(&overlay_for.as_ref().map(|i| i.span())), Expr::Collate { expr, collation } => expr .span() - .union(&union_spans(collation.0.iter().map(|i| i.span))), + .union(&union_spans(collation.0.iter().map(|i| i.span()))), Expr::Nested(expr) => expr.span(), Expr::Value(value) => value.span(), Expr::TypedString { .. } => Span::empty(), @@ -1462,7 +1462,7 @@ impl Spanned for Expr { object_name .0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(iter::once(token.0.span)), ), Expr::OuterJoin(expr) => expr.span(), @@ -1507,7 +1507,15 @@ impl Spanned for ObjectName { fn span(&self) -> Span { let ObjectName(segments) = self; - union_spans(segments.iter().map(|i| i.span)) + union_spans(segments.iter().map(|i| i.span())) + } +} + +impl Spanned for ObjectNamePart { + fn span(&self) -> Span { + match self { + ObjectNamePart::Identifier(ident) => ident.span, + } } } @@ -1538,7 +1546,7 @@ impl Spanned for Function { union_spans( name.0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(iter::once(args.span())) .chain(iter::once(parameters.span())) .chain(filter.iter().map(|i| i.span())) @@ -1624,7 +1632,7 @@ impl Spanned for SelectItem { object_name .0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(iter::once(wildcard_additional_options.span())), ), SelectItem::Wildcard(wildcard_additional_options) => wildcard_additional_options.span(), @@ -1734,7 +1742,7 @@ impl Spanned for TableFactor { } => union_spans( name.0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(alias.as_ref().map(|alias| { union_spans( iter::once(alias.name.span) @@ -1779,7 +1787,7 @@ impl Spanned for TableFactor { } => union_spans( name.0 .iter() - .map(|i| i.span) + .map(|i| i.span()) .chain(args.iter().map(|i| i.span())) .chain(alias.as_ref().map(|alias| alias.span())), ), @@ -1930,7 +1938,7 @@ impl Spanned for FunctionArgExpr { match self { FunctionArgExpr::Expr(expr) => expr.span(), FunctionArgExpr::QualifiedWildcard(object_name) => { - union_spans(object_name.0.iter().map(|i| i.span)) + union_spans(object_name.0.iter().map(|i| i.span())) } FunctionArgExpr::Wildcard => Span::empty(), } @@ -2141,7 +2149,7 @@ impl Spanned for TableObject { fn span(&self) -> Span { match self { TableObject::TableName(ObjectName(segments)) => { - union_spans(segments.iter().map(|i| i.span)) + union_spans(segments.iter().map(|i| i.span())) } TableObject::TableFunction(func) => func.span(), } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index c824ad2f3..457dbbaed 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -403,7 +403,7 @@ where /// ``` /// # use sqlparser::parser::Parser; /// # use sqlparser::dialect::GenericDialect; -/// # use sqlparser::ast::{ObjectName, visit_relations_mut}; +/// # use sqlparser::ast::{ObjectName, ObjectNamePart, Ident, visit_relations_mut}; /// # use core::ops::ControlFlow; /// let sql = "SELECT a FROM foo"; /// let mut statements = Parser::parse_sql(&GenericDialect{}, sql) @@ -411,7 +411,7 @@ where /// /// // visit statements, renaming table foo to bar /// visit_relations_mut(&mut statements, |table| { -/// table.0[0].value = table.0[0].value.replace("foo", "bar"); +/// table.0[0] = ObjectNamePart::Identifier(Ident::new("bar")); /// ControlFlow::<()>::Continue(()) /// }); /// @@ -529,7 +529,7 @@ where /// if matches!(expr, Expr::Identifier(col_name) if col_name.value == "x") { /// let old_expr = std::mem::replace(expr, Expr::Value(Value::Null)); /// *expr = Expr::Function(Function { -/// name: ObjectName(vec![Ident::new("f")]), +/// name: ObjectName::from(vec![Ident::new("f")]), /// uses_odbc_syntax: false, /// args: FunctionArguments::List(FunctionArgumentList { /// duplicate_treatment: None, diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 88e54016d..bd9afb191 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -651,7 +651,7 @@ pub fn parse_snowflake_stage_name(parser: &mut Parser) -> Result { parser.prev_token(); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c5b222acf..9cc8f0620 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -988,7 +988,7 @@ impl<'a> Parser<'a> { } Token::Mul => { return Ok(Expr::QualifiedWildcard( - ObjectName(id_parts), + ObjectName::from(id_parts), AttachedToken(next_token), )); } @@ -1128,7 +1128,7 @@ impl<'a> Parser<'a> { if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.clone().into_ident(w_span)]), + name: ObjectName::from(vec![w.clone().into_ident(w_span)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -1143,7 +1143,7 @@ impl<'a> Parser<'a> { | Keyword::CURRENT_DATE | Keyword::LOCALTIME | Keyword::LOCALTIMESTAMP => { - Ok(Some(self.parse_time_functions(ObjectName(vec![w.clone().into_ident(w_span)]))?)) + Ok(Some(self.parse_time_functions(ObjectName::from(vec![w.clone().into_ident(w_span)]))?)) } Keyword::CASE => Ok(Some(self.parse_case_expr()?)), Keyword::CONVERT => Ok(Some(self.parse_convert_expr(false)?)), @@ -1187,7 +1187,7 @@ impl<'a> Parser<'a> { let query = self.parse_query()?; self.expect_token(&Token::RParen)?; Ok(Some(Expr::Function(Function { - name: ObjectName(vec![w.clone().into_ident(w_span)]), + name: ObjectName::from(vec![w.clone().into_ident(w_span)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::Subquery(query), @@ -1232,7 +1232,7 @@ impl<'a> Parser<'a> { if let Some(expr) = self.parse_outer_join_expr(&id_parts) { Ok(expr) } else { - let mut expr = self.parse_function(ObjectName(id_parts))?; + let mut expr = self.parse_function(ObjectName::from(id_parts))?; // consume all period if it's a method chain expr = self.try_parse_method(expr)?; let fields = vec![]; @@ -1553,7 +1553,7 @@ impl<'a> Parser<'a> { return self.expected("an identifier or a '*' after '.'", self.peek_token()); }; Ok(Expr::QualifiedWildcard( - ObjectName(Self::exprs_to_idents(root, chain)?), + ObjectName::from(Self::exprs_to_idents(root, chain)?), AttachedToken(wildcard_token), )) } else if self.peek_token().token == Token::LParen { @@ -1566,7 +1566,7 @@ impl<'a> Parser<'a> { if let Some(expr) = self.parse_outer_join_expr(&id_parts) { Ok(expr) } else { - self.parse_function(ObjectName(id_parts)) + self.parse_function(ObjectName::from(id_parts)) } } else { if Self::is_all_ident(&root, &chain) { @@ -1694,7 +1694,7 @@ impl<'a> Parser<'a> { Token::Word(word) => word.into_ident(tok.span), _ => return p.expected("identifier", tok), }; - let func = match p.parse_function(ObjectName(vec![name]))? { + let func = match p.parse_function(ObjectName::from(vec![name]))? { Expr::Function(func) => func, _ => return p.expected("function", p.peek_token()), }; @@ -2197,7 +2197,7 @@ impl<'a> Parser<'a> { Some(expr) => Ok(expr), // Snowflake supports `position` as an ordinary function call // without the special `IN` syntax. - None => self.parse_function(ObjectName(vec![ident])), + None => self.parse_function(ObjectName::from(vec![ident])), } } @@ -4044,6 +4044,21 @@ impl<'a> Parser<'a> { Ok(values) } + /// Parse a period-separated list of 1+ items accepted by `F` + fn parse_period_separated(&mut self, mut f: F) -> Result, ParserError> + where + F: FnMut(&mut Parser<'a>) -> Result, + { + let mut values = vec![]; + loop { + values.push(f(self)?); + if !self.consume_token(&Token::Period) { + break; + } + } + Ok(values) + } + /// Parse a keyword-separated list of 1+ items accepted by `F` pub fn parse_keyword_separated( &mut self, @@ -4757,7 +4772,9 @@ impl<'a> Parser<'a> { let mut data_type = self.parse_data_type()?; if let DataType::Custom(n, _) = &data_type { // the first token is actually a name - name = Some(n.0[0].clone()); + match n.0[0].clone() { + ObjectNamePart::Identifier(ident) => name = Some(ident), + } data_type = self.parse_data_type()?; } @@ -9063,7 +9080,7 @@ impl<'a> Parser<'a> { } } } - Ok(ObjectName(idents)) + Ok(ObjectName::from(idents)) } /// Parse a possibly qualified, possibly quoted identifier, e.g. @@ -9079,20 +9096,26 @@ impl<'a> Parser<'a> { // 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(|ident| ident.value.contains('.')) + && idents.iter().any(|part| { + part.as_ident() + .is_some_and(|ident| ident.value.contains('.')) + }) { idents = idents .into_iter() - .flat_map(|ident| { - ident + .flat_map(|part| match part.as_ident() { + Some(ident) => ident .value .split('.') - .map(|value| Ident { - value: value.into(), - quote_style: ident.quote_style, - span: ident.span, + .map(|value| { + ObjectNamePart::Identifier(Ident { + value: value.into(), + quote_style: ident.quote_style, + span: ident.span, + }) }) - .collect::>() + .collect::>(), + None => vec![part], }) .collect() } @@ -10427,14 +10450,14 @@ impl<'a> Parser<'a> { } let variables = if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) { - OneOrManyWithParens::One(ObjectName(vec!["TIMEZONE".into()])) + OneOrManyWithParens::One(ObjectName::from(vec!["TIMEZONE".into()])) } else if self.dialect.supports_parenthesized_set_variables() && self.consume_token(&Token::LParen) { let variables = OneOrManyWithParens::Many( self.parse_comma_separated(|parser: &mut Parser<'a>| parser.parse_identifier())? .into_iter() - .map(|ident| ObjectName(vec![ident])) + .map(|ident| ObjectName::from(vec![ident])) .collect(), ); self.expect_token(&Token::RParen)?; @@ -11770,7 +11793,7 @@ impl<'a> Parser<'a> { Token::Word(w) => Ok(w.value), _ => self.expected("a function identifier", self.peek_token()), }?; - let expr = self.parse_function(ObjectName(vec![Ident::new(function_name)]))?; + let expr = self.parse_function(ObjectName::from(vec![Ident::new(function_name)]))?; let alias = if self.parse_keyword(Keyword::AS) { Some(self.parse_identifier()?) } else { @@ -11819,7 +11842,7 @@ impl<'a> Parser<'a> { 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_object_name(false)?.0; + let value_column = self.parse_period_separated(|p| p.parse_identifier())?; self.expect_keyword_is(Keyword::IN)?; self.expect_token(&Token::LParen)?; @@ -11955,10 +11978,9 @@ impl<'a> Parser<'a> { // https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-native-idp.html let ident = self.parse_identifier()?; if let GranteeName::ObjectName(namespace) = name { - name = GranteeName::ObjectName(ObjectName(vec![Ident::new(format!( - "{}:{}", - namespace, ident - ))])); + name = GranteeName::ObjectName(ObjectName::from(vec![Ident::new( + format!("{}:{}", namespace, ident), + )])); }; } Grantee { @@ -12267,9 +12289,10 @@ impl<'a> Parser<'a> { let mut name = self.parse_object_name(false)?; if self.dialect.supports_user_host_grantee() && name.0.len() == 1 + && name.0[0].as_ident().is_some() && self.consume_token(&Token::AtSign) { - let user = name.0.pop().unwrap(); + let user = name.0.pop().unwrap().as_ident().unwrap().clone(); let host = self.parse_identifier()?; Ok(GranteeName::UserHost { user, host }) } else { @@ -13781,7 +13804,7 @@ impl<'a> Parser<'a> { // [ OWNED BY { table_name.column_name | NONE } ] let owned_by = if self.parse_keywords(&[Keyword::OWNED, Keyword::BY]) { if self.parse_keywords(&[Keyword::NONE]) { - Some(ObjectName(vec![Ident::new("NONE")])) + Some(ObjectName::from(vec![Ident::new("NONE")])) } else { Some(self.parse_object_name(false)?) } @@ -14072,7 +14095,9 @@ impl<'a> Parser<'a> { .parse_one_of_keywords(&[Keyword::FROM, Keyword::IN]) .is_some() { - parent_name.0.insert(0, self.parse_identifier()?); + parent_name + .0 + .insert(0, ObjectNamePart::Identifier(self.parse_identifier()?)); } (None, Some(parent_name)) } @@ -14388,14 +14413,14 @@ mod tests { test_parse_data_type!( dialect, "GEOMETRY", - DataType::Custom(ObjectName(vec!["GEOMETRY".into()]), vec![]) + DataType::Custom(ObjectName::from(vec!["GEOMETRY".into()]), vec![]) ); test_parse_data_type!( dialect, "GEOMETRY(POINT)", DataType::Custom( - ObjectName(vec!["GEOMETRY".into()]), + ObjectName::from(vec!["GEOMETRY".into()]), vec!["POINT".to_string()] ) ); @@ -14404,7 +14429,7 @@ mod tests { dialect, "GEOMETRY(POINT, 4326)", DataType::Custom( - ObjectName(vec!["GEOMETRY".into()]), + ObjectName::from(vec!["GEOMETRY".into()]), vec!["POINT".to_string(), "4326".to_string()] ) ); @@ -14540,7 +14565,7 @@ mod tests { }}; } - let dummy_name = ObjectName(vec![Ident::new("dummy_name")]); + let dummy_name = ObjectName::from(vec![Ident::new("dummy_name")]); let dummy_authorization = Ident::new("dummy_authorization"); test_parse_schema_name!( diff --git a/src/test_utils.rs b/src/test_utils.rs index 1c322f654..f2e3adf09 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -353,7 +353,7 @@ pub fn table_alias(name: impl Into) -> Option { pub fn table(name: impl Into) -> TableFactor { TableFactor::Table { - name: ObjectName(vec![Ident::new(name.into())]), + name: ObjectName::from(vec![Ident::new(name.into())]), alias: None, args: None, with_hints: vec![], @@ -381,7 +381,7 @@ pub fn table_from_name(name: ObjectName) -> TableFactor { pub fn table_with_alias(name: impl Into, alias: impl Into) -> TableFactor { TableFactor::Table { - name: ObjectName(vec![Ident::new(name)]), + name: ObjectName::from(vec![Ident::new(name)]), alias: Some(TableAlias { name: Ident::new(alias), columns: vec![], @@ -406,7 +406,7 @@ pub fn join(relation: TableFactor) -> Join { pub fn call(function: &str, args: impl IntoIterator) -> Expr { Expr::Function(Function { - name: ObjectName(vec![Ident::new(function)]), + name: ObjectName::from(vec![Ident::new(function)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index a173a6cc9..cbb963761 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -222,7 +222,7 @@ fn parse_delete_statement() { .. }) => { assert_eq!( - table_from_name(ObjectName(vec![Ident::with_quote('"', "table")])), + table_from_name(ObjectName::from(vec![Ident::with_quote('"', "table")])), from[0].relation ); } @@ -249,7 +249,7 @@ fn parse_create_view_with_options() { } => { assert_eq!( name, - ObjectName(vec![ + ObjectName::from(vec![ "myproject".into(), "mydataset".into(), "newview".into() @@ -356,7 +356,7 @@ fn parse_create_table_with_unquoted_hyphen() { Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!( name, - ObjectName(vec![ + ObjectName::from(vec![ "my-pro-ject".into(), "mydataset".into(), "mytable".into() @@ -397,7 +397,7 @@ fn parse_create_table_with_options() { }) => { assert_eq!( name, - ObjectName(vec!["mydataset".into(), "newtable".into()]) + ObjectName::from(vec!["mydataset".into(), "newtable".into()]) ); assert_eq!( vec![ @@ -486,7 +486,7 @@ fn parse_nested_data_types() { let sql = "CREATE TABLE table (x STRUCT, b BYTES(42)>, y ARRAY>)"; match bigquery_and_generic().one_statement_parses_to(sql, sql) { Statement::CreateTable(CreateTable { name, columns, .. }) => { - assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!(name, ObjectName::from(vec!["table".into()])); assert_eq!( columns, vec![ @@ -1376,7 +1376,7 @@ fn parse_table_identifiers() { assert_eq!( select.from, vec![TableWithJoins { - relation: table_from_name(ObjectName(expected)), + relation: table_from_name(ObjectName::from(expected)), joins: vec![] },] ); @@ -1518,7 +1518,10 @@ fn parse_hyphenated_table_identifiers() { ) .from[0] .relation, - table_from_name(ObjectName(vec![Ident::new("foo-123"), Ident::new("bar")])), + table_from_name(ObjectName::from(vec![ + Ident::new("foo-123"), + Ident::new("bar") + ])), ); assert_eq!( @@ -1551,7 +1554,7 @@ fn parse_table_time_travel() { select.from, vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t1")]), + name: ObjectName::from(vec![Ident::new("t1")]), alias: None, args: None, with_hints: vec![], @@ -1630,11 +1633,11 @@ fn parse_merge() { let update_action = MergeAction::Update { assignments: vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("a")])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("a")])), value: Expr::Value(number("1")), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("b")])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("b")])), value: Expr::Value(number("2")), }, ], @@ -1650,7 +1653,7 @@ fn parse_merge() { assert!(!into); assert_eq!( TableFactor::Table { - name: ObjectName(vec![Ident::new("inventory")]), + name: ObjectName::from(vec![Ident::new("inventory")]), alias: Some(TableAlias { name: Ident::new("T"), columns: vec![], @@ -1667,7 +1670,7 @@ fn parse_merge() { ); assert_eq!( TableFactor::Table { - name: ObjectName(vec![Ident::new("newArrivals")]), + name: ObjectName::from(vec![Ident::new("newArrivals")]), alias: Some(TableAlias { name: Ident::new("S"), columns: vec![], @@ -1985,7 +1988,7 @@ fn parse_map_access_expr() { }), AccessExpr::Subscript(Subscript::Index { index: Expr::Function(Function { - name: ObjectName(vec![Ident::with_span( + name: ObjectName::from(vec![Ident::with_span( Span::new(Location::of(1, 11), Location::of(1, 22)), "safe_offset", )]), @@ -2037,7 +2040,7 @@ fn test_bigquery_create_function() { or_replace: true, temporary: true, if_not_exists: false, - name: ObjectName(vec![ + name: ObjectName::from(vec![ Ident::new("project1"), Ident::new("mydataset"), Ident::new("myfunction"), diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index fed4308fc..0f22db389 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -62,7 +62,7 @@ fn parse_map_access_expr() { })], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("foos")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("foos")])), joins: vec![], }], lateral_views: vec![], @@ -166,7 +166,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -185,7 +188,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -302,7 +305,7 @@ fn parse_alter_table_add_projection() { Statement::AlterTable { name, operations, .. } => { - assert_eq!(name, ObjectName(vec!["t0".into()])); + assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( operations[0], @@ -372,7 +375,7 @@ fn parse_alter_table_drop_projection() { Statement::AlterTable { name, operations, .. } => { - assert_eq!(name, ObjectName(vec!["t0".into()])); + assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( operations[0], @@ -405,7 +408,7 @@ fn parse_alter_table_clear_and_materialize_projection() { Statement::AlterTable { name, operations, .. } => { - assert_eq!(name, ObjectName(vec!["t0".into()])); + assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( operations[0], @@ -549,7 +552,7 @@ fn parse_clickhouse_data_types() { match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) { Statement::CreateTable(CreateTable { name, columns, .. }) => { - assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!(name, ObjectName::from(vec!["table".into()])); assert_eq!( columns, vec![ @@ -590,7 +593,7 @@ fn parse_create_table_with_nullable() { match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) { Statement::CreateTable(CreateTable { name, columns, .. }) => { - assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!(name, ObjectName::from(vec!["table".into()])); assert_eq!( columns, vec![ @@ -639,7 +642,7 @@ fn parse_create_table_with_nested_data_types() { match clickhouse().one_statement_parses_to(sql, "") { Statement::CreateTable(CreateTable { name, columns, .. }) => { - assert_eq!(name, ObjectName(vec!["table".into()])); + assert_eq!(name, ObjectName::from(vec!["table".into()])); assert_eq!( columns, vec![ @@ -755,7 +758,7 @@ fn parse_create_table_with_primary_key() { }) ); fn assert_function(actual: &Function, name: &str, arg: &str) -> bool { - assert_eq!(actual.name, ObjectName(vec![Ident::new(name)])); + assert_eq!(actual.name, ObjectName::from(vec![Ident::new(name)])); assert_eq!( actual.args, FunctionArguments::List(FunctionArgumentList { @@ -814,7 +817,7 @@ fn parse_create_table_with_variant_default_expressions() { options: vec![ColumnOptionDef { name: None, option: ColumnOption::Materialized(Expr::Function(Function { - name: ObjectName(vec![Ident::new("now")]), + name: ObjectName::from(vec![Ident::new("now")]), uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![], @@ -836,7 +839,7 @@ fn parse_create_table_with_variant_default_expressions() { options: vec![ColumnOptionDef { name: None, option: ColumnOption::Ephemeral(Some(Expr::Function(Function { - name: ObjectName(vec![Ident::new("now")]), + name: ObjectName::from(vec![Ident::new("now")]), uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![], @@ -867,7 +870,7 @@ fn parse_create_table_with_variant_default_expressions() { options: vec![ColumnOptionDef { name: None, option: ColumnOption::Alias(Expr::Function(Function { - name: ObjectName(vec![Ident::new("toString")]), + name: ObjectName::from(vec![Ident::new("toString")]), uses_odbc_syntax: false, args: FunctionArguments::List(FunctionArgumentList { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( @@ -895,14 +898,14 @@ fn parse_create_table_with_variant_default_expressions() { fn parse_create_view_with_fields_data_types() { match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) { Statement::CreateView { name, columns, .. } => { - assert_eq!(name, ObjectName(vec!["v".into()])); + assert_eq!(name, ObjectName::from(vec!["v".into()])); assert_eq!( columns, vec![ ViewColumnDef { name: "i".into(), data_type: Some(DataType::Custom( - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "int".into(), quote_style: Some('"'), span: Span::empty(), @@ -914,7 +917,7 @@ fn parse_create_view_with_fields_data_types() { ViewColumnDef { name: "f".into(), data_type: Some(DataType::Custom( - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "String".into(), quote_style: Some('"'), span: Span::empty(), @@ -1355,7 +1358,7 @@ fn parse_use() { // Test single identifier without quotes assert_eq!( clickhouse().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); @@ -1363,7 +1366,7 @@ fn parse_use() { // Test single identifier with different type of quotes assert_eq!( clickhouse().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e1ef2f909..6897d44ae 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -160,7 +160,7 @@ fn parse_insert_default_values() { assert_eq!(source, None); assert_eq!( table_name, - TableObject::TableName(ObjectName(vec!["test_table".into()])) + TableObject::TableName(ObjectName::from(vec!["test_table".into()])) ); } _ => unreachable!(), @@ -188,7 +188,7 @@ fn parse_insert_default_values() { assert_eq!(source, None); assert_eq!( table_name, - TableObject::TableName(ObjectName(vec!["test_table".into()])) + TableObject::TableName(ObjectName::from(vec!["test_table".into()])) ); } _ => unreachable!(), @@ -216,7 +216,7 @@ fn parse_insert_default_values() { assert_eq!(source, None); assert_eq!( table_name, - TableObject::TableName(ObjectName(vec!["test_table".into()])) + TableObject::TableName(ObjectName::from(vec!["test_table".into()])) ); } _ => unreachable!(), @@ -343,15 +343,15 @@ fn parse_update() { assignments, vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["a".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec!["a".into()])), value: Expr::Value(number("1")), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["b".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec!["b".into()])), value: Expr::Value(number("2")), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["c".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec!["c".into()])), value: Expr::Value(number("3")), }, ] @@ -396,11 +396,11 @@ fn parse_update_set_from() { stmt, Statement::Update { table: TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("t1")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], }, assignments: vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new("name")])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("name")])), value: Expr::CompoundIdentifier(vec![Ident::new("t2"), Ident::new("name")]) }], from: Some(UpdateTableFromKind::AfterSet(TableWithJoins { @@ -419,7 +419,7 @@ fn parse_update_set_from() { ], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("t1")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], }], lateral_views: vec![], @@ -488,7 +488,7 @@ fn parse_update_with_table_alias() { assert_eq!( TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("users")]), + name: ObjectName::from(vec![Ident::new("users")]), alias: Some(TableAlias { name: Ident::new("u"), columns: vec![], @@ -507,7 +507,7 @@ fn parse_update_with_table_alias() { ); assert_eq!( vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![ + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("u"), Ident::new("username") ])), @@ -577,7 +577,7 @@ fn parse_select_with_table_alias() { select.from, vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("lineitem")]), + name: ObjectName::from(vec![Ident::new("lineitem")]), alias: Some(TableAlias { name: Ident::new("l"), columns: vec![ @@ -628,7 +628,7 @@ fn parse_delete_statement() { .. }) => { assert_eq!( - table_from_name(ObjectName(vec![Ident::with_quote('"', "table")])), + table_from_name(ObjectName::from(vec![Ident::with_quote('"', "table")])), from[0].relation ); } @@ -659,22 +659,22 @@ fn parse_delete_statement_for_multi_tables() { .. }) => { assert_eq!( - ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), + ObjectName::from(vec![Ident::new("schema1"), Ident::new("table1")]), tables[0] ); assert_eq!( - ObjectName(vec![Ident::new("schema2"), Ident::new("table2")]), + ObjectName::from(vec![Ident::new("schema2"), Ident::new("table2")]), tables[1] ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema1"), Ident::new("table1") ])), from[0].relation ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema2"), Ident::new("table2") ])), @@ -695,28 +695,28 @@ fn parse_delete_statement_for_multi_tables_with_using() { .. }) => { assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema1"), Ident::new("table1") ])), from[0].relation ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema2"), Ident::new("table2") ])), from[1].relation ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema1"), Ident::new("table1") ])), using[0].relation ); assert_eq!( - table_from_name(ObjectName(vec![ + table_from_name(ObjectName::from(vec![ Ident::new("schema2"), Ident::new("table2") ])), @@ -742,7 +742,7 @@ fn parse_where_delete_statement() { .. }) => { assert_eq!( - table_from_name(ObjectName(vec![Ident::new("foo")])), + table_from_name(ObjectName::from(vec![Ident::new("foo")])), from[0].relation, ); @@ -777,7 +777,7 @@ fn parse_where_delete_with_alias_statement() { }) => { assert_eq!( TableFactor::Table { - name: ObjectName(vec![Ident::new("basket")]), + name: ObjectName::from(vec![Ident::new("basket")]), alias: Some(TableAlias { name: Ident::new("a"), columns: vec![], @@ -795,7 +795,7 @@ fn parse_where_delete_with_alias_statement() { assert_eq!( Some(vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("basket")]), + name: ObjectName::from(vec![Ident::new("basket")]), alias: Some(TableAlias { name: Ident::new("b"), columns: vec![], @@ -962,7 +962,7 @@ fn parse_select_into() { temporary: false, unlogged: false, table: false, - name: ObjectName(vec![Ident::new("table0")]), + name: ObjectName::from(vec![Ident::new("table0")]), }, only(&select.into) ); @@ -995,7 +995,7 @@ fn parse_select_wildcard() { let select = verified_only_select(sql); assert_eq!( &SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("foo")]), + ObjectName::from(vec![Ident::new("foo")]), WildcardAdditionalOptions::default() ), only(&select.projection) @@ -1005,7 +1005,7 @@ fn parse_select_wildcard() { let select = verified_only_select(sql); assert_eq!( &SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("myschema"), Ident::new("mytable"),]), + ObjectName::from(vec![Ident::new("myschema"), Ident::new("mytable"),]), WildcardAdditionalOptions::default(), ), only(&select.projection) @@ -1082,7 +1082,7 @@ fn parse_select_count_wildcard() { let select = verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("COUNT")]), + name: ObjectName::from(vec![Ident::new("COUNT")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -1105,7 +1105,7 @@ fn parse_select_count_distinct() { let select = verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("COUNT")]), + name: ObjectName::from(vec![Ident::new("COUNT")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -2342,7 +2342,7 @@ fn parse_select_having() { assert_eq!( Some(Expr::BinaryOp { left: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("COUNT")]), + name: ObjectName::from(vec![Ident::new("COUNT")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -2373,7 +2373,7 @@ fn parse_select_qualify() { assert_eq!( Some(Expr::BinaryOp { left: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("ROW_NUMBER")]), + name: ObjectName::from(vec![Ident::new("ROW_NUMBER")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -2780,7 +2780,7 @@ fn parse_listagg() { assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("LISTAGG")]), + name: ObjectName::from(vec![Ident::new("LISTAGG")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -2935,7 +2935,10 @@ fn parse_window_function_null_treatment_arg() { let SelectItem::UnnamedExpr(Expr::Function(actual)) = &projection[i] else { unreachable!() }; - assert_eq!(ObjectName(vec![Ident::new("FIRST_VALUE")]), actual.name); + assert_eq!( + ObjectName::from(vec![Ident::new("FIRST_VALUE")]), + actual.name + ); let FunctionArguments::List(arg_list) = &actual.args else { panic!("expected argument list") }; @@ -3231,7 +3234,7 @@ fn parse_create_table() { options: vec![ColumnOptionDef { name: None, option: ColumnOption::ForeignKey { - foreign_table: ObjectName(vec!["othertable".into()]), + foreign_table: ObjectName::from(vec!["othertable".into()]), referred_columns: vec!["a".into(), "b".into()], on_delete: None, on_update: None, @@ -3246,7 +3249,7 @@ fn parse_create_table() { options: vec![ColumnOptionDef { name: None, option: ColumnOption::ForeignKey { - foreign_table: ObjectName(vec!["othertable2".into()]), + foreign_table: ObjectName::from(vec!["othertable2".into()]), referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::NoAction), @@ -3262,7 +3265,7 @@ fn parse_create_table() { TableConstraint::ForeignKey { name: Some("fkey".into()), columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable3".into()]), + foreign_table: ObjectName::from(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, @@ -3271,7 +3274,7 @@ fn parse_create_table() { TableConstraint::ForeignKey { name: Some("fkey2".into()), columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), @@ -3280,7 +3283,7 @@ fn parse_create_table() { TableConstraint::ForeignKey { name: None, columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), @@ -3289,7 +3292,7 @@ fn parse_create_table() { TableConstraint::ForeignKey { name: None, columns: vec!["lng".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), @@ -3388,7 +3391,7 @@ fn parse_create_table_with_constraint_characteristics() { TableConstraint::ForeignKey { name: Some("fkey".into()), columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable3".into()]), + foreign_table: ObjectName::from(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, @@ -3401,7 +3404,7 @@ fn parse_create_table_with_constraint_characteristics() { TableConstraint::ForeignKey { name: Some("fkey2".into()), columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), @@ -3414,7 +3417,7 @@ fn parse_create_table_with_constraint_characteristics() { TableConstraint::ForeignKey { name: None, columns: vec!["lat".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), @@ -3427,7 +3430,7 @@ fn parse_create_table_with_constraint_characteristics() { TableConstraint::ForeignKey { name: None, columns: vec!["lng".into()], - foreign_table: ObjectName(vec!["othertable4".into()]), + foreign_table: ObjectName::from(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), @@ -3620,7 +3623,7 @@ fn parse_create_table_hive_array() { .. }) => { assert!(if_not_exists); - assert_eq!(name, ObjectName(vec!["something".into()])); + assert_eq!(name, ObjectName::from(vec!["something".into()])); assert_eq!( columns, vec![ @@ -3817,7 +3820,7 @@ fn parse_create_table_as_table() { match verified_stmt(sql1) { Statement::CreateTable(CreateTable { query, name, .. }) => { - assert_eq!(name, ObjectName(vec![Ident::new("new_table")])); + assert_eq!(name, ObjectName::from(vec![Ident::new("new_table")])); assert_eq!(query.unwrap(), expected_query1); } _ => unreachable!(), @@ -3844,7 +3847,7 @@ fn parse_create_table_as_table() { match verified_stmt(sql2) { Statement::CreateTable(CreateTable { query, name, .. }) => { - assert_eq!(name, ObjectName(vec![Ident::new("new_table")])); + assert_eq!(name, ObjectName::from(vec![Ident::new("new_table")])); assert_eq!(query.unwrap(), expected_query2); } _ => unreachable!(), @@ -3947,8 +3950,8 @@ fn parse_create_table_clone() { let sql = "CREATE OR REPLACE TABLE a CLONE a_tmp"; match verified_stmt(sql) { Statement::CreateTable(CreateTable { name, clone, .. }) => { - assert_eq!(ObjectName(vec![Ident::new("a")]), name); - assert_eq!(Some(ObjectName(vec![(Ident::new("a_tmp"))])), clone) + assert_eq!(ObjectName::from(vec![Ident::new("a")]), name); + assert_eq!(Some(ObjectName::from(vec![(Ident::new("a_tmp"))])), clone) } _ => unreachable!(), } @@ -4176,11 +4179,11 @@ fn parse_rename_table() { Statement::RenameTable(rename_tables) => { assert_eq!( vec![RenameTable { - old_name: ObjectName(vec![ + old_name: ObjectName::from(vec![ Ident::new("test".to_string()), Ident::new("test1".to_string()), ]), - new_name: ObjectName(vec![ + new_name: ObjectName::from(vec![ Ident::new("test_db".to_string()), Ident::new("test2".to_string()), ]), @@ -4198,16 +4201,16 @@ fn parse_rename_table() { assert_eq!( vec![ RenameTable { - old_name: ObjectName(vec![Ident::new("old_table1".to_string())]), - new_name: ObjectName(vec![Ident::new("new_table1".to_string())]), + old_name: ObjectName::from(vec![Ident::new("old_table1".to_string())]), + new_name: ObjectName::from(vec![Ident::new("new_table1".to_string())]), }, RenameTable { - old_name: ObjectName(vec![Ident::new("old_table2".to_string())]), - new_name: ObjectName(vec![Ident::new("new_table2".to_string())]), + old_name: ObjectName::from(vec![Ident::new("old_table2".to_string())]), + new_name: ObjectName::from(vec![Ident::new("new_table2".to_string())]), }, RenameTable { - old_name: ObjectName(vec![Ident::new("old_table3".to_string())]), - new_name: ObjectName(vec![Ident::new("new_table3".to_string())]), + old_name: ObjectName::from(vec![Ident::new("old_table3".to_string())]), + new_name: ObjectName::from(vec![Ident::new("new_table3".to_string())]), } ], rename_tables @@ -4802,7 +4805,7 @@ fn parse_named_argument_function() { assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("FUN")]), + name: ObjectName::from(vec![Ident::new("FUN")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -4842,7 +4845,7 @@ fn parse_named_argument_function_with_eq_operator() { .verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("FUN")]), + name: ObjectName::from(vec![Ident::new("FUN")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -4917,7 +4920,7 @@ fn parse_window_functions() { assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("row_number")]), + name: ObjectName::from(vec![Ident::new("row_number")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -5044,7 +5047,7 @@ fn test_parse_named_window() { projection: vec![ SelectItem::ExprWithAlias { expr: Expr::Function(Function { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "MIN".to_string(), quote_style: None, span: Span::empty(), @@ -5079,7 +5082,7 @@ fn test_parse_named_window() { }, SelectItem::ExprWithAlias { expr: Expr::Function(Function { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "MAX".to_string(), quote_style: None, span: Span::empty(), @@ -5115,7 +5118,7 @@ fn test_parse_named_window() { ], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "aggregate_test_100".to_string(), quote_style: None, span: Span::empty(), @@ -5729,7 +5732,7 @@ fn parse_interval_and_or_xor() { }))], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "test".to_string(), quote_style: None, span: Span::empty(), @@ -6341,11 +6344,11 @@ fn parse_implicit_join() { assert_eq!( vec![ TableWithJoins { - relation: table_from_name(ObjectName(vec!["t1".into()])), + relation: table_from_name(ObjectName::from(vec!["t1".into()])), joins: vec![], }, TableWithJoins { - relation: table_from_name(ObjectName(vec!["t2".into()])), + relation: table_from_name(ObjectName::from(vec!["t2".into()])), joins: vec![], }, ], @@ -6357,17 +6360,17 @@ fn parse_implicit_join() { assert_eq!( vec![ TableWithJoins { - relation: table_from_name(ObjectName(vec!["t1a".into()])), + relation: table_from_name(ObjectName::from(vec!["t1a".into()])), joins: vec![Join { - relation: table_from_name(ObjectName(vec!["t1b".into()])), + relation: table_from_name(ObjectName::from(vec!["t1b".into()])), global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], }, TableWithJoins { - relation: table_from_name(ObjectName(vec!["t2a".into()])), + relation: table_from_name(ObjectName::from(vec!["t2a".into()])), joins: vec![Join { - relation: table_from_name(ObjectName(vec!["t2b".into()])), + relation: table_from_name(ObjectName::from(vec!["t2b".into()])), global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -6383,7 +6386,7 @@ fn parse_cross_join() { let select = verified_only_select(sql); assert_eq!( Join { - relation: table_from_name(ObjectName(vec![Ident::new("t2")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("t2")])), global: false, join_operator: JoinOperator::CrossJoin, }, @@ -6401,7 +6404,7 @@ fn parse_joins_on() { ) -> Join { Join { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new(relation.into())]), + name: ObjectName::from(vec![Ident::new(relation.into())]), alias, args: None, with_hints: vec![], @@ -6530,7 +6533,7 @@ fn parse_joins_using() { ) -> Join { Join { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new(relation.into())]), + name: ObjectName::from(vec![Ident::new(relation.into())]), alias, args: None, with_hints: vec![], @@ -6541,7 +6544,9 @@ fn parse_joins_using() { sample: None, }, global: false, - join_operator: f(JoinConstraint::Using(vec![ObjectName(vec!["c1".into()])])), + join_operator: f(JoinConstraint::Using(vec![ObjectName::from(vec![ + "c1".into() + ])])), } } // Test parsing of aliases @@ -6606,7 +6611,7 @@ fn parse_natural_join() { fn natural_join(f: impl Fn(JoinConstraint) -> JoinOperator, alias: Option) -> Join { Join { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t2")]), + name: ObjectName::from(vec![Ident::new("t2")]), alias, args: None, with_hints: vec![], @@ -6878,7 +6883,7 @@ fn parse_derived_tables() { }), }, joins: vec![Join { - relation: table_from_name(ObjectName(vec!["t2".into()])), + relation: table_from_name(ObjectName::from(vec!["t2".into()])), global: false, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -7826,7 +7831,7 @@ fn lateral_function() { top_before_distinct: false, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "customer".to_string(), quote_style: None, span: Span::empty(), @@ -7834,7 +7839,7 @@ fn lateral_function() { joins: vec![Join { relation: TableFactor::Function { lateral: true, - name: ObjectName(vec!["generate_series".into()]), + name: ObjectName::from(vec!["generate_series".into()]), args: vec![ FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::CompoundIdentifier( @@ -7995,7 +8000,7 @@ fn parse_set_variable() { assert!(!hivevar); assert_eq!( variables, - OneOrManyWithParens::One(ObjectName(vec!["SOMETHING".into()])) + OneOrManyWithParens::One(ObjectName::from(vec!["SOMETHING".into()])) ); assert_eq!( value, @@ -8019,9 +8024,9 @@ fn parse_set_variable() { assert_eq!( variables, OneOrManyWithParens::Many(vec![ - ObjectName(vec!["a".into()]), - ObjectName(vec!["b".into()]), - ObjectName(vec!["c".into()]), + ObjectName::from(vec!["a".into()]), + ObjectName::from(vec!["b".into()]), + ObjectName::from(vec!["c".into()]), ]) ); assert_eq!( @@ -8095,7 +8100,7 @@ fn parse_set_role_as_variable() { assert!(!hivevar); assert_eq!( variables, - OneOrManyWithParens::One(ObjectName(vec!["role".into()])) + OneOrManyWithParens::One(ObjectName::from(vec!["role".into()])) ); assert_eq!( value, @@ -8142,7 +8147,7 @@ fn parse_set_time_zone() { assert!(!hivevar); assert_eq!( variable, - OneOrManyWithParens::One(ObjectName(vec!["TIMEZONE".into()])) + OneOrManyWithParens::One(ObjectName::from(vec!["TIMEZONE".into()])) ); assert_eq!( value, @@ -8698,7 +8703,7 @@ fn parse_merge() { assert_eq!( table, TableFactor::Table { - name: ObjectName(vec![Ident::new("s"), Ident::new("bar")]), + name: ObjectName::from(vec![Ident::new("s"), Ident::new("bar")]), alias: Some(TableAlias { name: Ident::new("dest"), columns: vec![], @@ -8730,7 +8735,7 @@ fn parse_merge() { )], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![ + relation: table_from_name(ObjectName::from(vec![ Ident::new("s"), Ident::new("foo") ])), @@ -8844,7 +8849,7 @@ fn parse_merge() { action: MergeAction::Update { assignments: vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![ + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("dest"), Ident::new("F") ])), @@ -8854,7 +8859,7 @@ fn parse_merge() { ]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![ + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("dest"), Ident::new("G") ])), @@ -8961,12 +8966,12 @@ fn test_lock_table() { let lock = ast.locks.pop().unwrap(); assert_eq!(lock.lock_type, LockType::Update); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert!(lock.nonblock.is_none()); @@ -8976,12 +8981,12 @@ fn test_lock_table() { let lock = ast.locks.pop().unwrap(); assert_eq!(lock.lock_type, LockType::Share); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert!(lock.nonblock.is_none()); @@ -8991,23 +8996,23 @@ fn test_lock_table() { let lock = ast.locks.remove(0); assert_eq!(lock.lock_type, LockType::Share); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert!(lock.nonblock.is_none()); let lock = ast.locks.remove(0); assert_eq!(lock.lock_type, LockType::Update); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "student".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert!(lock.nonblock.is_none()); } @@ -9020,12 +9025,12 @@ fn test_lock_nonblock() { let lock = ast.locks.pop().unwrap(); assert_eq!(lock.lock_type, LockType::Update); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert_eq!(lock.nonblock.unwrap(), NonBlock::SkipLocked); @@ -9035,12 +9040,12 @@ fn test_lock_nonblock() { let lock = ast.locks.pop().unwrap(); assert_eq!(lock.lock_type, LockType::Share); assert_eq!( - lock.of.unwrap().0, - vec![Ident { + lock.of.unwrap(), + ObjectName::from(vec![Ident { value: "school".to_string(), quote_style: None, span: Span::empty(), - }] + }]) ); assert_eq!(lock.nonblock.unwrap(), NonBlock::Nowait); } @@ -9222,7 +9227,7 @@ fn parse_time_functions() { let sql = format!("SELECT {}()", func_name); let select = verified_only_select(&sql); let select_localtime_func_call_ast = Function { - name: ObjectName(vec![Ident::new(func_name)]), + name: ObjectName::from(vec![Ident::new(func_name)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -9495,7 +9500,7 @@ fn parse_cache_table() { verified_stmt(format!("CACHE TABLE '{cache_table_name}'").as_str()), Statement::Cache { table_flag: None, - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![], query: None, @@ -9505,8 +9510,8 @@ fn parse_cache_table() { assert_eq!( verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}'").as_str()), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![], query: None, @@ -9521,8 +9526,8 @@ fn parse_cache_table() { .as_str() ), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![ SqlOption::KeyValue { @@ -9546,8 +9551,8 @@ fn parse_cache_table() { .as_str() ), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![ SqlOption::KeyValue { @@ -9571,8 +9576,8 @@ fn parse_cache_table() { .as_str() ), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: true, options: vec![ SqlOption::KeyValue { @@ -9591,8 +9596,8 @@ fn parse_cache_table() { assert_eq!( verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}' {sql}").as_str()), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![], query: Some(query.clone().into()), @@ -9602,8 +9607,8 @@ fn parse_cache_table() { assert_eq!( verified_stmt(format!("CACHE {table_flag} TABLE '{cache_table_name}' AS {sql}").as_str()), Statement::Cache { - table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), - table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + table_flag: Some(ObjectName::from(vec![Ident::new(table_flag)])), + table_name: ObjectName::from(vec![Ident::with_quote('\'', cache_table_name)]), has_as: true, options: vec![], query: Some(query.into()), @@ -9666,7 +9671,7 @@ fn parse_uncache_table() { assert_eq!( verified_stmt("UNCACHE TABLE 'table_name'"), Statement::UNCache { - table_name: ObjectName(vec![Ident::with_quote('\'', "table_name")]), + table_name: ObjectName::from(vec![Ident::with_quote('\'', "table_name")]), if_exists: false, } ); @@ -9674,7 +9679,7 @@ fn parse_uncache_table() { assert_eq!( verified_stmt("UNCACHE TABLE IF EXISTS 'table_name'"), Statement::UNCache { - table_name: ObjectName(vec![Ident::with_quote('\'', "table_name")]), + table_name: ObjectName::from(vec![Ident::with_quote('\'', "table_name")]), if_exists: true, } ); @@ -9881,7 +9886,7 @@ fn parse_pivot_table() { verified_only_select(sql).from[0].relation, Pivot { table: Box::new(TableFactor::Table { - name: ObjectName(vec![Ident::new("monthly_sales")]), + name: ObjectName::from(vec![Ident::new("monthly_sales")]), alias: Some(TableAlias { name: Ident::new("a"), columns: vec![] @@ -9957,7 +9962,7 @@ fn parse_unpivot_table() { verified_only_select(sql).from[0].relation, Unpivot { table: Box::new(TableFactor::Table { - name: ObjectName(vec![Ident::new("sales")]), + name: ObjectName::from(vec![Ident::new("sales")]), alias: Some(TableAlias { name: Ident::new("s"), columns: vec![] @@ -10028,7 +10033,7 @@ fn parse_pivot_unpivot_table() { Pivot { table: Box::new(Unpivot { table: Box::new(TableFactor::Table { - name: ObjectName(vec![Ident::new("census")]), + name: ObjectName::from(vec![Ident::new("census")]), alias: Some(TableAlias { name: Ident::new("c"), columns: vec![] @@ -10231,7 +10236,7 @@ fn parse_create_type() { verified_stmt("CREATE TYPE db.type_name AS (foo INT, bar TEXT COLLATE \"de_DE\")"); assert_eq!( Statement::CreateType { - name: ObjectName(vec![Ident::new("db"), Ident::new("type_name")]), + name: ObjectName::from(vec![Ident::new("db"), Ident::new("type_name")]), representation: UserDefinedTypeRepresentation::Composite { attributes: vec![ UserDefinedTypeCompositeAttributeDef { @@ -10242,7 +10247,7 @@ fn parse_create_type() { UserDefinedTypeCompositeAttributeDef { name: Ident::new("bar"), data_type: DataType::Text, - collation: Some(ObjectName(vec![Ident::with_quote('\"', "de_DE")])), + collation: Some(ObjectName::from(vec![Ident::with_quote('\"', "de_DE")])), } ] } @@ -10323,7 +10328,7 @@ fn parse_call() { )))], clauses: vec![], }), - name: ObjectName(vec![Ident::new("my_procedure")]), + name: ObjectName::from(vec![Ident::new("my_procedure")]), filter: None, null_treatment: None, over: None, @@ -10335,7 +10340,7 @@ fn parse_call() { #[test] fn parse_execute_stored_procedure() { let expected = Statement::Execute { - name: ObjectName(vec![ + name: ObjectName::from(vec![ Ident { value: "my_schema".to_string(), quote_style: None, @@ -10447,7 +10452,7 @@ fn parse_unload() { projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("tab")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("tab")])), joins: vec![], }], lateral_views: vec![], @@ -10600,7 +10605,7 @@ fn parse_map_access_expr() { }), AccessExpr::Subscript(Subscript::Index { index: Expr::Function(Function { - name: ObjectName(vec![Ident::with_span( + name: ObjectName::from(vec![Ident::with_span( Span::new(Location::of(1, 11), Location::of(1, 22)), "safe_offset", )]), @@ -10641,7 +10646,7 @@ fn parse_connect_by() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), ], from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("employees")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), joins: vec![], }], into: None, @@ -10721,7 +10726,7 @@ fn parse_connect_by() { SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), ], from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("employees")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), joins: vec![], }], into: None, @@ -10797,7 +10802,7 @@ fn test_selective_aggregation() { .projection, vec![ SelectItem::UnnamedExpr(Expr::Function(Function { - name: ObjectName(vec![Ident::new("ARRAY_AGG")]), + name: ObjectName::from(vec![Ident::new("ARRAY_AGG")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -10816,7 +10821,7 @@ fn test_selective_aggregation() { })), SelectItem::ExprWithAlias { expr: Expr::Function(Function { - name: ObjectName(vec![Ident::new("ARRAY_AGG")]), + name: ObjectName::from(vec![Ident::new("ARRAY_AGG")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -10876,7 +10881,7 @@ fn test_match_recognize() { use MatchRecognizeSymbol::*; use RepetitionQuantifier::*; - let table = table_from_name(ObjectName(vec![Ident::new("my_table")])); + let table = table_from_name(ObjectName::from(vec![Ident::new("my_table")])); fn check(options: &str, expect: TableFactor) { let select = all_dialects_where(|d| d.supports_match_recognize()).verified_only_select( @@ -11260,7 +11265,7 @@ fn parse_odbc_scalar_function() { else { unreachable!("expected function") }; - assert_eq!(name, &ObjectName(vec![Ident::new("my_func")])); + assert_eq!(name, &ObjectName::from(vec![Ident::new("my_func")])); assert!(uses_odbc_syntax); matches!(args, FunctionArguments::List(l) if l.args.len() == 2); @@ -12327,7 +12332,7 @@ fn parse_load_data() { assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(false, overwrite); assert_eq!( - ObjectName(vec![Ident::new("test"), Ident::new("my_table")]), + ObjectName::from(vec![Ident::new("test"), Ident::new("my_table")]), table_name ); assert_eq!(None, partitioned); @@ -12350,7 +12355,7 @@ fn parse_load_data() { assert_eq!(false, local); assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(true, overwrite); - assert_eq!(ObjectName(vec![Ident::new("my_table")]), table_name); + assert_eq!(ObjectName::from(vec![Ident::new("my_table")]), table_name); assert_eq!(None, partitioned); assert_eq!(None, table_format); } @@ -12387,7 +12392,7 @@ fn parse_load_data() { assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(false, overwrite); assert_eq!( - ObjectName(vec![Ident::new("test"), Ident::new("my_table")]), + ObjectName::from(vec![Ident::new("test"), Ident::new("my_table")]), table_name ); assert_eq!(None, partitioned); @@ -12425,7 +12430,7 @@ fn parse_load_data() { assert_eq!(true, local); assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(false, overwrite); - assert_eq!(ObjectName(vec![Ident::new("my_table")]), table_name); + assert_eq!(ObjectName::from(vec![Ident::new("my_table")]), table_name); assert_eq!( Some(vec![ Expr::BinaryOp { @@ -12461,7 +12466,7 @@ fn parse_load_data() { assert_eq!("/local/path/to/data.txt", inpath); assert_eq!(true, overwrite); assert_eq!( - ObjectName(vec![Ident::new("good"), Ident::new("my_table")]), + ObjectName::from(vec![Ident::new("good"), Ident::new("my_table")]), table_name ); assert_eq!( @@ -12815,7 +12820,7 @@ fn parse_composite_access_expr() { verified_expr("f(a).b"), Expr::CompoundFieldAccess { root: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("f")]), + name: ObjectName::from(vec![Ident::new("f")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -12839,7 +12844,7 @@ fn parse_composite_access_expr() { verified_expr("f(a).b.c"), Expr::CompoundFieldAccess { root: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("f")]), + name: ObjectName::from(vec![Ident::new("f")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -12865,7 +12870,7 @@ fn parse_composite_access_expr() { let stmt = verified_only_select("SELECT f(a).b FROM t WHERE f(a).b IS NOT NULL"); let expr = Expr::CompoundFieldAccess { root: Box::new(Expr::Function(Function { - name: ObjectName(vec![Ident::new("f")]), + name: ObjectName::from(vec![Ident::new("f")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index b9ca55d13..be7588de2 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -185,7 +185,9 @@ fn test_values_clause() { "SELECT * FROM values", )); assert_eq!( - Some(&table_from_name(ObjectName(vec![Ident::new("values")]))), + Some(&table_from_name(ObjectName::from(vec![Ident::new( + "values" + )]))), query .body .as_select() @@ -205,7 +207,7 @@ fn parse_use() { // Test single identifier without quotes assert_eq!( databricks().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); @@ -213,7 +215,7 @@ fn parse_use() { // Test single identifier with different type of quotes assert_eq!( databricks().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) @@ -225,21 +227,21 @@ fn parse_use() { // Test single identifier with keyword and different type of quotes assert_eq!( databricks().verified_stmt(&format!("USE CATALOG {0}my_catalog{0}", quote)), - Statement::Use(Use::Catalog(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Database(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Schema(ObjectName(vec![Ident::with_quote( + Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote( quote, "my_schema".to_string(), )]))) @@ -249,15 +251,19 @@ fn parse_use() { // Test single identifier with keyword and no quotes assert_eq!( databricks().verified_stmt("USE CATALOG my_catalog"), - Statement::Use(Use::Catalog(ObjectName(vec![Ident::new("my_catalog")]))) + Statement::Use(Use::Catalog(ObjectName::from(vec![Ident::new( + "my_catalog" + )]))) ); assert_eq!( databricks().verified_stmt("USE DATABASE my_schema"), - Statement::Use(Use::Database(ObjectName(vec![Ident::new("my_schema")]))) + Statement::Use(Use::Database(ObjectName::from(vec![Ident::new( + "my_schema" + )]))) ); assert_eq!( databricks().verified_stmt("USE SCHEMA my_schema"), - Statement::Use(Use::Schema(ObjectName(vec![Ident::new("my_schema")]))) + Statement::Use(Use::Schema(ObjectName::from(vec![Ident::new("my_schema")]))) ); // Test invalid syntax - missing identifier diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index ca7f926a9..aee6d654c 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -160,7 +160,7 @@ fn test_select_wildcard_with_exclude() { let select = duckdb().verified_only_select("SELECT name.* EXCLUDE department_id FROM employee_table"); let expected = SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("name")]), + ObjectName::from(vec![Ident::new("name")]), WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), ..Default::default() @@ -191,7 +191,7 @@ fn test_create_macro() { let expected = Statement::CreateMacro { or_replace: false, temporary: false, - name: ObjectName(vec![Ident::new("schema"), Ident::new("add")]), + name: ObjectName::from(vec![Ident::new("schema"), Ident::new("add")]), args: Some(vec![MacroArg::new("a"), MacroArg::new("b")]), definition: MacroDefinition::Expr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("a"))), @@ -208,7 +208,7 @@ fn test_create_macro_default_args() { let expected = Statement::CreateMacro { or_replace: false, temporary: false, - name: ObjectName(vec![Ident::new("add_default")]), + name: ObjectName::from(vec![Ident::new("add_default")]), args: Some(vec![ MacroArg::new("a"), MacroArg { @@ -236,7 +236,7 @@ fn test_create_table_macro() { let expected = Statement::CreateMacro { or_replace: true, temporary: true, - name: ObjectName(vec![Ident::new("dynamic_table")]), + name: ObjectName::from(vec![Ident::new("dynamic_table")]), args: Some(vec![ MacroArg::new("col1_value"), MacroArg::new("col2_value"), @@ -268,7 +268,7 @@ fn test_select_union_by_name() { top_before_distinct: false, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "capitals".to_string(), quote_style: None, span: Span::empty(), @@ -297,7 +297,7 @@ fn test_select_union_by_name() { top_before_distinct: false, into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "weather".to_string(), quote_style: None, span: Span::empty(), @@ -587,7 +587,7 @@ fn test_duckdb_named_argument_function_with_assignment_operator() { let select = duckdb_and_generic().verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("FUN")]), + name: ObjectName::from(vec![Ident::new("FUN")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -661,7 +661,7 @@ fn test_duckdb_union_datatype() { transient: Default::default(), volatile: Default::default(), iceberg: Default::default(), - name: ObjectName(vec!["tbl1".into()]), + name: ObjectName::from(vec!["tbl1".into()]), columns: vec![ ColumnDef { name: "one".into(), @@ -765,7 +765,7 @@ fn parse_use() { // Test single identifier without quotes assert_eq!( duckdb().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); @@ -773,7 +773,7 @@ fn parse_use() { // Test single identifier with different type of quotes assert_eq!( duckdb().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) @@ -785,7 +785,7 @@ fn parse_use() { // Test double identifier with different type of quotes assert_eq!( duckdb().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), - Statement::Use(Use::Object(ObjectName(vec![ + Statement::Use(Use::Object(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") ]))) @@ -794,7 +794,7 @@ fn parse_use() { // Test double identifier without quotes assert_eq!( duckdb().verified_stmt("USE mydb.my_schema"), - Statement::Use(Use::Object(ObjectName(vec![ + Statement::Use(Use::Object(ObjectName::from(vec![ Ident::new("mydb"), Ident::new("my_schema") ]))) diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 5349f1207..9c4e8f079 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -368,7 +368,7 @@ fn set_statement_with_minus() { Statement::SetVariable { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![ + variables: OneOrManyWithParens::One(ObjectName::from(vec![ Ident::new("hive"), Ident::new("tez"), Ident::new("java"), @@ -461,7 +461,10 @@ fn parse_delimited_identifiers() { json_path: _, sample: _, } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -480,7 +483,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -516,7 +519,7 @@ fn parse_use() { // Test single identifier without quotes assert_eq!( hive().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); @@ -524,7 +527,7 @@ fn parse_use() { // Test single identifier with different type of quotes assert_eq!( hive().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_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 da2b6160e..3c4017590 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -63,7 +63,7 @@ fn parse_table_time_travel() { select.from, vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t1")]), + name: ObjectName::from(vec![Ident::new("t1")]), alias: None, args: None, with_hints: vec![], @@ -159,7 +159,7 @@ fn parse_create_procedure() { })) } ]), - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test".into(), quote_style: None, span: Span::empty(), @@ -211,7 +211,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table")]), + name: ObjectName::from(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -270,7 +270,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table"),]), + name: ObjectName::from(vec![Ident::new("t_test_table"),]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -329,8 +329,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table")]), - + name: ObjectName::from(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -389,7 +388,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table")]), + name: ObjectName::from(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -428,7 +427,7 @@ fn parse_mssql_openjson() { assert_eq!( vec![TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("t_test_table")]), + name: ObjectName::from(vec![Ident::new("t_test_table")]), alias: Some(TableAlias { name: Ident::new("A"), columns: vec![] @@ -532,7 +531,7 @@ fn parse_mssql_create_role() { assert_eq_vec(&["mssql"], &names); assert_eq!( authorization_owner, - Some(ObjectName(vec![Ident { + Some(ObjectName::from(vec![Ident { value: "helena".into(), quote_style: None, span: Span::empty(), @@ -619,7 +618,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -638,7 +640,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -671,11 +673,11 @@ fn parse_table_name_in_square_brackets() { let select = ms().verified_only_select(r#"SELECT [a column] FROM [a schema].[a table]"#); if let TableFactor::Table { name, .. } = only(select.from).relation { assert_eq!( - vec![ + ObjectName::from(vec![ Ident::with_quote('[', "a schema"), Ident::with_quote('[', "a table") - ], - name.0 + ]), + name ); } else { panic!("Expecting TableFactor::Table"); @@ -1086,7 +1088,7 @@ fn parse_substring_in_select() { })], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "test".to_string(), quote_style: None, span: Span::empty(), @@ -1204,7 +1206,7 @@ fn parse_mssql_declare() { Statement::SetVariable { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("@bar")])), + variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("@bar")])), value: vec![Expr::Value(Value::Number("2".parse().unwrap(), false))], }, Statement::Query(Box::new(Query { @@ -1298,7 +1300,7 @@ fn parse_use() { // Test single identifier without quotes assert_eq!( ms().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); @@ -1306,7 +1308,7 @@ fn parse_use() { // Test single identifier with different type of quotes assert_eq!( ms().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) @@ -1408,7 +1410,7 @@ fn parse_create_table_with_valid_options() { }, value: Expr::Function( Function { - name: ObjectName( + name: ObjectName::from( vec![ Ident { value: "HASH".to_string(), @@ -1472,7 +1474,7 @@ fn parse_create_table_with_valid_options() { if_not_exists: false, transient: false, volatile: false, - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "mytable".to_string(), quote_style: None, span: Span::empty(), @@ -1648,7 +1650,7 @@ fn parse_create_table_with_identity_column() { transient: false, volatile: false, iceberg: false, - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "mytable".to_string(), quote_style: None, span: Span::empty(), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e93ac5695..fb72436ed 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -149,7 +149,7 @@ fn parse_flush() { read_lock: false, export: false, tables: vec![ - ObjectName(vec![ + ObjectName::from(vec![ Ident { value: "mek".to_string(), quote_style: Some('`'), @@ -161,7 +161,7 @@ fn parse_flush() { span: Span::empty(), } ]), - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "table2".to_string(), quote_style: None, span: Span::empty(), @@ -189,7 +189,7 @@ fn parse_flush() { read_lock: true, export: false, tables: vec![ - ObjectName(vec![ + ObjectName::from(vec![ Ident { value: "mek".to_string(), quote_style: Some('`'), @@ -201,7 +201,7 @@ fn parse_flush() { span: Span::empty(), } ]), - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "table2".to_string(), quote_style: None, span: Span::empty(), @@ -218,7 +218,7 @@ fn parse_flush() { read_lock: false, export: true, tables: vec![ - ObjectName(vec![ + ObjectName::from(vec![ Ident { value: "mek".to_string(), quote_style: Some('`'), @@ -230,7 +230,7 @@ fn parse_flush() { span: Span::empty(), } ]), - ObjectName(vec![Ident { + ObjectName::from(vec![Ident { value: "table2".to_string(), quote_style: None, span: Span::empty(), @@ -251,7 +251,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: None, limit_from: None, @@ -269,7 +269,10 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![ + Ident::new("mydb"), + Ident::new("mytable") + ])), }), filter_position: None, limit_from: None, @@ -287,7 +290,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: None, limit_from: None, @@ -305,7 +308,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: None, limit_from: None, @@ -323,7 +326,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: Some(ShowStatementFilterPosition::Suffix( ShowStatementFilter::Like("pattern".into()) @@ -343,7 +346,7 @@ fn parse_show_columns() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mytable")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mytable")])), }), filter_position: Some(ShowStatementFilterPosition::Suffix( ShowStatementFilter::Where(mysql_and_generic().verified_expr("1 = 2")) @@ -430,7 +433,7 @@ fn parse_show_tables() { show_in: Some(ShowStatementIn { clause: ShowStatementInClause::FROM, parent_type: None, - parent_name: Some(ObjectName(vec![Ident::new("mydb")])), + parent_name: Some(ObjectName::from(vec![Ident::new("mydb")])), }), filter_position: None } @@ -534,7 +537,7 @@ fn parse_show_extended_full() { #[test] fn parse_show_create() { - let obj_name = ObjectName(vec![Ident::new("myident")]); + let obj_name = ObjectName::from(vec![Ident::new("myident")]); for obj_type in &[ ShowCreateObject::Table, @@ -591,7 +594,7 @@ fn parse_use() { // Test single identifier without quotes assert_eq!( mysql_and_generic().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); @@ -600,7 +603,7 @@ fn parse_use() { assert_eq!( mysql_and_generic() .verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) @@ -617,7 +620,7 @@ fn parse_set_variables() { Statement::SetVariable { local: true, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec!["autocommit".into()])), + variables: OneOrManyWithParens::One(ObjectName::from(vec!["autocommit".into()])), value: vec![Expr::Value(number("1"))], } ); @@ -1017,7 +1020,7 @@ fn parse_create_table_comment_character_set() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::CharacterSet(ObjectName(vec![Ident::new( + option: ColumnOption::CharacterSet(ObjectName::from(vec![Ident::new( "utf8mb4" )])) }, @@ -1413,7 +1416,7 @@ fn parse_simple_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1471,7 +1474,7 @@ fn parse_ignore_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1518,7 +1521,7 @@ fn parse_priority_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1562,7 +1565,7 @@ fn parse_priority_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1607,14 +1610,14 @@ fn parse_insert_as() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::with_quote('`', "table")])), + TableObject::TableName(ObjectName::from(vec![Ident::with_quote('`', "table")])), table_name ); assert_eq!(vec![Ident::with_quote('`', "date")], columns); let insert_alias = insert_alias.unwrap(); assert_eq!( - ObjectName(vec![Ident::with_quote('`', "alias")]), + ObjectName::from(vec![Ident::with_quote('`', "alias")]), insert_alias.row_alias ); assert_eq!(Some(vec![]), insert_alias.col_aliases); @@ -1659,7 +1662,7 @@ fn parse_insert_as() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::with_quote('`', "table")])), + TableObject::TableName(ObjectName::from(vec![Ident::with_quote('`', "table")])), table_name ); assert_eq!( @@ -1668,7 +1671,7 @@ fn parse_insert_as() { ); let insert_alias = insert_alias.unwrap(); assert_eq!( - ObjectName(vec![Ident::with_quote('`', "alias")]), + ObjectName::from(vec![Ident::with_quote('`', "alias")]), insert_alias.row_alias ); assert_eq!( @@ -1719,7 +1722,7 @@ fn parse_replace_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tasks")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tasks")])), table_name ); assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); @@ -1766,7 +1769,7 @@ fn parse_empty_row_insert() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("tb")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("tb")])), table_name ); assert!(columns.is_empty()); @@ -1808,7 +1811,7 @@ fn parse_insert_with_on_duplicate_update() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("permission_groups")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("permission_groups")])), table_name ); assert_eq!( @@ -1855,31 +1858,31 @@ fn parse_insert_with_on_duplicate_update() { assert_eq!( Some(OnInsert::DuplicateKeyUpdate(vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "description".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("description"))]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "perm_create".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_create"))]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "perm_read".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_read"))]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "perm_update".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_update"))]), }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![Ident::new( + target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new( "perm_delete".to_string() )])), value: call("VALUES", [Expr::Identifier(Ident::new("perm_delete"))]), @@ -1910,7 +1913,7 @@ fn parse_select_with_numeric_prefix_column_name() { )))], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::with_quote( + relation: table_from_name(ObjectName::from(vec![Ident::with_quote( '"', "table" )])), joins: vec![] @@ -1962,7 +1965,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { ], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::with_quote( + relation: table_from_name(ObjectName::from(vec![Ident::with_quote( '"', "table" )])), joins: vec![] @@ -1997,7 +2000,7 @@ fn parse_insert_with_numeric_prefix_column_name() { .. }) => { assert_eq!( - TableObject::TableName(ObjectName(vec![Ident::new("s1"), Ident::new("t1")])), + TableObject::TableName(ObjectName::from(vec![Ident::new("s1"), Ident::new("t1")])), table_name ); assert_eq!(vec![Ident::new("123col_$@length123")], columns); @@ -2021,7 +2024,7 @@ fn parse_update_with_joins() { assert_eq!( TableWithJoins { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("orders")]), + name: ObjectName::from(vec![Ident::new("orders")]), alias: Some(TableAlias { name: Ident::new("o"), columns: vec![] @@ -2036,7 +2039,7 @@ fn parse_update_with_joins() { }, joins: vec![Join { relation: TableFactor::Table { - name: ObjectName(vec![Ident::new("customers")]), + name: ObjectName::from(vec![Ident::new("customers")]), alias: Some(TableAlias { name: Ident::new("c"), columns: vec![] @@ -2067,7 +2070,7 @@ fn parse_update_with_joins() { ); assert_eq!( vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec![ + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ Ident::new("o"), Ident::new("completed") ])), @@ -2255,7 +2258,7 @@ fn parse_alter_table_drop_primary_key() { #[test] fn parse_alter_table_change_column() { - let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_name = ObjectName::from(vec![Ident::new("orders")]); let expected_operation = AlterTableOperation::ChangeColumn { old_name: Ident::new("description"), new_name: Ident::new("desc"), @@ -2307,7 +2310,7 @@ fn parse_alter_table_change_column() { #[test] fn parse_alter_table_change_column_with_column_position() { - let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_name = ObjectName::from(vec![Ident::new("orders")]); let expected_operation_first = AlterTableOperation::ChangeColumn { old_name: Ident::new("description"), new_name: Ident::new("desc"), @@ -2355,7 +2358,7 @@ fn parse_alter_table_change_column_with_column_position() { #[test] fn parse_alter_table_modify_column() { - let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_name = ObjectName::from(vec![Ident::new("orders")]); let expected_operation = AlterTableOperation::ModifyColumn { col_name: Ident::new("description"), data_type: DataType::Text, @@ -2404,7 +2407,7 @@ fn parse_alter_table_modify_column() { #[test] fn parse_alter_table_modify_column_with_column_position() { - let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_name = ObjectName::from(vec![Ident::new("orders")]); let expected_operation_first = AlterTableOperation::ModifyColumn { col_name: Ident::new("description"), data_type: DataType::Text, @@ -2478,7 +2481,7 @@ fn parse_substring_in_select() { })], into: None, from: vec![TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident { + relation: table_from_name(ObjectName::from(vec![Ident { value: "test".to_string(), quote_style: None, span: Span::empty(), @@ -2873,10 +2876,12 @@ fn parse_create_table_with_column_collate() { vec![ColumnDef { name: Ident::new("id"), data_type: DataType::Text, - collation: Some(ObjectName(vec![Ident::new("utf8mb4_0900_ai_ci")])), + collation: Some(ObjectName::from(vec![Ident::new("utf8mb4_0900_ai_ci")])), options: vec![ColumnOptionDef { name: None, - option: ColumnOption::CharacterSet(ObjectName(vec![Ident::new("utf8mb4")])) + option: ColumnOption::CharacterSet(ObjectName::from(vec![Ident::new( + "utf8mb4" + )])) }], },], columns @@ -3039,7 +3044,7 @@ fn parse_grant() { ); assert_eq!( objects, - GrantObjects::Tables(vec![ObjectName(vec!["*".into(), "*".into()])]) + GrantObjects::Tables(vec![ObjectName::from(vec!["*".into(), "*".into()])]) ); assert!(!with_grant_option); assert!(granted_by.is_none()); @@ -3080,7 +3085,7 @@ fn parse_revoke() { ); assert_eq!( objects, - GrantObjects::Tables(vec![ObjectName(vec!["db1".into(), "*".into()])]) + GrantObjects::Tables(vec![ObjectName::from(vec!["db1".into(), "*".into()])]) ); if let [Grantee { grantee_type: GranteesType::None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0fca4cec1..b3eb4f10d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -402,7 +402,7 @@ fn parse_create_table_with_defaults() { unit: None } )), - collation: Some(ObjectName(vec![Ident::with_quote('"', "es_ES")])), + collation: Some(ObjectName::from(vec![Ident::with_quote('"', "es_ES")])), options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -1040,7 +1040,7 @@ fn test_copy_from() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1058,7 +1058,7 @@ fn test_copy_from() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1076,7 +1076,7 @@ fn test_copy_from() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1100,7 +1100,7 @@ fn test_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1118,7 +1118,7 @@ fn test_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1136,7 +1136,7 @@ fn test_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1177,7 +1177,7 @@ fn parse_copy_from() { pg_and_generic().one_statement_parses_to(sql, ""), Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["table".into()]), + table_name: ObjectName::from(vec!["table".into()]), columns: vec!["a".into(), "b".into()], }, to: false, @@ -1223,7 +1223,7 @@ fn parse_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1241,7 +1241,7 @@ fn parse_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["country".into()]), + table_name: ObjectName::from(vec!["country".into()]), columns: vec![], }, to: true, @@ -1258,7 +1258,7 @@ fn parse_copy_to() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["country".into()]), + table_name: ObjectName::from(vec!["country".into()]), columns: vec![], }, to: true, @@ -1344,7 +1344,7 @@ fn parse_copy_from_before_v9_0() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1373,7 +1373,7 @@ fn parse_copy_from_before_v9_0() { pg_and_generic().one_statement_parses_to(sql, ""), Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: false, @@ -1401,7 +1401,7 @@ fn parse_copy_to_before_v9_0() { stmt, Statement::Copy { source: CopySource::Table { - table_name: ObjectName(vec!["users".into()]), + table_name: ObjectName::from(vec!["users".into()]), columns: vec![], }, to: true, @@ -1433,7 +1433,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), + variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), value: vec![Expr::Identifier(Ident { value: "b".into(), quote_style: None, @@ -1448,7 +1448,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), + variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), value: vec![Expr::Value(Value::SingleQuotedString("b".into()))], } ); @@ -1459,7 +1459,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), + variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), value: vec![Expr::Value(number("0"))], } ); @@ -1470,7 +1470,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), + variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), value: vec![Expr::Identifier(Ident::new("DEFAULT"))], } ); @@ -1481,7 +1481,7 @@ fn parse_set() { Statement::SetVariable { local: true, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![Ident::new("a")])), + variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), value: vec![Expr::Identifier("b".into())], } ); @@ -1492,7 +1492,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![ + variables: OneOrManyWithParens::One(ObjectName::from(vec![ Ident::new("a"), Ident::new("b"), Ident::new("c") @@ -1514,7 +1514,7 @@ fn parse_set() { Statement::SetVariable { local: false, hivevar: false, - variables: OneOrManyWithParens::One(ObjectName(vec![ + variables: OneOrManyWithParens::One(ObjectName::from(vec![ Ident::new("hive"), Ident::new("tez"), Ident::new("auto"), @@ -1658,7 +1658,7 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName(vec!["a".into()]), + name: ObjectName::from(vec!["a".into()]), parameters: vec![], has_parentheses: false, using: vec![] @@ -1669,7 +1669,7 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName(vec!["a".into()]), + name: ObjectName::from(vec!["a".into()]), parameters: vec![ Expr::Value(number("1")), Expr::Value(Value::SingleQuotedString("t".to_string())) @@ -1684,7 +1684,7 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName(vec!["a".into()]), + name: ObjectName::from(vec!["a".into()]), parameters: vec![], has_parentheses: false, using: vec![ @@ -1793,7 +1793,9 @@ fn parse_pg_on_conflict() { assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from( + vec!["dname".into()] + )), value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "dname".into()]) },], selection: None @@ -1824,14 +1826,18 @@ fn parse_pg_on_conflict() { OnConflictAction::DoUpdate(DoUpdate { assignments: vec![ Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ + "dname".into() + ])), value: Expr::CompoundIdentifier(vec![ "EXCLUDED".into(), "dname".into() ]) }, Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["area".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from(vec![ + "area".into() + ])), value: Expr::CompoundIdentifier(vec!["EXCLUDED".into(), "area".into()]) }, ], @@ -1881,7 +1887,9 @@ fn parse_pg_on_conflict() { assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from( + vec!["dname".into()] + )), value: Expr::Value(Value::Placeholder("$1".to_string())) },], selection: Some(Expr::BinaryOp { @@ -1915,11 +1923,16 @@ fn parse_pg_on_conflict() { })), .. }) => { - assert_eq!(vec![Ident::from("distributors_did_pkey")], cname.0); + assert_eq!( + ObjectName::from(vec![Ident::from("distributors_did_pkey")]), + cname + ); assert_eq!( OnConflictAction::DoUpdate(DoUpdate { assignments: vec![Assignment { - target: AssignmentTarget::ColumnName(ObjectName(vec!["dname".into()])), + target: AssignmentTarget::ColumnName(ObjectName::from( + vec!["dname".into()] + )), value: Expr::Value(Value::Placeholder("$1".to_string())) },], selection: Some(Expr::BinaryOp { @@ -2624,7 +2637,7 @@ fn parse_array_subquery_expr() { let select = pg().verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("ARRAY")]), + name: ObjectName::from(vec![Ident::new("ARRAY")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::Subquery(Box::new(Query { @@ -2963,7 +2976,10 @@ fn parse_json_table_is_not_reserved() { TableFactor::Table { name: ObjectName(name), .. - } => assert_eq!("JSON_TABLE", name[0].value), + } => assert_eq!( + ObjectNamePart::Identifier(Ident::new("JSON_TABLE")), + name[0] + ), other => panic!("Expected: JSON_TABLE to be parsed as a table name, but got {other:?}"), } } @@ -3004,7 +3020,7 @@ fn test_composite_value() { SelectItem::UnnamedExpr(Expr::CompositeAccess { key: Ident::new("n"), expr: Box::new(Expr::Nested(Box::new(Expr::Function(Function { - name: ObjectName(vec![ + name: ObjectName::from(vec![ Ident::new("information_schema"), Ident::new("_pg_expandarray") ]), @@ -3185,7 +3201,7 @@ fn parse_current_functions() { let select = pg_and_generic().verified_only_select(sql); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("CURRENT_CATALOG")]), + name: ObjectName::from(vec![Ident::new("CURRENT_CATALOG")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -3198,7 +3214,7 @@ fn parse_current_functions() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("CURRENT_USER")]), + name: ObjectName::from(vec![Ident::new("CURRENT_USER")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -3211,7 +3227,7 @@ fn parse_current_functions() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("SESSION_USER")]), + name: ObjectName::from(vec![Ident::new("SESSION_USER")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -3224,7 +3240,7 @@ fn parse_current_functions() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::new("USER")]), + name: ObjectName::from(vec![Ident::new("USER")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::None, @@ -3536,7 +3552,7 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Set { - config_name: ObjectName(vec![Ident { + config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), @@ -3557,13 +3573,13 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Set { - config_name: ObjectName(vec![Ident { + config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), }]), config_value: SetConfigValue::Value(Expr::Value(number("100000"))), - in_database: Some(ObjectName(vec![Ident { + in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, span: Span::empty(), @@ -3582,13 +3598,13 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Set { - config_name: ObjectName(vec![Ident { + config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), }]), config_value: SetConfigValue::Value(Expr::Value(number("100000"))), - in_database: Some(ObjectName(vec![Ident { + in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, span: Span::empty(), @@ -3607,13 +3623,13 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Set { - config_name: ObjectName(vec![Ident { + config_name: ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), }]), config_value: SetConfigValue::Default, - in_database: Some(ObjectName(vec![Ident { + in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, span: Span::empty(), @@ -3648,12 +3664,12 @@ fn parse_alter_role() { span: Span::empty(), }, operation: AlterRoleOperation::Reset { - config_name: ResetConfig::ConfigName(ObjectName(vec![Ident { + config_name: ResetConfig::ConfigName(ObjectName::from(vec![Ident { value: "maintenance_work_mem".into(), quote_style: None, span: Span::empty(), }])), - in_database: Some(ObjectName(vec![Ident { + in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, span: Span::empty(), @@ -3679,7 +3695,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -3698,7 +3717,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -3754,7 +3773,7 @@ fn parse_create_function() { Statement::CreateFunction(CreateFunction { or_replace: false, temporary: false, - name: ObjectName(vec![Ident::new("add")]), + name: ObjectName::from(vec![Ident::new("add")]), args: Some(vec![ OperateFunctionArg::unnamed(DataType::Integer(None)), OperateFunctionArg::unnamed(DataType::Integer(None)), @@ -3798,7 +3817,7 @@ fn parse_drop_function() { Statement::DropFunction { if_exists: true, func_desc: vec![FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_func".to_string(), quote_style: None, span: Span::empty(), @@ -3815,7 +3834,7 @@ fn parse_drop_function() { Statement::DropFunction { if_exists: true, func_desc: vec![FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_func".to_string(), quote_style: None, span: Span::empty(), @@ -3841,7 +3860,7 @@ fn parse_drop_function() { if_exists: true, func_desc: vec![ FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_func1".to_string(), quote_style: None, span: Span::empty(), @@ -3860,7 +3879,7 @@ fn parse_drop_function() { ]), }, FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_func2".to_string(), quote_style: None, span: Span::empty(), @@ -3892,7 +3911,7 @@ fn parse_drop_procedure() { Statement::DropProcedure { if_exists: true, proc_desc: vec![FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_proc".to_string(), quote_style: None, span: Span::empty(), @@ -3909,7 +3928,7 @@ fn parse_drop_procedure() { Statement::DropProcedure { if_exists: true, proc_desc: vec![FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_proc".to_string(), quote_style: None, span: Span::empty(), @@ -3935,7 +3954,7 @@ fn parse_drop_procedure() { if_exists: true, proc_desc: vec![ FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_proc1".to_string(), quote_style: None, span: Span::empty(), @@ -3954,7 +3973,7 @@ fn parse_drop_procedure() { ]), }, FunctionDesc { - name: ObjectName(vec![Ident { + name: ObjectName::from(vec![Ident { value: "test_proc2".to_string(), quote_style: None, span: Span::empty(), @@ -4136,7 +4155,7 @@ fn parse_select_group_by_cube() { #[test] fn parse_truncate() { let truncate = pg_and_generic().verified_stmt("TRUNCATE db.table_name"); - let table_name = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); + let table_name = ObjectName::from(vec![Ident::new("db"), Ident::new("table_name")]); let table_names = vec![TruncateTableTarget { name: table_name.clone(), }]; @@ -4159,7 +4178,7 @@ fn parse_truncate_with_options() { let truncate = pg_and_generic() .verified_stmt("TRUNCATE TABLE ONLY db.table_name RESTART IDENTITY CASCADE"); - let table_name = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); + let table_name = ObjectName::from(vec![Ident::new("db"), Ident::new("table_name")]); let table_names = vec![TruncateTableTarget { name: table_name.clone(), }]; @@ -4184,8 +4203,8 @@ fn parse_truncate_with_table_list() { "TRUNCATE TABLE db.table_name, db.other_table_name RESTART IDENTITY CASCADE", ); - let table_name_a = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]); - let table_name_b = ObjectName(vec![Ident::new("db"), Ident::new("other_table_name")]); + let table_name_a = ObjectName::from(vec![Ident::new("db"), Ident::new("table_name")]); + let table_name_b = ObjectName::from(vec![Ident::new("db"), Ident::new("other_table_name")]); let table_names = vec![ TruncateTableTarget { @@ -4381,7 +4400,7 @@ fn test_simple_postgres_insert_with_alias() { or: None, ignore: false, into: true, - table: TableObject::TableName(ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName::from(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), @@ -4451,7 +4470,7 @@ fn test_simple_postgres_insert_with_alias() { or: None, ignore: false, into: true, - table: TableObject::TableName(ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName::from(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), @@ -4523,7 +4542,7 @@ fn test_simple_insert_with_quoted_alias() { or: None, ignore: false, into: true, - table: TableObject::TableName(ObjectName(vec![Ident { + table: TableObject::TableName(ObjectName::from(vec![Ident { value: "test_tables".to_string(), quote_style: None, span: Span::empty(), @@ -4720,10 +4739,10 @@ fn parse_create_simple_before_insert_trigger() { let expected = Statement::CreateTrigger { or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("check_insert")]), + name: ObjectName::from(vec![Ident::new("check_insert")]), period: TriggerPeriod::Before, events: vec![TriggerEvent::Insert], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], trigger_object: TriggerObject::Row, @@ -4732,7 +4751,7 @@ fn parse_create_simple_before_insert_trigger() { exec_body: TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_insert")]), + name: ObjectName::from(vec![Ident::new("check_account_insert")]), args: None, }, }, @@ -4748,10 +4767,10 @@ fn parse_create_after_update_trigger_with_condition() { let expected = Statement::CreateTrigger { or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("check_update")]), + name: ObjectName::from(vec![Ident::new("check_update")]), period: TriggerPeriod::After, events: vec![TriggerEvent::Update(vec![])], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], trigger_object: TriggerObject::Row, @@ -4767,7 +4786,7 @@ fn parse_create_after_update_trigger_with_condition() { exec_body: TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_update")]), + name: ObjectName::from(vec![Ident::new("check_account_update")]), args: None, }, }, @@ -4783,10 +4802,10 @@ fn parse_create_instead_of_delete_trigger() { let expected = Statement::CreateTrigger { or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("check_delete")]), + name: ObjectName::from(vec![Ident::new("check_delete")]), period: TriggerPeriod::InsteadOf, events: vec![TriggerEvent::Delete], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], trigger_object: TriggerObject::Row, @@ -4795,7 +4814,7 @@ fn parse_create_instead_of_delete_trigger() { exec_body: TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_deletes")]), + name: ObjectName::from(vec![Ident::new("check_account_deletes")]), args: None, }, }, @@ -4811,14 +4830,14 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { let expected = Statement::CreateTrigger { or_replace: false, is_constraint: true, - name: ObjectName(vec![Ident::new("check_multiple_events")]), + name: ObjectName::from(vec![Ident::new("check_multiple_events")]), period: TriggerPeriod::Before, events: vec![ TriggerEvent::Insert, TriggerEvent::Update(vec![]), TriggerEvent::Delete, ], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![], trigger_object: TriggerObject::Row, @@ -4827,7 +4846,7 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() { exec_body: TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_changes")]), + name: ObjectName::from(vec![Ident::new("check_account_changes")]), args: None, }, }, @@ -4847,21 +4866,21 @@ fn parse_create_trigger_with_referencing() { let expected = Statement::CreateTrigger { or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("check_referencing")]), + name: ObjectName::from(vec![Ident::new("check_referencing")]), period: TriggerPeriod::Before, events: vec![TriggerEvent::Insert], - table_name: ObjectName(vec![Ident::new("accounts")]), + table_name: ObjectName::from(vec![Ident::new("accounts")]), referenced_table_name: None, referencing: vec![ TriggerReferencing { refer_type: TriggerReferencingType::NewTable, is_as: true, - transition_relation_name: ObjectName(vec![Ident::new("new_accounts")]), + transition_relation_name: ObjectName::from(vec![Ident::new("new_accounts")]), }, TriggerReferencing { refer_type: TriggerReferencingType::OldTable, is_as: true, - transition_relation_name: ObjectName(vec![Ident::new("old_accounts")]), + transition_relation_name: ObjectName::from(vec![Ident::new("old_accounts")]), }, ], trigger_object: TriggerObject::Row, @@ -4870,7 +4889,7 @@ fn parse_create_trigger_with_referencing() { exec_body: TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("check_account_referencing")]), + name: ObjectName::from(vec![Ident::new("check_account_referencing")]), args: None, }, }, @@ -4929,8 +4948,8 @@ fn parse_drop_trigger() { pg().verified_stmt(sql), Statement::DropTrigger { if_exists, - trigger_name: ObjectName(vec![Ident::new("check_update")]), - table_name: ObjectName(vec![Ident::new("table_name")]), + trigger_name: ObjectName::from(vec![Ident::new("check_update")]), + table_name: ObjectName::from(vec![Ident::new("table_name")]), option } ); @@ -5044,7 +5063,7 @@ fn parse_trigger_related_functions() { transient: false, volatile: false, iceberg: false, - name: ObjectName(vec![Ident::new("emp")]), + name: ObjectName::from(vec![Ident::new("emp")]), columns: vec![ ColumnDef { name: "empname".into(), @@ -5126,7 +5145,7 @@ fn parse_trigger_related_functions() { or_replace: false, temporary: false, if_not_exists: false, - name: ObjectName(vec![Ident::new("emp_stamp")]), + name: ObjectName::from(vec![Ident::new("emp_stamp")]), args: None, return_type: Some(DataType::Trigger), function_body: Some( @@ -5161,10 +5180,10 @@ fn parse_trigger_related_functions() { Statement::CreateTrigger { or_replace: false, is_constraint: false, - name: ObjectName(vec![Ident::new("emp_stamp")]), + name: ObjectName::from(vec![Ident::new("emp_stamp")]), period: TriggerPeriod::Before, events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![])], - table_name: ObjectName(vec![Ident::new("emp")]), + table_name: ObjectName::from(vec![Ident::new("emp")]), referenced_table_name: None, referencing: vec![], trigger_object: TriggerObject::Row, @@ -5173,7 +5192,7 @@ fn parse_trigger_related_functions() { exec_body: TriggerExecBody { exec_type: TriggerExecBodyType::Function, func_desc: FunctionDesc { - name: ObjectName(vec![Ident::new("emp_stamp")]), + name: ObjectName::from(vec![Ident::new("emp_stamp")]), args: None, } }, @@ -5186,8 +5205,8 @@ fn parse_trigger_related_functions() { drop_trigger, Statement::DropTrigger { if_exists: false, - trigger_name: ObjectName(vec![Ident::new("emp_stamp")]), - table_name: ObjectName(vec![Ident::new("emp")]), + trigger_name: ObjectName::from(vec![Ident::new("emp_stamp")]), + table_name: ObjectName::from(vec![Ident::new("emp")]), option: None } ); diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 857d378bc..c4b897f01 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -39,7 +39,7 @@ fn test_square_brackets_over_db_schema_table_name() { assert_eq!( select.from[0], TableWithJoins { - relation: table_from_name(ObjectName(vec![ + relation: table_from_name(ObjectName::from(vec![ Ident { value: "test_schema".to_string(), quote_style: Some('['), @@ -81,7 +81,7 @@ fn test_double_quotes_over_db_schema_table_name() { assert_eq!( select.from[0], TableWithJoins { - relation: table_from_name(ObjectName(vec![ + relation: table_from_name(ObjectName::from(vec![ Ident { value: "test_schema".to_string(), quote_style: Some('"'), @@ -114,7 +114,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -133,7 +136,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -297,7 +300,7 @@ fn test_parse_json_path_from() { TableFactor::Table { name, json_path, .. } => { - assert_eq!(name, &ObjectName(vec![Ident::new("src")])); + assert_eq!(name, &ObjectName::from(vec![Ident::new("src")])); assert_eq!( json_path, &Some(JsonPath { @@ -321,7 +324,7 @@ fn test_parse_json_path_from() { TableFactor::Table { name, json_path, .. } => { - assert_eq!(name, &ObjectName(vec![Ident::new("src")])); + assert_eq!(name, &ObjectName::from(vec![Ident::new("src")])); assert_eq!( json_path, &Some(JsonPath { @@ -354,7 +357,7 @@ fn test_parse_json_path_from() { } => { assert_eq!( name, - &ObjectName(vec![Ident::new("src"), Ident::new("a"), Ident::new("b")]) + &ObjectName::from(vec![Ident::new("src"), Ident::new("a"), Ident::new("b")]) ); assert_eq!(json_path, &None); } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 3320400e9..2b2350936 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -634,7 +634,7 @@ fn test_snowflake_create_table_with_collated_column() { vec![ColumnDef { name: "a".into(), data_type: DataType::Text, - collation: Some(ObjectName(vec![Ident::with_quote('\'', "de_DE")])), + collation: Some(ObjectName::from(vec![Ident::with_quote('\'', "de_DE")])), options: vec![] },] ); @@ -818,7 +818,7 @@ fn test_snowflake_create_table_with_several_column_options() { ColumnDef { name: "b".into(), data_type: DataType::Text, - collation: Some(ObjectName(vec![Ident::with_quote('\'', "de_DE")])), + collation: Some(ObjectName::from(vec![Ident::with_quote('\'', "de_DE")])), options: vec![ ColumnOptionDef { name: None, @@ -1274,7 +1274,10 @@ fn parse_delimited_identifiers() { version, .. } => { - assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); + assert_eq!( + ObjectName::from(vec![Ident::with_quote('"', "a table")]), + name + ); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); @@ -1293,7 +1296,7 @@ fn parse_delimited_identifiers() { ); assert_eq!( &Expr::Function(Function { - name: ObjectName(vec![Ident::with_quote('"', "myfun")]), + name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -1365,7 +1368,7 @@ fn test_select_wildcard_with_exclude() { let select = snowflake_and_generic() .verified_only_select("SELECT name.* EXCLUDE department_id FROM employee_table"); let expected = SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("name")]), + ObjectName::from(vec![Ident::new("name")]), WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), ..Default::default() @@ -1402,7 +1405,7 @@ fn test_select_wildcard_with_rename() { "SELECT name.* RENAME (department_id AS new_dep, employee_id AS new_emp) FROM employee_table", ); let expected = SelectItem::QualifiedWildcard( - ObjectName(vec![Ident::new("name")]), + ObjectName::from(vec![Ident::new("name")]), WildcardAdditionalOptions { opt_rename: Some(RenameSelectItem::Multiple(vec![ IdentWithAlias { @@ -1505,7 +1508,7 @@ fn test_alter_table_clustering() { Expr::Identifier(Ident::new("c1")), Expr::Identifier(Ident::with_quote('"', "c2")), Expr::Function(Function { - name: ObjectName(vec![Ident::new("TO_DATE")]), + name: ObjectName::from(vec![Ident::new("TO_DATE")]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -2034,11 +2037,11 @@ fn test_copy_into() { } => { assert_eq!( into, - ObjectName(vec![Ident::new("my_company"), Ident::new("emp_basic")]) + ObjectName::from(vec![Ident::new("my_company"), Ident::new("emp_basic")]) ); assert_eq!( from_stage, - ObjectName(vec![Ident::with_quote('\'', "gcs://mybucket/./../a.csv")]) + ObjectName::from(vec![Ident::with_quote('\'', "gcs://mybucket/./../a.csv")]) ); assert!(files.is_none()); assert!(pattern.is_none()); @@ -2069,7 +2072,7 @@ fn test_copy_into_with_stage_params() { //assert_eq!("s3://load/files/", stage_params.url.unwrap()); assert_eq!( from_stage, - ObjectName(vec![Ident::with_quote('\'', "s3://load/files/")]) + ObjectName::from(vec![Ident::with_quote('\'', "s3://load/files/")]) ); assert_eq!("myint", stage_params.storage_integration.unwrap()); assert_eq!( @@ -2128,7 +2131,7 @@ fn test_copy_into_with_stage_params() { } => { assert_eq!( from_stage, - ObjectName(vec![Ident::with_quote('\'', "s3://load/files/")]) + ObjectName::from(vec![Ident::with_quote('\'', "s3://load/files/")]) ); assert_eq!("myint", stage_params.storage_integration.unwrap()); } @@ -2182,7 +2185,7 @@ fn test_copy_into_with_transformations() { } => { assert_eq!( from_stage, - ObjectName(vec![Ident::new("@schema"), Ident::new("general_finished")]) + ObjectName::from(vec![Ident::new("@schema"), Ident::new("general_finished")]) ); assert_eq!( from_transformations.as_ref().unwrap()[0], @@ -2291,17 +2294,17 @@ fn test_snowflake_stage_object_names() { "@~/path", ]; let mut allowed_object_names = [ - ObjectName(vec![Ident::new("my_company"), Ident::new("emp_basic")]), - ObjectName(vec![Ident::new("@namespace"), Ident::new("%table_name")]), - ObjectName(vec![ + ObjectName::from(vec![Ident::new("my_company"), Ident::new("emp_basic")]), + ObjectName::from(vec![Ident::new("@namespace"), Ident::new("%table_name")]), + ObjectName::from(vec![ Ident::new("@namespace"), Ident::new("%table_name/path"), ]), - ObjectName(vec![ + ObjectName::from(vec![ Ident::new("@namespace"), Ident::new("stage_name/path"), ]), - ObjectName(vec![Ident::new("@~/path")]), + ObjectName::from(vec![Ident::new("@~/path")]), ]; for it in allowed_formatted_names @@ -2330,10 +2333,13 @@ fn test_snowflake_copy_into() { Statement::CopyIntoSnowflake { into, from_stage, .. } => { - assert_eq!(into, ObjectName(vec![Ident::new("a"), Ident::new("b")])); + assert_eq!( + into, + ObjectName::from(vec![Ident::new("a"), Ident::new("b")]) + ); assert_eq!( from_stage, - ObjectName(vec![Ident::new("@namespace"), Ident::new("stage_name")]) + ObjectName::from(vec![Ident::new("@namespace"), Ident::new("stage_name")]) ) } _ => unreachable!(), @@ -2350,14 +2356,14 @@ fn test_snowflake_copy_into_stage_name_ends_with_parens() { } => { assert_eq!( into, - ObjectName(vec![ + ObjectName::from(vec![ Ident::new("SCHEMA"), Ident::new("SOME_MONITORING_SYSTEM") ]) ); assert_eq!( from_stage, - ObjectName(vec![Ident::new("@schema"), Ident::new("general_finished")]) + ObjectName::from(vec![Ident::new("@schema"), Ident::new("general_finished")]) ) } _ => unreachable!(), @@ -2771,7 +2777,7 @@ fn parse_use() { // Test single identifier without quotes assert_eq!( snowflake().verified_stmt(&format!("USE {}", object_name)), - Statement::Use(Use::Object(ObjectName(vec![Ident::new( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::new( object_name.to_string() )]))) ); @@ -2779,7 +2785,7 @@ fn parse_use() { // Test single identifier with different type of quotes assert_eq!( snowflake().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)), - Statement::Use(Use::Object(ObjectName(vec![Ident::with_quote( + Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote( quote, object_name.to_string(), )]))) @@ -2791,7 +2797,7 @@ fn parse_use() { // Test double identifier with different type of quotes assert_eq!( snowflake().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)), - Statement::Use(Use::Object(ObjectName(vec![ + Statement::Use(Use::Object(ObjectName::from(vec![ Ident::with_quote(quote, "CATALOG"), Ident::with_quote(quote, "my_schema") ]))) @@ -2800,7 +2806,7 @@ fn parse_use() { // Test double identifier without quotes assert_eq!( snowflake().verified_stmt("USE mydb.my_schema"), - Statement::Use(Use::Object(ObjectName(vec![ + Statement::Use(Use::Object(ObjectName::from(vec![ Ident::new("mydb"), Ident::new("my_schema") ]))) @@ -2810,35 +2816,35 @@ fn parse_use() { // 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)), - Statement::Use(Use::Database(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Schema(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Schema(ObjectName(vec![ + 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)), - Statement::Use(Use::Role(ObjectName(vec![Ident::with_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)), - Statement::Use(Use::Warehouse(ObjectName(vec![Ident::with_quote( + Statement::Use(Use::Warehouse(ObjectName::from(vec![Ident::with_quote( quote, "my_wh".to_string(), )]))) @@ -3076,7 +3082,7 @@ fn parse_ls_and_rm() { .verified_stmt("LIST @SNOWFLAKE_KAFKA_CONNECTOR_externalDataLakeSnowflakeConnector_STAGE_call_tracker_stream/"); match statement { Statement::List(command) => { - assert_eq!(command.stage, ObjectName(vec!["@SNOWFLAKE_KAFKA_CONNECTOR_externalDataLakeSnowflakeConnector_STAGE_call_tracker_stream/".into()])); + assert_eq!(command.stage, ObjectName::from(vec!["@SNOWFLAKE_KAFKA_CONNECTOR_externalDataLakeSnowflakeConnector_STAGE_call_tracker_stream/".into()])); assert!(command.pattern.is_none()); } _ => unreachable!(), @@ -3088,7 +3094,7 @@ fn parse_ls_and_rm() { Statement::Remove(command) => { assert_eq!( command.stage, - ObjectName(vec!["@my_csv_stage/analysis/".into()]) + ObjectName::from(vec!["@my_csv_stage/analysis/".into()]) ); assert_eq!(command.pattern, Some(".*data_0.*".to_string())); } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index edd1365f4..3a612f70a 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -418,7 +418,7 @@ fn parse_window_function_with_filter() { assert_eq!( select.projection, vec![SelectItem::UnnamedExpr(Expr::Function(Function { - name: ObjectName(vec![Ident::new(func_name)]), + name: ObjectName::from(vec![Ident::new(func_name)]), uses_odbc_syntax: false, parameters: FunctionArguments::None, args: FunctionArguments::List(FunctionArgumentList { @@ -469,8 +469,8 @@ fn parse_update_tuple_row_values() { or: None, assignments: vec![Assignment { target: AssignmentTarget::Tuple(vec![ - ObjectName(vec![Ident::new("a"),]), - ObjectName(vec![Ident::new("b"),]), + ObjectName::from(vec![Ident::new("a"),]), + ObjectName::from(vec![Ident::new("b"),]), ]), value: Expr::Tuple(vec![ Expr::Value(Value::Number("1".parse().unwrap(), false)), @@ -479,7 +479,7 @@ fn parse_update_tuple_row_values() { }], selection: None, table: TableWithJoins { - relation: table_from_name(ObjectName(vec![Ident::new("x")])), + relation: table_from_name(ObjectName::from(vec![Ident::new("x")])), joins: vec![], }, from: None, From cbe59a2d8b08cf01469f48e8f65bf74bf286f882 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:15:36 +0100 Subject: [PATCH 699/806] Enable GROUP BY exp for Snowflake dialect (#1683) --- src/dialect/snowflake.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index bd9afb191..eb8ea4de9 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -305,6 +305,11 @@ impl Dialect for SnowflakeDialect { fn supports_timestamp_versioning(&self) -> bool { true } + + /// See: + fn supports_group_by_expr(&self) -> bool { + true + } } fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { From 74163b148ed984cb73146699998f615f7b22a642 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:20:00 +0100 Subject: [PATCH 700/806] Add support for parsing empty dictionary expressions (#1684) --- src/parser/mod.rs | 3 ++- tests/sqlparser_common.rs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9cc8f0620..0d2973c7f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2854,7 +2854,8 @@ impl<'a> Parser<'a> { fn parse_duckdb_struct_literal(&mut self) -> Result { self.expect_token(&Token::LBrace)?; - let fields = self.parse_comma_separated(Self::parse_duckdb_dictionary_field)?; + let fields = + self.parse_comma_separated0(Self::parse_duckdb_dictionary_field, Token::RBrace)?; self.expect_token(&Token::RBrace)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6897d44ae..5a1e812d0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11291,6 +11291,8 @@ fn test_dictionary_syntax() { ); } + check("{}", Expr::Dictionary(vec![])); + check( "{'Alberta': 'Edmonton', 'Manitoba': 'Winnipeg'}", Expr::Dictionary(vec![ From fdbe864d0d507068fcd666cebc4507e368503d9d Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Tue, 28 Jan 2025 08:45:14 +0100 Subject: [PATCH 701/806] Support multiple tables in `UPDATE FROM` clause (#1681) --- src/ast/mod.rs | 4 ++-- src/ast/query.rs | 4 ++-- src/ast/spans.rs | 9 +++++---- src/parser/mod.rs | 6 ++++-- tests/sqlparser_common.rs | 11 +++++++---- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b473dc11f..e64b7d3db 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3872,13 +3872,13 @@ impl fmt::Display for Statement { } write!(f, "{table}")?; if let Some(UpdateTableFromKind::BeforeSet(from)) = from { - write!(f, " FROM {from}")?; + write!(f, " FROM {}", display_comma_separated(from))?; } if !assignments.is_empty() { write!(f, " SET {}", display_comma_separated(assignments))?; } if let Some(UpdateTableFromKind::AfterSet(from)) = from { - write!(f, " FROM {from}")?; + write!(f, " FROM {}", display_comma_separated(from))?; } if let Some(selection) = selection { write!(f, " WHERE {selection}")?; diff --git a/src/ast/query.rs b/src/ast/query.rs index 4053dd239..e982c7f0d 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2829,8 +2829,8 @@ impl fmt::Display for ValueTableMode { pub enum UpdateTableFromKind { /// Update Statement where the 'FROM' clause is before the 'SET' keyword (Supported by Snowflake) /// For Example: `UPDATE FROM t1 SET t1.name='aaa'` - BeforeSet(TableWithJoins), + BeforeSet(Vec), /// Update Statement where the 'FROM' clause is after the 'SET' keyword (Which is the standard way) /// For Example: `UPDATE SET t1.name='aaa' FROM t1` - AfterSet(TableWithJoins), + AfterSet(Vec), } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 5316bfbda..aed1c6c2b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2138,10 +2138,11 @@ impl Spanned for SelectInto { impl Spanned for UpdateTableFromKind { fn span(&self) -> Span { - match self { - UpdateTableFromKind::BeforeSet(from) => from.span(), - UpdateTableFromKind::AfterSet(from) => from.span(), - } + let from = match self { + UpdateTableFromKind::BeforeSet(from) => from, + UpdateTableFromKind::AfterSet(from) => from, + }; + union_spans(from.iter().map(|t| t.span())) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0d2973c7f..c6e1eb196 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12617,7 +12617,7 @@ impl<'a> Parser<'a> { let table = self.parse_table_and_joins()?; let from_before_set = if self.parse_keyword(Keyword::FROM) { Some(UpdateTableFromKind::BeforeSet( - self.parse_table_and_joins()?, + self.parse_table_with_joins()?, )) } else { None @@ -12625,7 +12625,9 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::SET)?; let assignments = self.parse_comma_separated(Parser::parse_assignment)?; let from = if from_before_set.is_none() && self.parse_keyword(Keyword::FROM) { - Some(UpdateTableFromKind::AfterSet(self.parse_table_and_joins()?)) + Some(UpdateTableFromKind::AfterSet( + self.parse_table_with_joins()?, + )) } else { from_before_set }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5a1e812d0..5c11b2901 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -403,7 +403,7 @@ fn parse_update_set_from() { target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("name")])), value: Expr::CompoundIdentifier(vec![Ident::new("t2"), Ident::new("name")]) }], - from: Some(UpdateTableFromKind::AfterSet(TableWithJoins { + from: Some(UpdateTableFromKind::AfterSet(vec![TableWithJoins { relation: TableFactor::Derived { lateral: false, subquery: Box::new(Query { @@ -455,7 +455,7 @@ fn parse_update_set_from() { }) }, joins: vec![] - })), + }])), selection: Some(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("t1"), @@ -471,6 +471,9 @@ fn parse_update_set_from() { or: None, } ); + + let sql = "UPDATE T SET a = b FROM U, (SELECT foo FROM V) AS W WHERE 1 = 1"; + dialects.verified_stmt(sql); } #[test] @@ -13051,8 +13054,8 @@ fn parse_select_without_projection() { #[test] fn parse_update_from_before_select() { - all_dialects() - .verified_stmt("UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name WHERE t1.id = t2.id"); + verified_stmt("UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name WHERE t1.id = t2.id"); + verified_stmt("UPDATE t1 FROM U, (SELECT id FROM V) AS W SET a = b WHERE 1 = 1"); let query = "UPDATE t1 FROM (SELECT name, id FROM t1 GROUP BY id) AS t2 SET name = t2.name FROM (SELECT name from t2) AS t2"; From f7b0812b01111c678c595f6f79b8d2f5cf5cb305 Mon Sep 17 00:00:00 2001 From: AvivDavid-Satori <107786696+AvivDavid-Satori@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:41:03 +0200 Subject: [PATCH 702/806] Add support for mysql table hints (#1675) --- src/ast/mod.rs | 10 ++--- src/ast/query.rs | 82 +++++++++++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/mod.rs | 4 ++ src/dialect/mysql.rs | 14 +++++++ src/parser/mod.rs | 67 ++++++++++++++++++++++++++++++ src/test_utils.rs | 3 ++ tests/sqlparser_bigquery.rs | 3 ++ tests/sqlparser_common.rs | 78 +++++++++++++++++++++++++++++++++++ tests/sqlparser_hive.rs | 1 + tests/sqlparser_mssql.rs | 6 +++ tests/sqlparser_mysql.rs | 2 + 12 files changed, 266 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e64b7d3db..6917b7c96 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -69,11 +69,11 @@ pub use self::query::{ OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableSample, - TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, - TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, - TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, - WildcardAdditionalOptions, With, WithFill, + 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 e982c7f0d..09058f760 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -975,6 +975,81 @@ pub struct TableFunctionArgs { pub settings: 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 TableIndexHintType { + Use, + Ignore, + Force, +} + +impl fmt::Display for TableIndexHintType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + TableIndexHintType::Use => "USE", + TableIndexHintType::Ignore => "IGNORE", + TableIndexHintType::Force => "FORCE", + }) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableIndexType { + Index, + Key, +} + +impl fmt::Display for TableIndexType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + TableIndexType::Index => "INDEX", + TableIndexType::Key => "KEY", + }) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableIndexHintForClause { + Join, + OrderBy, + GroupBy, +} + +impl fmt::Display for TableIndexHintForClause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + TableIndexHintForClause::Join => "JOIN", + TableIndexHintForClause::OrderBy => "ORDER BY", + TableIndexHintForClause::GroupBy => "GROUP BY", + }) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TableIndexHints { + pub hint_type: TableIndexHintType, + pub index_type: TableIndexType, + pub for_clause: Option, + pub index_names: Vec, +} + +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, "({})", display_comma_separated(&self.index_names)) + } +} + /// A table name or a parenthesized subquery with an optional alias #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1009,6 +1084,9 @@ pub enum TableFactor { /// Optional table sample modifier /// See: sample: Option, + /// Optional index hints(mysql) + /// See: + index_hints: Vec, }, Derived { lateral: bool, @@ -1590,6 +1668,7 @@ impl fmt::Display for TableFactor { with_ordinality, json_path, sample, + index_hints, } => { write!(f, "{name}")?; if let Some(json_path) = json_path { @@ -1618,6 +1697,9 @@ impl fmt::Display for TableFactor { if let Some(alias) = alias { write!(f, " AS {alias}")?; } + if !index_hints.is_empty() { + write!(f, " {}", display_separated(index_hints, " "))?; + } if !with_hints.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_hints))?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index aed1c6c2b..8f72c26f5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1739,6 +1739,7 @@ impl Spanned for TableFactor { partitions: _, json_path: _, sample: _, + index_hints: _, } => union_spans( name.0 .iter() diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 9fc16cd56..6329c5cfc 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -854,6 +854,10 @@ pub trait Dialect: Debug + Any { fn supports_string_escape_constant(&self) -> bool { false } + /// Returns true if the dialect supports the table hints in the `FROM` clause. + fn supports_table_hints(&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 535b4298a..a67fe67b0 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -25,6 +25,10 @@ use crate::{ parser::{Parser, ParserError}, }; +use super::keywords; + +const RESERVED_FOR_TABLE_ALIAS_MYSQL: &[Keyword] = &[Keyword::USE, Keyword::IGNORE, Keyword::FORCE]; + /// A [`Dialect`] for [MySQL](https://www.mysql.com/) #[derive(Debug)] pub struct MySqlDialect {} @@ -111,6 +115,16 @@ impl Dialect for MySqlDialect { fn supports_user_host_grantee(&self) -> bool { true } + + fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { + explicit + || (!keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) + && !RESERVED_FOR_TABLE_ALIAS_MYSQL.contains(kw)) + } + + fn supports_table_hints(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c6e1eb196..c8ff01f70 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8910,6 +8910,64 @@ impl<'a> Parser<'a> { } } + fn parse_table_index_hints(&mut self) -> Result, ParserError> { + let mut hints = vec![]; + while let Some(hint_type) = + self.parse_one_of_keywords(&[Keyword::USE, Keyword::IGNORE, Keyword::FORCE]) + { + let hint_type = match hint_type { + Keyword::USE => TableIndexHintType::Use, + Keyword::IGNORE => TableIndexHintType::Ignore, + Keyword::FORCE => TableIndexHintType::Force, + _ => { + return self.expected( + "expected to match USE/IGNORE/FORCE keyword", + self.peek_token(), + ) + } + }; + let index_type = match self.parse_one_of_keywords(&[Keyword::INDEX, Keyword::KEY]) { + Some(Keyword::INDEX) => TableIndexType::Index, + Some(Keyword::KEY) => TableIndexType::Key, + _ => { + return self.expected("expected to match INDEX/KEY keyword", self.peek_token()) + } + }; + let for_clause = if self.parse_keyword(Keyword::FOR) { + let clause = if self.parse_keyword(Keyword::JOIN) { + TableIndexHintForClause::Join + } else if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + TableIndexHintForClause::OrderBy + } else if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { + TableIndexHintForClause::GroupBy + } else { + return self.expected( + "expected to match FOR/ORDER BY/GROUP BY table hint in for clause", + self.peek_token(), + ); + }; + Some(clause) + } else { + None + }; + + self.expect_token(&Token::LParen)?; + let index_names = if self.peek_token().token != Token::RParen { + self.parse_comma_separated(Parser::parse_identifier)? + } else { + vec![] + }; + self.expect_token(&Token::RParen)?; + hints.push(TableIndexHints { + hint_type, + index_type, + for_clause, + index_names, + }); + } + Ok(hints) + } + /// Wrapper for parse_optional_alias_inner, left for backwards-compatibility /// but new flows should use the context-specific methods such as `maybe_parse_select_item_alias` /// and `maybe_parse_table_alias`. @@ -11257,6 +11315,14 @@ impl<'a> Parser<'a> { let alias = self.maybe_parse_table_alias()?; + // MYSQL-specific table hints: + let index_hints = if self.dialect.supports_table_hints() { + self.maybe_parse(|p| p.parse_table_index_hints())? + .unwrap_or(vec![]) + } else { + vec![] + }; + // MSSQL-specific table hints: let mut with_hints = vec![]; if self.parse_keyword(Keyword::WITH) { @@ -11285,6 +11351,7 @@ impl<'a> Parser<'a> { with_ordinality, json_path, sample, + index_hints, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { diff --git a/src/test_utils.rs b/src/test_utils.rs index f2e3adf09..208984223 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -362,6 +362,7 @@ pub fn table(name: impl Into) -> TableFactor { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], } } @@ -376,6 +377,7 @@ pub fn table_from_name(name: ObjectName) -> TableFactor { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], } } @@ -393,6 +395,7 @@ pub fn table_with_alias(name: impl Into, alias: impl Into) -> Ta with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index cbb963761..45d87a8b8 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1565,6 +1565,7 @@ fn parse_table_time_travel() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![] },] @@ -1665,6 +1666,7 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, table ); @@ -1682,6 +1684,7 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, source ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5c11b2901..2489ce2d9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -503,6 +503,7 @@ fn parse_update_with_table_alias() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![], }, @@ -596,6 +597,7 @@ fn parse_select_with_table_alias() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![], }] @@ -792,6 +794,7 @@ fn parse_where_delete_with_alias_statement() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, from[0].relation, ); @@ -810,6 +813,7 @@ fn parse_where_delete_with_alias_statement() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![], }]), @@ -6416,6 +6420,7 @@ fn parse_joins_on() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, global, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -6545,6 +6550,7 @@ fn parse_joins_using() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, global: false, join_operator: f(JoinConstraint::Using(vec![ObjectName::from(vec![ @@ -6623,6 +6629,7 @@ fn parse_natural_join() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, global: false, join_operator: f(JoinConstraint::Natural), @@ -8718,6 +8725,7 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], } ); assert_eq!(table, table_no_into); @@ -9901,6 +9909,7 @@ fn parse_pivot_table() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }), aggregate_functions: vec![ expected_function("a", None), @@ -9977,6 +9986,7 @@ fn parse_unpivot_table() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }), value: Ident { value: "quantity".to_string(), @@ -10023,6 +10033,73 @@ fn parse_unpivot_table() { ); } +#[test] +fn parse_select_table_with_index_hints() { + let supported_dialects = all_dialects_where(|d| d.supports_table_hints()); + let s = supported_dialects.verified_only_select( + "SELECT * FROM t1 USE INDEX (i1) IGNORE INDEX FOR ORDER BY (i2) ORDER BY a", + ); + if let TableFactor::Table { index_hints, .. } = &s.from[0].relation { + assert_eq!( + vec![ + TableIndexHints { + hint_type: TableIndexHintType::Use, + index_names: vec!["i1".into()], + index_type: TableIndexType::Index, + for_clause: None, + }, + TableIndexHints { + hint_type: TableIndexHintType::Ignore, + index_names: vec!["i2".into()], + index_type: TableIndexType::Index, + for_clause: Some(TableIndexHintForClause::OrderBy), + }, + ], + *index_hints + ); + } else { + panic!("Expected TableFactor::Table"); + } + supported_dialects.verified_stmt("SELECT * FROM t1 USE INDEX (i1) USE INDEX (i1, i1)"); + supported_dialects.verified_stmt( + "SELECT * FROM t1 USE INDEX () IGNORE INDEX (i2) USE INDEX (i1) USE INDEX (i2)", + ); + supported_dialects.verified_stmt("SELECT * FROM t1 FORCE INDEX FOR JOIN (i2)"); + supported_dialects.verified_stmt("SELECT * FROM t1 IGNORE INDEX FOR JOIN (i2)"); + supported_dialects.verified_stmt( + "SELECT * FROM t USE INDEX (index1) IGNORE INDEX FOR ORDER BY (index1) IGNORE INDEX FOR GROUP BY (index1) WHERE A = B", + ); + + // Test that dialects that don't support table hints will keep parsing the USE as table alias + let sql = "SELECT * FROM T USE LIMIT 1"; + let unsupported_dialects = all_dialects_where(|d| !d.supports_table_hints()); + let select = unsupported_dialects + .verified_only_select_with_canonical(sql, "SELECT * FROM T AS USE LIMIT 1"); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier( + Ident::new("T") + )]), + alias: Some(TableAlias { + name: Ident::new("USE"), + columns: vec![], + }), + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![], + }] + ); +} + #[test] fn parse_pivot_unpivot_table() { let sql = concat!( @@ -10048,6 +10125,7 @@ fn parse_pivot_unpivot_table() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }), value: Ident { value: "population".to_string(), diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 9c4e8f079..5d710b17d 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -460,6 +460,7 @@ fn parse_delimited_identifiers() { partitions: _, json_path: _, sample: _, + index_hints: _, } => { assert_eq!( ObjectName::from(vec![Ident::with_quote('"', "a table")]), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 3c4017590..9046e9e74 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -74,6 +74,7 @@ fn parse_table_time_travel() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![] }, joins: vec![] },] @@ -223,6 +224,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -282,6 +284,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -341,6 +344,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -400,6 +404,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![], }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -439,6 +444,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, + index_hints: vec![], }, joins: vec![Join { relation: TableFactor::OpenJsonTable { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index fb72436ed..501dce3e3 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2036,6 +2036,7 @@ fn parse_update_with_joins() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -2051,6 +2052,7 @@ fn parse_update_with_joins() { with_ordinality: false, json_path: None, sample: None, + index_hints: vec![], }, global: false, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { From 8de3a62948d3384b9c13a387b0039984d51752fb Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Tue, 28 Jan 2025 11:33:07 +0100 Subject: [PATCH 703/806] BigQuery: Add support for select expr star (#1680) --- src/ast/mod.rs | 13 +++--- src/ast/query.rs | 34 +++++++++++++-- src/ast/spans.rs | 18 +++++--- src/dialect/bigquery.rs | 5 +++ src/dialect/mod.rs | 11 +++++ src/parser/mod.rs | 30 ++++++++++--- tests/sqlparser_bigquery.rs | 11 +++-- tests/sqlparser_common.rs | 85 +++++++++++++++++++++++++++++++++++- tests/sqlparser_duckdb.rs | 2 +- tests/sqlparser_snowflake.rs | 4 +- 10 files changed, 183 insertions(+), 30 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6917b7c96..d3a028b0b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -68,12 +68,13 @@ pub use self::query::{ NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectInto, SelectItem, 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, + 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 09058f760..747d925c9 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -586,6 +586,20 @@ impl fmt::Display for Cte { } } +/// Represents an expression behind a wildcard expansion in a projection. +/// `SELECT T.* FROM T; +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SelectItemQualifiedWildcardKind { + /// Expression is an object name. + /// e.g. `alias.*` or even `schema.table.*` + ObjectName(ObjectName), + /// Select star on an arbitrary expression. + /// e.g. `STRUCT('foo').*` + Expr(Expr), +} + /// One item of the comma-separated list following `SELECT` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -595,12 +609,24 @@ pub enum SelectItem { UnnamedExpr(Expr), /// An expression, followed by `[ AS ] alias` ExprWithAlias { expr: Expr, alias: Ident }, - /// `alias.*` or even `schema.table.*` - QualifiedWildcard(ObjectName, WildcardAdditionalOptions), + /// An expression, followed by a wildcard expansion. + /// e.g. `alias.*`, `STRUCT('foo').*` + QualifiedWildcard(SelectItemQualifiedWildcardKind, WildcardAdditionalOptions), /// An unqualified `*` Wildcard(WildcardAdditionalOptions), } +impl fmt::Display for SelectItemQualifiedWildcardKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + SelectItemQualifiedWildcardKind::ObjectName(object_name) => { + write!(f, "{object_name}.*") + } + SelectItemQualifiedWildcardKind::Expr(expr) => write!(f, "{expr}.*"), + } + } +} + /// Single aliased identifier /// /// # Syntax @@ -867,8 +893,8 @@ impl fmt::Display for SelectItem { match &self { SelectItem::UnnamedExpr(expr) => write!(f, "{expr}"), SelectItem::ExprWithAlias { expr, alias } => write!(f, "{expr} AS {alias}"), - SelectItem::QualifiedWildcard(prefix, additional_options) => { - write!(f, "{prefix}.*")?; + SelectItem::QualifiedWildcard(kind, additional_options) => { + write!(f, "{kind}")?; write!(f, "{additional_options}")?; Ok(()) } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8f72c26f5..58fa27aa8 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +use crate::ast::query::SelectItemQualifiedWildcardKind; use core::iter; use crate::tokenizer::Span; @@ -1623,16 +1624,23 @@ impl Spanned for JsonPathElem { } } +impl Spanned for SelectItemQualifiedWildcardKind { + fn span(&self) -> Span { + match self { + SelectItemQualifiedWildcardKind::ObjectName(object_name) => object_name.span(), + SelectItemQualifiedWildcardKind::Expr(expr) => expr.span(), + } + } +} + impl Spanned for SelectItem { fn span(&self) -> Span { match self { SelectItem::UnnamedExpr(expr) => expr.span(), SelectItem::ExprWithAlias { expr, alias } => expr.span().union(&alias.span), - SelectItem::QualifiedWildcard(object_name, wildcard_additional_options) => union_spans( - object_name - .0 - .iter() - .map(|i| i.span()) + SelectItem::QualifiedWildcard(kind, wildcard_additional_options) => union_spans( + [kind.span()] + .into_iter() .chain(iter::once(wildcard_additional_options.span())), ), SelectItem::Wildcard(wildcard_additional_options) => wildcard_additional_options.span(), diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 716174391..b45754213 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -83,6 +83,11 @@ impl Dialect for BigQueryDialect { true } + /// See + fn supports_select_expr_star(&self) -> bool { + true + } + // See fn supports_timestamp_versioning(&self) -> bool { true diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6329c5cfc..bc3c0c967 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -447,6 +447,17 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports wildcard expansion on + /// arbitrary expressions in projections. + /// + /// Example: + /// ```sql + /// SELECT STRUCT('foo').* FROM T + /// ``` + fn supports_select_expr_star(&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/parser/mod.rs b/src/parser/mod.rs index c8ff01f70..179c120bb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1528,10 +1528,17 @@ impl<'a> Parser<'a> { // function array_agg traverses this control flow if dialect_of!(self is PostgreSqlDialect) { ending_wildcard = Some(next_token); - break; } else { - return self.expected("an identifier after '.'", next_token); + // Put back the consumed .* tokens before exiting. + // If this expression is being parsed in the + // context of a projection, then this could imply + // a wildcard expansion. For example: + // `SELECT STRUCT('foo').* FROM T` + self.prev_token(); // * + self.prev_token(); // . } + + break; } Token::SingleQuotedString(s) => { let expr = Expr::Identifier(Ident::with_quote('\'', s)); @@ -1568,18 +1575,18 @@ impl<'a> Parser<'a> { } else { self.parse_function(ObjectName::from(id_parts)) } + } else if chain.is_empty() { + Ok(root) } else { if Self::is_all_ident(&root, &chain) { return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents( root, chain, )?)); } - if chain.is_empty() { - return Ok(root); - } + Ok(Expr::CompoundFieldAccess { root: Box::new(root), - access_chain: chain.clone(), + access_chain: chain, }) } } @@ -12935,7 +12942,7 @@ impl<'a> Parser<'a> { pub fn parse_select_item(&mut self) -> Result { match self.parse_wildcard_expr()? { Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard( - prefix, + SelectItemQualifiedWildcardKind::ObjectName(prefix), self.parse_wildcard_additional_options(token.0)?, )), Expr::Wildcard(token) => Ok(SelectItem::Wildcard( @@ -12965,6 +12972,15 @@ impl<'a> Parser<'a> { alias, }) } + expr if self.dialect.supports_select_expr_star() + && self.consume_tokens(&[Token::Period, Token::Mul]) => + { + let wildcard_token = self.get_previous_token().clone(); + Ok(SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(expr), + self.parse_wildcard_additional_options(wildcard_token)?, + )) + } expr => self .maybe_parse_select_item_alias() .map(|alias| match alias { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 45d87a8b8..921a37a8a 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1540,9 +1540,6 @@ fn parse_hyphenated_table_identifiers() { ])) }) ); - - let error_sql = "select foo-bar.* from foo-bar"; - assert!(bigquery().parse_sql_statements(error_sql).is_err()); } #[test] @@ -2204,6 +2201,14 @@ fn parse_extract_weekday() { ); } +#[test] +fn bigquery_select_expr_star() { + bigquery() + .verified_only_select("SELECT STRUCT((SELECT foo FROM T WHERE true)).* FROM T"); + bigquery().verified_only_select("SELECT [STRUCT('foo')][0].* EXCEPT (foo) FROM T"); + bigquery().verified_only_select("SELECT myfunc()[0].* FROM T"); +} + #[test] fn test_select_as_struct() { bigquery().verified_only_select("SELECT * FROM (SELECT AS VALUE STRUCT(123 AS a, false AS b))"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2489ce2d9..5dd74e1fa 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1002,7 +1002,7 @@ fn parse_select_wildcard() { let select = verified_only_select(sql); assert_eq!( &SelectItem::QualifiedWildcard( - ObjectName::from(vec![Ident::new("foo")]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("foo")])), WildcardAdditionalOptions::default() ), only(&select.projection) @@ -1012,7 +1012,10 @@ fn parse_select_wildcard() { let select = verified_only_select(sql); assert_eq!( &SelectItem::QualifiedWildcard( - ObjectName::from(vec![Ident::new("myschema"), Ident::new("mytable"),]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![ + Ident::new("myschema"), + Ident::new("mytable"), + ])), WildcardAdditionalOptions::default(), ), only(&select.projection) @@ -1057,6 +1060,84 @@ fn parse_column_aliases() { one_statement_parses_to("SELECT a.col + 1 newname FROM foo AS a", sql); } +#[test] +fn parse_select_expr_star() { + let dialects = all_dialects_where(|d| d.supports_select_expr_star()); + + // Identifier wildcard expansion. + let select = dialects.verified_only_select("SELECT foo.bar.* FROM T"); + let SelectItem::QualifiedWildcard(SelectItemQualifiedWildcardKind::ObjectName(object_name), _) = + only(&select.projection) + else { + unreachable!( + "expected wildcard select item: got {:?}", + &select.projection[0] + ) + }; + assert_eq!( + &ObjectName::from( + ["foo", "bar"] + .into_iter() + .map(Ident::new) + .collect::>() + ), + object_name + ); + + // Arbitrary compound expression with wildcard expansion. + let select = dialects.verified_only_select("SELECT foo - bar.* FROM T"); + let SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(Expr::BinaryOp { left, op, right }), + _, + ) = only(&select.projection) + else { + unreachable!( + "expected wildcard select item: got {:?}", + &select.projection[0] + ) + }; + let (Expr::Identifier(left), BinaryOperator::Minus, Expr::Identifier(right)) = + (left.as_ref(), op, right.as_ref()) + else { + unreachable!("expected binary op expr: got {:?}", &select.projection[0]) + }; + assert_eq!(&Ident::new("foo"), left); + assert_eq!(&Ident::new("bar"), right); + + // Arbitrary expression wildcard expansion. + let select = dialects.verified_only_select("SELECT myfunc().foo.* FROM T"); + let SelectItem::QualifiedWildcard( + SelectItemQualifiedWildcardKind::Expr(Expr::CompoundFieldAccess { root, access_chain }), + _, + ) = only(&select.projection) + else { + unreachable!("expected wildcard expr: got {:?}", &select.projection[0]) + }; + assert!(matches!(root.as_ref(), Expr::Function(_))); + assert_eq!(1, access_chain.len()); + assert!(matches!( + &access_chain[0], + AccessExpr::Dot(Expr::Identifier(_)) + )); + + dialects.one_statement_parses_to( + "SELECT 2. * 3 FROM T", + #[cfg(feature = "bigdecimal")] + "SELECT 2 * 3 FROM T", + #[cfg(not(feature = "bigdecimal"))] + "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"); + assert_eq!( + ParserError::ParserError("Expected: end of statement, found: .".to_string()), + res.unwrap_err() + ); +} + #[test] fn test_eof_after_as() { let res = parse_sql_statements("SELECT foo AS"); diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index aee6d654c..4289ebd1f 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -160,7 +160,7 @@ fn test_select_wildcard_with_exclude() { let select = duckdb().verified_only_select("SELECT name.* EXCLUDE department_id FROM employee_table"); let expected = SelectItem::QualifiedWildcard( - ObjectName::from(vec![Ident::new("name")]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])), WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), ..Default::default() diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2b2350936..f8b58dd15 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1368,7 +1368,7 @@ fn test_select_wildcard_with_exclude() { let select = snowflake_and_generic() .verified_only_select("SELECT name.* EXCLUDE department_id FROM employee_table"); let expected = SelectItem::QualifiedWildcard( - ObjectName::from(vec![Ident::new("name")]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])), WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Single(Ident::new("department_id"))), ..Default::default() @@ -1405,7 +1405,7 @@ fn test_select_wildcard_with_rename() { "SELECT name.* RENAME (department_id AS new_dep, employee_id AS new_emp) FROM employee_table", ); let expected = SelectItem::QualifiedWildcard( - ObjectName::from(vec![Ident::new("name")]), + SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])), WildcardAdditionalOptions { opt_rename: Some(RenameSelectItem::Multiple(vec![ IdentWithAlias { From 269967a6ac4f4d9799cccb6c97142823123ed2c5 Mon Sep 17 00:00:00 2001 From: Paul Grau Date: Tue, 28 Jan 2025 15:26:08 +0200 Subject: [PATCH 704/806] Support underscore separators in numbers for Clickhouse. Fixes #1659 (#1677) --- src/dialect/clickhouse.rs | 4 ++ src/dialect/mod.rs | 5 +++ src/dialect/postgresql.rs | 4 ++ src/tokenizer.rs | 74 +++++++++++++++++++++++++++++++++-- tests/sqlparser_clickhouse.rs | 15 +++++++ 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 884dfcbcb..830b3da90 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -59,6 +59,10 @@ impl Dialect for ClickHouseDialect { true } + fn supports_numeric_literal_underscores(&self) -> bool { + true + } + // ClickHouse uses this for some FORMAT expressions in `INSERT` context, e.g. when inserting // with FORMAT JSONEachRow a raw JSON key-value expression is valid and expected. // diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index bc3c0c967..817f5f325 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -304,6 +304,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports numbers containing underscores, e.g. `10_000_000` + fn supports_numeric_literal_underscores(&self) -> bool { + false + } + /// Returns true if the dialects supports specifying null treatment /// as part of a window function's parameter list as opposed /// to after the parameter list. diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index d4f2a032e..5ce4250fb 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -249,6 +249,10 @@ impl Dialect for PostgreSqlDialect { fn supports_string_escape_constant(&self) -> bool { true } + + fn supports_numeric_literal_underscores(&self) -> bool { + true + } } pub fn parse_create(parser: &mut Parser) -> Option> { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 309f09d81..7742e8fae 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1136,12 +1136,24 @@ impl<'a> Tokenizer<'a> { } // numbers and period '0'..='9' | '.' => { - let mut s = peeking_take_while(chars, |ch| ch.is_ascii_digit()); + // 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| { + self.dialect.supports_numeric_literal_underscores() + && ch == '_' + && next_char.is_some_and(|next_ch| next_ch.is_ascii_hexdigit()) + }; + + let mut s = peeking_next_take_while(chars, |ch, next_ch| { + ch.is_ascii_digit() || is_number_separator(ch, next_ch) + }); // match binary literal that starts with 0x if s == "0" && chars.peek() == Some(&'x') { chars.next(); - let s2 = peeking_take_while(chars, |ch| ch.is_ascii_hexdigit()); + let s2 = peeking_next_take_while(chars, |ch, next_ch| { + ch.is_ascii_hexdigit() || is_number_separator(ch, next_ch) + }); return Ok(Some(Token::HexStringLiteral(s2))); } @@ -1150,7 +1162,10 @@ impl<'a> Tokenizer<'a> { s.push('.'); chars.next(); } - s += &peeking_take_while(chars, |ch| ch.is_ascii_digit()); + + s += &peeking_next_take_while(chars, |ch, next_ch| { + ch.is_ascii_digit() || is_number_separator(ch, next_ch) + }); // No number -> Token::Period if s == "." { @@ -1946,6 +1961,24 @@ fn peeking_take_while(chars: &mut State, mut predicate: impl FnMut(char) -> bool s } +/// Same as peeking_take_while, but also passes the next character to the predicate. +fn peeking_next_take_while( + chars: &mut State, + mut predicate: impl FnMut(char, Option) -> bool, +) -> String { + let mut s = String::new(); + while let Some(&ch) = chars.peek() { + let next_char = chars.peekable.clone().nth(1); + if predicate(ch, next_char) { + chars.next(); // consume + s.push(ch); + } else { + break; + } + } + s +} + fn unescape_single_quoted_string(chars: &mut State<'_>) -> Option { Unescape::new(chars).unescape() } @@ -2227,6 +2260,41 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_numeric_literal_underscore() { + let dialect = GenericDialect {}; + let sql = String::from("SELECT 10_000"); + let mut tokenizer = Tokenizer::new(&dialect, &sql); + let tokens = tokenizer.tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number("10".to_string(), false), + Token::make_word("_000", None), + ]; + compare(expected, tokens); + + all_dialects_where(|dialect| dialect.supports_numeric_literal_underscores()).tokenizes_to( + "SELECT 10_000, _10_000, 10_00_, 10___0", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Number("10_000".to_string(), false), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::make_word("_10_000", None), // leading underscore tokenizes as a word (parsed as column identifier) + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number("10_00".to_string(), false), + Token::make_word("_", None), // trailing underscores tokenizes as a word (syntax error in some dialects) + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::Number("10".to_string(), false), + Token::make_word("___0", None), // multiple underscores tokenizes as a word (syntax error in some dialects) + ], + ); + } + #[test] fn tokenize_select_exponent() { let sql = String::from("SELECT 1e10, 1e-10, 1e+10, 1ea, 1e-10a, 1e-10-10"); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 0f22db389..5b0638a4b 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1649,6 +1649,21 @@ fn parse_table_sample() { clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10 OFFSET 1 / 2"); } +#[test] +fn parse_numbers_with_underscore() { + let canonical = if cfg!(feature = "bigdecimal") { + "SELECT 10000" + } else { + "SELECT 10_000" + }; + let select = clickhouse().verified_only_select_with_canonical("SELECT 10_000", canonical); + + assert_eq!( + select.projection, + vec![SelectItem::UnnamedExpr(Expr::Value(number("10_000")))] + ) +} + fn clickhouse() -> TestedDialects { TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) } From 7980c866a3824af7e1937ffda274657b5dbae99d Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Wed, 29 Jan 2025 12:03:55 +0100 Subject: [PATCH 705/806] BigQuery: Fix column identifier reserved keywords list (#1678) --- src/dialect/bigquery.rs | 26 +++++++++++++++ src/dialect/mod.rs | 12 +++++-- src/parser/mod.rs | 65 +++++++++++++++++++++++++++---------- tests/sqlparser_bigquery.rs | 9 +++++ tests/sqlparser_common.rs | 42 ++++++++++++++++-------- 5 files changed, 120 insertions(+), 34 deletions(-) diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index b45754213..bb1a0d5ce 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -16,6 +16,28 @@ // under the License. use crate::dialect::Dialect; +use crate::keywords::Keyword; +use crate::parser::Parser; + +/// These keywords are disallowed as column identifiers. Such that +/// `SELECT 5 AS FROM T` is rejected by BigQuery. +const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ + Keyword::WITH, + Keyword::SELECT, + Keyword::WHERE, + Keyword::GROUP, + Keyword::HAVING, + Keyword::ORDER, + Keyword::LATERAL, + Keyword::LIMIT, + Keyword::FETCH, + Keyword::UNION, + Keyword::EXCEPT, + Keyword::INTERSECT, + Keyword::FROM, + Keyword::INTO, + Keyword::END, +]; /// A [`Dialect`] for [Google Bigquery](https://cloud.google.com/bigquery/) #[derive(Debug, Default)] @@ -92,4 +114,8 @@ impl Dialect for BigQueryDialect { fn supports_timestamp_versioning(&self) -> bool { true } + + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !RESERVED_FOR_COLUMN_ALIAS.contains(kw) + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 817f5f325..b648869d2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -804,7 +804,7 @@ pub trait Dialect: Debug + Any { keywords::RESERVED_FOR_IDENTIFIER.contains(&kw) } - // Returns reserved keywords when looking to parse a [TableFactor]. + /// Returns reserved keywords when looking to parse a `TableFactor`. /// See [Self::supports_from_trailing_commas] fn get_reserved_keywords_for_table_factor(&self) -> &[Keyword] { keywords::RESERVED_FOR_TABLE_FACTOR @@ -844,11 +844,17 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the specified keyword should be parsed as a column identifier. + /// See [keywords::RESERVED_FOR_COLUMN_ALIAS] + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) + } + /// Returns true if the specified keyword should be parsed as a select item alias. /// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided /// to enable looking ahead if needed. - fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { - explicit || !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) + fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + explicit || self.is_column_alias(kw, parser) } /// Returns true if the specified keyword should be parsed as a table factor alias. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 179c120bb..b09360078 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3951,7 +3951,7 @@ impl<'a> Parser<'a> { self.parse_comma_separated_with_trailing_commas( |p| p.parse_select_item(), trailing_commas, - None, + Self::is_reserved_for_column_alias, ) } @@ -3985,30 +3985,42 @@ impl<'a> Parser<'a> { self.parse_comma_separated_with_trailing_commas( Parser::parse_table_and_joins, trailing_commas, - Some(self.dialect.get_reserved_keywords_for_table_factor()), + |kw, _parser| { + self.dialect + .get_reserved_keywords_for_table_factor() + .contains(kw) + }, ) } /// Parse the comma of a comma-separated syntax element. + /// `R` is a predicate that should return true if the next + /// keyword is a reserved keyword. /// Allows for control over trailing commas + /// /// Returns true if there is a next element - fn is_parse_comma_separated_end_with_trailing_commas( + fn is_parse_comma_separated_end_with_trailing_commas( &mut self, trailing_commas: bool, - reserved_keywords: Option<&[Keyword]>, - ) -> bool { - let reserved_keywords = reserved_keywords.unwrap_or(keywords::RESERVED_FOR_COLUMN_ALIAS); + is_reserved_keyword: &R, + ) -> bool + where + R: Fn(&Keyword, &mut Parser) -> bool, + { if !self.consume_token(&Token::Comma) { true } else if trailing_commas { - let token = self.peek_token().token; - match token { - Token::Word(ref kw) if reserved_keywords.contains(&kw.keyword) => true, + let token = self.next_token().token; + let is_end = match token { + Token::Word(ref kw) if is_reserved_keyword(&kw.keyword, self) => true, Token::RParen | Token::SemiColon | Token::EOF | Token::RBracket | Token::RBrace => { true } _ => false, - } + }; + self.prev_token(); + + is_end } else { false } @@ -4017,7 +4029,10 @@ impl<'a> Parser<'a> { /// Parse the comma of a comma-separated syntax element. /// Returns true if there is a next element fn is_parse_comma_separated_end(&mut self) -> bool { - self.is_parse_comma_separated_end_with_trailing_commas(self.options.trailing_commas, None) + self.is_parse_comma_separated_end_with_trailing_commas( + self.options.trailing_commas, + &Self::is_reserved_for_column_alias, + ) } /// Parse a comma-separated list of 1+ items accepted by `F` @@ -4025,26 +4040,33 @@ impl<'a> Parser<'a> { where F: FnMut(&mut Parser<'a>) -> Result, { - self.parse_comma_separated_with_trailing_commas(f, self.options.trailing_commas, None) + self.parse_comma_separated_with_trailing_commas( + f, + self.options.trailing_commas, + Self::is_reserved_for_column_alias, + ) } - /// Parse a comma-separated list of 1+ items accepted by `F` - /// Allows for control over trailing commas - fn parse_comma_separated_with_trailing_commas( + /// Parse a comma-separated list of 1+ items accepted by `F`. + /// `R` is a predicate that should return true if the next + /// keyword is a reserved keyword. + /// Allows for control over trailing commas. + fn parse_comma_separated_with_trailing_commas( &mut self, mut f: F, trailing_commas: bool, - reserved_keywords: Option<&[Keyword]>, + is_reserved_keyword: R, ) -> Result, ParserError> where F: FnMut(&mut Parser<'a>) -> Result, + R: Fn(&Keyword, &mut Parser) -> bool, { let mut values = vec![]; loop { values.push(f(self)?); if self.is_parse_comma_separated_end_with_trailing_commas( trailing_commas, - reserved_keywords, + &is_reserved_keyword, ) { break; } @@ -4118,6 +4140,13 @@ impl<'a> Parser<'a> { self.parse_comma_separated(f) } + /// Default implementation of a predicate that returns true if + /// the specified keyword is reserved for column alias. + /// See [Dialect::is_column_alias] + fn is_reserved_for_column_alias(kw: &Keyword, parser: &mut Parser) -> bool { + !parser.dialect.is_column_alias(kw, parser) + } + /// Run a parser method `f`, reverting back to the current position if unsuccessful. /// Returns `None` if `f` returns an error pub fn maybe_parse(&mut self, f: F) -> Result, ParserError> @@ -9394,7 +9423,7 @@ impl<'a> Parser<'a> { let cols = self.parse_comma_separated_with_trailing_commas( Parser::parse_view_column, self.dialect.supports_column_definition_trailing_commas(), - None, + Self::is_reserved_for_column_alias, )?; self.expect_token(&Token::RParen)?; Ok(cols) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 921a37a8a..853bffeef 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -213,6 +213,15 @@ fn parse_raw_literal() { ); } +#[test] +fn parse_big_query_non_reserved_column_alias() { + let sql = r#"SELECT OFFSET, EXPLAIN, ANALYZE, SORT, TOP, VIEW FROM T"#; + bigquery().verified_stmt(sql); + + let sql = r#"SELECT 1 AS OFFSET, 2 AS EXPLAIN, 3 AS ANALYZE FROM T"#; + bigquery().verified_stmt(sql); +} + #[test] fn parse_delete_statement() { let sql = "DELETE \"table\" WHERE 1"; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5dd74e1fa..119adc7e4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -253,8 +253,13 @@ fn parse_insert_default_values() { #[test] fn parse_insert_select_returning() { - verified_stmt("INSERT INTO t SELECT 1 RETURNING 2"); - let stmt = verified_stmt("INSERT INTO t SELECT x RETURNING x AS y"); + // Dialects that support `RETURNING` as a column identifier do + // not support this syntax. + let dialects = + all_dialects_where(|d| !d.is_column_alias(&Keyword::RETURNING, &mut Parser::new(d))); + + dialects.verified_stmt("INSERT INTO t SELECT 1 RETURNING 2"); + let stmt = dialects.verified_stmt("INSERT INTO t SELECT x RETURNING x AS y"); match stmt { Statement::Insert(Insert { returning: Some(ret), @@ -6993,9 +6998,6 @@ fn parse_union_except_intersect_minus() { verified_stmt("SELECT 1 EXCEPT SELECT 2"); verified_stmt("SELECT 1 EXCEPT ALL SELECT 2"); verified_stmt("SELECT 1 EXCEPT DISTINCT SELECT 1"); - verified_stmt("SELECT 1 MINUS SELECT 2"); - verified_stmt("SELECT 1 MINUS ALL SELECT 2"); - verified_stmt("SELECT 1 MINUS DISTINCT SELECT 1"); verified_stmt("SELECT 1 INTERSECT SELECT 2"); verified_stmt("SELECT 1 INTERSECT ALL SELECT 2"); verified_stmt("SELECT 1 INTERSECT DISTINCT SELECT 1"); @@ -7014,6 +7016,13 @@ fn parse_union_except_intersect_minus() { verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT BY NAME SELECT 9 AS y, 8 AS x"); verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT ALL BY NAME SELECT 9 AS y, 8 AS x"); verified_stmt("SELECT 1 AS x, 2 AS y INTERSECT DISTINCT BY NAME SELECT 9 AS y, 8 AS x"); + + // Dialects that support `MINUS` as column identifier + // do not support `MINUS` as a set operator. + let dialects = all_dialects_where(|d| !d.is_column_alias(&Keyword::MINUS, &mut Parser::new(d))); + dialects.verified_stmt("SELECT 1 MINUS SELECT 2"); + dialects.verified_stmt("SELECT 1 MINUS ALL SELECT 2"); + dialects.verified_stmt("SELECT 1 MINUS DISTINCT SELECT 1"); } #[test] @@ -7690,19 +7699,26 @@ fn parse_invalid_subquery_without_parens() { #[test] fn parse_offset() { + // Dialects that support `OFFSET` as column identifiers + // don't support this syntax. + 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 ast = verified_query("SELECT foo FROM bar OFFSET 2 ROWS"); + let ast = dialects.verified_query("SELECT foo FROM bar OFFSET 2 ROWS"); assert_eq!(ast.offset, expect); - let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 OFFSET 2 ROWS"); + let ast = dialects.verified_query("SELECT foo FROM bar WHERE foo = 4 OFFSET 2 ROWS"); assert_eq!(ast.offset, expect); - let ast = verified_query("SELECT foo FROM bar ORDER BY baz OFFSET 2 ROWS"); + let ast = dialects.verified_query("SELECT foo FROM bar ORDER BY baz OFFSET 2 ROWS"); assert_eq!(ast.offset, expect); - let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS"); + let ast = + dialects.verified_query("SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS"); assert_eq!(ast.offset, expect); - let ast = verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS) OFFSET 2 ROWS"); + let ast = + dialects.verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS) OFFSET 2 ROWS"); assert_eq!(ast.offset, expect); match *ast.body { SetExpr::Select(s) => match only(s.from).relation { @@ -7713,7 +7729,7 @@ fn parse_offset() { }, _ => panic!("Test broke"), } - let ast = verified_query("SELECT 'foo' OFFSET 0 ROWS"); + let ast = dialects.verified_query("SELECT 'foo' OFFSET 0 ROWS"); assert_eq!( ast.offset, Some(Offset { @@ -7721,7 +7737,7 @@ fn parse_offset() { rows: OffsetRows::Rows, }) ); - let ast = verified_query("SELECT 'foo' OFFSET 1 ROW"); + let ast = dialects.verified_query("SELECT 'foo' OFFSET 1 ROW"); assert_eq!( ast.offset, Some(Offset { @@ -7729,7 +7745,7 @@ fn parse_offset() { rows: OffsetRows::Row, }) ); - let ast = verified_query("SELECT 'foo' OFFSET 1"); + let ast = dialects.verified_query("SELECT 'foo' OFFSET 1"); assert_eq!( ast.offset, Some(Offset { From 784605c9132d7e14f9e3b396c26d63af086ff885 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:50:36 +0100 Subject: [PATCH 706/806] Fix bug when parsing a Snowflake stage with `;` suffix (#1688) --- src/dialect/snowflake.rs | 2 +- tests/sqlparser_snowflake.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index eb8ea4de9..d775ffc36 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -625,7 +625,7 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result break, + Token::Whitespace(_) | Token::SemiColon => break, Token::Period => { parser.prev_token(); break; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index f8b58dd15..1310b984f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3102,6 +3102,10 @@ fn parse_ls_and_rm() { }; snowflake().verified_stmt(r#"LIST @"STAGE_WITH_QUOTES""#); + // Semi-colon after stage name - should terminate the stage name + snowflake() + .parse_sql_statements("LIST @db1.schema1.stage1/dir1/;") + .unwrap(); } #[test] From 252fdbab823220e7ea29895d7c7230e49d6fb742 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Wed, 29 Jan 2025 22:15:57 -0800 Subject: [PATCH 707/806] Allow plain JOIN without turning it into INNER (#1692) --- src/ast/query.rs | 10 +++++++++- src/ast/spans.rs | 1 + src/parser/mod.rs | 8 ++++++-- src/test_utils.rs | 2 +- tests/sqlparser_bigquery.rs | 2 +- tests/sqlparser_common.rs | 31 ++++++++++++++++++++++--------- tests/sqlparser_mysql.rs | 2 +- tests/sqlparser_postgres.rs | 2 +- 8 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 747d925c9..239e14554 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2038,13 +2038,20 @@ impl fmt::Display for Join { } match &self.join_operator { - JoinOperator::Inner(constraint) => write!( + JoinOperator::Join(constraint) => write!( f, " {}JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) ), + JoinOperator::Inner(constraint) => write!( + f, + " {}INNER JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + ), JoinOperator::LeftOuter(constraint) => write!( f, " {}LEFT JOIN {}{}", @@ -2128,6 +2135,7 @@ impl fmt::Display for Join { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum JoinOperator { + Join(JoinConstraint), Inner(JoinConstraint), LeftOuter(JoinConstraint), RightOuter(JoinConstraint), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 58fa27aa8..8c46fc073 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2001,6 +2001,7 @@ impl Spanned for Join { impl Spanned for JoinOperator { fn span(&self) -> Span { match self { + JoinOperator::Join(join_constraint) => join_constraint.span(), JoinOperator::Inner(join_constraint) => join_constraint.span(), JoinOperator::LeftOuter(join_constraint) => join_constraint.span(), JoinOperator::RightOuter(join_constraint) => join_constraint.span(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b09360078..aff4c8d38 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11000,9 +11000,13 @@ impl<'a> Parser<'a> { let join_operator_type = match peek_keyword { Keyword::INNER | Keyword::JOIN => { - let _ = self.parse_keyword(Keyword::INNER); // [ INNER ] + let inner = self.parse_keyword(Keyword::INNER); // [ INNER ] self.expect_keyword_is(Keyword::JOIN)?; - JoinOperator::Inner + if inner { + JoinOperator::Inner + } else { + JoinOperator::Join + } } kw @ Keyword::LEFT | kw @ Keyword::RIGHT => { let _ = self.next_token(); // consume LEFT/RIGHT diff --git a/src/test_utils.rs b/src/test_utils.rs index 208984223..6270ac42b 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -403,7 +403,7 @@ pub fn join(relation: TableFactor) -> Join { Join { relation, global: false, - join_operator: JoinOperator::Inner(JoinConstraint::Natural), + join_operator: JoinOperator::Join(JoinConstraint::Natural), } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 853bffeef..55de4801d 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1602,7 +1602,7 @@ fn parse_join_constraint_unnest_alias() { with_ordinality: false, }, global: false, - join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), op: BinaryOperator::Eq, right: Box::new(Expr::Identifier("c2".into())), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 119adc7e4..99660507d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6457,7 +6457,7 @@ fn parse_implicit_join() { joins: vec![Join { relation: table_from_name(ObjectName::from(vec!["t1b".into()])), global: false, - join_operator: JoinOperator::Inner(JoinConstraint::Natural), + join_operator: JoinOperator::Join(JoinConstraint::Natural), }], }, TableWithJoins { @@ -6465,7 +6465,7 @@ fn parse_implicit_join() { joins: vec![Join { relation: table_from_name(ObjectName::from(vec!["t2b".into()])), global: false, - join_operator: JoinOperator::Inner(JoinConstraint::Natural), + join_operator: JoinOperator::Join(JoinConstraint::Natural), }], }, ], @@ -6523,7 +6523,7 @@ fn parse_joins_on() { "t2", table_alias("foo"), false, - JoinOperator::Inner, + JoinOperator::Join, )] ); one_statement_parses_to( @@ -6533,7 +6533,7 @@ fn parse_joins_on() { // Test parsing of different join operators assert_eq!( only(&verified_only_select("SELECT * FROM t1 JOIN t2 ON c1 = c2").from).joins, - vec![join_with_constraint("t2", None, false, JoinOperator::Inner)] + vec![join_with_constraint("t2", None, false, JoinOperator::Join)] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT JOIN t2 ON c1 = c2").from).joins, @@ -6650,7 +6650,7 @@ fn parse_joins_using() { vec![join_with_constraint( "t2", table_alias("foo"), - JoinOperator::Inner, + JoinOperator::Join, )] ); one_statement_parses_to( @@ -6660,6 +6660,10 @@ fn parse_joins_using() { // Test parsing of different join operators assert_eq!( only(&verified_only_select("SELECT * FROM t1 JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::Join)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 INNER JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::Inner)] ); assert_eq!( @@ -6722,9 +6726,14 @@ fn parse_natural_join() { } } - // if not specified, inner join as default + // unspecified join assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL JOIN t2").from).joins, + vec![natural_join(JoinOperator::Join, None)] + ); + // inner join explicitly + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 NATURAL INNER JOIN t2").from).joins, vec![natural_join(JoinOperator::Inner, None)] ); // left join explicitly @@ -6748,7 +6757,7 @@ fn parse_natural_join() { // natural join another table with alias assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL JOIN t2 AS t3").from).joins, - vec![natural_join(JoinOperator::Inner, table_alias("t3"))] + vec![natural_join(JoinOperator::Join, table_alias("t3"))] ); let sql = "SELECT * FROM t1 natural"; @@ -6816,8 +6825,12 @@ fn parse_join_nesting() { #[test] fn parse_join_syntax_variants() { one_statement_parses_to( - "SELECT c1 FROM t1 INNER JOIN t2 USING(c1)", "SELECT c1 FROM t1 JOIN t2 USING(c1)", + "SELECT c1 FROM t1 JOIN t2 USING(c1)", + ); + one_statement_parses_to( + "SELECT c1 FROM t1 INNER JOIN t2 USING(c1)", + "SELECT c1 FROM t1 INNER JOIN t2 USING(c1)", ); one_statement_parses_to( "SELECT c1 FROM t1 LEFT OUTER JOIN t2 USING(c1)", @@ -6981,7 +6994,7 @@ fn parse_derived_tables() { joins: vec![Join { relation: table_from_name(ObjectName::from(vec!["t2".into()])), global: false, - join_operator: JoinOperator::Inner(JoinConstraint::Natural), + join_operator: JoinOperator::Join(JoinConstraint::Natural), }], }), alias: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 501dce3e3..2e6dfc72b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2055,7 +2055,7 @@ fn parse_update_with_joins() { index_hints: vec![], }, global: false, - join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("o"), Ident::new("customer_id") diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b3eb4f10d..9bccda11a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4371,7 +4371,7 @@ fn parse_join_constraint_unnest_alias() { with_ordinality: false, }, global: false, - join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { + join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), op: BinaryOperator::Eq, right: Box::new(Expr::Identifier("c2".into())), From a7e984099fdded3cbcfbf236a7b800b7ace4252f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20SAISSY?= Date: Thu, 30 Jan 2025 07:50:30 +0100 Subject: [PATCH 708/806] Fix DDL generation in case of an empty arguments function. (#1690) --- src/ast/ddl.rs | 2 ++ tests/sqlparser_postgres.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1b5ccda26..4fe6b0bb2 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2011,6 +2011,8 @@ impl fmt::Display for CreateFunction { )?; if let Some(args) = &self.args { write!(f, "({})", display_comma_separated(args))?; + } else { + write!(f, "()")?; } if let Some(return_type) = &self.return_type { write!(f, " RETURNS {return_type}")?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9bccda11a..5a2f726af 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3802,6 +3802,7 @@ fn parse_create_function_detailed() { pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE PARALLEL UNSAFE RETURN a + b"); pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE CALLED ON NULL INPUT PARALLEL UNSAFE RETURN a + b"); pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION increment(i INTEGER) RETURNS INTEGER LANGUAGE plpgsql AS $$ BEGIN RETURN i + 1; END; $$"#); + pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION no_arg() RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN DELETE FROM my_table; END; $$"#); } #[test] fn parse_incorrect_create_function_parallel() { From 9c384a91940b4b621b02b0fee7d40b5ec2709bbd Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Thu, 30 Jan 2025 23:45:31 +0100 Subject: [PATCH 709/806] Fix `CREATE FUNCTION` round trip for Hive dialect (#1693) --- src/ast/ddl.rs | 2 -- src/parser/mod.rs | 11 +++++------ tests/sqlparser_postgres.rs | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 4fe6b0bb2..1b5ccda26 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2011,8 +2011,6 @@ impl fmt::Display for CreateFunction { )?; if let Some(args) = &self.args { write!(f, "({})", display_comma_separated(args))?; - } else { - write!(f, "()")?; } if let Some(return_type) = &self.return_type { write!(f, " RETURNS {return_type}")?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index aff4c8d38..df5c19a38 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4553,14 +4553,13 @@ impl<'a> Parser<'a> { temporary: bool, ) -> Result { let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; - let args = if self.consume_token(&Token::RParen) { - self.prev_token(); - None + let args = if Token::RParen != self.peek_token_ref().token { + self.parse_comma_separated(Parser::parse_function_arg)? } else { - Some(self.parse_comma_separated(Parser::parse_function_arg)?) + vec![] }; - self.expect_token(&Token::RParen)?; let return_type = if self.parse_keyword(Keyword::RETURNS) { @@ -4656,7 +4655,7 @@ impl<'a> Parser<'a> { or_replace, temporary, name, - args, + args: Some(args), return_type, behavior: body.behavior, called_on_null: body.called_on_null, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5a2f726af..93af04980 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5147,7 +5147,7 @@ fn parse_trigger_related_functions() { temporary: false, if_not_exists: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), - args: None, + args: Some(vec![]), return_type: Some(DataType::Trigger), function_body: Some( CreateFunctionBody::AsBeforeOptions( From 94b2ff7191ee2d2a2134a0264461c65e9d683e72 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Fri, 31 Jan 2025 00:05:36 +0100 Subject: [PATCH 710/806] Make numeric literal underscore test dialect agnostic (#1685) --- tests/sqlparser_clickhouse.rs | 15 --------------- tests/sqlparser_common.rs | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 5b0638a4b..0f22db389 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1649,21 +1649,6 @@ fn parse_table_sample() { clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10 OFFSET 1 / 2"); } -#[test] -fn parse_numbers_with_underscore() { - let canonical = if cfg!(feature = "bigdecimal") { - "SELECT 10000" - } else { - "SELECT 10_000" - }; - let select = clickhouse().verified_only_select_with_canonical("SELECT 10_000", canonical); - - assert_eq!( - select.projection, - vec![SelectItem::UnnamedExpr(Expr::Value(number("10_000")))] - ) -} - fn clickhouse() -> TestedDialects { TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 99660507d..6f6eccfdf 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -55,6 +55,24 @@ use sqlparser::ast::Expr::{Identifier, UnaryOp}; use sqlparser::ast::Value::Number; use sqlparser::test_utils::all_dialects_except; +#[test] +fn parse_numeric_literal_underscore() { + let dialects = all_dialects_where(|d| d.supports_numeric_literal_underscores()); + + let canonical = if cfg!(feature = "bigdecimal") { + "SELECT 10000" + } else { + "SELECT 10_000" + }; + + let select = dialects.verified_only_select_with_canonical("SELECT 10_000", canonical); + + assert_eq!( + select.projection, + vec![UnnamedExpr(Expr::Value(number("10_000")))] + ); +} + #[test] fn parse_insert_values() { let row = vec![ From aeaafbe6e4f44e21851e13a4d4a8de8518df2263 Mon Sep 17 00:00:00 2001 From: gstvg <28798827+gstvg@users.noreply.github.com> Date: Fri, 31 Jan 2025 02:59:16 -0300 Subject: [PATCH 711/806] Extend lambda support for ClickHouse and DuckDB dialects (#1686) --- src/ast/mod.rs | 4 ++- src/dialect/clickhouse.rs | 5 +++ src/dialect/duckdb.rs | 5 +++ tests/sqlparser_common.rs | 65 +++++++++++++++++++++++++++++++++++ tests/sqlparser_databricks.rs | 63 --------------------------------- 5 files changed, 78 insertions(+), 64 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d3a028b0b..5a1be7734 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1045,7 +1045,9 @@ pub enum Expr { /// param -> expr | (param1, ...) -> expr /// ``` /// - /// See . + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/functions#higher-order-functions---operator-and-lambdaparams-expr-function) + /// [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), } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 830b3da90..9a0884a51 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -70,4 +70,9 @@ impl Dialect for ClickHouseDialect { fn supports_dictionary_syntax(&self) -> bool { true } + + /// See + fn supports_lambda_functions(&self) -> bool { + true + } } diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index a2699d850..c41aec81d 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -65,6 +65,11 @@ impl Dialect for DuckDbDialect { true } + /// See + fn supports_lambda_functions(&self) -> bool { + true + } + // DuckDB is compatible with PostgreSQL syntax for this statement, // although not all features may be implemented. fn supports_explain_with_utility_options(&self) -> bool { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6f6eccfdf..a695204c2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -13332,3 +13332,68 @@ fn test_trailing_commas_in_from() { "SELECT 1, 2 FROM (SELECT * FROM t1), (SELECT * FROM t2)", ); } + +#[test] +fn test_lambdas() { + let dialects = all_dialects_where(|d| d.supports_lambda_functions()); + + #[rustfmt::skip] + let sql = concat!( + "SELECT array_sort(array('Hello', 'World'), ", + "(p1, p2) -> CASE WHEN p1 = p2 THEN 0 ", + "WHEN reverse(p1) < reverse(p2) THEN -1 ", + "ELSE 1 END)", + ); + pretty_assertions::assert_eq!( + SelectItem::UnnamedExpr(call( + "array_sort", + [ + call( + "array", + [ + Expr::Value(Value::SingleQuotedString("Hello".to_owned())), + Expr::Value(Value::SingleQuotedString("World".to_owned())) + ] + ), + Expr::Lambda(LambdaFunction { + params: OneOrManyWithParens::Many(vec![Ident::new("p1"), Ident::new("p2")]), + body: Box::new(Expr::Case { + operand: None, + conditions: vec![ + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("p1"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("p2"))) + }, + Expr::BinaryOp { + left: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p1"))] + )), + op: BinaryOperator::Lt, + right: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p2"))] + )) + } + ], + results: vec![ + Expr::Value(number("0")), + Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(number("1"))) + } + ], + else_result: Some(Box::new(Expr::Value(number("1")))) + }) + }) + ] + )), + dialects.verified_only_select(sql).projection[0] + ); + + dialects.verified_expr( + "map_zip_with(map(1, 'a', 2, 'b'), map(1, 'x', 2, 'y'), (k, v1, v2) -> concat(v1, v2))", + ); + dialects.verified_expr("transform(array(1, 2, 3), x -> x + 1)"); +} diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index be7588de2..8338a0e71 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -83,69 +83,6 @@ fn test_databricks_exists() { ); } -#[test] -fn test_databricks_lambdas() { - #[rustfmt::skip] - let sql = concat!( - "SELECT array_sort(array('Hello', 'World'), ", - "(p1, p2) -> CASE WHEN p1 = p2 THEN 0 ", - "WHEN reverse(p1) < reverse(p2) THEN -1 ", - "ELSE 1 END)", - ); - pretty_assertions::assert_eq!( - SelectItem::UnnamedExpr(call( - "array_sort", - [ - call( - "array", - [ - Expr::Value(Value::SingleQuotedString("Hello".to_owned())), - Expr::Value(Value::SingleQuotedString("World".to_owned())) - ] - ), - Expr::Lambda(LambdaFunction { - params: OneOrManyWithParens::Many(vec![Ident::new("p1"), Ident::new("p2")]), - body: Box::new(Expr::Case { - operand: None, - conditions: vec![ - Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("p1"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Identifier(Ident::new("p2"))) - }, - Expr::BinaryOp { - left: Box::new(call( - "reverse", - [Expr::Identifier(Ident::new("p1"))] - )), - op: BinaryOperator::Lt, - right: Box::new(call( - "reverse", - [Expr::Identifier(Ident::new("p2"))] - )) - } - ], - results: vec![ - Expr::Value(number("0")), - Expr::UnaryOp { - op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(number("1"))) - } - ], - else_result: Some(Box::new(Expr::Value(number("1")))) - }) - }) - ] - )), - databricks().verified_only_select(sql).projection[0] - ); - - databricks().verified_expr( - "map_zip_with(map(1, 'a', 2, 'b'), map(1, 'x', 2, 'y'), (k, v1, v2) -> concat(v1, v2))", - ); - databricks().verified_expr("transform(array(1, 2, 3), x -> x + 1)"); -} - #[test] fn test_values_clause() { let values = Values { From 447142c6d0e8e835b357833e2e9a4e9fee661f9d Mon Sep 17 00:00:00 2001 From: Paul Grau Date: Fri, 31 Jan 2025 08:04:41 +0200 Subject: [PATCH 712/806] Make TypedString preserve quote style (#1679) --- src/ast/mod.rs | 8 ++- src/ast/spans.rs | 4 +- src/ast/value.rs | 26 ++++++++++ src/parser/mod.rs | 2 +- tests/sqlparser_bigquery.rs | 100 +++++++++++++++++++++++------------- tests/sqlparser_common.rs | 45 +++++++++++----- tests/sqlparser_postgres.rs | 2 +- 7 files changed, 132 insertions(+), 55 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5a1be7734..bccc580b3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -898,6 +898,8 @@ pub enum Expr { /// IntroducedString { introducer: String, + /// The value of the constant. + /// Hint: you can unwrap the string value using `value.into_string()`. value: Value, }, /// A constant of form ` 'value'`. @@ -905,7 +907,9 @@ pub enum Expr { /// as well as constants of other types (a non-standard PostgreSQL extension). TypedString { data_type: DataType, - value: String, + /// The value of the constant. + /// Hint: you can unwrap the string value using `value.into_string()`. + value: Value, }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), @@ -1622,7 +1626,7 @@ impl fmt::Display for Expr { Expr::IntroducedString { introducer, value } => write!(f, "{introducer} {value}"), Expr::TypedString { data_type, value } => { write!(f, "{data_type}")?; - write!(f, " '{}'", &value::escape_single_quote_string(value)) + write!(f, " {value}") } Expr::Function(fun) => write!(f, "{fun}"), Expr::Method(method) => write!(f, "{method}"), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8c46fc073..f37c0194f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1266,7 +1266,7 @@ impl Spanned for AssignmentTarget { /// f.e. `IS NULL ` reports as `::span`. /// /// Missing spans: -/// - [Expr::TypedString] +/// - [Expr::TypedString] # missing span for data_type /// - [Expr::MatchAgainst] # MySQL specific /// - [Expr::RLike] # MySQL specific /// - [Expr::Struct] # BigQuery specific @@ -1362,7 +1362,7 @@ impl Spanned for Expr { .union(&union_spans(collation.0.iter().map(|i| i.span()))), Expr::Nested(expr) => expr.span(), Expr::Value(value) => value.span(), - Expr::TypedString { .. } => Span::empty(), + Expr::TypedString { value, .. } => value.span(), Expr::Function(function) => function.span(), Expr::GroupingSets(vec) => { union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span()))) diff --git a/src/ast/value.rs b/src/ast/value.rs index 1b16646be..5798b5404 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -97,6 +97,32 @@ pub enum Value { Placeholder(String), } +impl Value { + /// If the underlying literal is a string, regardless of quote style, returns the associated string value + pub fn into_string(self) -> Option { + match self { + Value::SingleQuotedString(s) + | Value::DoubleQuotedString(s) + | Value::TripleSingleQuotedString(s) + | Value::TripleDoubleQuotedString(s) + | Value::SingleQuotedByteStringLiteral(s) + | Value::DoubleQuotedByteStringLiteral(s) + | Value::TripleSingleQuotedByteStringLiteral(s) + | Value::TripleDoubleQuotedByteStringLiteral(s) + | Value::SingleQuotedRawStringLiteral(s) + | Value::DoubleQuotedRawStringLiteral(s) + | Value::TripleSingleQuotedRawStringLiteral(s) + | Value::TripleDoubleQuotedRawStringLiteral(s) + | Value::EscapedStringLiteral(s) + | Value::UnicodeStringLiteral(s) + | Value::NationalStringLiteral(s) + | Value::HexStringLiteral(s) => Some(s), + Value::DollarQuotedString(s) => Some(s.value), + _ => None, + } + } +} + impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index df5c19a38..ca858c42e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1316,7 +1316,7 @@ impl<'a> Parser<'a> { DataType::Custom(..) => parser_err!("dummy", loc), data_type => Ok(Expr::TypedString { data_type, - value: parser.parse_literal_string()?, + value: parser.parse_value()?, }), } })?; diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 55de4801d..c5dfb27b5 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -39,43 +39,45 @@ fn parse_literal_string() { r#"'''triple-single'unescaped''', "#, r#""double\"escaped", "#, r#""""triple-double\"escaped""", "#, - r#""""triple-double"unescaped""""#, + r#""""triple-double"unescaped""", "#, + r#""""triple-double'unescaped""", "#, + r#"'''triple-single"unescaped'''"#, ); let dialect = TestedDialects::new_with_options( vec![Box::new(BigQueryDialect {})], ParserOptions::new().with_unescape(false), ); let select = dialect.verified_only_select(sql); - assert_eq!(10, select.projection.len()); + assert_eq!(12, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedString("single".to_string())), + &Expr::Value(Value::SingleQuotedString("single".into())), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedString("double".to_string())), + &Expr::Value(Value::DoubleQuotedString("double".into())), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedString("triple-single".to_string())), + &Expr::Value(Value::TripleSingleQuotedString("triple-single".into())), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedString("triple-double".to_string())), + &Expr::Value(Value::TripleDoubleQuotedString("triple-double".into())), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::SingleQuotedString(r#"single\'escaped"#.to_string())), + &Expr::Value(Value::SingleQuotedString(r#"single\'escaped"#.into())), expr_from_projection(&select.projection[4]) ); assert_eq!( &Expr::Value(Value::TripleSingleQuotedString( - r#"triple-single\'escaped"#.to_string() + r#"triple-single\'escaped"#.into() )), expr_from_projection(&select.projection[5]) ); assert_eq!( &Expr::Value(Value::TripleSingleQuotedString( - r#"triple-single'unescaped"#.to_string() + r#"triple-single'unescaped"#.into() )), expr_from_projection(&select.projection[6]) ); @@ -95,6 +97,18 @@ fn parse_literal_string() { )), expr_from_projection(&select.projection[9]) ); + assert_eq!( + &Expr::Value(Value::TripleDoubleQuotedString( + r#"triple-double'unescaped"#.to_string() + )), + expr_from_projection(&select.projection[10]) + ); + assert_eq!( + &Expr::Value(Value::TripleSingleQuotedString( + r#"triple-single"unescaped"#.to_string() + )), + expr_from_projection(&select.projection[11]) + ); } #[test] @@ -588,7 +602,7 @@ fn parse_tuple_struct_literal() { &Expr::Tuple(vec![ Expr::Value(number("1")), Expr::Value(number("1.0")), - Expr::Value(Value::SingleQuotedString("123".to_string())), + Expr::Value(Value::SingleQuotedString("123".into())), Expr::Value(Value::Boolean(true)) ]), expr_from_projection(&select.projection[1]) @@ -616,7 +630,7 @@ fn parse_typeless_struct_syntax() { assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("abc".to_string())),], + values: vec![Expr::Value(Value::SingleQuotedString("abc".into())),], fields: Default::default() }, expr_from_projection(&select.projection[1]) @@ -639,7 +653,7 @@ fn parse_typeless_struct_syntax() { name: Ident::from("a") }, Expr::Named { - expr: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(), + expr: Expr::Value(Value::SingleQuotedString("abc".into())).into(), name: Ident::from("b") }, ], @@ -804,9 +818,7 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(4, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString( - "2011-05-05".to_string() - )),], + values: vec![Expr::Value(Value::DoubleQuotedString("2011-05-05".into())),], fields: vec![StructField { field_name: None, field_type: DataType::Date @@ -818,7 +830,7 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Datetime(None), - value: "1999-01-01 01:23:34.45".to_string() + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) },], fields: vec![StructField { field_name: None, @@ -854,7 +866,7 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!( &Expr::Struct { values: vec![Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString("2".to_string()))), + value: Box::new(Expr::Value(Value::SingleQuotedString("2".into()))), leading_field: Some(DateTimeField::Hour), leading_precision: None, last_field: None, @@ -871,7 +883,9 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::JSON, - value: r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.to_string() + value: Value::SingleQuotedString( + r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() + ) },], fields: vec![StructField { field_name: None, @@ -886,7 +900,7 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString("foo".to_string())),], + values: vec![Expr::Value(Value::DoubleQuotedString("foo".into())),], fields: vec![StructField { field_name: None, field_type: DataType::String(Some(42)) @@ -898,7 +912,7 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: "2008-12-25 15:30:00 America/Los_Angeles".to_string() + value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) },], fields: vec![StructField { field_name: None, @@ -912,7 +926,7 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: "15:30:00".to_string() + value: Value::SingleQuotedString("15:30:00".into()) },], fields: vec![StructField { field_name: None, @@ -929,7 +943,7 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), - value: "1".to_string() + value: Value::SingleQuotedString("1".into()) },], fields: vec![StructField { field_name: None, @@ -942,7 +956,7 @@ fn parse_typed_struct_syntax_bigquery() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: "1".to_string() + value: Value::SingleQuotedString("1".into()) },], fields: vec![StructField { field_name: None, @@ -1119,9 +1133,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(4, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString( - "2011-05-05".to_string() - )),], + values: vec![Expr::Value(Value::SingleQuotedString("2011-05-05".into())),], fields: vec![StructField { field_name: None, field_type: DataType::Date @@ -1133,7 +1145,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Datetime(None), - value: "1999-01-01 01:23:34.45".to_string() + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) },], fields: vec![StructField { field_name: None, @@ -1169,7 +1181,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!( &Expr::Struct { values: vec![Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + value: Box::new(Expr::Value(Value::SingleQuotedString("1".into()))), leading_field: Some(DateTimeField::Month), leading_precision: None, last_field: None, @@ -1186,7 +1198,9 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::JSON, - value: r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.to_string() + value: Value::SingleQuotedString( + r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() + ) },], fields: vec![StructField { field_name: None, @@ -1201,7 +1215,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("foo".to_string())),], + values: vec![Expr::Value(Value::SingleQuotedString("foo".into())),], fields: vec![StructField { field_name: None, field_type: DataType::String(Some(42)) @@ -1213,7 +1227,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: "2008-12-25 15:30:00 America/Los_Angeles".to_string() + value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) },], fields: vec![StructField { field_name: None, @@ -1227,7 +1241,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: "15:30:00".to_string() + value: Value::SingleQuotedString("15:30:00".into()) },], fields: vec![StructField { field_name: None, @@ -1244,7 +1258,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), - value: "1".to_string() + value: Value::SingleQuotedString("1".into()) },], fields: vec![StructField { field_name: None, @@ -1257,7 +1271,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { &Expr::Struct { values: vec![Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: "1".to_string() + value: Value::SingleQuotedString("1".into()) },], fields: vec![StructField { field_name: None, @@ -1285,7 +1299,7 @@ fn parse_typed_struct_with_field_name_bigquery() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString("foo".to_string())),], + values: vec![Expr::Value(Value::DoubleQuotedString("foo".into())),], fields: vec![StructField { field_name: Some(Ident::from("y")), field_type: DataType::String(None) @@ -1332,7 +1346,7 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("foo".to_string())),], + values: vec![Expr::Value(Value::SingleQuotedString("foo".into())),], fields: vec![StructField { field_name: Some(Ident::from("y")), field_type: DataType::String(None) @@ -2234,6 +2248,20 @@ fn test_select_as_value() { assert_eq!(Some(ValueTableMode::AsValue), select.value_table_mode); } +#[test] +fn test_triple_quote_typed_strings() { + bigquery().verified_expr(r#"JSON '''{"foo":"bar's"}'''"#); + + let expr = bigquery().verified_expr(r#"JSON """{"foo":"bar's"}""""#); + assert_eq!( + Expr::TypedString { + data_type: DataType::JSON, + value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()) + }, + expr + ); +} + #[test] fn test_array_agg() { bigquery_and_generic().verified_expr("ARRAY_AGG(state)"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a695204c2..6113a3703 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5409,7 +5409,7 @@ fn parse_literal_date() { assert_eq!( &Expr::TypedString { data_type: DataType::Date, - value: "1999-01-01".into(), + value: Value::SingleQuotedString("1999-01-01".into()), }, expr_from_projection(only(&select.projection)), ); @@ -5422,7 +5422,7 @@ fn parse_literal_time() { assert_eq!( &Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), - value: "01:23:34".into(), + value: Value::SingleQuotedString("01:23:34".into()), }, expr_from_projection(only(&select.projection)), ); @@ -5435,7 +5435,7 @@ fn parse_literal_datetime() { assert_eq!( &Expr::TypedString { data_type: DataType::Datetime(None), - value: "1999-01-01 01:23:34.45".into(), + value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), }, expr_from_projection(only(&select.projection)), ); @@ -5448,7 +5448,7 @@ fn parse_literal_timestamp_without_time_zone() { assert_eq!( &Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: "1999-01-01 01:23:34".into(), + value: Value::SingleQuotedString("1999-01-01 01:23:34".into()), }, expr_from_projection(only(&select.projection)), ); @@ -5463,7 +5463,7 @@ fn parse_literal_timestamp_with_time_zone() { assert_eq!( &Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::Tz), - value: "1999-01-01 01:23:34Z".into(), + value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()), }, expr_from_projection(only(&select.projection)), ); @@ -6015,7 +6015,8 @@ fn parse_json_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::JSON, - value: r#"{ + value: Value::SingleQuotedString( + r#"{ "id": 10, "type": "fruit", "name": "apple", @@ -6035,12 +6036,30 @@ fn parse_json_keyword() { ] } }"# - .into() + .to_string() + ) }, expr_from_projection(only(&select.projection)), ); } +#[test] +fn parse_typed_strings() { + let expr = verified_expr(r#"JSON '{"foo":"bar"}'"#); + assert_eq!( + Expr::TypedString { + data_type: DataType::JSON, + value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()) + }, + expr + ); + + if let Expr::TypedString { data_type, value } = expr { + assert_eq!(DataType::JSON, data_type); + assert_eq!(r#"{"foo":"bar"}"#, value.into_string().unwrap()); + } +} + #[test] fn parse_bignumeric_keyword() { let sql = r#"SELECT BIGNUMERIC '0'"#; @@ -6048,7 +6067,7 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: r#"0"#.into() + value: Value::SingleQuotedString(r#"0"#.into()) }, expr_from_projection(only(&select.projection)), ); @@ -6059,7 +6078,7 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: r#"123456"#.into() + value: Value::SingleQuotedString(r#"123456"#.into()) }, expr_from_projection(only(&select.projection)), ); @@ -6070,7 +6089,7 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: r#"-3.14"#.into() + value: Value::SingleQuotedString(r#"-3.14"#.into()) }, expr_from_projection(only(&select.projection)), ); @@ -6081,7 +6100,7 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: r#"-0.54321"#.into() + value: Value::SingleQuotedString(r#"-0.54321"#.into()) }, expr_from_projection(only(&select.projection)), ); @@ -6092,7 +6111,7 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: r#"1.23456e05"#.into() + value: Value::SingleQuotedString(r#"1.23456e05"#.into()) }, expr_from_projection(only(&select.projection)), ); @@ -6103,7 +6122,7 @@ fn parse_bignumeric_keyword() { assert_eq!( &Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), - value: r#"-9.876e-3"#.into() + value: Value::SingleQuotedString(r#"-9.876e-3"#.into()) }, expr_from_projection(only(&select.projection)), ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 93af04980..ee4aa2a0a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4638,7 +4638,7 @@ fn parse_at_time_zone() { left: Box::new(Expr::AtTimeZone { timestamp: Box::new(Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), - value: "2001-09-28 01:00".to_owned(), + value: Value::SingleQuotedString("2001-09-28 01:00".to_string()), }), time_zone: Box::new(Expr::Cast { kind: CastKind::DoubleColon, From c3256a80d7b9708c6abf025f24a72779277f3a34 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 1 Feb 2025 13:42:38 +0100 Subject: [PATCH 713/806] Do not parse ASOF and MATCH_CONDITION as table factor aliases (#1698) --- src/keywords.rs | 2 ++ tests/sqlparser_snowflake.rs | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/keywords.rs b/src/keywords.rs index 02ce04988..5937d7755 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1000,6 +1000,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::ANTI, Keyword::SEMI, Keyword::RETURNING, + Keyword::ASOF, + Keyword::MATCH_CONDITION, // for MSSQL-specific OUTER APPLY (seems reserved in most dialects) Keyword::OUTER, Keyword::SET, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 1310b984f..1036ce1fa 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2731,6 +2731,20 @@ fn asof_joins() { "ON s.state = p.state ", "ORDER BY s.observed", )); + + // Test without explicit aliases + #[rustfmt::skip] + snowflake_and_generic().verified_query(concat!( + "SELECT * ", + "FROM snowtime ", + "ASOF JOIN raintime ", + "MATCH_CONDITION (snowtime.observed >= raintime.observed) ", + "ON snowtime.state = raintime.state ", + "ASOF JOIN preciptime ", + "MATCH_CONDITION (showtime.observed >= preciptime.observed) ", + "ON showtime.state = preciptime.state ", + "ORDER BY showtime.observed", + )); } #[test] From 906f39534176f389b85e485044ca85d77caccaac Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 3 Feb 2025 08:21:17 +0100 Subject: [PATCH 714/806] Add support for GRANT on some common Snowflake objects (#1699) --- src/ast/mod.rs | 20 ++++++++++++++++++++ src/parser/mod.rs | 15 +++++++++++++-- tests/sqlparser_common.rs | 4 ++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bccc580b3..c0d3ea574 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5884,12 +5884,20 @@ pub enum GrantObjects { AllSequencesInSchema { schemas: Vec }, /// Grant privileges on `ALL TABLES IN SCHEMA [, ...]` AllTablesInSchema { schemas: Vec }, + /// Grant privileges on specific databases + Databases(Vec), /// Grant privileges on specific schemas Schemas(Vec), /// Grant privileges on specific sequences Sequences(Vec), /// Grant privileges on specific tables Tables(Vec), + /// Grant privileges on specific views + Views(Vec), + /// Grant privileges on specific warehouses + Warehouses(Vec), + /// Grant privileges on specific integrations + Integrations(Vec), } impl fmt::Display for GrantObjects { @@ -5898,12 +5906,24 @@ impl fmt::Display for GrantObjects { GrantObjects::Sequences(sequences) => { write!(f, "SEQUENCE {}", display_comma_separated(sequences)) } + GrantObjects::Databases(databases) => { + write!(f, "DATABASE {}", display_comma_separated(databases)) + } GrantObjects::Schemas(schemas) => { write!(f, "SCHEMA {}", display_comma_separated(schemas)) } GrantObjects::Tables(tables) => { write!(f, "{}", display_comma_separated(tables)) } + GrantObjects::Views(views) => { + write!(f, "VIEW {}", display_comma_separated(views)) + } + GrantObjects::Warehouses(warehouses) => { + write!(f, "WAREHOUSE {}", display_comma_separated(warehouses)) + } + GrantObjects::Integrations(integrations) => { + write!(f, "INTEGRATION {}", display_comma_separated(integrations)) + } GrantObjects::AllSequencesInSchema { schemas } => { write!( f, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ca858c42e..7d2d407d8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12139,13 +12139,24 @@ impl<'a> Parser<'a> { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, } } else { - let object_type = - self.parse_one_of_keywords(&[Keyword::SEQUENCE, Keyword::SCHEMA, Keyword::TABLE]); + let object_type = self.parse_one_of_keywords(&[ + Keyword::SEQUENCE, + Keyword::DATABASE, + Keyword::SCHEMA, + Keyword::TABLE, + Keyword::VIEW, + Keyword::WAREHOUSE, + Keyword::INTEGRATION, + ]); let objects = self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); match object_type { + Some(Keyword::DATABASE) => GrantObjects::Databases(objects?), Some(Keyword::SCHEMA) => GrantObjects::Schemas(objects?), Some(Keyword::SEQUENCE) => GrantObjects::Sequences(objects?), + Some(Keyword::WAREHOUSE) => GrantObjects::Warehouses(objects?), + Some(Keyword::INTEGRATION) => GrantObjects::Integrations(objects?), + Some(Keyword::VIEW) => GrantObjects::Views(objects?), Some(Keyword::TABLE) | None => GrantObjects::Tables(objects?), _ => unreachable!(), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6113a3703..643ac357a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8779,6 +8779,10 @@ 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 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"); + verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1"); } #[test] From 257da5a82c8a1e6c565abfa52bd490b211775069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20SAISSY?= Date: Mon, 3 Feb 2025 08:27:37 +0100 Subject: [PATCH 715/806] Add RETURNS TABLE() support for CREATE FUNCTION in Postgresql (#1687) Co-authored-by: Ifeanyi Ubah --- src/ast/data_type.rs | 5 +++++ src/parser/mod.rs | 22 ++++++++++++++++++++++ tests/sqlparser_postgres.rs | 1 + 3 files changed, 28 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 02aa6cc9f..1f2b6be97 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -45,6 +45,10 @@ 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(...) + /// + /// [postgresql]: https://www.postgresql.org/docs/15/sql-createfunction.html + Table(Vec), /// Fixed-length character type e.g. CHARACTER(10) Character(Option), /// Fixed-length char type e.g. CHAR(10) @@ -630,6 +634,7 @@ 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)), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7d2d407d8..931d97001 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8867,6 +8867,10 @@ impl<'a> Parser<'a> { let _ = self.parse_keyword(Keyword::TYPE); Ok(DataType::AnyType) } + Keyword::TABLE => { + let columns = self.parse_returns_table_columns()?; + Ok(DataType::Table(columns)) + } _ => { self.prev_token(); let type_name = self.parse_object_name(false)?; @@ -8894,6 +8898,24 @@ impl<'a> Parser<'a> { Ok((data, trailing_bracket)) } + 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, + collation: None, + options: Vec::new(), // No constraints expected here + }) + } + + fn parse_returns_table_columns(&mut self) -> Result, ParserError> { + self.expect_token(&Token::LParen)?; + let columns = self.parse_comma_separated(Parser::parse_returns_table_column)?; + self.expect_token(&Token::RParen)?; + Ok(columns) + } + pub fn parse_string_values(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let mut values = Vec::new(); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ee4aa2a0a..62da0f574 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3803,6 +3803,7 @@ fn parse_create_function_detailed() { pg_and_generic().verified_stmt("CREATE OR REPLACE FUNCTION add(a INTEGER, IN b INTEGER = 1) RETURNS INTEGER LANGUAGE SQL STABLE CALLED ON NULL INPUT PARALLEL UNSAFE RETURN a + b"); pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION increment(i INTEGER) RETURNS INTEGER LANGUAGE plpgsql AS $$ BEGIN RETURN i + 1; END; $$"#); pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION no_arg() RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN DELETE FROM my_table; END; $$"#); + pg_and_generic().verified_stmt(r#"CREATE OR REPLACE FUNCTION return_table(i INTEGER) RETURNS TABLE(id UUID, is_active BOOLEAN) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT NULL::UUID, NULL::BOOLEAN; END; $$"#); } #[test] fn parse_incorrect_create_function_parallel() { From ec948eaf6e9099451d69c525bb6987ebc721bec7 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:17:47 +0100 Subject: [PATCH 716/806] Add parsing for GRANT ROLE and GRANT DATABASE ROLE in Snowflake dialect (#1689) --- src/ast/mod.rs | 20 ++++++-- src/parser/mod.rs | 89 +++++++++++++++++++----------------- tests/sqlparser_common.rs | 17 ++++--- tests/sqlparser_mysql.rs | 10 +++- tests/sqlparser_snowflake.rs | 12 +++++ 5 files changed, 94 insertions(+), 54 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c0d3ea574..17b5276aa 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3230,7 +3230,7 @@ pub enum Statement { /// ``` Grant { privileges: Privileges, - objects: GrantObjects, + objects: Option, grantees: Vec, with_grant_option: bool, granted_by: Option, @@ -3240,7 +3240,7 @@ pub enum Statement { /// ``` Revoke { privileges: Privileges, - objects: GrantObjects, + objects: Option, grantees: Vec, granted_by: Option, cascade: Option, @@ -4785,7 +4785,9 @@ impl fmt::Display for Statement { granted_by, } => { write!(f, "GRANT {privileges} ")?; - write!(f, "ON {objects} ")?; + if let Some(objects) = objects { + write!(f, "ON {objects} ")?; + } write!(f, "TO {}", display_comma_separated(grantees))?; if *with_grant_option { write!(f, " WITH GRANT OPTION")?; @@ -4803,7 +4805,9 @@ impl fmt::Display for Statement { cascade, } => { write!(f, "REVOKE {privileges} ")?; - write!(f, "ON {objects} ")?; + if let Some(objects) = objects { + write!(f, "ON {objects} ")?; + } write!(f, "FROM {}", display_comma_separated(grantees))?; if let Some(grantor) = granted_by { write!(f, " GRANTED BY {grantor}")?; @@ -5503,6 +5507,9 @@ pub enum Action { Create { obj_type: Option, }, + DatabaseRole { + role: ObjectName, + }, Delete, EvolveSchema, Execute { @@ -5536,6 +5543,9 @@ pub enum Action { }, Replicate, ResolveAll, + Role { + role: Ident, + }, Select { columns: Option>, }, @@ -5565,6 +5575,7 @@ impl fmt::Display for Action { write!(f, " {obj_type}")? } } + Action::DatabaseRole { role } => write!(f, "DATABASE ROLE {role}")?, Action::Delete => f.write_str("DELETE")?, Action::EvolveSchema => f.write_str("EVOLVE SCHEMA")?, Action::Execute { obj_type } => { @@ -5591,6 +5602,7 @@ impl fmt::Display for Action { Action::References { .. } => f.write_str("REFERENCES")?, Action::Replicate => f.write_str("REPLICATE")?, Action::ResolveAll => f.write_str("RESOLVE ALL")?, + Action::Role { role } => write!(f, "ROLE {role}")?, Action::Select { .. } => f.write_str("SELECT")?, Action::Temporary => f.write_str("TEMPORARY")?, Action::Trigger => f.write_str("TRIGGER")?, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 931d97001..28cba0393 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12130,7 +12130,7 @@ impl<'a> Parser<'a> { pub fn parse_grant_revoke_privileges_objects( &mut self, - ) -> Result<(Privileges, GrantObjects), ParserError> { + ) -> Result<(Privileges, Option), ParserError> { let privileges = if self.parse_keyword(Keyword::ALL) { Privileges::All { with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), @@ -12140,48 +12140,49 @@ impl<'a> Parser<'a> { Privileges::Actions(actions) }; - self.expect_keyword_is(Keyword::ON)?; - - let objects = if self.parse_keywords(&[ - Keyword::ALL, - Keyword::TABLES, - Keyword::IN, - Keyword::SCHEMA, - ]) { - GrantObjects::AllTablesInSchema { - schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, - } - } else if self.parse_keywords(&[ - Keyword::ALL, - Keyword::SEQUENCES, - Keyword::IN, - Keyword::SCHEMA, - ]) { - GrantObjects::AllSequencesInSchema { - schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, - } - } else { - let object_type = self.parse_one_of_keywords(&[ - Keyword::SEQUENCE, - Keyword::DATABASE, + let objects = if self.parse_keyword(Keyword::ON) { + if self.parse_keywords(&[Keyword::ALL, Keyword::TABLES, Keyword::IN, Keyword::SCHEMA]) { + Some(GrantObjects::AllTablesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::SEQUENCES, + Keyword::IN, Keyword::SCHEMA, - Keyword::TABLE, - Keyword::VIEW, - Keyword::WAREHOUSE, - Keyword::INTEGRATION, - ]); - let objects = - self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); - match object_type { - Some(Keyword::DATABASE) => GrantObjects::Databases(objects?), - Some(Keyword::SCHEMA) => GrantObjects::Schemas(objects?), - Some(Keyword::SEQUENCE) => GrantObjects::Sequences(objects?), - Some(Keyword::WAREHOUSE) => GrantObjects::Warehouses(objects?), - Some(Keyword::INTEGRATION) => GrantObjects::Integrations(objects?), - Some(Keyword::VIEW) => GrantObjects::Views(objects?), - Some(Keyword::TABLE) | None => GrantObjects::Tables(objects?), - _ => unreachable!(), + ]) { + Some(GrantObjects::AllSequencesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else { + let object_type = self.parse_one_of_keywords(&[ + Keyword::SEQUENCE, + Keyword::DATABASE, + Keyword::DATABASE, + Keyword::SCHEMA, + Keyword::TABLE, + Keyword::VIEW, + Keyword::WAREHOUSE, + Keyword::INTEGRATION, + Keyword::VIEW, + Keyword::WAREHOUSE, + Keyword::INTEGRATION, + ]); + let objects = + self.parse_comma_separated(|p| p.parse_object_name_with_wildcards(false, true)); + match object_type { + Some(Keyword::DATABASE) => Some(GrantObjects::Databases(objects?)), + Some(Keyword::SCHEMA) => Some(GrantObjects::Schemas(objects?)), + Some(Keyword::SEQUENCE) => Some(GrantObjects::Sequences(objects?)), + Some(Keyword::WAREHOUSE) => Some(GrantObjects::Warehouses(objects?)), + Some(Keyword::INTEGRATION) => Some(GrantObjects::Integrations(objects?)), + Some(Keyword::VIEW) => Some(GrantObjects::Views(objects?)), + Some(Keyword::TABLE) | None => Some(GrantObjects::Tables(objects?)), + _ => unreachable!(), + } } + } else { + None }; Ok((privileges, objects)) @@ -12208,6 +12209,9 @@ impl<'a> Parser<'a> { Ok(Action::AttachPolicy) } else if self.parse_keywords(&[Keyword::BIND, Keyword::SERVICE, Keyword::ENDPOINT]) { Ok(Action::BindServiceEndpoint) + } else if self.parse_keywords(&[Keyword::DATABASE, Keyword::ROLE]) { + let role = self.parse_object_name(false)?; + Ok(Action::DatabaseRole { role }) } else if self.parse_keywords(&[Keyword::EVOLVE, Keyword::SCHEMA]) { Ok(Action::EvolveSchema) } else if self.parse_keywords(&[Keyword::IMPORT, Keyword::SHARE]) { @@ -12273,6 +12277,9 @@ impl<'a> Parser<'a> { Ok(Action::Read) } else if self.parse_keyword(Keyword::REPLICATE) { Ok(Action::Replicate) + } else if self.parse_keyword(Keyword::ROLE) { + let role = self.parse_identifier()?; + Ok(Action::Role { role }) } else if self.parse_keyword(Keyword::SELECT) { Ok(Action::Select { columns: parse_columns(self)?, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 643ac357a..4fcb37b6d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8637,7 +8637,7 @@ fn parse_grant() { granted_by, .. } => match (privileges, objects) { - (Privileges::Actions(actions), GrantObjects::Tables(objects)) => { + (Privileges::Actions(actions), Some(GrantObjects::Tables(objects))) => { assert_eq!( vec![ Action::Select { columns: None }, @@ -8687,7 +8687,7 @@ fn parse_grant() { with_grant_option, .. } => match (privileges, objects) { - (Privileges::Actions(actions), GrantObjects::AllTablesInSchema { schemas }) => { + (Privileges::Actions(actions), Some(GrantObjects::AllTablesInSchema { schemas })) => { assert_eq!(vec![Action::Insert { columns: None }], actions); assert_eq_vec(&["public"], &schemas); assert_eq_vec(&["browser"], &grantees); @@ -8707,7 +8707,7 @@ fn parse_grant() { granted_by, .. } => match (privileges, objects, granted_by) { - (Privileges::Actions(actions), GrantObjects::Sequences(objects), None) => { + (Privileges::Actions(actions), Some(GrantObjects::Sequences(objects)), None) => { assert_eq!( vec![Action::Usage, Action::Select { columns: None }], actions @@ -8744,7 +8744,7 @@ fn parse_grant() { Privileges::All { with_privileges_keyword, }, - GrantObjects::Schemas(schemas), + Some(GrantObjects::Schemas(schemas)), ) => { assert!(!with_privileges_keyword); assert_eq_vec(&["aa", "b"], &schemas); @@ -8761,7 +8761,10 @@ fn parse_grant() { objects, .. } => match (privileges, objects) { - (Privileges::Actions(actions), GrantObjects::AllSequencesInSchema { schemas }) => { + ( + Privileges::Actions(actions), + Some(GrantObjects::AllSequencesInSchema { schemas }), + ) => { assert_eq!(vec![Action::Usage], actions); assert_eq_vec(&["bus"], &schemas); } @@ -8791,7 +8794,7 @@ fn test_revoke() { match verified_stmt(sql) { Statement::Revoke { privileges, - objects: GrantObjects::Tables(tables), + objects: Some(GrantObjects::Tables(tables)), grantees, granted_by, cascade, @@ -8817,7 +8820,7 @@ fn test_revoke_with_cascade() { match all_dialects_except(|d| d.is::()).verified_stmt(sql) { Statement::Revoke { privileges, - objects: GrantObjects::Tables(tables), + objects: Some(GrantObjects::Tables(tables)), grantees, granted_by, cascade, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 2e6dfc72b..9f00a9213 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3046,7 +3046,10 @@ fn parse_grant() { ); assert_eq!( objects, - GrantObjects::Tables(vec![ObjectName::from(vec!["*".into(), "*".into()])]) + Some(GrantObjects::Tables(vec![ObjectName::from(vec![ + "*".into(), + "*".into() + ])])) ); assert!(!with_grant_option); assert!(granted_by.is_none()); @@ -3087,7 +3090,10 @@ fn parse_revoke() { ); assert_eq!( objects, - GrantObjects::Tables(vec![ObjectName::from(vec!["db1".into(), "*".into()])]) + Some(GrantObjects::Tables(vec![ObjectName::from(vec![ + "db1".into(), + "*".into() + ])])) ); if let [Grantee { grantee_type: GranteesType::None, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 1036ce1fa..c68ada3c9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3263,3 +3263,15 @@ fn test_grant_account_privileges() { } } } + +#[test] +fn test_grant_role_to() { + snowflake_and_generic().verified_stmt("GRANT ROLE r1 TO ROLE r2"); + snowflake_and_generic().verified_stmt("GRANT ROLE r1 TO USER u1"); +} + +#[test] +fn test_grant_database_role_to() { + snowflake_and_generic().verified_stmt("GRANT DATABASE ROLE r1 TO ROLE r2"); + snowflake_and_generic().verified_stmt("GRANT DATABASE ROLE db1.sc1.r1 TO ROLE db1.sc1.r2"); +} From 486b29ffab6092d01807fee84701e397bcd73bd9 Mon Sep 17 00:00:00 2001 From: wugeer Date: Wed, 5 Feb 2025 01:33:12 +0800 Subject: [PATCH 717/806] Add support for `CREATE/ALTER/DROP CONNECTOR` syntax (#1701) --- src/ast/ddl.rs | 83 ++++++++++++++++++- src/ast/mod.rs | 71 ++++++++++++++-- src/ast/spans.rs | 3 + src/dialect/mod.rs | 1 + src/keywords.rs | 2 + src/parser/alter.rs | 45 +++++++++- src/parser/mod.rs | 61 +++++++++++++- tests/sqlparser_common.rs | 169 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 420 insertions(+), 15 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1b5ccda26..35e01e618 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,10 +30,10 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, CreateFunctionBody, CreateFunctionUsing, DataType, - Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, - Ident, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, - SequenceOptions, SqlOption, Tag, Value, + display_comma_separated, display_separated, CommentDef, CreateFunctionBody, + CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, + FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, + OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, }; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -338,6 +338,23 @@ impl fmt::Display for Owner { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterConnectorOwner { + User(Ident), + Role(Ident), +} + +impl fmt::Display for AlterConnectorOwner { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterConnectorOwner::User(ident) => write!(f, "USER {ident}"), + AlterConnectorOwner::Role(ident) => write!(f, "ROLE {ident}"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2055,3 +2072,61 @@ impl fmt::Display for CreateFunction { Ok(()) } } + +/// ```sql +/// CREATE CONNECTOR [IF NOT EXISTS] connector_name +/// [TYPE datasource_type] +/// [URL datasource_url] +/// [COMMENT connector_comment] +/// [WITH DCPROPERTIES(property_name=property_value, ...)] +/// ``` +/// +/// [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-CreateDataConnectorCreateConnector) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateConnector { + pub name: Ident, + pub if_not_exists: bool, + pub connector_type: Option, + pub url: Option, + pub comment: Option, + pub with_dcproperties: Option>, +} + +impl fmt::Display for CreateConnector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE CONNECTOR {if_not_exists}{name}", + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + name = self.name, + )?; + + if let Some(connector_type) = &self.connector_type { + write!(f, " TYPE '{connector_type}'")?; + } + + if let Some(url) = &self.url { + write!(f, " URL '{url}'")?; + } + + if let Some(comment) = &self.comment { + write!(f, " COMMENT = '{comment}'")?; + } + + if let Some(with_dcproperties) = &self.with_dcproperties { + write!( + f, + " WITH DCPROPERTIES({})", + display_comma_separated(with_dcproperties) + )?; + } + + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 17b5276aa..35c6dcc1f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -47,14 +47,14 @@ pub use self::dcl::{ AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ - AlterColumnOperation, AlterIndexOperation, AlterPolicyOperation, AlterTableOperation, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateFunction, Deduplicate, DeferrableInitial, DropBehavior, - GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexOption, - IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, ProcedureParam, - ReferentialAction, TableConstraint, TagsColumnOption, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, ViewColumnDef, + AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, + AlterTableOperation, 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, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -2646,6 +2646,11 @@ pub enum Statement { with_check: Option, }, /// ```sql + /// CREATE CONNECTOR + /// ``` + /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-CreateDataConnectorCreateConnector) + CreateConnector(CreateConnector), + /// ```sql /// ALTER TABLE /// ``` AlterTable { @@ -2697,6 +2702,20 @@ pub enum Statement { operation: AlterPolicyOperation, }, /// ```sql + /// ALTER CONNECTOR connector_name SET DCPROPERTIES(property_name=property_value, ...); + /// or + /// ALTER CONNECTOR connector_name SET URL new_url; + /// or + /// ALTER CONNECTOR connector_name SET OWNER [USER|ROLE] user_or_role; + /// ``` + /// (Hive-specific) + AlterConnector { + name: Ident, + properties: Option>, + url: Option, + owner: Option, + }, + /// ```sql /// ATTACH DATABASE 'path/to/file' AS alias /// ``` /// (SQLite-specific) @@ -2795,6 +2814,11 @@ pub enum Statement { drop_behavior: Option, }, /// ```sql + /// DROP CONNECTOR + /// ``` + /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-DropConnector) + DropConnector { if_exists: bool, name: Ident }, + /// ```sql /// DECLARE /// ``` /// Declare Cursor Variables @@ -4354,6 +4378,7 @@ impl fmt::Display for Statement { Ok(()) } + Statement::CreateConnector(create_connector) => create_connector.fmt(f), Statement::AlterTable { name, if_exists, @@ -4411,6 +4436,28 @@ impl fmt::Display for Statement { } => { write!(f, "ALTER POLICY {name} ON {table_name}{operation}") } + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + write!(f, "ALTER CONNECTOR {name}")?; + if let Some(properties) = properties { + write!( + f, + " SET DCPROPERTIES({})", + display_comma_separated(properties) + )?; + } + if let Some(url) = url { + write!(f, " SET URL '{url}'")?; + } + if let Some(owner) = owner { + write!(f, " SET OWNER {owner}")?; + } + Ok(()) + } Statement::Drop { object_type, if_exists, @@ -4498,6 +4545,14 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::DropConnector { if_exists, name } => { + write!( + f, + "DROP CONNECTOR {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" } + )?; + Ok(()) + } Statement::Discard { object_type } => { write!(f, "DISCARD {object_type}")?; Ok(()) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f37c0194f..b26900857 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -398,6 +398,7 @@ impl Spanned for Statement { Statement::CreateIndex(create_index) => create_index.span(), Statement::CreateRole { .. } => Span::empty(), Statement::CreateSecret { .. } => Span::empty(), + Statement::CreateConnector { .. } => Span::empty(), Statement::AlterTable { name, if_exists: _, @@ -487,7 +488,9 @@ impl Spanned for Statement { Statement::OptimizeTable { .. } => Span::empty(), Statement::CreatePolicy { .. } => Span::empty(), Statement::AlterPolicy { .. } => Span::empty(), + Statement::AlterConnector { .. } => Span::empty(), Statement::DropPolicy { .. } => Span::empty(), + Statement::DropConnector { .. } => Span::empty(), Statement::ShowDatabases { .. } => Span::empty(), Statement::ShowSchemas { .. } => Span::empty(), Statement::ShowViews { .. } => Span::empty(), diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b648869d2..205395f6c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -876,6 +876,7 @@ pub trait Dialect: Debug + Any { fn supports_string_escape_constant(&self) -> bool { false } + /// Returns true if the dialect supports the table hints in the `FROM` clause. fn supports_table_hints(&self) -> bool { false diff --git a/src/keywords.rs b/src/keywords.rs index 5937d7755..5f36fa737 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -201,6 +201,7 @@ define_keywords!( CONFLICT, CONNECT, CONNECTION, + CONNECTOR, CONSTRAINT, CONTAINS, CONTINUE, @@ -246,6 +247,7 @@ define_keywords!( DAYOFWEEK, DAYOFYEAR, DAYS, + DCPROPERTIES, DEALLOCATE, DEC, DECADE, diff --git a/src/parser/alter.rs b/src/parser/alter.rs index bb6782c13..bff462ee0 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -18,8 +18,8 @@ use alloc::vec; use super::{Parser, ParserError}; use crate::{ ast::{ - AlterPolicyOperation, AlterRoleOperation, Expr, Password, ResetConfig, RoleOption, - SetConfigValue, Statement, + AlterConnectorOwner, AlterPolicyOperation, AlterRoleOperation, Expr, Password, ResetConfig, + RoleOption, SetConfigValue, Statement, }, dialect::{MsSqlDialect, PostgreSqlDialect}, keywords::Keyword, @@ -99,6 +99,47 @@ impl Parser<'_> { } } + /// Parse an `ALTER CONNECTOR` statement + /// ```sql + /// ALTER CONNECTOR connector_name SET DCPROPERTIES(property_name=property_value, ...); + /// + /// ALTER CONNECTOR connector_name SET URL new_url; + /// + /// ALTER CONNECTOR connector_name SET OWNER [USER|ROLE] user_or_role; + /// ``` + pub fn parse_alter_connector(&mut self) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::SET)?; + + let properties = match self.parse_options_with_keywords(&[Keyword::DCPROPERTIES])? { + properties if !properties.is_empty() => Some(properties), + _ => None, + }; + + let url = if self.parse_keyword(Keyword::URL) { + Some(self.parse_literal_string()?) + } else { + None + }; + + let owner = if self.parse_keywords(&[Keyword::OWNER, Keyword::USER]) { + let owner = self.parse_identifier()?; + Some(AlterConnectorOwner::User(owner)) + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::ROLE]) { + let owner = self.parse_identifier()?; + Some(AlterConnectorOwner::Role(owner)) + } else { + None + }; + + Ok(Statement::AlterConnector { + name, + properties, + url, + owner, + }) + } + fn parse_mssql_alter_role(&mut self) -> Result { let role_name = self.parse_identifier()?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 28cba0393..6d84ff843 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4268,6 +4268,8 @@ impl<'a> Parser<'a> { self.parse_create_type() } else if self.parse_keyword(Keyword::PROCEDURE) { self.parse_create_procedure(or_alter) + } else if self.parse_keyword(Keyword::CONNECTOR) { + self.parse_create_connector() } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -5580,6 +5582,49 @@ impl<'a> Parser<'a> { }) } + /// ```sql + /// CREATE CONNECTOR [IF NOT EXISTS] connector_name + /// [TYPE datasource_type] + /// [URL datasource_url] + /// [COMMENT connector_comment] + /// [WITH DCPROPERTIES(property_name=property_value, ...)] + /// ``` + /// + /// [Hive Documentation](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-CreateDataConnectorCreateConnector) + pub fn parse_create_connector(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_identifier()?; + + let connector_type = if self.parse_keyword(Keyword::TYPE) { + Some(self.parse_literal_string()?) + } else { + None + }; + + let url = if self.parse_keyword(Keyword::URL) { + Some(self.parse_literal_string()?) + } else { + None + }; + + let comment = self.parse_optional_inline_comment()?; + + let with_dcproperties = + match self.parse_options_with_keywords(&[Keyword::WITH, Keyword::DCPROPERTIES])? { + properties if !properties.is_empty() => Some(properties), + _ => None, + }; + + Ok(Statement::CreateConnector(CreateConnector { + name, + if_not_exists, + connector_type, + url, + comment, + with_dcproperties, + })) + } + pub fn parse_drop(&mut self) -> Result { // MySQL dialect supports `TEMPORARY` let temporary = dialect_of!(self is MySqlDialect | GenericDialect | DuckDbDialect) @@ -5609,6 +5654,8 @@ impl<'a> Parser<'a> { return self.parse_drop_function(); } else if self.parse_keyword(Keyword::POLICY) { return self.parse_drop_policy(); + } else if self.parse_keyword(Keyword::CONNECTOR) { + return self.parse_drop_connector(); } else if self.parse_keyword(Keyword::PROCEDURE) { return self.parse_drop_procedure(); } else if self.parse_keyword(Keyword::SECRET) { @@ -5619,7 +5666,7 @@ impl<'a> Parser<'a> { return self.parse_drop_extension(); } else { return self.expected( - "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, or VIEW after DROP", self.peek_token(), ); }; @@ -5693,6 +5740,16 @@ impl<'a> Parser<'a> { drop_behavior, }) } + /// ```sql + /// DROP CONNECTOR [IF EXISTS] name + /// ``` + /// + /// See [Hive](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362034#LanguageManualDDL-DropConnector) + fn parse_drop_connector(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier()?; + Ok(Statement::DropConnector { if_exists, name }) + } /// ```sql /// DROP PROCEDURE [ IF EXISTS ] name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] [, ...] @@ -7989,6 +8046,7 @@ impl<'a> Parser<'a> { Keyword::INDEX, Keyword::ROLE, Keyword::POLICY, + Keyword::CONNECTOR, ])?; match object_type { Keyword::VIEW => self.parse_alter_view(), @@ -8041,6 +8099,7 @@ impl<'a> Parser<'a> { } Keyword::ROLE => self.parse_alter_role(), Keyword::POLICY => self.parse_alter_policy(), + Keyword::CONNECTOR => self.parse_alter_connector(), // unreachable because expect_one_of_keywords used above _ => unreachable!(), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4fcb37b6d..3a6183a15 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12289,6 +12289,175 @@ fn test_alter_policy() { ); } +#[test] +fn test_create_connector() { + let sql = "CREATE CONNECTOR my_connector \ + TYPE 'jdbc' \ + URL 'jdbc:mysql://localhost:3306/mydb' \ + WITH DCPROPERTIES('user' = 'root', 'password' = 'password')"; + let dialects = all_dialects(); + match dialects.verified_stmt(sql) { + Statement::CreateConnector(CreateConnector { + name, + connector_type, + url, + with_dcproperties, + .. + }) => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!(connector_type, Some("jdbc".to_string())); + assert_eq!(url, Some("jdbc:mysql://localhost:3306/mydb".to_string())); + assert_eq!( + with_dcproperties, + Some(vec![ + SqlOption::KeyValue { + key: Ident::with_quote('\'', "user"), + value: Expr::Value(Value::SingleQuotedString("root".to_string())) + }, + SqlOption::KeyValue { + key: Ident::with_quote('\'', "password"), + value: Expr::Value(Value::SingleQuotedString("password".to_string())) + } + ]) + ); + } + _ => unreachable!(), + } + + // omit IF NOT EXISTS/TYPE/URL/COMMENT/WITH DCPROPERTIES clauses is allowed + dialects.verified_stmt("CREATE CONNECTOR my_connector"); + + // missing connector name + assert_eq!( + dialects + .parse_sql_statements("CREATE CONNECTOR") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" + ); +} + +#[test] +fn test_drop_connector() { + let dialects = all_dialects(); + match dialects.verified_stmt("DROP CONNECTOR IF EXISTS my_connector") { + Statement::DropConnector { if_exists, name } => { + assert_eq!(if_exists, true); + assert_eq!(name.to_string(), "my_connector"); + } + _ => unreachable!(), + } + + // omit IF EXISTS is allowed + dialects.verified_stmt("DROP CONNECTOR my_connector"); + + // missing connector name + assert_eq!( + dialects + .parse_sql_statements("DROP CONNECTOR") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" + ); +} + +#[test] +fn test_alter_connector() { + let dialects = all_dialects(); + match dialects.verified_stmt( + "ALTER CONNECTOR my_connector SET DCPROPERTIES('user' = 'root', 'password' = 'password')", + ) { + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!( + properties, + Some(vec![ + SqlOption::KeyValue { + key: Ident::with_quote('\'', "user"), + value: Expr::Value(Value::SingleQuotedString("root".to_string())) + }, + SqlOption::KeyValue { + key: Ident::with_quote('\'', "password"), + value: Expr::Value(Value::SingleQuotedString("password".to_string())) + } + ]) + ); + assert_eq!(url, None); + assert_eq!(owner, None); + } + _ => unreachable!(), + } + + match dialects + .verified_stmt("ALTER CONNECTOR my_connector SET URL 'jdbc:mysql://localhost:3306/mydb'") + { + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!(properties, None); + assert_eq!(url, Some("jdbc:mysql://localhost:3306/mydb".to_string())); + assert_eq!(owner, None); + } + _ => unreachable!(), + } + + match dialects.verified_stmt("ALTER CONNECTOR my_connector SET OWNER USER 'root'") { + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!(properties, None); + assert_eq!(url, None); + assert_eq!( + owner, + Some(AlterConnectorOwner::User(Ident::with_quote('\'', "root"))) + ); + } + _ => unreachable!(), + } + + match dialects.verified_stmt("ALTER CONNECTOR my_connector SET OWNER ROLE 'admin'") { + Statement::AlterConnector { + name, + properties, + url, + owner, + } => { + assert_eq!(name.to_string(), "my_connector"); + assert_eq!(properties, None); + assert_eq!(url, None); + assert_eq!( + owner, + Some(AlterConnectorOwner::Role(Ident::with_quote('\'', "admin"))) + ); + } + _ => unreachable!(), + } + + // Wrong option name + assert_eq!( + dialects + .parse_sql_statements( + "ALTER CONNECTOR my_connector SET WRONG 'jdbc:mysql://localhost:3306/mydb'" + ) + .unwrap_err() + .to_string(), + "sql parser error: Expected: end of statement, found: WRONG" + ); +} + #[test] fn test_select_where_with_like_or_ilike_any() { verified_stmt(r#"SELECT * FROM x WHERE a ILIKE ANY '%abc%'"#); From 751dc5afce794398bcfc148aa6b2f9ecf3b6b5a9 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:23:27 +0100 Subject: [PATCH 718/806] Parse Snowflake COPY INTO (#1669) --- src/ast/helpers/stmt_data_loading.rs | 10 +- src/ast/mod.rs | 102 +++++++---- src/ast/spans.rs | 7 +- src/dialect/snowflake.rs | 250 +++++++++++++++------------ tests/sqlparser_snowflake.rs | 197 +++++++++++++++++---- 5 files changed, 381 insertions(+), 185 deletions(-) diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index 42e1df06b..77de5d9ec 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -58,6 +58,7 @@ pub enum DataLoadingOptionType { STRING, BOOLEAN, ENUM, + NUMBER, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -128,12 +129,9 @@ impl fmt::Display for DataLoadingOption { DataLoadingOptionType::STRING => { write!(f, "{}='{}'", self.option_name, self.value)?; } - DataLoadingOptionType::ENUM => { - // single quote is omitted - write!(f, "{}={}", self.option_name, self.value)?; - } - DataLoadingOptionType::BOOLEAN => { - // single quote is omitted + DataLoadingOptionType::ENUM + | DataLoadingOptionType::BOOLEAN + | DataLoadingOptionType::NUMBER => { write!(f, "{}={}", self.option_name, self.value)?; } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 35c6dcc1f..dc944c9e7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2498,24 +2498,30 @@ pub enum Statement { values: Vec>, }, /// ```sql - /// COPY INTO + /// COPY INTO
| /// ``` - /// See + /// See: + /// + /// + /// /// Copy Into syntax available for Snowflake is different than the one implemented in /// Postgres. Although they share common prefix, it is reasonable to implement them /// in different enums. This can be refactored later once custom dialects /// are allowed to have custom Statements. CopyIntoSnowflake { + kind: CopyIntoSnowflakeKind, into: ObjectName, - from_stage: ObjectName, - from_stage_alias: Option, + from_obj: Option, + from_obj_alias: Option, stage_params: StageParamsObject, from_transformations: Option>, + from_query: Option>, files: Option>, pattern: Option, file_format: DataLoadingOptions, copy_options: DataLoadingOptions, validation_mode: Option, + partition: Option>, }, /// ```sql /// CLOSE @@ -5048,60 +5054,69 @@ impl fmt::Display for Statement { Ok(()) } Statement::CopyIntoSnowflake { + kind, into, - from_stage, - from_stage_alias, + from_obj, + from_obj_alias, stage_params, from_transformations, + from_query, files, pattern, file_format, copy_options, validation_mode, + partition, } => { write!(f, "COPY INTO {}", into)?; - if from_transformations.is_none() { - // Standard data load - write!(f, " FROM {}{}", from_stage, stage_params)?; - if from_stage_alias.as_ref().is_some() { - write!(f, " AS {}", from_stage_alias.as_ref().unwrap())?; - } - } else { + if let Some(from_transformations) = from_transformations { // Data load with transformation - write!( - f, - " FROM (SELECT {} FROM {}{}", - display_separated(from_transformations.as_ref().unwrap(), ", "), - from_stage, - stage_params, - )?; - if from_stage_alias.as_ref().is_some() { - write!(f, " AS {}", from_stage_alias.as_ref().unwrap())?; + if let Some(from_stage) = from_obj { + write!( + f, + " FROM (SELECT {} FROM {}{}", + display_separated(from_transformations, ", "), + from_stage, + stage_params + )?; + } + if let Some(from_obj_alias) = 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)?; + if let Some(from_obj_alias) = from_obj_alias { + write!(f, " AS {from_obj_alias}")?; + } + } else if let Some(from_query) = from_query { + // Data unload from query + write!(f, " FROM ({from_query})")?; } - if files.is_some() { - write!( - f, - " FILES = ('{}')", - display_separated(files.as_ref().unwrap(), "', '") - )?; + + if let Some(files) = files { + write!(f, " FILES = ('{}')", display_separated(files, "', '"))?; + } + if let Some(pattern) = pattern { + write!(f, " PATTERN = '{}'", pattern)?; } - if pattern.is_some() { - write!(f, " PATTERN = '{}'", pattern.as_ref().unwrap())?; + if let Some(partition) = partition { + write!(f, " PARTITION BY {partition}")?; } if !file_format.options.is_empty() { write!(f, " FILE_FORMAT=({})", file_format)?; } if !copy_options.options.is_empty() { - write!(f, " COPY_OPTIONS=({})", copy_options)?; + match kind { + CopyIntoSnowflakeKind::Table => { + write!(f, " COPY_OPTIONS=({})", copy_options)? + } + CopyIntoSnowflakeKind::Location => write!(f, " {copy_options}")?, + } } - if validation_mode.is_some() { - write!( - f, - " VALIDATION_MODE = {}", - validation_mode.as_ref().unwrap() - )?; + if let Some(validation_mode) = validation_mode { + write!(f, " VALIDATION_MODE = {}", validation_mode)?; } Ok(()) } @@ -8543,6 +8558,19 @@ impl Display for StorageSerializationPolicy { } } +/// Variants of the Snowflake `COPY INTO` statement +#[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 CopyIntoSnowflakeKind { + /// Loads data from files to a table + /// See: + Table, + /// Unloads data from a table or query to external files + /// See: + Location, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index b26900857..f0c38942e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -333,8 +333,8 @@ impl Spanned for Statement { } => source.span(), Statement::CopyIntoSnowflake { into: _, - from_stage: _, - from_stage_alias: _, + from_obj: _, + from_obj_alias: _, stage_params: _, from_transformations: _, files: _, @@ -342,6 +342,9 @@ impl Spanned for Statement { file_format: _, copy_options: _, validation_mode: _, + kind: _, + from_query: _, + partition: _, } => Span::empty(), Statement::Close { cursor } => match cursor { CloseCursor::All => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index d775ffc36..fc1926719 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -23,23 +23,26 @@ use crate::ast::helpers::stmt_data_loading::{ StageLoadSelectItem, StageParamsObject, }; use crate::ast::{ - ColumnOption, ColumnPolicy, ColumnPolicyProperty, Ident, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, - RowAccessPolicy, Statement, TagsColumnOption, WrappedCollection, + ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, ObjectName, RowAccessPolicy, Statement, TagsColumnOption, + WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; -use crate::tokenizer::Token; +use crate::tokenizer::{Token, Word}; +#[cfg(not(feature = "std"))] +use alloc::boxed::Box; #[cfg(not(feature = "std"))] use alloc::string::String; #[cfg(not(feature = "std"))] use alloc::vec::Vec; #[cfg(not(feature = "std"))] use alloc::{format, vec}; -use sqlparser::ast::StorageSerializationPolicy; use super::keywords::RESERVED_FOR_IDENTIFIER; +use sqlparser::ast::StorageSerializationPolicy; /// A [`Dialect`] for [Snowflake](https://www.snowflake.com/) #[derive(Debug, Default)] @@ -665,24 +668,49 @@ pub fn parse_snowflake_stage_name(parser: &mut Parser) -> Result` +/// and `COPY INTO ` which have different syntax. pub fn parse_copy_into(parser: &mut Parser) -> Result { - let into: ObjectName = parse_snowflake_stage_name(parser)?; + let kind = match parser.peek_token().token { + // Indicates an internal stage + Token::AtSign => CopyIntoSnowflakeKind::Location, + // Indicates an external stage, i.e. s3://, gcs:// or azure:// + Token::SingleQuotedString(s) if s.contains("://") => CopyIntoSnowflakeKind::Location, + _ => CopyIntoSnowflakeKind::Table, + }; + let mut files: Vec = vec![]; let mut from_transformations: Option> = None; - let from_stage_alias; - let from_stage: ObjectName; - let stage_params: StageParamsObject; + let mut from_stage_alias = None; + let mut from_stage = None; + let mut stage_params = StageParamsObject { + url: None, + encryption: DataLoadingOptions { options: vec![] }, + endpoint: None, + storage_integration: None, + credentials: DataLoadingOptions { options: vec![] }, + }; + let mut from_query = None; + let mut partition = None; + let mut file_format = Vec::new(); + let mut pattern = None; + let mut validation_mode = None; + let mut copy_options = Vec::new(); + + let into: ObjectName = parse_snowflake_stage_name(parser)?; + if kind == CopyIntoSnowflakeKind::Location { + stage_params = parse_stage_params(parser)?; + } parser.expect_keyword_is(Keyword::FROM)?; - // check if data load transformations are present match parser.next_token().token { - Token::LParen => { - // data load with transformations + Token::LParen if kind == CopyIntoSnowflakeKind::Table => { + // Data load with transformations parser.expect_keyword_is(Keyword::SELECT)?; from_transformations = parse_select_items_for_data_load(parser)?; parser.expect_keyword_is(Keyword::FROM)?; - from_stage = parse_snowflake_stage_name(parser)?; + from_stage = Some(parse_snowflake_stage_name(parser)?); stage_params = parse_stage_params(parser)?; // as @@ -696,9 +724,14 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { }; parser.expect_token(&Token::RParen)?; } + Token::LParen if kind == CopyIntoSnowflakeKind::Location => { + // Data unload with a query + from_query = Some(parser.parse_query()?); + parser.expect_token(&Token::RParen)?; + } _ => { parser.prev_token(); - from_stage = parse_snowflake_stage_name(parser)?; + from_stage = Some(parse_snowflake_stage_name(parser)?); stage_params = parse_stage_params(parser)?; // as @@ -711,67 +744,71 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { None }; } - }; + } - // [ files ] - if parser.parse_keyword(Keyword::FILES) { - parser.expect_token(&Token::Eq)?; - parser.expect_token(&Token::LParen)?; - let mut continue_loop = true; - while continue_loop { - continue_loop = false; + loop { + // FILE_FORMAT + if parser.parse_keyword(Keyword::FILE_FORMAT) { + parser.expect_token(&Token::Eq)?; + file_format = parse_parentheses_options(parser)?; + // PARTITION BY + } else if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { + partition = Some(Box::new(parser.parse_expr()?)) + // FILES + } else if parser.parse_keyword(Keyword::FILES) { + parser.expect_token(&Token::Eq)?; + parser.expect_token(&Token::LParen)?; + let mut continue_loop = true; + while continue_loop { + continue_loop = false; + let next_token = parser.next_token(); + match next_token.token { + Token::SingleQuotedString(s) => files.push(s), + _ => parser.expected("file token", next_token)?, + }; + if parser.next_token().token.eq(&Token::Comma) { + continue_loop = true; + } else { + parser.prev_token(); // not a comma, need to go back + } + } + parser.expect_token(&Token::RParen)?; + // PATTERN + } else if parser.parse_keyword(Keyword::PATTERN) { + parser.expect_token(&Token::Eq)?; let next_token = parser.next_token(); - match next_token.token { - Token::SingleQuotedString(s) => files.push(s), - _ => parser.expected("file token", next_token)?, - }; - if parser.next_token().token.eq(&Token::Comma) { - continue_loop = true; - } else { - parser.prev_token(); // not a comma, need to go back + pattern = Some(match next_token.token { + Token::SingleQuotedString(s) => s, + _ => parser.expected("pattern", next_token)?, + }); + // VALIDATION MODE + } else if parser.parse_keyword(Keyword::VALIDATION_MODE) { + parser.expect_token(&Token::Eq)?; + validation_mode = Some(parser.next_token().token.to_string()); + // COPY OPTIONS + } else if parser.parse_keyword(Keyword::COPY_OPTIONS) { + parser.expect_token(&Token::Eq)?; + copy_options = parse_parentheses_options(parser)?; + } else { + match parser.next_token().token { + Token::SemiColon | Token::EOF => break, + Token::Comma => continue, + // In `COPY INTO ` the copy options do not have a shared key + // like in `COPY INTO
` + Token::Word(key) => copy_options.push(parse_copy_option(parser, key)?), + _ => return parser.expected("another copy option, ; or EOF'", parser.peek_token()), } } - parser.expect_token(&Token::RParen)?; - } - - // [ pattern ] - let mut pattern = None; - if parser.parse_keyword(Keyword::PATTERN) { - parser.expect_token(&Token::Eq)?; - let next_token = parser.next_token(); - pattern = Some(match next_token.token { - Token::SingleQuotedString(s) => s, - _ => parser.expected("pattern", next_token)?, - }); - } - - // [ file_format] - let mut file_format = Vec::new(); - if parser.parse_keyword(Keyword::FILE_FORMAT) { - parser.expect_token(&Token::Eq)?; - file_format = parse_parentheses_options(parser)?; - } - - // [ copy_options ] - let mut copy_options = Vec::new(); - if parser.parse_keyword(Keyword::COPY_OPTIONS) { - parser.expect_token(&Token::Eq)?; - copy_options = parse_parentheses_options(parser)?; - } - - // [ VALIDATION_MODE ] - let mut validation_mode = None; - if parser.parse_keyword(Keyword::VALIDATION_MODE) { - parser.expect_token(&Token::Eq)?; - validation_mode = Some(parser.next_token().token.to_string()); } Ok(Statement::CopyIntoSnowflake { + kind, into, - from_stage, - from_stage_alias, + from_obj: from_stage, + from_obj_alias: from_stage_alias, stage_params, from_transformations, + from_query, files: if files.is_empty() { None } else { Some(files) }, pattern, file_format: DataLoadingOptions { @@ -781,6 +818,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { options: copy_options, }, validation_mode, + partition, }) } @@ -930,55 +968,55 @@ fn parse_stage_params(parser: &mut Parser) -> Result Result, ParserError> { let mut options: Vec = Vec::new(); - parser.expect_token(&Token::LParen)?; loop { match parser.next_token().token { Token::RParen => break, - Token::Word(key) => { - parser.expect_token(&Token::Eq)?; - if parser.parse_keyword(Keyword::TRUE) { - options.push(DataLoadingOption { - option_name: key.value, - option_type: DataLoadingOptionType::BOOLEAN, - value: "TRUE".to_string(), - }); - Ok(()) - } else if parser.parse_keyword(Keyword::FALSE) { - options.push(DataLoadingOption { - option_name: key.value, - option_type: DataLoadingOptionType::BOOLEAN, - value: "FALSE".to_string(), - }); - Ok(()) - } else { - match parser.next_token().token { - Token::SingleQuotedString(value) => { - options.push(DataLoadingOption { - option_name: key.value, - option_type: DataLoadingOptionType::STRING, - value, - }); - Ok(()) - } - Token::Word(word) => { - options.push(DataLoadingOption { - option_name: key.value, - option_type: DataLoadingOptionType::ENUM, - value: word.value, - }); - Ok(()) - } - _ => parser.expected("expected option value", parser.peek_token()), - } - } - } - _ => parser.expected("another option or ')'", parser.peek_token()), - }?; + Token::Comma => continue, + Token::Word(key) => options.push(parse_copy_option(parser, key)?), + _ => return parser.expected("another option or ')'", parser.peek_token()), + }; } Ok(options) } +/// Parses a `KEY = VALUE` construct based on the specified key +fn parse_copy_option(parser: &mut Parser, key: Word) -> Result { + parser.expect_token(&Token::Eq)?; + if parser.parse_keyword(Keyword::TRUE) { + Ok(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::BOOLEAN, + value: "TRUE".to_string(), + }) + } else if parser.parse_keyword(Keyword::FALSE) { + Ok(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::BOOLEAN, + value: "FALSE".to_string(), + }) + } else { + match parser.next_token().token { + Token::SingleQuotedString(value) => Ok(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::STRING, + value, + }), + Token::Word(word) => Ok(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::ENUM, + value: word.value, + }), + Token::Number(n, _) => Ok(DataLoadingOption { + option_name: key.value, + option_type: DataLoadingOptionType::NUMBER, + value: n, + }), + _ => parser.expected("expected option value", parser.peek_token()), + } + } +} + /// Parsing a property of identity or autoincrement column option /// Syntax: /// ```sql diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c68ada3c9..a18f1a4d8 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2028,20 +2028,25 @@ fn test_copy_into() { ); match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { + kind, into, - from_stage, + from_obj, files, pattern, validation_mode, .. } => { + assert_eq!(kind, CopyIntoSnowflakeKind::Table); assert_eq!( into, ObjectName::from(vec![Ident::new("my_company"), Ident::new("emp_basic")]) ); assert_eq!( - from_stage, - ObjectName::from(vec![Ident::with_quote('\'', "gcs://mybucket/./../a.csv")]) + from_obj, + Some(ObjectName::from(vec![Ident::with_quote( + '\'', + "gcs://mybucket/./../a.csv" + )])) ); assert!(files.is_none()); assert!(pattern.is_none()); @@ -2050,6 +2055,60 @@ fn test_copy_into() { _ => unreachable!(), }; assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + let sql = concat!("COPY INTO 's3://a/b/c/data.parquet' ", "FROM db.sc.tbl ", "PARTITION BY ('date=' || to_varchar(dt, 'YYYY-MM-DD') || '/hour=' || to_varchar(date_part(hour, ts)))"); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + kind, + into, + from_obj, + from_query, + partition, + .. + } => { + assert_eq!(kind, CopyIntoSnowflakeKind::Location); + assert_eq!( + into, + ObjectName::from(vec![Ident::with_quote('\'', "s3://a/b/c/data.parquet")]) + ); + assert_eq!( + from_obj, + Some(ObjectName::from(vec![ + Ident::new("db"), + Ident::new("sc"), + Ident::new("tbl") + ])) + ); + assert!(from_query.is_none()); + assert!(partition.is_some()); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + let sql = concat!( + "COPY INTO 's3://a/b/c/data.parquet' ", + "FROM (SELECT * FROM tbl)" + ); + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + kind, + into, + from_obj, + from_query, + .. + } => { + assert_eq!(kind, CopyIntoSnowflakeKind::Location); + assert_eq!( + into, + ObjectName::from(vec![Ident::with_quote('\'', "s3://a/b/c/data.parquet")]) + ); + assert!(from_query.is_some()); + assert!(from_obj.is_none()); + } + _ => unreachable!(), + }; + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); } #[test] @@ -2065,14 +2124,17 @@ fn test_copy_into_with_stage_params() { match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { - from_stage, + from_obj, stage_params, .. } => { //assert_eq!("s3://load/files/", stage_params.url.unwrap()); assert_eq!( - from_stage, - ObjectName::from(vec![Ident::with_quote('\'', "s3://load/files/")]) + from_obj, + Some(ObjectName::from(vec![Ident::with_quote( + '\'', + "s3://load/files/" + )])) ); assert_eq!("myint", stage_params.storage_integration.unwrap()); assert_eq!( @@ -2125,13 +2187,16 @@ fn test_copy_into_with_stage_params() { match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { - from_stage, + from_obj, stage_params, .. } => { assert_eq!( - from_stage, - ObjectName::from(vec![Ident::with_quote('\'', "s3://load/files/")]) + from_obj, + Some(ObjectName::from(vec![Ident::with_quote( + '\'', + "s3://load/files/" + )])) ); assert_eq!("myint", stage_params.storage_integration.unwrap()); } @@ -2154,13 +2219,13 @@ fn test_copy_into_with_files_and_pattern_and_verification() { files, pattern, validation_mode, - from_stage_alias, + from_obj_alias, .. } => { assert_eq!(files.unwrap(), vec!["file1.json", "file2.json"]); assert_eq!(pattern.unwrap(), ".*employees0[1-5].csv.gz"); assert_eq!(validation_mode.unwrap(), "RETURN_7_ROWS"); - assert_eq!(from_stage_alias.unwrap(), Ident::new("some_alias")); + assert_eq!(from_obj_alias.unwrap(), Ident::new("some_alias")); } _ => unreachable!(), } @@ -2179,13 +2244,16 @@ fn test_copy_into_with_transformations() { match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { - from_stage, + from_obj, from_transformations, .. } => { assert_eq!( - from_stage, - ObjectName::from(vec![Ident::new("@schema"), Ident::new("general_finished")]) + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@schema"), + Ident::new("general_finished") + ])) ); assert_eq!( from_transformations.as_ref().unwrap()[0], @@ -2254,6 +2322,41 @@ fn test_copy_into_file_format() { snowflake_without_unescape().verified_stmt(sql).to_string(), sql ); + + // Test commas in file format + let sql = concat!( + "COPY INTO my_company.emp_basic ", + "FROM 'gcs://mybucket/./../a.csv' ", + "FILES = ('file1.json', 'file2.json') ", + "PATTERN = '.*employees0[1-5].csv.gz' ", + r#"FILE_FORMAT=(COMPRESSION=AUTO, BINARY_FORMAT=HEX, ESCAPE='\\')"# + ); + + match snowflake_without_unescape() + .parse_sql_statements(sql) + .unwrap() + .first() + .unwrap() + { + Statement::CopyIntoSnowflake { file_format, .. } => { + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "COMPRESSION".to_string(), + option_type: DataLoadingOptionType::ENUM, + value: "AUTO".to_string() + })); + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "BINARY_FORMAT".to_string(), + option_type: DataLoadingOptionType::ENUM, + value: "HEX".to_string() + })); + assert!(file_format.options.contains(&DataLoadingOption { + option_name: "ESCAPE".to_string(), + option_type: DataLoadingOptionType::STRING, + value: r#"\\"#.to_string() + })); + } + _ => unreachable!(), + } } #[test] @@ -2285,16 +2388,8 @@ fn test_copy_into_copy_options() { } #[test] -fn test_snowflake_stage_object_names() { - let allowed_formatted_names = [ - "my_company.emp_basic", - "@namespace.%table_name", - "@namespace.%table_name/path", - "@namespace.stage_name/path", - "@~/path", - ]; +fn test_snowflake_stage_object_names_into_location() { let mut allowed_object_names = [ - ObjectName::from(vec![Ident::new("my_company"), Ident::new("emp_basic")]), ObjectName::from(vec![Ident::new("@namespace"), Ident::new("%table_name")]), ObjectName::from(vec![ Ident::new("@namespace"), @@ -2307,7 +2402,39 @@ fn test_snowflake_stage_object_names() { ObjectName::from(vec![Ident::new("@~/path")]), ]; - for it in allowed_formatted_names + let allowed_names_into_location = [ + "@namespace.%table_name", + "@namespace.%table_name/path", + "@namespace.stage_name/path", + "@~/path", + ]; + for it in allowed_names_into_location + .iter() + .zip(allowed_object_names.iter_mut()) + { + let (formatted_name, object_name) = it; + let sql = format!( + "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'", + formatted_name + ); + match snowflake().verified_stmt(&sql) { + Statement::CopyIntoSnowflake { into, .. } => { + assert_eq!(into.0, object_name.0) + } + _ => unreachable!(), + } + } +} + +#[test] +fn test_snowflake_stage_object_names_into_table() { + let mut allowed_object_names = [ + ObjectName::from(vec![Ident::new("my_company"), Ident::new("emp_basic")]), + ObjectName::from(vec![Ident::new("emp_basic")]), + ]; + + let allowed_names_into_table = ["my_company.emp_basic", "emp_basic"]; + for it in allowed_names_into_table .iter() .zip(allowed_object_names.iter_mut()) { @@ -2330,16 +2457,17 @@ fn test_snowflake_copy_into() { let sql = "COPY INTO a.b FROM @namespace.stage_name"; assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); match snowflake().verified_stmt(sql) { - Statement::CopyIntoSnowflake { - into, from_stage, .. - } => { + Statement::CopyIntoSnowflake { into, from_obj, .. } => { assert_eq!( into, ObjectName::from(vec![Ident::new("a"), Ident::new("b")]) ); assert_eq!( - from_stage, - ObjectName::from(vec![Ident::new("@namespace"), Ident::new("stage_name")]) + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@namespace"), + Ident::new("stage_name") + ])) ) } _ => unreachable!(), @@ -2351,9 +2479,7 @@ fn test_snowflake_copy_into_stage_name_ends_with_parens() { let sql = "COPY INTO SCHEMA.SOME_MONITORING_SYSTEM FROM (SELECT t.$1:st AS st FROM @schema.general_finished)"; assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); match snowflake().verified_stmt(sql) { - Statement::CopyIntoSnowflake { - into, from_stage, .. - } => { + Statement::CopyIntoSnowflake { into, from_obj, .. } => { assert_eq!( into, ObjectName::from(vec![ @@ -2362,8 +2488,11 @@ fn test_snowflake_copy_into_stage_name_ends_with_parens() { ]) ); assert_eq!( - from_stage, - ObjectName::from(vec![Ident::new("@schema"), Ident::new("general_finished")]) + from_obj, + Some(ObjectName::from(vec![ + Ident::new("@schema"), + Ident::new("general_finished") + ])) ) } _ => unreachable!(), From 443f492b4b9443d45266c77b073b50b91bcb95ed Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Wed, 5 Feb 2025 20:47:17 +0100 Subject: [PATCH 719/806] Require space after -- to start single line comment in MySQL (#1705) --- src/dialect/mod.rs | 9 ++++ src/dialect/mysql.rs | 4 ++ src/tokenizer.rs | 105 ++++++++++++++++++++++++++++++++++++--- tests/sqlparser_mysql.rs | 15 ++++++ 4 files changed, 127 insertions(+), 6 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 205395f6c..965e6c77f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -881,6 +881,15 @@ pub trait Dialect: Debug + Any { fn supports_table_hints(&self) -> bool { false } + + /// Returns true if this dialect requires a whitespace character after `--` to start a single line comment. + /// + /// MySQL: + /// e.g. UPDATE account SET balance=balance--1 + // WHERE account_id=5752 ^^^ will be interpreted as two minus signs instead of a comment + fn requires_single_line_comment_whitespace(&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 a67fe67b0..55b91ad22 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -125,6 +125,10 @@ impl Dialect for MySqlDialect { fn supports_table_hints(&self) -> bool { true } + + fn requires_single_line_comment_whitespace(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 7742e8fae..d4e530c9d 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1229,14 +1229,26 @@ impl<'a> Tokenizer<'a> { // operators '-' => { chars.next(); // consume the '-' + match chars.peek() { Some('-') => { - chars.next(); // consume the second '-', starting a single-line comment - let comment = self.tokenize_single_line_comment(chars); - Ok(Some(Token::Whitespace(Whitespace::SingleLineComment { - prefix: "--".to_owned(), - comment, - }))) + let mut is_comment = true; + if self.dialect.requires_single_line_comment_whitespace() { + is_comment = Some(' ') == chars.peekable.clone().nth(1); + } + + if is_comment { + chars.next(); // consume second '-' + let comment = self.tokenize_single_line_comment(chars); + return Ok(Some(Token::Whitespace( + Whitespace::SingleLineComment { + prefix: "--".to_owned(), + comment, + }, + ))); + } + + self.start_binop(chars, "-", Token::Minus) } Some('>') => { chars.next(); @@ -3685,4 +3697,85 @@ mod tests { ], ); } + + #[test] + fn test_whitespace_required_after_single_line_comment() { + all_dialects_where(|dialect| dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT --'abc'", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Minus, + Token::Minus, + Token::SingleQuotedString("abc".to_string()), + ], + ); + + all_dialects_where(|dialect| dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT -- 'abc'", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: " 'abc'".to_string(), + }), + ], + ); + + all_dialects_where(|dialect| dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT --", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Minus, + Token::Minus, + ], + ); + } + + #[test] + fn test_whitespace_not_required_after_single_line_comment() { + all_dialects_where(|dialect| !dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT --'abc'", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: "'abc'".to_string(), + }), + ], + ); + + all_dialects_where(|dialect| !dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT -- 'abc'", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: " 'abc'".to_string(), + }), + ], + ); + + all_dialects_where(|dialect| !dialect.requires_single_line_comment_whitespace()) + .tokenizes_to( + "SELECT --", + vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Whitespace(Whitespace::SingleLineComment { + prefix: "--".to_string(), + comment: "".to_string(), + }), + ], + ); + } } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 9f00a9213..6bf9076d2 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3250,3 +3250,18 @@ fn parse_double_precision() { "CREATE TABLE foo (bar DOUBLE(11,0))", ); } + +#[test] +fn parse_looks_like_single_line_comment() { + mysql().one_statement_parses_to( + "UPDATE account SET balance=balance--1 WHERE account_id=5752", + "UPDATE account SET balance = balance - -1 WHERE account_id = 5752", + ); + mysql().one_statement_parses_to( + r#" + UPDATE account SET balance=balance-- 1 + WHERE account_id=5752 + "#, + "UPDATE account SET balance = balance WHERE account_id = 5752", + ); +} From 0b8ba91156a27ffd56a20339fa89a927611aa806 Mon Sep 17 00:00:00 2001 From: DanCodedThis <94703934+DanCodedThis@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:11:02 +0200 Subject: [PATCH 720/806] Add suppport for Show Objects statement for the Snowflake parser (#1702) Co-authored-by: Denys Tsomenko Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 25 ++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 25 +++++++++++++++++++- src/keywords.rs | 1 + src/parser/mod.rs | 2 +- tests/sqlparser_snowflake.rs | 45 ++++++++++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dc944c9e7..de2bbafc2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3010,6 +3010,12 @@ pub enum Statement { show_options: ShowStatementOptions, }, /// ```sql + /// SHOW OBJECTS LIKE 'line%' IN mydb.public + /// ``` + /// Snowflake-specific statement + /// + ShowObjects(ShowObjects), + /// ```sql /// SHOW TABLES /// ``` ShowTables { @@ -4703,6 +4709,17 @@ impl fmt::Display for Statement { )?; Ok(()) } + Statement::ShowObjects(ShowObjects { + terse, + show_options, + }) => { + write!( + f, + "SHOW {terse}OBJECTS{show_options}", + terse = if *terse { "TERSE " } else { "" }, + )?; + Ok(()) + } Statement::ShowTables { terse, history, @@ -8343,6 +8360,14 @@ impl fmt::Display for ShowStatementIn { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ShowObjects { + pub terse: bool, + pub show_options: ShowStatementOptions, +} + /// MSSQL's json null clause /// /// ```plaintext diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f0c38942e..1af5387c9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -496,6 +496,7 @@ impl Spanned for Statement { Statement::DropConnector { .. } => Span::empty(), Statement::ShowDatabases { .. } => Span::empty(), Statement::ShowSchemas { .. } => Span::empty(), + Statement::ShowObjects { .. } => Span::empty(), Statement::ShowViews { .. } => Span::empty(), Statement::LISTEN { .. } => Span::empty(), Statement::NOTIFY { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index fc1926719..68166cbef 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -25,7 +25,7 @@ use crate::ast::helpers::stmt_data_loading::{ use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, ObjectName, RowAccessPolicy, Statement, TagsColumnOption, + IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects, Statement, TagsColumnOption, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; @@ -185,6 +185,19 @@ impl Dialect for SnowflakeDialect { return Some(parse_file_staging_command(kw, parser)); } + if parser.parse_keyword(Keyword::SHOW) { + let terse = parser.parse_keyword(Keyword::TERSE); + if parser.parse_keyword(Keyword::OBJECTS) { + return Some(parse_show_objects(terse, parser)); + } + //Give back Keyword::TERSE + if terse { + parser.prev_token(); + } + //Give back Keyword::SHOW + parser.prev_token(); + } + None } @@ -1092,3 +1105,13 @@ fn parse_column_tags(parser: &mut Parser, with: bool) -> Result +fn parse_show_objects(terse: bool, parser: &mut Parser) -> Result { + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowObjects(ShowObjects { + terse, + show_options, + })) +} diff --git a/src/keywords.rs b/src/keywords.rs index 5f36fa737..0178bf555 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -588,6 +588,7 @@ define_keywords!( NUMERIC, NVARCHAR, OBJECT, + OBJECTS, OCCURRENCES_REGEX, OCTETS, OCTET_LENGTH, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6d84ff843..ca7924155 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14231,7 +14231,7 @@ impl<'a> Parser<'a> { false } - fn parse_show_stmt_options(&mut self) -> Result { + pub(crate) fn parse_show_stmt_options(&mut self) -> Result { let show_in; let mut filter_position = None; if self.dialect.supports_show_like_before_in() { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a18f1a4d8..ffcd4e69d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3083,6 +3083,7 @@ fn test_parentheses_overflow() { #[test] fn test_show_databases() { snowflake().verified_stmt("SHOW DATABASES"); + snowflake().verified_stmt("SHOW TERSE DATABASES"); snowflake().verified_stmt("SHOW DATABASES HISTORY"); snowflake().verified_stmt("SHOW DATABASES LIKE '%abc%'"); snowflake().verified_stmt("SHOW DATABASES STARTS WITH 'demo_db'"); @@ -3095,6 +3096,7 @@ fn test_show_databases() { #[test] fn test_parse_show_schemas() { snowflake().verified_stmt("SHOW SCHEMAS"); + snowflake().verified_stmt("SHOW TERSE SCHEMAS"); snowflake().verified_stmt("SHOW SCHEMAS IN ACCOUNT"); snowflake().verified_stmt("SHOW SCHEMAS IN ACCOUNT abc"); snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE"); @@ -3104,9 +3106,51 @@ fn test_parse_show_schemas() { snowflake().verified_stmt("SHOW SCHEMAS IN DATABASE STARTS WITH 'abc' LIMIT 20 FROM 'xyz'"); } +#[test] +fn test_parse_show_objects() { + snowflake().verified_stmt("SHOW OBJECTS"); + snowflake().verified_stmt("SHOW OBJECTS IN abc"); + snowflake().verified_stmt("SHOW OBJECTS LIKE '%test%' IN abc"); + snowflake().verified_stmt("SHOW OBJECTS IN ACCOUNT"); + snowflake().verified_stmt("SHOW OBJECTS IN DATABASE"); + snowflake().verified_stmt("SHOW OBJECTS IN DATABASE abc"); + snowflake().verified_stmt("SHOW OBJECTS IN SCHEMA"); + snowflake().verified_stmt("SHOW OBJECTS IN SCHEMA abc"); + snowflake().verified_stmt("SHOW TERSE OBJECTS"); + snowflake().verified_stmt("SHOW TERSE OBJECTS IN abc"); + snowflake().verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc"); + snowflake().verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc STARTS WITH 'b'"); + snowflake().verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc STARTS WITH 'b' LIMIT 10"); + snowflake() + .verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc STARTS WITH 'b' LIMIT 10 FROM 'x'"); + match snowflake().verified_stmt("SHOW TERSE OBJECTS LIKE '%test%' IN abc") { + Statement::ShowObjects(ShowObjects { + terse, + show_options, + }) => { + assert!(terse); + let name = match show_options.show_in { + Some(ShowStatementIn { + parent_name: Some(val), + .. + }) => val.to_string(), + _ => unreachable!(), + }; + assert_eq!("abc", name); + let like = match show_options.filter_position { + Some(ShowStatementFilterPosition::Infix(ShowStatementFilter::Like(val))) => val, + _ => unreachable!(), + }; + assert_eq!("%test%", like); + } + _ => unreachable!(), + } +} + #[test] fn test_parse_show_tables() { snowflake().verified_stmt("SHOW TABLES"); + snowflake().verified_stmt("SHOW TERSE TABLES"); snowflake().verified_stmt("SHOW TABLES IN ACCOUNT"); snowflake().verified_stmt("SHOW TABLES IN DATABASE"); snowflake().verified_stmt("SHOW TABLES IN DATABASE xyz"); @@ -3129,6 +3173,7 @@ fn test_parse_show_tables() { #[test] fn test_show_views() { snowflake().verified_stmt("SHOW VIEWS"); + snowflake().verified_stmt("SHOW TERSE VIEWS"); snowflake().verified_stmt("SHOW VIEWS IN ACCOUNT"); snowflake().verified_stmt("SHOW VIEWS IN DATABASE"); snowflake().verified_stmt("SHOW VIEWS IN DATABASE xyz"); From 86abbd6028d72cee5fa299bc171f492fb722157c Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:14:03 +0100 Subject: [PATCH 721/806] Fix incorrect parsing of JsonAccess bracket notation after cast in Snowflake (#1708) --- src/dialect/duckdb.rs | 5 +++++ src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 6 ++++++ src/dialect/postgresql.rs | 5 +++++ src/parser/mod.rs | 17 +++++++---------- tests/sqlparser_snowflake.rs | 26 ++++++++++++++++++++++++++ 6 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index c41aec81d..e8dcf853c 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -80,4 +80,9 @@ impl Dialect for DuckDbDialect { fn supports_load_extension(&self) -> bool { true } + + // See DuckDB + fn supports_array_typedef_size(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 4021b5753..7aaf00742 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -143,4 +143,8 @@ impl Dialect for GenericDialect { fn supports_string_escape_constant(&self) -> bool { true } + + fn supports_array_typedef_size(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 965e6c77f..6b04bacc1 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -890,6 +890,12 @@ pub trait Dialect: Debug + Any { fn requires_single_line_comment_whitespace(&self) -> bool { false } + + /// Returns true if the dialect supports size definition for array types. + /// For example: ```CREATE TABLE my_table (my_array INT[3])```. + fn supports_array_typedef_size(&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 5ce4250fb..74b963e82 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -253,6 +253,11 @@ impl Dialect for PostgreSqlDialect { fn supports_numeric_literal_underscores(&self) -> bool { true } + + /// See: + fn supports_array_typedef_size(&self) -> bool { + true + } } pub fn parse_create(parser: &mut Parser) -> Option> { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ca7924155..30124bbc2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8943,16 +8943,13 @@ impl<'a> Parser<'a> { _ => self.expected_at("a data type name", next_token_index), }?; - // Parse array data types. Note: this is postgresql-specific and different from - // Keyword::ARRAY syntax from above - while self.consume_token(&Token::LBracket) { - let size = if dialect_of!(self is GenericDialect | DuckDbDialect | PostgreSqlDialect) { - self.maybe_parse(|p| p.parse_literal_uint())? - } else { - None - }; - self.expect_token(&Token::RBracket)?; - data = DataType::Array(ArrayElemTypeDef::SquareBracket(Box::new(data), size)) + if self.dialect.supports_array_typedef_size() { + // Parse array data type size + while self.consume_token(&Token::LBracket) { + let size = self.maybe_parse(|p| p.parse_literal_uint())?; + self.expect_token(&Token::RBracket)?; + data = DataType::Array(ArrayElemTypeDef::SquareBracket(Box::new(data), size)) + } } Ok((data, trailing_bracket)) } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ffcd4e69d..b0c215a5a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1256,6 +1256,32 @@ fn parse_semi_structured_data_traversal() { .to_string(), "sql parser error: Expected: variant object key name, found: 42" ); + + // casting a json access and accessing an array element + assert_eq!( + snowflake().verified_expr("a:b::ARRAY[1]"), + Expr::JsonAccess { + value: Box::new(Expr::Cast { + kind: CastKind::DoubleColon, + data_type: DataType::Array(ArrayElemTypeDef::None), + format: None, + expr: Box::new(Expr::JsonAccess { + value: Box::new(Expr::Identifier(Ident::new("a"))), + path: JsonPath { + path: vec![JsonPathElem::Dot { + key: "b".to_string(), + quoted: false + }] + } + }) + }), + path: JsonPath { + path: vec![JsonPathElem::Bracket { + key: Expr::Value(number("1")) + }] + } + } + ); } #[test] From cad49232c1247ed942de985ac958212071946528 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 7 Feb 2025 22:24:29 -0800 Subject: [PATCH 722/806] Parse Postgres VARBIT datatype (#1703) --- src/ast/data_type.rs | 8 +++++++- src/keywords.rs | 1 + src/parser/mod.rs | 1 + tests/sqlparser_postgres.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 1f2b6be97..4f1f21e0b 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -328,10 +328,15 @@ pub enum DataType { /// [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), - /// Variable-length bit string e.g. [Postgres] + /// `BIT VARYING(n)`: Variable-length bit string e.g. [Postgres] /// /// [Postgres]: https://www.postgresql.org/docs/current/datatype-bit.html BitVarying(Option), + /// `VARBIT(n)`: Variable-length bit string. [Postgres] alias for `BIT VARYING` + /// + /// [Postgres]: https://www.postgresql.org/docs/current/datatype.html + VarBit(Option), + /// /// Custom type such as enums Custom(ObjectName, Vec), /// Arrays @@ -550,6 +555,7 @@ impl fmt::Display for DataType { DataType::BitVarying(size) => { format_type_with_optional_length(f, "BIT VARYING", size, false) } + DataType::VarBit(size) => format_type_with_optional_length(f, "VARBIT", size, false), DataType::Array(ty) => match ty { ArrayElemTypeDef::None => write!(f, "ARRAY"), ArrayElemTypeDef::SquareBracket(t, None) => write!(f, "{t}[]"), diff --git a/src/keywords.rs b/src/keywords.rs index 0178bf555..a67f6bc76 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -925,6 +925,7 @@ define_keywords!( VALUES, VALUE_OF, VARBINARY, + VARBIT, VARCHAR, VARIABLES, VARYING, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 30124bbc2..de2dc6602 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8779,6 +8779,7 @@ impl<'a> Parser<'a> { Ok(DataType::Bit(self.parse_optional_precision()?)) } } + Keyword::VARBIT => Ok(DataType::VarBit(self.parse_optional_precision()?)), Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), Keyword::DATE32 => Ok(DataType::Date32), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 62da0f574..d8d97b491 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5327,3 +5327,29 @@ fn parse_bitstring_literal() { ))] ); } + +#[test] +fn parse_varbit_datatype() { + match pg_and_generic().verified_stmt("CREATE TABLE foo (x VARBIT, y VARBIT(42))") { + Statement::CreateTable(CreateTable { columns, .. }) => { + assert_eq!( + columns, + vec![ + ColumnDef { + name: "x".into(), + data_type: DataType::VarBit(None), + collation: None, + options: vec![], + }, + ColumnDef { + name: "y".into(), + data_type: DataType::VarBit(Some(42)), + collation: None, + options: vec![], + } + ] + ); + } + _ => unreachable!(), + } +} From 46cfcfe8f7afc63356930fd7e3ff1c549ebbf41e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 9 Feb 2025 06:10:58 +0100 Subject: [PATCH 723/806] Implement FROM-first selects (#1713) --- src/ast/mod.rs | 4 +- src/ast/query.rs | 29 +++++++++++++- src/ast/spans.rs | 1 + src/dialect/clickhouse.rs | 4 ++ src/dialect/duckdb.rs | 4 ++ src/dialect/mod.rs | 11 ++++++ src/parser/mod.rs | 52 ++++++++++++++++++++++--- tests/sqlparser_clickhouse.rs | 1 + tests/sqlparser_common.rs | 71 +++++++++++++++++++++++++++++++++++ tests/sqlparser_duckdb.rs | 2 + tests/sqlparser_mssql.rs | 3 ++ tests/sqlparser_mysql.rs | 8 ++++ tests/sqlparser_postgres.rs | 3 ++ 13 files changed, 184 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index de2bbafc2..f693ab7d8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -68,8 +68,8 @@ pub use self::query::{ NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, - Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, + 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, diff --git a/src/ast/query.rs b/src/ast/query.rs index 239e14554..0446bcb75 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -275,6 +275,19 @@ impl fmt::Display for Table { } } +/// What did this select look like? +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SelectFlavor { + /// `SELECT *` + Standard, + /// `FROM ... SELECT *` + FromFirst, + /// `FROM *` + FromFirstNoSelect, +} + /// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may /// appear either as the only body item of a `Query`, or as an operand /// to a set operation like `UNION`. @@ -328,11 +341,23 @@ pub struct Select { pub value_table_mode: Option, /// STARTING WITH .. CONNECT BY pub connect_by: Option, + /// Was this a FROM-first query? + pub flavor: SelectFlavor, } impl fmt::Display for Select { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "SELECT")?; + match self.flavor { + SelectFlavor::Standard => { + write!(f, "SELECT")?; + } + SelectFlavor::FromFirst => { + write!(f, "FROM {} SELECT", display_comma_separated(&self.from))?; + } + SelectFlavor::FromFirstNoSelect => { + write!(f, "FROM {}", display_comma_separated(&self.from))?; + } + } if let Some(value_table_mode) = self.value_table_mode { write!(f, " {value_table_mode}")?; @@ -360,7 +385,7 @@ impl fmt::Display for Select { write!(f, " {into}")?; } - if !self.from.is_empty() { + if self.flavor == SelectFlavor::Standard && !self.from.is_empty() { write!(f, " FROM {}", display_comma_separated(&self.from))?; } if !self.lateral_views.is_empty() { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1af5387c9..18f6f6e2f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2077,6 +2077,7 @@ impl Spanned for Select { value_table_mode: _, // todo, BigQuery specific connect_by, top_before_distinct: _, + flavor: _, } = self; union_spans( diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 9a0884a51..7d479219d 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -75,4 +75,8 @@ impl Dialect for ClickHouseDialect { fn supports_lambda_functions(&self) -> bool { true } + + fn supports_from_first_select(&self) -> bool { + true + } } diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index e8dcf853c..a2e7fb6c0 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -85,4 +85,8 @@ impl Dialect for DuckDbDialect { fn supports_array_typedef_size(&self) -> bool { true } + + fn supports_from_first_select(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6b04bacc1..933203605 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -463,6 +463,17 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports "FROM-first" selects. + /// + /// Example: + /// ```sql + /// FROM table + /// SELECT * + /// ``` + fn supports_from_first_select(&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/parser/mod.rs b/src/parser/mod.rs index de2dc6602..188e941b5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -528,7 +528,7 @@ impl<'a> Parser<'a> { Keyword::DESCRIBE => self.parse_explain(DescribeAlias::Describe), Keyword::EXPLAIN => self.parse_explain(DescribeAlias::Explain), Keyword::ANALYZE => self.parse_analyze(), - Keyword::SELECT | Keyword::WITH | Keyword::VALUES => { + Keyword::SELECT | Keyword::WITH | Keyword::VALUES | Keyword::FROM => { self.prev_token(); self.parse_query().map(Statement::Query) } @@ -10218,7 +10218,9 @@ impl<'a> Parser<'a> { pub fn parse_query_body(&mut self, precedence: u8) -> Result, ParserError> { // We parse the expression using a Pratt parser, as in `parse_expr()`. // Start by parsing a restricted SELECT or a `(subquery)`: - let expr = if self.peek_keyword(Keyword::SELECT) { + let expr = if self.peek_keyword(Keyword::SELECT) + || (self.peek_keyword(Keyword::FROM) && self.dialect.supports_from_first_select()) + { SetExpr::Select(self.parse_select().map(Box::new)?) } else if self.consume_token(&Token::LParen) { // CTEs are not allowed here, but the parser currently accepts them @@ -10317,6 +10319,39 @@ impl<'a> Parser<'a> { /// Parse a restricted `SELECT` statement (no CTEs / `UNION` / `ORDER BY`) pub fn parse_select(&mut self) -> Result { + let mut from_first = None; + + if self.dialect.supports_from_first_select() && self.peek_keyword(Keyword::FROM) { + let from_token = self.expect_keyword(Keyword::FROM)?; + let from = self.parse_table_with_joins()?; + if !self.peek_keyword(Keyword::SELECT) { + return Ok(Select { + select_token: AttachedToken(from_token), + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![], + into: None, + from, + 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::FromFirstNoSelect, + }); + } + from_first = Some(from); + } + let select_token = self.expect_keyword(Keyword::SELECT)?; let value_table_mode = if dialect_of!(self is BigQueryDialect) && self.parse_keyword(Keyword::AS) { @@ -10371,10 +10406,12 @@ impl<'a> Parser<'a> { // otherwise they may be parsed as an alias as part of the `projection` // or `from`. - let from = if self.parse_keyword(Keyword::FROM) { - self.parse_table_with_joins()? + let (from, from_first) = if let Some(from) = from_first.take() { + (from, true) + } else if self.parse_keyword(Keyword::FROM) { + (self.parse_table_with_joins()?, false) } else { - vec![] + (vec![], false) }; let mut lateral_views = vec![]; @@ -10506,6 +10543,11 @@ impl<'a> Parser<'a> { qualify, value_table_mode, connect_by, + flavor: if from_first { + SelectFlavor::FromFirst + } else { + SelectFlavor::Standard + }, }) } diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 0f22db389..76a56e709 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -101,6 +101,7 @@ fn parse_map_access_expr() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }, select ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3a6183a15..f8c0a26b8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -461,6 +461,7 @@ fn parse_update_set_from() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -5289,6 +5290,7 @@ fn test_parse_named_window() { window_before_qualify: true, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }; assert_eq!(actual_select_only, expected); } @@ -5915,6 +5917,7 @@ fn parse_interval_and_or_xor() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -8022,6 +8025,7 @@ fn lateral_function() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }; assert_eq!(actual_select_only, expected); } @@ -8919,6 +8923,7 @@ fn parse_merge() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -10703,6 +10708,7 @@ fn parse_unload() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), with: None, limit: None, @@ -10913,6 +10919,7 @@ fn parse_connect_by() { ))))), }], }), + flavor: SelectFlavor::Standard, }; let connect_by_1 = concat!( @@ -10997,6 +11004,7 @@ fn parse_connect_by() { ))))), }], }), + flavor: SelectFlavor::Standard, } ); @@ -11860,6 +11868,7 @@ fn test_extract_seconds_ok() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -13592,3 +13601,65 @@ fn test_lambdas() { ); dialects.verified_expr("transform(array(1, 2, 3), x -> x + 1)"); } + +#[test] +fn test_select_from_first() { + let dialects = all_dialects_where(|d| d.supports_from_first_select()); + let q1 = "FROM capitals"; + let q2 = "FROM capitals SELECT *"; + + for (q, flavor, projection) in [ + (q1, SelectFlavor::FromFirstNoSelect, vec![]), + ( + q2, + SelectFlavor::FromFirst, + vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], + ), + ] { + let ast = dialects.verified_query(q); + let expected = Query { + with: None, + body: Box::new(SetExpr::Select(Box::new(Select { + select_token: AttachedToken::empty(), + distinct: None, + top: None, + projection, + top_before_distinct: false, + into: None, + from: vec![TableWithJoins { + relation: table_from_name(ObjectName::from(vec![Ident { + value: "capitals".to_string(), + quote_style: None, + span: Span::empty(), + }])), + joins: 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, + }))), + order_by: None, + limit: None, + offset: None, + fetch: None, + locks: vec![], + limit_by: vec![], + for_clause: None, + settings: None, + format_clause: None, + }; + assert_eq!(expected, ast); + assert_eq!(ast.to_string(), q); + } +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 4289ebd1f..43e12746c 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -288,6 +288,7 @@ fn test_select_union_by_name() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), right: Box::::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), @@ -317,6 +318,7 @@ fn test_select_union_by_name() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), }); assert_eq!(ast.body, expected); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9046e9e74..e6a12b9c7 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -137,6 +137,7 @@ fn parse_create_procedure() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))) }))], params: Some(vec![ @@ -1114,6 +1115,7 @@ fn parse_substring_in_select() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -1251,6 +1253,7 @@ fn parse_mssql_declare() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))) })) ], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 6bf9076d2..48fcbbf9b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1111,6 +1111,7 @@ fn parse_escaped_quote_identifiers_with_escape() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -1164,6 +1165,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -1211,6 +1213,7 @@ fn parse_escaped_backticks_with_escape() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -1262,6 +1265,7 @@ fn parse_escaped_backticks_with_no_escape() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -1931,6 +1935,7 @@ fn parse_select_with_numeric_prefix_column_name() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))) ); } @@ -1983,6 +1988,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))) ); } @@ -2503,6 +2509,7 @@ fn parse_substring_in_select() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -2799,6 +2806,7 @@ fn parse_hex_string_introducer() { value_table_mode: None, into: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d8d97b491..3a4504ebb 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1315,6 +1315,7 @@ fn parse_copy_to() { qualify: None, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), order_by: None, limit: None, @@ -2666,6 +2667,7 @@ fn parse_array_subquery_expr() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), right: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), @@ -2688,6 +2690,7 @@ fn parse_array_subquery_expr() { window_before_qualify: false, value_table_mode: None, connect_by: None, + flavor: SelectFlavor::Standard, }))), }), order_by: None, From 322209a9f34d447464e0eff46627785f02869fb3 Mon Sep 17 00:00:00 2001 From: Justin Joyce Date: Mon, 10 Feb 2025 05:51:22 +0000 Subject: [PATCH 724/806] Enable custom dialects to support `MATCH() AGAINST()` (#1719) --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 5 +++++ src/dialect/mysql.rs | 4 ++++ src/parser/mod.rs | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 7aaf00742..dbd5cab28 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -147,4 +147,8 @@ impl Dialect for GenericDialect { fn supports_array_typedef_size(&self) -> bool { true } + + fn supports_match_against(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 933203605..a57c25d5e 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -479,6 +479,11 @@ pub trait Dialect: Debug + Any { false } + /// Does the dialect support the `MATCH() AGAINST()` syntax? + fn supports_match_against(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 55b91ad22..8a0da87e4 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -129,6 +129,10 @@ impl Dialect for MySqlDialect { fn requires_single_line_comment_whitespace(&self) -> bool { true } + + fn supports_match_against(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 188e941b5..c4f65a1a0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1198,7 +1198,7 @@ impl<'a> Parser<'a> { }))) } Keyword::NOT => Ok(Some(self.parse_not()?)), - Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => { + Keyword::MATCH if self.dialect.supports_match_against() => { Ok(Some(self.parse_match_against()?)) } Keyword::STRUCT if self.dialect.supports_struct_literal() => { From 3e94877f03c5b47659093a2d55c3bc04e042013c Mon Sep 17 00:00:00 2001 From: Emil Date: Tue, 11 Feb 2025 16:22:50 +0100 Subject: [PATCH 725/806] Support group by cube/rollup etc in BigQuery (#1720) --- src/dialect/bigquery.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index bb1a0d5ce..5354a645b 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -115,6 +115,11 @@ impl Dialect for BigQueryDialect { true } + // See + fn supports_group_by_expr(&self) -> bool { + true + } + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { !RESERVED_FOR_COLUMN_ALIAS.contains(kw) } From a5bbb5e8ac52c98fe0f3e85e3b25c24048deade9 Mon Sep 17 00:00:00 2001 From: Tyler Brinks Date: Thu, 13 Feb 2025 03:40:35 -0700 Subject: [PATCH 726/806] Add support for MS Varbinary(MAX) (#1714) (#1715) --- src/ast/data_type.rs | 44 ++++++++++++++++++++++++++++--- src/ast/mod.rs | 4 +-- src/parser/mod.rs | 20 +++++++++++++- tests/sqlparser_common.rs | 2 +- tests/sqlparser_mssql.rs | 55 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 4f1f21e0b..bd46f8bdb 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -85,7 +85,7 @@ pub enum DataType { /// /// [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), + Varbinary(Option), /// Large binary object with optional length e.g. BLOB, BLOB(1000), [standard], [Oracle] /// /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#binary-large-object-string-type @@ -408,9 +408,7 @@ impl fmt::Display for DataType { } DataType::Clob(size) => format_type_with_optional_length(f, "CLOB", size, false), DataType::Binary(size) => format_type_with_optional_length(f, "BINARY", size, false), - DataType::Varbinary(size) => { - format_type_with_optional_length(f, "VARBINARY", size, false) - } + DataType::Varbinary(size) => format_varbinary_type(f, "VARBINARY", size), DataType::Blob(size) => format_type_with_optional_length(f, "BLOB", size, false), DataType::TinyBlob => write!(f, "TINYBLOB"), DataType::MediumBlob => write!(f, "MEDIUMBLOB"), @@ -673,6 +671,18 @@ fn format_character_string_type( Ok(()) } +fn format_varbinary_type( + f: &mut fmt::Formatter, + sql_type: &str, + size: &Option, +) -> fmt::Result { + write!(f, "{sql_type}")?; + if let Some(size) = size { + write!(f, "({size})")?; + } + Ok(()) +} + fn format_datetime_precision_and_tz( f: &mut fmt::Formatter, sql_type: &'static str, @@ -862,6 +872,32 @@ impl fmt::Display for CharLengthUnits { } } +#[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 BinaryLength { + IntegerLength { + /// Default (if VARYING) + length: u64, + }, + /// VARBINARY(MAX) used in T-SQL (Microsoft SQL Server) + Max, +} + +impl fmt::Display for BinaryLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BinaryLength::IntegerLength { length } => { + write!(f, "{}", length)?; + } + BinaryLength::Max => { + write!(f, "MAX")?; + } + } + Ok(()) + } +} + /// Represents the data type of the elements in an array (if any) as well as /// the syntax used to declare the array. /// diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f693ab7d8..5835447db 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,8 +40,8 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::tokenizer::Span; pub use self::data_type::{ - ArrayElemTypeDef, CharLengthUnits, CharacterLength, DataType, EnumMember, ExactNumberInfo, - StructBracketKind, TimezoneInfo, + ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember, + ExactNumberInfo, StructBracketKind, TimezoneInfo, }; pub use self::dcl::{ AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c4f65a1a0..6d3290d23 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8766,7 +8766,7 @@ impl<'a> Parser<'a> { } Keyword::CLOB => Ok(DataType::Clob(self.parse_optional_precision()?)), Keyword::BINARY => Ok(DataType::Binary(self.parse_optional_precision()?)), - Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_optional_precision()?)), + Keyword::VARBINARY => Ok(DataType::Varbinary(self.parse_optional_binary_length()?)), Keyword::BLOB => Ok(DataType::Blob(self.parse_optional_precision()?)), Keyword::TINYBLOB => Ok(DataType::TinyBlob), Keyword::MEDIUMBLOB => Ok(DataType::MediumBlob), @@ -9650,6 +9650,16 @@ impl<'a> Parser<'a> { } } + pub fn parse_optional_binary_length(&mut self) -> Result, ParserError> { + if self.consume_token(&Token::LParen) { + let binary_length = self.parse_binary_length()?; + self.expect_token(&Token::RParen)?; + Ok(Some(binary_length)) + } else { + Ok(None) + } + } + pub fn parse_character_length(&mut self) -> Result { if self.parse_keyword(Keyword::MAX) { return Ok(CharacterLength::Max); @@ -9665,6 +9675,14 @@ impl<'a> Parser<'a> { Ok(CharacterLength::IntegerLength { length, unit }) } + pub fn parse_binary_length(&mut self) -> Result { + if self.parse_keyword(Keyword::MAX) { + return Ok(BinaryLength::Max); + } + let length = self.parse_literal_uint()?; + Ok(BinaryLength::IntegerLength { length }) + } + pub fn parse_optional_precision_scale( &mut self, ) -> Result<(Option, Option), ParserError> { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f8c0a26b8..aef4d0d72 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2633,7 +2633,7 @@ fn parse_cast() { &Expr::Cast { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Varbinary(Some(50)), + data_type: DataType::Varbinary(Some(BinaryLength::IntegerLength { length: 50 })), format: None, }, expr_from_projection(only(&select.projection)) diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index e6a12b9c7..6865bdd4e 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -26,7 +26,7 @@ use helpers::attached_token::AttachedToken; use sqlparser::tokenizer::Span; use test_utils::*; -use sqlparser::ast::DataType::{Int, Text}; +use sqlparser::ast::DataType::{Int, Text, Varbinary}; use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; @@ -1796,6 +1796,59 @@ fn parse_mssql_set_session_value() { ms().verified_stmt("SET ANSI_NULLS, ANSI_PADDING ON"); } +#[test] +fn parse_mssql_varbinary_max_length() { + let sql = "CREATE TABLE example (var_binary_col VARBINARY(MAX))"; + + match ms_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!( + name, + ObjectName::from(vec![Ident { + value: "example".to_string(), + quote_style: None, + span: Span::empty(), + }]) + ); + assert_eq!( + columns, + vec![ColumnDef { + name: Ident::new("var_binary_col"), + data_type: Varbinary(Some(BinaryLength::Max)), + collation: None, + options: vec![] + },], + ); + } + _ => unreachable!(), + } + + let sql = "CREATE TABLE example (var_binary_col VARBINARY(50))"; + + match ms_and_generic().verified_stmt(sql) { + Statement::CreateTable(CreateTable { name, columns, .. }) => { + assert_eq!( + name, + ObjectName::from(vec![Ident { + value: "example".to_string(), + quote_style: None, + span: Span::empty(), + }]) + ); + assert_eq!( + columns, + vec![ColumnDef { + name: Ident::new("var_binary_col"), + data_type: Varbinary(Some(BinaryLength::IntegerLength { length: 50 })), + collation: None, + options: vec![] + },], + ); + } + _ => unreachable!(), + } +} + fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } From 1c0e5d355438343915b86e75956e0103e4f937eb Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Sat, 15 Feb 2025 00:16:10 +0800 Subject: [PATCH 727/806] Add supports for Hive's `SELECT ... GROUP BY .. GROUPING SETS` syntax (#1653) Co-authored-by: Ifeanyi Ubah --- src/ast/query.rs | 10 +++- src/dialect/clickhouse.rs | 10 ++++ src/dialect/generic.rs | 4 ++ src/dialect/hive.rs | 11 +++-- src/dialect/mod.rs | 6 +++ src/parser/mod.rs | 10 +++- tests/sqlparser_clickhouse.rs | 55 ---------------------- tests/sqlparser_common.rs | 86 +++++++++++++++++++++++++++++++++++ 8 files changed, 132 insertions(+), 60 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 0446bcb75..097a4ad4c 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2547,13 +2547,18 @@ impl fmt::Display for SelectInto { /// e.g. GROUP BY year WITH ROLLUP WITH TOTALS /// /// [ClickHouse]: -#[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 GroupByWithModifier { Rollup, Cube, Totals, + /// Hive supports GROUP BY GROUPING SETS syntax. + /// e.g. GROUP BY year , month GROUPING SETS((year,month),(year),(month)) + /// + /// [Hive]: + GroupingSets(Expr), } impl fmt::Display for GroupByWithModifier { @@ -2562,6 +2567,9 @@ impl fmt::Display for GroupByWithModifier { GroupByWithModifier::Rollup => write!(f, "WITH ROLLUP"), GroupByWithModifier::Cube => write!(f, "WITH CUBE"), GroupByWithModifier::Totals => write!(f, "WITH TOTALS"), + GroupByWithModifier::GroupingSets(expr) => { + write!(f, "{expr}") + } } } } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 7d479219d..37da2ab5d 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -79,4 +79,14 @@ impl Dialect for ClickHouseDialect { fn supports_from_first_select(&self) -> bool { true } + + // See + fn supports_group_by_expr(&self) -> bool { + true + } + + /// See + fn supports_group_by_with_modifier(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index dbd5cab28..e04a288d6 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -48,6 +48,10 @@ impl Dialect for GenericDialect { true } + fn supports_group_by_with_modifier(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index 80f44cf7c..3e15d395b 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -52,18 +52,23 @@ impl Dialect for HiveDialect { true } - /// See Hive + /// See fn supports_bang_not_operator(&self) -> bool { true } - /// See Hive + /// See fn supports_load_data(&self) -> bool { true } - /// See Hive + /// See fn supports_table_sample_before_alias(&self) -> bool { true } + + /// See + fn supports_group_by_with_modifier(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a57c25d5e..031fe9676 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -245,6 +245,12 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialects supports `GROUP BY` modifiers prefixed by a `WITH` keyword. + /// Example: `GROUP BY value WITH ROLLUP`. + fn supports_group_by_with_modifier(&self) -> bool { + false + } + /// Returns true if the dialect supports CONNECT BY. fn supports_connect_by(&self) -> bool { false diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6d3290d23..7c03c4e7e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9148,7 +9148,7 @@ impl<'a> Parser<'a> { }; let mut modifiers = vec![]; - if dialect_of!(self is ClickHouseDialect | GenericDialect) { + if self.dialect.supports_group_by_with_modifier() { loop { if !self.parse_keyword(Keyword::WITH) { break; @@ -9171,6 +9171,14 @@ 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))?; + self.expect_token(&Token::RParen)?; + modifiers.push(GroupByWithModifier::GroupingSets(Expr::GroupingSets( + result, + ))); + }; let group_by = match expressions { None => GroupByExpr::All(modifiers), Some(exprs) => GroupByExpr::Expressions(exprs, modifiers), diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 76a56e709..b94d6f698 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1069,61 +1069,6 @@ fn parse_create_materialized_view() { clickhouse_and_generic().verified_stmt(sql); } -#[test] -fn parse_group_by_with_modifier() { - let clauses = ["x", "a, b", "ALL"]; - let modifiers = [ - "WITH ROLLUP", - "WITH CUBE", - "WITH TOTALS", - "WITH ROLLUP WITH CUBE", - ]; - let expected_modifiers = [ - vec![GroupByWithModifier::Rollup], - vec![GroupByWithModifier::Cube], - vec![GroupByWithModifier::Totals], - vec![GroupByWithModifier::Rollup, GroupByWithModifier::Cube], - ]; - for clause in &clauses { - for (modifier, expected_modifier) in modifiers.iter().zip(expected_modifiers.iter()) { - let sql = format!("SELECT * FROM t GROUP BY {clause} {modifier}"); - match clickhouse_and_generic().verified_stmt(&sql) { - Statement::Query(query) => { - let group_by = &query.body.as_select().unwrap().group_by; - if clause == &"ALL" { - assert_eq!(group_by, &GroupByExpr::All(expected_modifier.to_vec())); - } else { - assert_eq!( - group_by, - &GroupByExpr::Expressions( - clause - .split(", ") - .map(|c| Identifier(Ident::new(c))) - .collect(), - expected_modifier.to_vec() - ) - ); - } - } - _ => unreachable!(), - } - } - } - - // invalid cases - let invalid_cases = [ - "SELECT * FROM t GROUP BY x WITH", - "SELECT * FROM t GROUP BY x WITH ROLLUP CUBE", - "SELECT * FROM t GROUP BY x WITH WITH ROLLUP", - "SELECT * FROM t GROUP BY WITH ROLLUP", - ]; - for sql in invalid_cases { - clickhouse_and_generic() - .parse_sql_statements(sql) - .expect_err("Expected: one of ROLLUP or CUBE or TOTALS, found: WITH"); - } -} - #[test] fn parse_select_order_by_with_fill_interpolate() { let sql = "SELECT id, fname, lname FROM customer WHERE id < 5 \ diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index aef4d0d72..e4fa2e837 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2447,6 +2447,92 @@ fn parse_select_group_by_all() { ); } +#[test] +fn parse_group_by_with_modifier() { + let clauses = ["x", "a, b", "ALL"]; + let modifiers = [ + "WITH ROLLUP", + "WITH CUBE", + "WITH TOTALS", + "WITH ROLLUP WITH CUBE", + ]; + let expected_modifiers = [ + vec![GroupByWithModifier::Rollup], + vec![GroupByWithModifier::Cube], + vec![GroupByWithModifier::Totals], + vec![GroupByWithModifier::Rollup, GroupByWithModifier::Cube], + ]; + let dialects = all_dialects_where(|d| d.supports_group_by_with_modifier()); + + for clause in &clauses { + for (modifier, expected_modifier) in modifiers.iter().zip(expected_modifiers.iter()) { + let sql = format!("SELECT * FROM t GROUP BY {clause} {modifier}"); + match dialects.verified_stmt(&sql) { + Statement::Query(query) => { + let group_by = &query.body.as_select().unwrap().group_by; + if clause == &"ALL" { + assert_eq!(group_by, &GroupByExpr::All(expected_modifier.to_vec())); + } else { + assert_eq!( + group_by, + &GroupByExpr::Expressions( + clause + .split(", ") + .map(|c| Identifier(Ident::new(c))) + .collect(), + expected_modifier.to_vec() + ) + ); + } + } + _ => unreachable!(), + } + } + } + + // invalid cases + let invalid_cases = [ + "SELECT * FROM t GROUP BY x WITH", + "SELECT * FROM t GROUP BY x WITH ROLLUP CUBE", + "SELECT * FROM t GROUP BY x WITH WITH ROLLUP", + "SELECT * FROM t GROUP BY WITH ROLLUP", + ]; + for sql in invalid_cases { + dialects + .parse_sql_statements(sql) + .expect_err("Expected: one of ROLLUP or CUBE or TOTALS, found: WITH"); + } +} + +#[test] +fn parse_group_by_special_grouping_sets() { + let sql = "SELECT a, b, SUM(c) FROM tab1 GROUP BY a, b GROUPING SETS ((a, b), (a), (b), ())"; + match all_dialects().verified_stmt(sql) { + 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![] + ]))] + ) + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_select_having() { let sql = "SELECT foo FROM bar GROUP BY foo HAVING COUNT(*) > 1"; From 68c41a9d5a99eea1f98b254d7c26fdbe0a1a4cf7 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 14 Feb 2025 08:23:00 -0800 Subject: [PATCH 728/806] Differentiate LEFT JOIN from LEFT OUTER JOIN (#1726) --- src/ast/query.rs | 20 +++++++++++-- src/ast/spans.rs | 2 ++ src/parser/mod.rs | 6 ++-- tests/sqlparser_common.rs | 60 +++++++++++++++++++++++++++------------ 4 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 097a4ad4c..c52a98b63 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2077,20 +2077,34 @@ impl fmt::Display for Join { self.relation, suffix(constraint) ), - JoinOperator::LeftOuter(constraint) => write!( + JoinOperator::Left(constraint) => write!( f, " {}LEFT JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) ), - JoinOperator::RightOuter(constraint) => write!( + JoinOperator::LeftOuter(constraint) => write!( + f, + " {}LEFT OUTER JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + ), + JoinOperator::Right(constraint) => write!( f, " {}RIGHT JOIN {}{}", prefix(constraint), self.relation, suffix(constraint) ), + JoinOperator::RightOuter(constraint) => write!( + f, + " {}RIGHT OUTER JOIN {}{}", + prefix(constraint), + self.relation, + suffix(constraint) + ), JoinOperator::FullOuter(constraint) => write!( f, " {}FULL JOIN {}{}", @@ -2162,7 +2176,9 @@ impl fmt::Display for Join { pub enum JoinOperator { Join(JoinConstraint), Inner(JoinConstraint), + Left(JoinConstraint), LeftOuter(JoinConstraint), + Right(JoinConstraint), RightOuter(JoinConstraint), FullOuter(JoinConstraint), CrossJoin, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 18f6f6e2f..574478692 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2010,7 +2010,9 @@ impl Spanned for JoinOperator { match self { JoinOperator::Join(join_constraint) => join_constraint.span(), JoinOperator::Inner(join_constraint) => join_constraint.span(), + JoinOperator::Left(join_constraint) => join_constraint.span(), JoinOperator::LeftOuter(join_constraint) => join_constraint.span(), + JoinOperator::Right(join_constraint) => join_constraint.span(), JoinOperator::RightOuter(join_constraint) => join_constraint.span(), JoinOperator::FullOuter(join_constraint) => join_constraint.span(), JoinOperator::CrossJoin => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7c03c4e7e..903fadaa5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5740,7 +5740,7 @@ impl<'a> Parser<'a> { drop_behavior, }) } - /// ```sql + /// ```sql /// DROP CONNECTOR [IF EXISTS] name /// ``` /// @@ -11190,9 +11190,9 @@ impl<'a> Parser<'a> { } Some(Keyword::JOIN) => { if is_left { - JoinOperator::LeftOuter + JoinOperator::Left } else { - JoinOperator::RightOuter + JoinOperator::Right } } _ => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e4fa2e837..85845ea2a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6661,8 +6661,16 @@ fn parse_joins_on() { only(&verified_only_select("SELECT * FROM t1 JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint("t2", None, false, JoinOperator::Join)] ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 INNER JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, false, JoinOperator::Inner)] + ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, false, JoinOperator::Left)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 LEFT OUTER JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint( "t2", None, @@ -6672,6 +6680,10 @@ fn parse_joins_on() { ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 RIGHT JOIN t2 ON c1 = c2").from).joins, + vec![join_with_constraint("t2", None, false, JoinOperator::Right)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 RIGHT OUTER JOIN t2 ON c1 = c2").from).joins, vec![join_with_constraint( "t2", None, @@ -6794,10 +6806,18 @@ fn parse_joins_using() { ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 LEFT JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::Left)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 LEFT OUTER JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::LeftOuter)] ); assert_eq!( only(&verified_only_select("SELECT * FROM t1 RIGHT JOIN t2 USING(c1)").from).joins, + vec![join_with_constraint("t2", None, JoinOperator::Right)] + ); + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 RIGHT OUTER JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::RightOuter)] ); assert_eq!( @@ -6857,20 +6877,34 @@ fn parse_natural_join() { only(&verified_only_select("SELECT * FROM t1 NATURAL JOIN t2").from).joins, vec![natural_join(JoinOperator::Join, None)] ); + // inner join explicitly assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL INNER JOIN t2").from).joins, vec![natural_join(JoinOperator::Inner, None)] ); + // left join explicitly assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL LEFT JOIN t2").from).joins, + vec![natural_join(JoinOperator::Left, None)] + ); + + // left outer join explicitly + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 NATURAL LEFT OUTER JOIN t2").from).joins, vec![natural_join(JoinOperator::LeftOuter, None)] ); // right join explicitly assert_eq!( only(&verified_only_select("SELECT * FROM t1 NATURAL RIGHT JOIN t2").from).joins, + vec![natural_join(JoinOperator::Right, None)] + ); + + // right outer join explicitly + assert_eq!( + only(&verified_only_select("SELECT * FROM t1 NATURAL RIGHT OUTER JOIN t2").from).joins, vec![natural_join(JoinOperator::RightOuter, None)] ); @@ -6950,22 +6984,12 @@ fn parse_join_nesting() { #[test] fn parse_join_syntax_variants() { - one_statement_parses_to( - "SELECT c1 FROM t1 JOIN t2 USING(c1)", - "SELECT c1 FROM t1 JOIN t2 USING(c1)", - ); - one_statement_parses_to( - "SELECT c1 FROM t1 INNER JOIN t2 USING(c1)", - "SELECT c1 FROM t1 INNER JOIN t2 USING(c1)", - ); - one_statement_parses_to( - "SELECT c1 FROM t1 LEFT OUTER JOIN t2 USING(c1)", - "SELECT c1 FROM t1 LEFT JOIN t2 USING(c1)", - ); - one_statement_parses_to( - "SELECT c1 FROM t1 RIGHT OUTER JOIN t2 USING(c1)", - "SELECT c1 FROM t1 RIGHT JOIN t2 USING(c1)", - ); + verified_stmt("SELECT c1 FROM t1 JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 INNER JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 LEFT JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 LEFT OUTER JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 RIGHT JOIN t2 USING(c1)"); + verified_stmt("SELECT c1 FROM t1 RIGHT OUTER JOIN t2 USING(c1)"); one_statement_parses_to( "SELECT c1 FROM t1 FULL OUTER JOIN t2 USING(c1)", "SELECT c1 FROM t1 FULL JOIN t2 USING(c1)", @@ -8027,7 +8051,7 @@ fn lateral_derived() { let join = &from.joins[0]; assert_eq!( join.join_operator, - JoinOperator::LeftOuter(JoinConstraint::On(Expr::Value(test_utils::number("1")))) + JoinOperator::Left(JoinConstraint::On(Expr::Value(test_utils::number("1")))) ); if let TableFactor::Derived { lateral, @@ -8095,7 +8119,7 @@ fn lateral_function() { alias: None, }, global: false, - join_operator: JoinOperator::LeftOuter(JoinConstraint::None), + join_operator: JoinOperator::Left(JoinConstraint::None), }], }], lateral_views: vec![], From c75a99262102da1ac795c4272a640d7e36b0e157 Mon Sep 17 00:00:00 2001 From: Jesse Stuart Date: Mon, 17 Feb 2025 14:12:59 -0500 Subject: [PATCH 729/806] Add support for Postgres `ALTER TYPE` (#1727) --- src/ast/ddl.rs | 89 ++++++++++++++++++++++++++++++ src/ast/mod.rs | 24 ++++++--- src/ast/spans.rs | 2 + src/dialect/postgresql.rs | 44 --------------- src/parser/mod.rs | 69 ++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 104 ++++++++++++++++++++++++++++++++---- 6 files changed, 271 insertions(+), 61 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 35e01e618..f90252000 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -640,6 +640,95 @@ impl fmt::Display for AlterIndexOperation { } } +/// An `ALTER TYPE` statement (`Statement::AlterType`) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterType { + pub name: ObjectName, + pub operation: AlterTypeOperation, +} + +/// An [AlterType] operation +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTypeOperation { + Rename(AlterTypeRename), + AddValue(AlterTypeAddValue), + RenameValue(AlterTypeRenameValue), +} + +/// See [AlterTypeOperation::Rename] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterTypeRename { + pub new_name: Ident, +} + +/// See [AlterTypeOperation::AddValue] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterTypeAddValue { + pub if_not_exists: bool, + pub value: Ident, + pub position: Option, +} + +/// See [AlterTypeAddValue] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTypeAddValuePosition { + Before(Ident), + After(Ident), +} + +/// See [AlterTypeOperation::RenameValue] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterTypeRenameValue { + pub from: Ident, + pub to: Ident, +} + +impl fmt::Display for AlterTypeOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Rename(AlterTypeRename { new_name }) => { + write!(f, "RENAME TO {new_name}") + } + Self::AddValue(AlterTypeAddValue { + if_not_exists, + value, + position, + }) => { + write!(f, "ADD VALUE")?; + if *if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " {value}")?; + match position { + Some(AlterTypeAddValuePosition::Before(neighbor_value)) => { + write!(f, " BEFORE {neighbor_value}")?; + } + Some(AlterTypeAddValuePosition::After(neighbor_value)) => { + write!(f, " AFTER {neighbor_value}")?; + } + None => {} + }; + Ok(()) + } + Self::RenameValue(AlterTypeRenameValue { from, to }) => { + write!(f, "RENAME VALUE {from} TO {to}") + } + } + } +} + /// An `ALTER COLUMN` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5835447db..49c7b6fd7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -48,13 +48,15 @@ pub use self::dcl::{ }; pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, - AlterTableOperation, 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, + 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, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -2691,6 +2693,11 @@ pub enum Statement { with_options: Vec, }, /// ```sql + /// ALTER TYPE + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertype.html) + /// ``` + AlterType(AlterType), + /// ```sql /// ALTER ROLE /// ``` AlterRole { @@ -4438,6 +4445,9 @@ impl fmt::Display for Statement { } write!(f, " AS {query}") } + Statement::AlterType(AlterType { name, operation }) => { + write!(f, "ALTER TYPE {name} {operation}") + } Statement::AlterRole { name, operation } => { write!(f, "ALTER ROLE {name} {operation}") } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 574478692..191471675 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -215,6 +215,7 @@ impl Spanned for Values { /// - [Statement::CopyIntoSnowflake] /// - [Statement::CreateSecret] /// - [Statement::CreateRole] +/// - [Statement::AlterType] /// - [Statement::AlterRole] /// - [Statement::AttachDatabase] /// - [Statement::AttachDuckDBDatabase] @@ -427,6 +428,7 @@ impl Spanned for Statement { .chain(with_options.iter().map(|i| i.span())), ), // These statements need to be implemented + Statement::AlterType { .. } => Span::empty(), Statement::AlterRole { .. } => Span::empty(), Statement::AttachDatabase { .. } => Span::empty(), Statement::AttachDuckDBDatabase { .. } => Span::empty(), diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 74b963e82..60284364f 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -28,7 +28,6 @@ // limitations under the License. use log::debug; -use crate::ast::{ObjectName, Statement, UserDefinedTypeRepresentation}; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -135,15 +134,6 @@ impl Dialect for PostgreSqlDialect { } } - fn parse_statement(&self, parser: &mut Parser) -> Option> { - if parser.parse_keyword(Keyword::CREATE) { - parser.prev_token(); // unconsume the CREATE in case we don't end up parsing anything - parse_create(parser) - } else { - None - } - } - fn supports_filter_during_aggregation(&self) -> bool { true } @@ -259,37 +249,3 @@ impl Dialect for PostgreSqlDialect { true } } - -pub fn parse_create(parser: &mut Parser) -> Option> { - let name = parser.maybe_parse(|parser| -> Result { - parser.expect_keyword_is(Keyword::CREATE)?; - parser.expect_keyword_is(Keyword::TYPE)?; - let name = parser.parse_object_name(false)?; - parser.expect_keyword_is(Keyword::AS)?; - parser.expect_keyword_is(Keyword::ENUM)?; - Ok(name) - }); - - match name { - Ok(name) => name.map(|name| parse_create_type_as_enum(parser, name)), - Err(e) => Some(Err(e)), - } -} - -// https://www.postgresql.org/docs/current/sql-createtype.html -pub fn parse_create_type_as_enum( - parser: &mut Parser, - name: ObjectName, -) -> Result { - if !parser.consume_token(&Token::LParen) { - return parser.expected("'(' after CREATE TYPE AS ENUM", parser.peek_token()); - } - - let labels = parser.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; - parser.expect_token(&Token::RParen)?; - - Ok(Statement::CreateType { - name, - representation: UserDefinedTypeRepresentation::Enum { labels }, - }) -} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 903fadaa5..88a9281fe 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8042,6 +8042,7 @@ impl<'a> Parser<'a> { pub fn parse_alter(&mut self) -> Result { let object_type = self.expect_one_of_keywords(&[ Keyword::VIEW, + Keyword::TYPE, Keyword::TABLE, Keyword::INDEX, Keyword::ROLE, @@ -8050,6 +8051,7 @@ impl<'a> Parser<'a> { ])?; 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 ] @@ -8122,6 +8124,55 @@ impl<'a> Parser<'a> { }) } + /// Parse a [Statement::AlterType] + pub fn parse_alter_type(&mut self) -> Result { + let name = self.parse_object_name(false)?; + + if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + let new_name = self.parse_identifier()?; + Ok(Statement::AlterType(AlterType { + name, + operation: AlterTypeOperation::Rename(AlterTypeRename { new_name }), + })) + } else if self.parse_keywords(&[Keyword::ADD, Keyword::VALUE]) { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let new_enum_value = self.parse_identifier()?; + let position = if self.parse_keyword(Keyword::BEFORE) { + Some(AlterTypeAddValuePosition::Before(self.parse_identifier()?)) + } else if self.parse_keyword(Keyword::AFTER) { + Some(AlterTypeAddValuePosition::After(self.parse_identifier()?)) + } else { + None + }; + + Ok(Statement::AlterType(AlterType { + name, + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists, + value: new_enum_value, + position, + }), + })) + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::VALUE]) { + let existing_enum_value = self.parse_identifier()?; + self.expect_keyword(Keyword::TO)?; + let new_enum_value = self.parse_identifier()?; + + Ok(Statement::AlterType(AlterType { + name, + operation: AlterTypeOperation::RenameValue(AlterTypeRenameValue { + from: existing_enum_value, + to: new_enum_value, + }), + })) + } else { + return self.expected_ref( + "{RENAME TO | { RENAME | ADD } VALUE}", + self.peek_token_ref(), + ); + } + } + /// Parse a `CALL procedure_name(arg1, arg2, ...)` /// or `CALL procedure_name` statement pub fn parse_call(&mut self) -> Result { @@ -14222,6 +14273,10 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(false)?; self.expect_keyword_is(Keyword::AS)?; + if self.parse_keyword(Keyword::ENUM) { + return self.parse_create_type_enum(name); + } + let mut attributes = vec![]; if !self.consume_token(&Token::LParen) || self.consume_token(&Token::RParen) { return Ok(Statement::CreateType { @@ -14258,6 +14313,20 @@ impl<'a> Parser<'a> { }) } + /// Parse remainder of `CREATE TYPE AS ENUM` statement (see [Statement::CreateType] and [Self::parse_create_type]) + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtype.html) + pub fn parse_create_type_enum(&mut self, name: ObjectName) -> Result { + self.expect_token(&Token::LParen)?; + let labels = self.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; + self.expect_token(&Token::RParen)?; + + Ok(Statement::CreateType { + name, + representation: UserDefinedTypeRepresentation::Enum { labels }, + }) + } + fn parse_parenthesized_identifiers(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let partitions = self.parse_comma_separated(|p| p.parse_identifier())?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3a4504ebb..b9a5202f0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5293,15 +5293,8 @@ fn arrow_cast_precedence() { #[test] fn parse_create_type_as_enum() { - let statement = pg().one_statement_parses_to( - r#"CREATE TYPE public.my_type AS ENUM ( - 'label1', - 'label2', - 'label3', - 'label4' - );"#, - "CREATE TYPE public.my_type AS ENUM ('label1', 'label2', 'label3', 'label4')", - ); + let sql = "CREATE TYPE public.my_type AS ENUM ('label1', 'label2', 'label3', 'label4')"; + let statement = pg_and_generic().verified_stmt(sql); match statement { Statement::CreateType { name, @@ -5316,10 +5309,101 @@ fn parse_create_type_as_enum() { labels ); } - _ => unreachable!(), + _ => unreachable!("{:?} should parse to Statement::CreateType", sql), } } +#[test] +fn parse_alter_type() { + struct TestCase { + sql: &'static str, + name: &'static str, + operation: AlterTypeOperation, + } + vec![ + TestCase { + sql: "ALTER TYPE public.my_type RENAME TO my_new_type", + name: "public.my_type", + operation: AlterTypeOperation::Rename(AlterTypeRename { + new_name: Ident::new("my_new_type"), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE IF NOT EXISTS 'label3.5' BEFORE 'label4'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: true, + value: Ident::with_quote('\'', "label3.5"), + position: Some(AlterTypeAddValuePosition::Before(Ident::with_quote( + '\'', "label4", + ))), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE 'label3.5' BEFORE 'label4'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: false, + value: Ident::with_quote('\'', "label3.5"), + position: Some(AlterTypeAddValuePosition::Before(Ident::with_quote( + '\'', "label4", + ))), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE IF NOT EXISTS 'label3.5' AFTER 'label3'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: true, + value: Ident::with_quote('\'', "label3.5"), + position: Some(AlterTypeAddValuePosition::After(Ident::with_quote( + '\'', "label3", + ))), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE 'label3.5' AFTER 'label3'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: false, + value: Ident::with_quote('\'', "label3.5"), + position: Some(AlterTypeAddValuePosition::After(Ident::with_quote( + '\'', "label3", + ))), + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE IF NOT EXISTS 'label5'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: true, + value: Ident::with_quote('\'', "label5"), + position: None, + }), + }, + TestCase { + sql: "ALTER TYPE public.my_type ADD VALUE 'label5'", + name: "public.my_type", + operation: AlterTypeOperation::AddValue(AlterTypeAddValue { + if_not_exists: false, + value: Ident::with_quote('\'', "label5"), + position: None, + }), + }, + ] + .into_iter() + .enumerate() + .for_each(|(index, tc)| { + let statement = pg_and_generic().verified_stmt(tc.sql); + if let Statement::AlterType(AlterType { name, operation }) = statement { + assert_eq!(tc.name, name.to_string(), "TestCase[{index}].name"); + assert_eq!(tc.operation, operation, "TestCase[{index}].operation"); + } else { + unreachable!("{:?} should parse to Statement::AlterType", tc.sql); + } + }); +} + #[test] fn parse_bitstring_literal() { let select = pg_and_generic().verified_only_select("SELECT B'111'"); From 3e90a18f6d8cf6b5e240705c0f1fc2ad44330a95 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Wed, 19 Feb 2025 18:49:42 +0100 Subject: [PATCH 730/806] Replace `Method` and `CompositeAccess` with `CompoundFieldAccess` (#1716) --- src/ast/mod.rs | 26 --- src/ast/spans.rs | 2 - src/dialect/mod.rs | 17 +- src/dialect/mssql.rs | 8 +- src/dialect/postgresql.rs | 2 + src/dialect/snowflake.rs | 5 + src/parser/mod.rs | 377 ++++++++++++++++++++---------------- tests/sqlparser_common.rs | 123 +++++++++--- tests/sqlparser_postgres.rs | 50 +++-- 9 files changed, 347 insertions(+), 263 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 49c7b6fd7..1f05d590d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -661,11 +661,6 @@ pub enum Expr { /// The path to the data to extract. path: JsonPath, }, - /// CompositeAccess eg: SELECT foo(bar).z, (information_schema._pg_expandarray(array['i','i'])).n - CompositeAccess { - expr: Box, - key: Ident, - }, /// `IS FALSE` operator IsFalse(Box), /// `IS NOT FALSE` operator @@ -915,23 +910,6 @@ pub enum Expr { }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), - /// Arbitrary expr method call - /// - /// Syntax: - /// - /// `.....` - /// - /// > `arbitrary-expr` can be any expression including a function call. - /// - /// Example: - /// - /// ```sql - /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') - /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') - /// ``` - /// - /// (mssql): - Method(Method), /// `CASE [] WHEN THEN ... [ELSE ] END` /// /// Note we only recognize a complete single expression as ``, @@ -1631,7 +1609,6 @@ impl fmt::Display for Expr { write!(f, " {value}") } Expr::Function(fun) => write!(f, "{fun}"), - Expr::Method(method) => write!(f, "{method}"), Expr::Case { operand, conditions, @@ -1789,9 +1766,6 @@ impl fmt::Display for Expr { Expr::JsonAccess { value, path } => { write!(f, "{value}{path}") } - Expr::CompositeAccess { expr, key } => { - write!(f, "{expr}.{key}") - } Expr::AtTimeZone { timestamp, time_zone, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 191471675..091a22f04 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1288,7 +1288,6 @@ impl Spanned for Expr { match self { Expr::Identifier(ident) => ident.span, Expr::CompoundIdentifier(vec) => union_spans(vec.iter().map(|i| i.span)), - Expr::CompositeAccess { expr, key } => expr.span().union(&key.span), Expr::CompoundFieldAccess { root, access_chain } => { union_spans(iter::once(root.span()).chain(access_chain.iter().map(|i| i.span()))) } @@ -1478,7 +1477,6 @@ impl Spanned for Expr { Expr::OuterJoin(expr) => expr.span(), Expr::Prior(expr) => expr.span(), Expr::Lambda(_) => Span::empty(), - Expr::Method(_) => Span::empty(), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 031fe9676..cb86cf7c6 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -251,6 +251,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN. + fn supports_outer_join_operator(&self) -> bool { + false + } + /// Returns true if the dialect supports CONNECT BY. fn supports_connect_by(&self) -> bool { false @@ -352,15 +357,6 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if the dialect supports method calls, for example: - /// - /// ```sql - /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') - /// ``` - fn supports_methods(&self) -> bool { - false - } - /// Returns true if the dialect supports multiple variable assignment /// using parentheses in a `SET` variable declaration. /// @@ -581,6 +577,7 @@ pub trait Dialect: Debug + Any { 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)), + Token::Period => Ok(p!(Period)), Token::Eq | Token::Lt | Token::LtEq @@ -654,6 +651,7 @@ pub trait Dialect: Debug + Any { /// Uses (APPROXIMATELY) as a reference fn prec_value(&self, prec: Precedence) -> u8 { match prec { + Precedence::Period => 100, Precedence::DoubleColon => 50, Precedence::AtTz => 41, Precedence::MulDivModOp => 40, @@ -925,6 +923,7 @@ pub trait Dialect: Debug + Any { /// higher number -> higher precedence #[derive(Debug, Clone, Copy)] pub enum Precedence { + Period, DoubleColon, AtTz, MulDivModOp, diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 7d8611cb0..980f5ec3f 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -46,6 +46,10 @@ impl Dialect for MsSqlDialect { true } + fn supports_outer_join_operator(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } @@ -63,10 +67,6 @@ impl Dialect for MsSqlDialect { false } - fn supports_methods(&self) -> bool { - true - } - fn supports_named_fn_args_with_colon_operator(&self) -> bool { true } diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 60284364f..3a3f0e4c3 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -37,6 +37,7 @@ use crate::tokenizer::Token; #[derive(Debug)] pub struct PostgreSqlDialect {} +const PERIOD_PREC: u8 = 200; const DOUBLE_COLON_PREC: u8 = 140; const BRACKET_PREC: u8 = 130; const COLLATE_PREC: u8 = 120; @@ -144,6 +145,7 @@ impl Dialect for PostgreSqlDialect { fn prec_value(&self, prec: Precedence) -> u8 { match prec { + Precedence::Period => PERIOD_PREC, Precedence::DoubleColon => DOUBLE_COLON_PREC, Precedence::AtTz => AT_TZ_PREC, Precedence::MulDivModOp => MUL_DIV_MOD_OP_PREC, diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 68166cbef..bac9c49c7 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -87,6 +87,11 @@ impl Dialect for SnowflakeDialect { true } + /// See + fn supports_outer_join_operator(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 88a9281fe..9d75bcdb5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1021,6 +1021,8 @@ impl<'a> Parser<'a> { debug!("parsing expr"); let mut expr = self.parse_prefix()?; + expr = self.parse_compound_expr(expr, vec![])?; + debug!("prefix: {:?}", expr); loop { let next_precedence = self.get_next_precedence()?; @@ -1030,6 +1032,12 @@ impl<'a> Parser<'a> { break; } + // The period operator is handled exclusively by the + // compound field access parsing. + if Token::Period == self.peek_token_ref().token { + break; + } + expr = self.parse_infix(expr, next_precedence)?; } Ok(expr) @@ -1105,8 +1113,8 @@ impl<'a> Parser<'a> { } } - // Tries to parse an expression by matching the specified word to known keywords that have a special meaning in the dialect. - // Returns `None if no match is found. + /// Tries to parse an expression by matching the specified word to known keywords that have a special meaning in the dialect. + /// Returns `None if no match is found. fn parse_expr_prefix_by_reserved_word( &mut self, w: &Word, @@ -1203,7 +1211,7 @@ impl<'a> Parser<'a> { } Keyword::STRUCT if self.dialect.supports_struct_literal() => { let struct_expr = self.parse_struct_literal()?; - Ok(Some(self.parse_compound_field_access(struct_expr, vec![])?)) + Ok(Some(struct_expr)) } Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => { let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?; @@ -1216,35 +1224,16 @@ impl<'a> Parser<'a> { } } - // Tries to parse an expression by a word that is not known to have a special meaning in the dialect. + /// Tries to parse an expression by a word that is not known to have a special meaning in the dialect. fn parse_expr_prefix_by_unreserved_word( &mut self, w: &Word, w_span: Span, ) -> Result { match self.peek_token().token { - Token::Period => self.parse_compound_field_access( - Expr::Identifier(w.clone().into_ident(w_span)), - vec![], - ), - Token::LParen => { + Token::LParen if !self.peek_outer_join_operator() => { let id_parts = vec![w.clone().into_ident(w_span)]; - if let Some(expr) = self.parse_outer_join_expr(&id_parts) { - Ok(expr) - } else { - let mut expr = self.parse_function(ObjectName::from(id_parts))?; - // consume all period if it's a method chain - expr = self.try_parse_method(expr)?; - let fields = vec![]; - self.parse_compound_field_access(expr, fields) - } - } - Token::LBracket if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) => - { - let ident = Expr::Identifier(w.clone().into_ident(w_span)); - let mut fields = vec![]; - self.parse_multi_dim_subscript(&mut fields)?; - self.parse_compound_field_access(ident, fields) + self.parse_function(ObjectName::from(id_parts)) } // string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html Token::SingleQuotedString(_) @@ -1453,25 +1442,7 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; - let expr = self.try_parse_method(expr)?; - if !self.consume_token(&Token::Period) { - Ok(expr) - } else { - let tok = self.next_token(); - let key = match tok.token { - Token::Word(word) => word.into_ident(tok.span), - _ => { - return parser_err!( - format!("Expected identifier, found: {tok}"), - tok.span.start - ) - } - }; - Ok(Expr::CompositeAccess { - expr: Box::new(expr), - key, - }) - } + Ok(expr) } Token::Placeholder(_) | Token::Colon | Token::AtSign => { self.prev_token(); @@ -1484,8 +1455,6 @@ impl<'a> Parser<'a> { _ => self.expected_at("an expression", next_token_index), }?; - let expr = self.try_parse_method(expr)?; - if self.parse_keyword(Keyword::COLLATE) { Ok(Expr::Collate { expr: Box::new(expr), @@ -1499,62 +1468,72 @@ impl<'a> Parser<'a> { /// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`. /// If all the fields are `Expr::Identifier`s, return an [Expr::CompoundIdentifier] instead. /// If only the root exists, return the root. - /// If self supports [Dialect::supports_partiql], it will fall back when occurs [Token::LBracket] for JsonAccess parsing. - pub fn parse_compound_field_access( + /// Parses compound expressions which may be delimited by period + /// or bracket notation. + /// For example: `a.b.c`, `a.b[1]`. + pub fn parse_compound_expr( &mut self, root: Expr, mut chain: Vec, ) -> Result { let mut ending_wildcard: Option = None; - let mut ending_lbracket = false; - while self.consume_token(&Token::Period) { - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => { - let expr = Expr::Identifier(w.into_ident(next_token.span)); - chain.push(AccessExpr::Dot(expr)); - if self.peek_token().token == Token::LBracket { - if self.dialect.supports_partiql() { - self.next_token(); - ending_lbracket = true; - break; + loop { + if self.consume_token(&Token::Period) { + let next_token = self.peek_token_ref(); + match &next_token.token { + Token::Mul => { + // Postgres explicitly allows funcnm(tablenm.*) and the + // function array_agg traverses this control flow + if dialect_of!(self is PostgreSqlDialect) { + ending_wildcard = Some(self.next_token()); } else { - self.parse_multi_dim_subscript(&mut chain)? + // Put back the consumed `.` tokens before exiting. + // If this expression is being parsed in the + // context of a projection, then the `.*` could imply + // a wildcard expansion. For example: + // `SELECT STRUCT('foo').* FROM T` + self.prev_token(); // . } + + break; } - } - Token::Mul => { - // Postgres explicitly allows funcnm(tablenm.*) and the - // function array_agg traverses this control flow - if dialect_of!(self is PostgreSqlDialect) { - ending_wildcard = Some(next_token); - } else { - // Put back the consumed .* tokens before exiting. - // If this expression is being parsed in the - // context of a projection, then this could imply - // a wildcard expansion. For example: - // `SELECT STRUCT('foo').* FROM T` - self.prev_token(); // * - self.prev_token(); // . + Token::SingleQuotedString(s) => { + let expr = + Expr::Identifier(Ident::with_quote_and_span('\'', next_token.span, s)); + chain.push(AccessExpr::Dot(expr)); + self.advance_token(); // The consumed string } - - break; - } - Token::SingleQuotedString(s) => { - let expr = Expr::Identifier(Ident::with_quote('\'', s)); - chain.push(AccessExpr::Dot(expr)); - } - _ => { - return self.expected("an identifier or a '*' after '.'", next_token); + // Fallback to parsing an arbitrary expression. + _ => match self.parse_subexpr(self.dialect.prec_value(Precedence::Period))? { + // If we get back a compound field access or identifier, + // we flatten the nested expression. + // For example if the current root is `foo` + // and we get back a compound identifier expression `bar.baz` + // The full expression should be `foo.bar.baz` (i.e. + // a root with an access chain with 2 entries) and not + // `foo.(bar.baz)` (i.e. a root with an access chain with + // 1 entry`). + Expr::CompoundFieldAccess { root, access_chain } => { + chain.push(AccessExpr::Dot(*root)); + chain.extend(access_chain); + } + Expr::CompoundIdentifier(parts) => chain + .extend(parts.into_iter().map(Expr::Identifier).map(AccessExpr::Dot)), + expr => { + chain.push(AccessExpr::Dot(expr)); + } + }, } + } else if !self.dialect.supports_partiql() + && self.peek_token_ref().token == Token::LBracket + { + self.parse_multi_dim_subscript(&mut chain)?; + } else { + break; } } - // if dialect supports partiql, we need to go back one Token::LBracket for the JsonAccess parsing - if self.dialect.supports_partiql() && ending_lbracket { - self.prev_token(); - } - + let tok_index = self.get_current_index(); if let Some(wildcard_token) = ending_wildcard { if !Self::is_all_ident(&root, &chain) { return self.expected("an identifier or a '*' after '.'", self.peek_token()); @@ -1563,32 +1542,112 @@ impl<'a> Parser<'a> { ObjectName::from(Self::exprs_to_idents(root, chain)?), AttachedToken(wildcard_token), )) - } else if self.peek_token().token == Token::LParen { + } else if self.maybe_parse_outer_join_operator() { if !Self::is_all_ident(&root, &chain) { - // consume LParen - self.next_token(); - return self.expected("an identifier or a '*' after '.'", self.peek_token()); + return self.expected_at("column identifier before (+)", tok_index); }; - let id_parts = Self::exprs_to_idents(root, chain)?; - if let Some(expr) = self.parse_outer_join_expr(&id_parts) { - Ok(expr) + let expr = if chain.is_empty() { + root } else { - self.parse_function(ObjectName::from(id_parts)) - } - } else if chain.is_empty() { - Ok(root) + Expr::CompoundIdentifier(Self::exprs_to_idents(root, chain)?) + }; + Ok(Expr::OuterJoin(expr.into())) } else { - if Self::is_all_ident(&root, &chain) { - return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents( - root, chain, - )?)); + Self::build_compound_expr(root, chain) + } + } + + /// Combines a root expression and access chain to form + /// a compound expression. Which may be a [Expr::CompoundFieldAccess] + /// or other special cased expressions like [Expr::CompoundIdentifier], + /// [Expr::OuterJoin]. + fn build_compound_expr( + root: Expr, + mut access_chain: Vec, + ) -> Result { + if access_chain.is_empty() { + return Ok(root); + } + + if Self::is_all_ident(&root, &access_chain) { + return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents( + root, + access_chain, + )?)); + } + + // Flatten qualified function calls. + // For example, the expression `a.b.c.foo(1,2,3)` should + // represent a function called `a.b.c.foo`, rather than + // a composite expression. + if matches!(root, Expr::Identifier(_)) + && matches!( + access_chain.last(), + Some(AccessExpr::Dot(Expr::Function(_))) + ) + && access_chain + .iter() + .rev() + .skip(1) // All except the Function + .all(|access| matches!(access, AccessExpr::Dot(Expr::Identifier(_)))) + { + let Some(AccessExpr::Dot(Expr::Function(mut func))) = access_chain.pop() else { + return parser_err!("expected function expression", root.span().start); + }; + + let compound_func_name = [root] + .into_iter() + .chain(access_chain.into_iter().flat_map(|access| match access { + AccessExpr::Dot(expr) => Some(expr), + _ => None, + })) + .flat_map(|expr| match expr { + Expr::Identifier(ident) => Some(ident), + _ => None, + }) + .map(ObjectNamePart::Identifier) + .chain(func.name.0) + .collect::>(); + func.name = ObjectName(compound_func_name); + + return Ok(Expr::Function(func)); + } + + // Flatten qualified outer join expressions. + // For example, the expression `T.foo(+)` should + // represent an outer join on the column name `T.foo` + // rather than a composite expression. + if access_chain.len() == 1 + && matches!( + access_chain.last(), + Some(AccessExpr::Dot(Expr::OuterJoin(_))) + ) + { + let Some(AccessExpr::Dot(Expr::OuterJoin(inner_expr))) = access_chain.pop() else { + return parser_err!("expected (+) expression", root.span().start); + }; + + if !Self::is_all_ident(&root, &[]) { + return parser_err!("column identifier before (+)", root.span().start); + }; + + let token_start = root.span().start; + let mut idents = Self::exprs_to_idents(root, vec![])?; + match *inner_expr { + Expr::CompoundIdentifier(suffix) => idents.extend(suffix), + Expr::Identifier(suffix) => idents.push(suffix), + _ => { + return parser_err!("column identifier before (+)", token_start); + } } - Ok(Expr::CompoundFieldAccess { - root: Box::new(root), - access_chain: chain, - }) + return Ok(Expr::OuterJoin(Expr::CompoundIdentifier(idents).into())); } + + Ok(Expr::CompoundFieldAccess { + root: Box::new(root), + access_chain, + }) } /// Check if the root is an identifier and all fields are identifiers. @@ -1625,20 +1684,23 @@ impl<'a> Parser<'a> { } } - /// Try to parse OuterJoin expression `(+)` - fn parse_outer_join_expr(&mut self, id_parts: &[Ident]) -> Option { - if dialect_of!(self is SnowflakeDialect | MsSqlDialect) - && self.consume_tokens(&[Token::LParen, Token::Plus, Token::RParen]) - { - Some(Expr::OuterJoin(Box::new( - match <[Ident; 1]>::try_from(id_parts.to_vec()) { - Ok([ident]) => Expr::Identifier(ident), - Err(parts) => Expr::CompoundIdentifier(parts), - }, - ))) - } else { - None + /// Returns true if the next tokens indicate the outer join operator `(+)`. + fn peek_outer_join_operator(&mut self) -> bool { + if !self.dialect.supports_outer_join_operator() { + return false; } + + let [maybe_lparen, maybe_plus, maybe_rparen] = self.peek_tokens_ref(); + Token::LParen == maybe_lparen.token + && Token::Plus == maybe_plus.token + && Token::RParen == maybe_rparen.token + } + + /// If the next tokens indicates the outer join operator `(+)`, consume + /// the tokens and return true. + fn maybe_parse_outer_join_operator(&mut self) -> bool { + self.dialect.supports_outer_join_operator() + && self.consume_tokens(&[Token::LParen, Token::Plus, Token::RParen]) } pub fn parse_utility_options(&mut self) -> Result, ParserError> { @@ -1688,41 +1750,6 @@ impl<'a> Parser<'a> { }) } - /// Parses method call expression - fn try_parse_method(&mut self, expr: Expr) -> Result { - if !self.dialect.supports_methods() { - return Ok(expr); - } - let method_chain = self.maybe_parse(|p| { - let mut method_chain = Vec::new(); - while p.consume_token(&Token::Period) { - let tok = p.next_token(); - let name = match tok.token { - Token::Word(word) => word.into_ident(tok.span), - _ => return p.expected("identifier", tok), - }; - let func = match p.parse_function(ObjectName::from(vec![name]))? { - Expr::Function(func) => func, - _ => return p.expected("function", p.peek_token()), - }; - method_chain.push(func); - } - if !method_chain.is_empty() { - Ok(method_chain) - } else { - p.expected("function", p.peek_token()) - } - })?; - if let Some(method_chain) = method_chain { - Ok(Expr::Method(Method { - expr: Box::new(expr), - method_chain, - })) - } else { - Ok(expr) - } - } - /// Tries to parse the body of an [ODBC function] call. /// i.e. without the enclosing braces /// @@ -3281,21 +3308,9 @@ impl<'a> Parser<'a> { op: UnaryOperator::PGPostfixFactorial, expr: Box::new(expr), }) - } else if Token::LBracket == *tok { - if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) - { - let mut chain = vec![]; - // back to LBracket - self.prev_token(); - self.parse_multi_dim_subscript(&mut chain)?; - self.parse_compound_field_access(expr, chain) - } else if self.dialect.supports_partiql() { - self.prev_token(); - self.parse_json_access(expr) - } else { - parser_err!("Array subscripting is not supported", tok.span.start) - } - } else if dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == *tok { + } else if Token::LBracket == *tok && self.dialect.supports_partiql() + || (dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == *tok) + { self.prev_token(); self.parse_json_access(expr) } else { @@ -3605,6 +3620,26 @@ impl<'a> Parser<'a> { }) } + /// Returns references to the `N` next non-whitespace tokens + /// that have not yet been processed. + /// + /// See [`Self::peek_tokens`] for an example. + pub fn peek_tokens_ref(&self) -> [&TokenWithSpan; N] { + let mut index = self.index; + core::array::from_fn(|_| loop { + let token = self.tokens.get(index); + index += 1; + if let Some(TokenWithSpan { + token: Token::Whitespace(_), + span: _, + }) = token + { + continue; + } + break token.unwrap_or(&EOF_TOKEN); + }) + } + /// Return nth non-whitespace token that has not yet been processed pub fn peek_nth_token(&self, n: usize) -> TokenWithSpan { self.peek_nth_token_ref(n).clone() diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 85845ea2a..11e43daeb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -73,6 +73,23 @@ fn parse_numeric_literal_underscore() { ); } +#[test] +fn parse_function_object_name() { + let select = verified_only_select("SELECT a.b.c.d(1, 2, 3) FROM T"); + let Expr::Function(func) = expr_from_projection(&select.projection[0]) else { + unreachable!() + }; + assert_eq!( + ObjectName::from( + ["a", "b", "c", "d"] + .into_iter() + .map(Ident::new) + .collect::>() + ), + func.name, + ); +} + #[test] fn parse_insert_values() { let row = vec![ @@ -936,6 +953,44 @@ fn parse_select_distinct_tuple() { ); } +#[test] +fn parse_outer_join_operator() { + let dialects = all_dialects_where(|d| d.supports_outer_join_operator()); + + let select = dialects.verified_only_select("SELECT 1 FROM T WHERE a = b (+)"); + assert_eq!( + select.selection, + Some(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::OuterJoin(Box::new(Expr::Identifier(Ident::new("b"))))) + }) + ); + + let select = dialects.verified_only_select("SELECT 1 FROM T WHERE t1.c1 = t2.c2.d3 (+)"); + assert_eq!( + select.selection, + Some(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("t1"), + Ident::new("c1") + ])), + op: BinaryOperator::Eq, + right: Box::new(Expr::OuterJoin(Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("t2"), + Ident::new("c2"), + Ident::new("d3"), + ])))) + }) + ); + + let res = dialects.parse_sql_statements("SELECT 1 FROM T WHERE 1 = 2 (+)"); + assert_eq!( + ParserError::ParserError("Expected: column identifier before (+), found: 2".to_string()), + res.unwrap_err() + ); +} + #[test] fn parse_select_distinct_on() { let sql = "SELECT DISTINCT ON (album_id) name FROM track ORDER BY album_id, milliseconds"; @@ -12623,68 +12678,76 @@ fn test_try_convert() { #[test] fn parse_method_select() { - let dialects = all_dialects_where(|d| d.supports_methods()); - let _ = dialects.verified_only_select( + let _ = verified_only_select( "SELECT LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T", ); - let _ = dialects.verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"); - let _ = dialects - .verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"); + let _ = verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"); + let _ = verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"); // `CONVERT` support - let dialects = all_dialects_where(|d| { - d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() - }); + let dialects = + all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value()); let _ = dialects.verified_only_select("SELECT CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T"); } #[test] fn parse_method_expr() { - let dialects = all_dialects_where(|d| d.supports_methods()); - let expr = dialects - .verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')"); + let expr = + verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')"); match expr { - Expr::Method(Method { expr, method_chain }) => { - assert!(matches!(*expr, Expr::Function(_))); + Expr::CompoundFieldAccess { root, access_chain } => { + assert!(matches!(*root, Expr::Function(_))); assert!(matches!( - method_chain[..], - [Function { .. }, Function { .. }] + access_chain[..], + [ + AccessExpr::Dot(Expr::Function(_)), + AccessExpr::Dot(Expr::Function(_)) + ] )); } _ => unreachable!(), } - let expr = dialects.verified_expr( + + let expr = verified_expr( "(SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')", ); match expr { - Expr::Method(Method { expr, method_chain }) => { - assert!(matches!(*expr, Expr::Subquery(_))); - assert!(matches!(method_chain[..], [Function { .. }])); + Expr::CompoundFieldAccess { root, access_chain } => { + assert!(matches!(*root, Expr::Subquery(_))); + assert!(matches!( + access_chain[..], + [AccessExpr::Dot(Expr::Function(_))] + )); } _ => unreachable!(), } - let expr = dialects.verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')"); + let expr = verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')"); match expr { - Expr::Method(Method { expr, method_chain }) => { - assert!(matches!(*expr, Expr::Cast { .. })); - assert!(matches!(method_chain[..], [Function { .. }])); + Expr::CompoundFieldAccess { root, access_chain } => { + assert!(matches!(*root, Expr::Cast { .. })); + assert!(matches!( + access_chain[..], + [AccessExpr::Dot(Expr::Function(_))] + )); } _ => unreachable!(), } // `CONVERT` support - let dialects = all_dialects_where(|d| { - d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() - }); + let dialects = + all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value()); let expr = dialects.verified_expr( "CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')", ); match expr { - Expr::Method(Method { expr, method_chain }) => { - assert!(matches!(*expr, Expr::Convert { .. })); + Expr::CompoundFieldAccess { root, access_chain } => { + assert!(matches!(*root, Expr::Convert { .. })); assert!(matches!( - method_chain[..], - [Function { .. }, Function { .. }] + access_chain[..], + [ + AccessExpr::Dot(Expr::Function(_)), + AccessExpr::Dot(Expr::Function(_)) + ] )); } _ => unreachable!(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b9a5202f0..25c152733 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2991,38 +2991,45 @@ fn parse_json_table_is_not_reserved() { fn test_composite_value() { let sql = "SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9"; let select = pg().verified_only_select(sql); + + let Expr::CompoundFieldAccess { root, access_chain } = + expr_from_projection(&select.projection[0]) + else { + unreachable!("expected projection: got {:?}", &select.projection[0]); + }; assert_eq!( - SelectItem::UnnamedExpr(Expr::CompositeAccess { - key: Ident::new("name"), - expr: Box::new(Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ - Ident::new("on_hand"), - Ident::new("item") - ])))) - }), - select.projection[0] + root.as_ref(), + &Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("on_hand"), + Ident::new("item") + ]))) + ); + assert_eq!( + access_chain.as_slice(), + &[AccessExpr::Dot(Expr::Identifier(Ident::new("name")))] ); assert_eq!( - select.selection, - Some(Expr::BinaryOp { - left: Box::new(Expr::CompositeAccess { - key: Ident::new("price"), - expr: Box::new(Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ + select.selection.as_ref().unwrap(), + &Expr::BinaryOp { + left: Box::new(Expr::CompoundFieldAccess { + root: Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![ Ident::new("on_hand"), Ident::new("item") - ])))) + ]))) + .into(), + access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("price")))] }), op: BinaryOperator::Gt, right: Box::new(Expr::Value(number("9"))) - }) + } ); let sql = "SELECT (information_schema._pg_expandarray(ARRAY['i', 'i'])).n"; let select = pg().verified_only_select(sql); assert_eq!( - SelectItem::UnnamedExpr(Expr::CompositeAccess { - key: Ident::new("n"), - expr: Box::new(Expr::Nested(Box::new(Expr::Function(Function { + &Expr::CompoundFieldAccess { + root: Box::new(Expr::Nested(Box::new(Expr::Function(Function { name: ObjectName::from(vec![ Ident::new("information_schema"), Ident::new("_pg_expandarray") @@ -3046,9 +3053,10 @@ fn test_composite_value() { filter: None, over: None, within_group: vec![], - })))) - }), - select.projection[0] + })))), + access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("n")))], + }, + expr_from_projection(&select.projection[0]) ); } From b482562618caa3efa89c2f42f87472b00a270926 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Wed, 19 Feb 2025 18:54:14 +0100 Subject: [PATCH 731/806] Add support for `EXECUTE IMMEDIATE` (#1717) --- src/ast/mod.rs | 31 +++++++++++++++++--------- src/dialect/bigquery.rs | 5 +++++ src/dialect/mod.rs | 5 +++++ src/dialect/snowflake.rs | 5 +++++ src/parser/mod.rs | 26 ++++++++++++++++------ tests/sqlparser_common.rs | 41 ++++++++++++++++++++++++++++++++-- tests/sqlparser_postgres.rs | 44 +++++++++++++++++++++++-------------- 7 files changed, 122 insertions(+), 35 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1f05d590d..efdad164e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3269,18 +3269,21 @@ pub enum Statement { /// Note: this is a PostgreSQL-specific statement. Deallocate { name: Ident, prepare: bool }, /// ```sql - /// EXECUTE name [ ( parameter [, ...] ) ] [USING ] + /// An `EXECUTE` statement /// ``` /// - /// Note: this statement is supported by Postgres and MSSQL, with slight differences in syntax. - /// /// Postgres: /// MSSQL: + /// BigQuery: + /// Snowflake: Execute { - name: ObjectName, + name: Option, parameters: Vec, has_parentheses: bool, - using: Vec, + /// Is this an `EXECUTE IMMEDIATE` + immediate: bool, + into: Vec, + using: Vec, }, /// ```sql /// PREPARE name [ ( data_type [, ...] ) ] AS statement @@ -4889,6 +4892,8 @@ impl fmt::Display for Statement { name, parameters, has_parentheses, + immediate, + into, using, } => { let (open, close) = if *has_parentheses { @@ -4896,11 +4901,17 @@ impl fmt::Display for Statement { } else { (if parameters.is_empty() { "" } else { " " }, "") }; - write!( - f, - "EXECUTE {name}{open}{}{close}", - display_comma_separated(parameters), - )?; + write!(f, "EXECUTE")?; + if *immediate { + write!(f, " IMMEDIATE")?; + } + if let Some(name) = name { + write!(f, " {name}")?; + } + write!(f, "{open}{}{close}", display_comma_separated(parameters),)?; + if !into.is_empty() { + write!(f, " INTO {}", display_comma_separated(into))?; + } if !using.is_empty() { write!(f, " USING {}", display_comma_separated(using))?; }; diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 5354a645b..b8e7e4cf0 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -110,6 +110,11 @@ impl Dialect for BigQueryDialect { true } + /// See + fn supports_execute_immediate(&self) -> bool { + true + } + // See fn supports_timestamp_versioning(&self) -> bool { true diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index cb86cf7c6..cf267a0f2 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -261,6 +261,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `EXECUTE IMMEDIATE` statements. + fn supports_execute_immediate(&self) -> bool { + false + } + /// Returns true if the dialect supports the MATCH_RECOGNIZE operation. fn supports_match_recognize(&self) -> bool { false diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index bac9c49c7..6702b94b2 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -96,6 +96,11 @@ impl Dialect for SnowflakeDialect { true } + /// See + fn supports_execute_immediate(&self) -> bool { + true + } + fn supports_match_recognize(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9d75bcdb5..0ef0cc41a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13849,7 +13849,14 @@ impl<'a> Parser<'a> { } pub fn parse_execute(&mut self) -> Result { - let name = self.parse_object_name(false)?; + let name = if self.dialect.supports_execute_immediate() + && self.parse_keyword(Keyword::IMMEDIATE) + { + None + } else { + let name = self.parse_object_name(false)?; + Some(name) + }; let has_parentheses = self.consume_token(&Token::LParen); @@ -13866,19 +13873,24 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; } - let mut using = vec![]; - if self.parse_keyword(Keyword::USING) { - using.push(self.parse_expr()?); + let into = if self.parse_keyword(Keyword::INTO) { + self.parse_comma_separated(Self::parse_identifier)? + } else { + vec![] + }; - while self.consume_token(&Token::Comma) { - using.push(self.parse_expr()?); - } + let using = if self.parse_keyword(Keyword::USING) { + self.parse_comma_separated(Self::parse_expr_with_alias)? + } else { + vec![] }; Ok(Statement::Execute { + immediate: name.is_none(), name, parameters, has_parentheses, + into, using, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 11e43daeb..3cc455b84 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10745,7 +10745,7 @@ fn parse_call() { #[test] fn parse_execute_stored_procedure() { let expected = Statement::Execute { - name: ObjectName::from(vec![ + name: Some(ObjectName::from(vec![ Ident { value: "my_schema".to_string(), quote_style: None, @@ -10756,13 +10756,15 @@ fn parse_execute_stored_procedure() { quote_style: None, span: Span::empty(), }, - ]), + ])), parameters: vec![ Expr::Value(Value::NationalStringLiteral("param1".to_string())), Expr::Value(Value::NationalStringLiteral("param2".to_string())), ], has_parentheses: false, + immediate: false, using: vec![], + into: vec![], }; assert_eq!( // Microsoft SQL Server does not use parentheses around arguments for EXECUTE @@ -10779,6 +10781,41 @@ fn parse_execute_stored_procedure() { ); } +#[test] +fn parse_execute_immediate() { + let dialects = all_dialects_where(|d| d.supports_execute_immediate()); + + let expected = Statement::Execute { + parameters: vec![Expr::Value(Value::SingleQuotedString( + "SELECT 1".to_string(), + ))], + immediate: true, + using: vec![ExprWithAlias { + expr: Expr::Value(number("1")), + alias: Some(Ident::new("b")), + }], + into: vec![Ident::new("a")], + name: None, + has_parentheses: false, + }; + + let stmt = dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1' INTO a USING 1 AS b"); + assert_eq!(expected, stmt); + + dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1' INTO a, b USING 1 AS x, y"); + dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1' USING 1 AS x, y"); + dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1' INTO a, b"); + dialects.verified_stmt("EXECUTE IMMEDIATE 'SELECT 1'"); + dialects.verified_stmt("EXECUTE 'SELECT 1'"); + + assert_eq!( + ParserError::ParserError("Expected: identifier, found: ,".to_string()), + dialects + .parse_sql_statements("EXECUTE IMMEDIATE 'SELECT 1' USING 1 AS, y") + .unwrap_err() + ); +} + #[test] fn parse_create_table_collate() { pg_and_generic().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 25c152733..7eb8086ea 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1659,10 +1659,12 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName::from(vec!["a".into()]), + name: Some(ObjectName::from(vec!["a".into()])), parameters: vec![], has_parentheses: false, - using: vec![] + using: vec![], + immediate: false, + into: vec![] } ); @@ -1670,13 +1672,15 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName::from(vec!["a".into()]), + name: Some(ObjectName::from(vec!["a".into()])), parameters: vec![ Expr::Value(number("1")), Expr::Value(Value::SingleQuotedString("t".to_string())) ], has_parentheses: true, - using: vec![] + using: vec![], + immediate: false, + into: vec![] } ); @@ -1685,23 +1689,31 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: ObjectName::from(vec!["a".into()]), + name: Some(ObjectName::from(vec!["a".into()])), parameters: vec![], has_parentheses: false, using: vec![ - Expr::Cast { - kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::Number("1337".parse().unwrap(), false))), - data_type: DataType::SmallInt(None), - format: None + ExprWithAlias { + expr: Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::Value(Value::Number("1337".parse().unwrap(), false))), + data_type: DataType::SmallInt(None), + format: None + }, + alias: None }, - Expr::Cast { - kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::Number("7331".parse().unwrap(), false))), - data_type: DataType::SmallInt(None), - format: None + ExprWithAlias { + expr: Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::Value(Value::Number("7331".parse().unwrap(), false))), + data_type: DataType::SmallInt(None), + format: None + }, + alias: None }, - ] + ], + immediate: false, + into: vec![] } ); } From 97f0be699120f2fffa57d9d6aaad0c53c484ea1b Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Wed, 19 Feb 2025 21:26:20 -0800 Subject: [PATCH 732/806] Treat COLLATE like any other column option (#1731) --- src/ast/ddl.rs | 6 ++-- src/ast/helpers/stmt_create_table.rs | 1 - src/ast/spans.rs | 8 ++--- src/parser/mod.rs | 15 +++------ tests/sqlparser_bigquery.rs | 5 --- tests/sqlparser_clickhouse.rs | 12 ------- tests/sqlparser_common.rs | 29 +++++------------ tests/sqlparser_duckdb.rs | 6 ---- tests/sqlparser_mssql.rs | 11 +++---- tests/sqlparser_mysql.rs | 48 +++++++++------------------- tests/sqlparser_postgres.rs | 42 +++++++----------------- tests/sqlparser_snowflake.rs | 24 +++++++------- tests/sqlparser_sqlite.rs | 4 --- 13 files changed, 60 insertions(+), 151 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f90252000..1fbc45603 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1206,7 +1206,6 @@ impl fmt::Display for ProcedureParam { pub struct ColumnDef { pub name: Ident, pub data_type: DataType, - pub collation: Option, pub options: Vec, } @@ -1217,9 +1216,6 @@ impl fmt::Display for ColumnDef { } else { write!(f, "{} {}", self.name, self.data_type)?; } - if let Some(collation) = &self.collation { - write!(f, " COLLATE {collation}")?; - } for option in &self.options { write!(f, " {option}")?; } @@ -1566,6 +1562,7 @@ pub enum ColumnOption { /// - ... DialectSpecific(Vec), CharacterSet(ObjectName), + Collation(ObjectName), Comment(String), OnUpdate(Expr), /// `Generated`s are modifiers that follow a column definition in a `CREATE @@ -1665,6 +1662,7 @@ impl fmt::Display for ColumnOption { Check(expr) => write!(f, "CHECK ({expr})"), DialectSpecific(val) => write!(f, "{}", display_separated(val, " ")), CharacterSet(n) => write!(f, "CHARACTER SET {n}"), + Collation(n) => write!(f, "COLLATE {n}"), Comment(v) => write!(f, "COMMENT '{}'", escape_single_quote_string(v)), OnUpdate(expr) => write!(f, "ON UPDATE {expr}"), Generated { diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 2a44cef3e..344e9dec9 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -47,7 +47,6 @@ use crate::parser::ParserError; /// .columns(vec![ColumnDef { /// name: Ident::new("c1"), /// data_type: DataType::Int(None), -/// collation: None, /// options: vec![], /// }]); /// // You can access internal elements with ease diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 091a22f04..de39e50d6 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -604,15 +604,10 @@ impl Spanned for ColumnDef { let ColumnDef { name, data_type: _, // enum - collation, options, } = self; - union_spans( - core::iter::once(name.span) - .chain(collation.iter().map(|i| i.span())) - .chain(options.iter().map(|i| i.span())), - ) + union_spans(core::iter::once(name.span).chain(options.iter().map(|i| i.span()))) } } @@ -767,6 +762,7 @@ impl Spanned for ColumnOption { ColumnOption::Check(expr) => expr.span(), ColumnOption::DialectSpecific(_) => Span::empty(), ColumnOption::CharacterSet(object_name) => object_name.span(), + ColumnOption::Collation(object_name) => object_name.span(), ColumnOption::Comment(_) => Span::empty(), ColumnOption::OnUpdate(expr) => expr.span(), ColumnOption::Generated { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0ef0cc41a..9c021d918 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6889,11 +6889,6 @@ impl<'a> Parser<'a> { } else { self.parse_data_type()? }; - let mut collation = if self.parse_keyword(Keyword::COLLATE) { - Some(self.parse_object_name(false)?) - } else { - None - }; let mut options = vec![]; loop { if self.parse_keyword(Keyword::CONSTRAINT) { @@ -6908,10 +6903,6 @@ impl<'a> Parser<'a> { } } else if let Some(option) = self.parse_optional_column_option()? { options.push(ColumnOptionDef { name: None, option }); - } else if dialect_of!(self is MySqlDialect | SnowflakeDialect | GenericDialect) - && self.parse_keyword(Keyword::COLLATE) - { - collation = Some(self.parse_object_name(false)?); } else { break; }; @@ -6919,7 +6910,6 @@ impl<'a> Parser<'a> { Ok(ColumnDef { name, data_type, - collation, options, }) } @@ -6956,6 +6946,10 @@ impl<'a> Parser<'a> { Ok(Some(ColumnOption::CharacterSet( self.parse_object_name(false)?, ))) + } else if self.parse_keywords(&[Keyword::COLLATE]) { + Ok(Some(ColumnOption::Collation( + self.parse_object_name(false)?, + ))) } else if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { Ok(Some(ColumnOption::NotNull)) } else if self.parse_keywords(&[Keyword::COMMENT]) { @@ -9047,7 +9041,6 @@ impl<'a> Parser<'a> { Ok(ColumnDef { name, data_type, - collation: None, options: Vec::new(), // No constraints expected here }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index c5dfb27b5..52aa3b3b4 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -389,7 +389,6 @@ fn parse_create_table_with_unquoted_hyphen() { vec![ColumnDef { name: Ident::new("x"), data_type: DataType::Int64, - collation: None, options: vec![] },], columns @@ -427,7 +426,6 @@ fn parse_create_table_with_options() { ColumnDef { name: Ident::new("x"), data_type: DataType::Int64, - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -447,7 +445,6 @@ fn parse_create_table_with_options() { ColumnDef { name: Ident::new("y"), data_type: DataType::Bool, - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { @@ -530,7 +527,6 @@ fn parse_nested_data_types() { ], StructBracketKind::AngleBrackets ), - collation: None, options: vec![], }, ColumnDef { @@ -544,7 +540,6 @@ fn parse_nested_data_types() { StructBracketKind::AngleBrackets ), ))), - collation: None, options: vec![], }, ] diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index b94d6f698..34f684c69 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -528,7 +528,6 @@ fn column_def(name: Ident, data_type: DataType) -> ColumnDef { ColumnDef { name, data_type, - collation: None, options: vec![], } } @@ -617,7 +616,6 @@ fn parse_create_table_with_nullable() { ColumnDef { name: "d".into(), data_type: DataType::Date32, - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Null @@ -661,7 +659,6 @@ fn parse_create_table_with_nested_data_types() { DataType::LowCardinality(Box::new(DataType::String(None))) ) ]), - collation: None, options: vec![], }, ColumnDef { @@ -678,7 +675,6 @@ fn parse_create_table_with_nested_data_types() { } ]) ))), - collation: None, options: vec![], }, ColumnDef { @@ -695,7 +691,6 @@ fn parse_create_table_with_nested_data_types() { )) }, ]), - collation: None, options: vec![], }, ColumnDef { @@ -704,7 +699,6 @@ fn parse_create_table_with_nested_data_types() { Box::new(DataType::String(None)), Box::new(DataType::UInt16) ), - collation: None, options: vec![], }, ] @@ -736,13 +730,11 @@ fn parse_create_table_with_primary_key() { ColumnDef { name: Ident::with_quote('`', "i"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ColumnDef { name: Ident::with_quote('`', "k"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ], @@ -814,7 +806,6 @@ fn parse_create_table_with_variant_default_expressions() { ColumnDef { name: Ident::new("a"), data_type: DataType::Datetime(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Materialized(Expr::Function(Function { @@ -836,7 +827,6 @@ fn parse_create_table_with_variant_default_expressions() { ColumnDef { name: Ident::new("b"), data_type: DataType::Datetime(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Ephemeral(Some(Expr::Function(Function { @@ -858,7 +848,6 @@ fn parse_create_table_with_variant_default_expressions() { ColumnDef { name: Ident::new("c"), data_type: DataType::Datetime(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Ephemeral(None) @@ -867,7 +856,6 @@ fn parse_create_table_with_variant_default_expressions() { ColumnDef { name: Ident::new("d"), data_type: DataType::String(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Alias(Expr::Function(Function { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3cc455b84..a4e83be06 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3426,7 +3426,6 @@ fn parse_create_table() { length: 100, unit: None, })), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -3435,7 +3434,6 @@ fn parse_create_table() { ColumnDef { name: "lat".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Null, @@ -3444,13 +3442,11 @@ fn parse_create_table() { ColumnDef { name: "lng".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![], }, ColumnDef { name: "constrained".into(), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -3483,7 +3479,6 @@ fn parse_create_table() { ColumnDef { name: "ref".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::ForeignKey { @@ -3498,7 +3493,6 @@ fn parse_create_table() { ColumnDef { name: "ref2".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::ForeignKey { @@ -3615,7 +3609,6 @@ fn parse_create_table_with_constraint_characteristics() { length: 100, unit: None, })), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -3624,7 +3617,6 @@ fn parse_create_table_with_constraint_characteristics() { ColumnDef { name: "lat".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Null, @@ -3633,7 +3625,6 @@ fn parse_create_table_with_constraint_characteristics() { ColumnDef { name: "lng".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![], }, ] @@ -3768,7 +3759,6 @@ fn parse_create_table_column_constraint_characteristics() { vec![ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Unique { @@ -3883,13 +3873,11 @@ fn parse_create_table_hive_array() { ColumnDef { name: Ident::new("name"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("val"), data_type: DataType::Array(expected), - collation: None, options: vec![], }, ], @@ -4255,7 +4243,6 @@ fn parse_create_external_table() { length: 100, unit: None, })), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -4264,7 +4251,6 @@ fn parse_create_external_table() { ColumnDef { name: "lat".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Null, @@ -4273,7 +4259,6 @@ fn parse_create_external_table() { ColumnDef { name: "lng".into(), data_type: DataType::Double(ExactNumberInfo::None), - collation: None, options: vec![], }, ] @@ -4326,7 +4311,6 @@ fn parse_create_or_replace_external_table() { length: 100, unit: None, })), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -10818,7 +10802,14 @@ fn parse_execute_immediate() { #[test] fn parse_create_table_collate() { - pg_and_generic().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); + all_dialects().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); + // check ordering is preserved + all_dialects().verified_stmt( + "CREATE TABLE tbl (foo INT, bar TEXT CHARACTER SET utf8mb4 COLLATE \"de_DE\")", + ); + all_dialects().verified_stmt( + "CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\" CHARACTER SET utf8mb4)", + ); } #[test] @@ -10997,7 +10988,6 @@ fn test_parse_inline_comment() { vec![ColumnDef { name: Ident::new("id".to_string()), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: Comment("comment without equal".to_string()), @@ -13584,7 +13574,6 @@ fn parse_create_table_with_enum_types() { ], Some(8) ), - collation: None, options: vec![], }, ColumnDef { @@ -13602,7 +13591,6 @@ fn parse_create_table_with_enum_types() { ], Some(16) ), - collation: None, options: vec![], }, ColumnDef { @@ -13614,7 +13602,6 @@ fn parse_create_table_with_enum_types() { ], None ), - collation: None, options: vec![], } ], diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 43e12746c..05e60d556 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -60,7 +60,6 @@ fn test_struct() { vec![ColumnDef { name: "s".into(), data_type: struct_type1.clone(), - collation: None, options: vec![], }] ); @@ -75,7 +74,6 @@ fn test_struct() { Box::new(struct_type1), None )), - collation: None, options: vec![], }] ); @@ -120,7 +118,6 @@ fn test_struct() { Box::new(struct_type2), None )), - collation: None, options: vec![], }] ); @@ -671,7 +668,6 @@ fn test_duckdb_union_datatype() { field_name: "a".into(), field_type: DataType::Int(None) }]), - collation: Default::default(), options: Default::default() }, ColumnDef { @@ -686,7 +682,6 @@ fn test_duckdb_union_datatype() { field_type: DataType::Int(None) } ]), - collation: Default::default(), options: Default::default() }, ColumnDef { @@ -698,7 +693,6 @@ fn test_duckdb_union_datatype() { field_type: DataType::Int(None) }]) }]), - collation: Default::default(), options: Default::default() } ], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 6865bdd4e..7b1277599 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1496,7 +1496,6 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - collation: None, options: vec![], }, ColumnDef { @@ -1506,7 +1505,7 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - collation: None, + options: vec![], }, ColumnDef { @@ -1516,7 +1515,7 @@ fn parse_create_table_with_valid_options() { span: Span::empty(), }, data_type: Int(None,), - collation: None, + options: vec![], }, ], @@ -1671,7 +1670,7 @@ fn parse_create_table_with_identity_column() { span: Span::empty(), }, data_type: Int(None,), - collation: None, + options: column_options, },], constraints: vec![], @@ -1815,7 +1814,7 @@ fn parse_mssql_varbinary_max_length() { vec![ColumnDef { name: Ident::new("var_binary_col"), data_type: Varbinary(Some(BinaryLength::Max)), - collation: None, + options: vec![] },], ); @@ -1840,7 +1839,7 @@ fn parse_mssql_varbinary_max_length() { vec![ColumnDef { name: Ident::new("var_binary_col"), data_type: Varbinary(Some(BinaryLength::IntegerLength { length: 50 })), - collation: None, + options: vec![] },], ); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 48fcbbf9b..44c8350fa 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -636,7 +636,6 @@ fn parse_create_table_auto_increment() { vec![ColumnDef { name: Ident::new("bar"), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -726,7 +725,6 @@ fn parse_create_table_primary_and_unique_key() { ColumnDef { name: Ident::new("id"), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -746,7 +744,6 @@ fn parse_create_table_primary_and_unique_key() { ColumnDef { name: Ident::new("bar"), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -896,7 +893,6 @@ fn parse_create_table_set_enum() { ColumnDef { name: Ident::new("bar"), data_type: DataType::Set(vec!["a".to_string(), "b".to_string()]), - collation: None, options: vec![], }, ColumnDef { @@ -908,7 +904,6 @@ fn parse_create_table_set_enum() { ], None ), - collation: None, options: vec![], } ], @@ -935,7 +930,6 @@ fn parse_create_table_engine_default_charset() { vec![ColumnDef { name: Ident::new("id"), data_type: DataType::Int(Some(11)), - collation: None, options: vec![], },], columns @@ -968,7 +962,6 @@ fn parse_create_table_collate() { vec![ColumnDef { name: Ident::new("id"), data_type: DataType::Int(Some(11)), - collation: None, options: vec![], },], columns @@ -1016,7 +1009,6 @@ fn parse_create_table_comment_character_set() { vec![ColumnDef { name: Ident::new("s"), data_type: DataType::Text, - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -1063,7 +1055,6 @@ fn parse_quote_identifiers() { vec![ColumnDef { name: Ident::with_quote('`', "BEGIN"), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Unique { @@ -1326,31 +1317,26 @@ fn parse_create_table_with_minimum_display_width() { ColumnDef { name: Ident::new("bar_tinyint"), data_type: DataType::TinyInt(Some(3)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_smallint"), data_type: DataType::SmallInt(Some(5)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_mediumint"), data_type: DataType::MediumInt(Some(6)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_int"), data_type: DataType::Int(Some(11)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_bigint"), data_type: DataType::BigInt(Some(20)), - collation: None, options: vec![], } ], @@ -1372,31 +1358,26 @@ fn parse_create_table_unsigned() { ColumnDef { name: Ident::new("bar_tinyint"), data_type: DataType::UnsignedTinyInt(Some(3)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_smallint"), data_type: DataType::UnsignedSmallInt(Some(5)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_mediumint"), data_type: DataType::UnsignedMediumInt(Some(13)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_int"), data_type: DataType::UnsignedInt(Some(11)), - collation: None, options: vec![], }, ColumnDef { name: Ident::new("bar_bigint"), data_type: DataType::UnsignedBigInt(Some(20)), - collation: None, options: vec![], }, ], @@ -2159,7 +2140,6 @@ fn parse_alter_table_add_column() { column_def: ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![], }, column_position: Some(MySQLColumnPosition::First), @@ -2189,7 +2169,6 @@ fn parse_alter_table_add_column() { column_def: ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![], }, column_position: Some(MySQLColumnPosition::After(Ident { @@ -2229,7 +2208,6 @@ fn parse_alter_table_add_columns() { column_def: ColumnDef { name: "a".into(), data_type: DataType::Text, - collation: None, options: vec![], }, column_position: Some(MySQLColumnPosition::First), @@ -2240,7 +2218,6 @@ fn parse_alter_table_add_columns() { column_def: ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![], }, column_position: Some(MySQLColumnPosition::After(Ident { @@ -2593,7 +2570,6 @@ fn parse_table_column_option_on_update() { vec![ColumnDef { name: Ident::with_quote('`', "modification_time"), data_type: DataType::Datetime(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::OnUpdate(call("CURRENT_TIMESTAMP", [])), @@ -2878,21 +2854,27 @@ fn parse_convert_using() { #[test] fn parse_create_table_with_column_collate() { let sql = "CREATE TABLE tb (id TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci)"; - let canonical = "CREATE TABLE tb (id TEXT COLLATE utf8mb4_0900_ai_ci CHARACTER SET utf8mb4)"; - match mysql().one_statement_parses_to(sql, canonical) { + match mysql().verified_stmt(sql) { Statement::CreateTable(CreateTable { name, columns, .. }) => { assert_eq!(name.to_string(), "tb"); assert_eq!( vec![ColumnDef { name: Ident::new("id"), data_type: DataType::Text, - collation: Some(ObjectName::from(vec![Ident::new("utf8mb4_0900_ai_ci")])), - options: vec![ColumnOptionDef { - name: None, - option: ColumnOption::CharacterSet(ObjectName::from(vec![Ident::new( - "utf8mb4" - )])) - }], + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::CharacterSet(ObjectName::from(vec![Ident::new( + "utf8mb4" + )])) + }, + ColumnOptionDef { + name: None, + option: ColumnOption::Collation(ObjectName::from(vec![Ident::new( + "utf8mb4_0900_ai_ci" + )])) + } + ], },], columns ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7eb8086ea..d400792d9 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -363,7 +363,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "customer_id".into(), data_type: DataType::Integer(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Default( @@ -374,7 +373,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "store_id".into(), data_type: DataType::SmallInt(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -388,7 +386,6 @@ fn parse_create_table_with_defaults() { unit: None } )), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull, @@ -402,11 +399,18 @@ fn parse_create_table_with_defaults() { unit: None } )), - collation: Some(ObjectName::from(vec![Ident::with_quote('"', "es_ES")])), - options: vec![ColumnOptionDef { - name: None, - option: ColumnOption::NotNull, - }], + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Collation(ObjectName::from(vec![ + Ident::with_quote('"', "es_ES") + ])), + }, + ColumnOptionDef { + name: None, + option: ColumnOption::NotNull, + } + ], }, ColumnDef { name: "email".into(), @@ -416,13 +420,11 @@ fn parse_create_table_with_defaults() { unit: None } )), - collation: None, options: vec![], }, ColumnDef { name: "address_id".into(), data_type: DataType::SmallInt(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull @@ -431,7 +433,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "activebool".into(), data_type: DataType::Boolean, - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -446,7 +447,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "create_date".into(), data_type: DataType::Date, - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -461,7 +461,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "last_update".into(), data_type: DataType::Timestamp(None, TimezoneInfo::WithoutTimeZone), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -476,7 +475,6 @@ fn parse_create_table_with_defaults() { ColumnDef { name: "active".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::NotNull @@ -842,7 +840,6 @@ fn parse_alter_table_add_columns() { column_def: ColumnDef { name: "a".into(), data_type: DataType::Text, - collation: None, options: vec![], }, column_position: None, @@ -853,7 +850,6 @@ fn parse_alter_table_add_columns() { column_def: ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![], }, column_position: None, @@ -4291,37 +4287,31 @@ fn parse_create_table_with_alias() { ColumnDef { name: "int8_col".into(), data_type: DataType::Int8(None), - collation: None, options: vec![] }, ColumnDef { name: "int4_col".into(), data_type: DataType::Int4(None), - collation: None, options: vec![] }, ColumnDef { name: "int2_col".into(), data_type: DataType::Int2(None), - collation: None, options: vec![] }, ColumnDef { name: "float8_col".into(), data_type: DataType::Float8, - collation: None, options: vec![] }, ColumnDef { name: "float4_col".into(), data_type: DataType::Float4, - collation: None, options: vec![] }, ColumnDef { name: "bool_col".into(), data_type: DataType::Bool, - collation: None, options: vec![] }, ] @@ -4343,13 +4333,11 @@ fn parse_create_table_with_partition_by() { ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![] }, ColumnDef { name: "b".into(), data_type: DataType::Text, - collation: None, options: vec![] } ], @@ -5093,25 +5081,21 @@ fn parse_trigger_related_functions() { ColumnDef { name: "empname".into(), data_type: DataType::Text, - collation: None, options: vec![], }, ColumnDef { name: "salary".into(), data_type: DataType::Integer(None), - collation: None, options: vec![], }, ColumnDef { name: "last_date".into(), data_type: DataType::Timestamp(None, TimezoneInfo::None), - collation: None, options: vec![], }, ColumnDef { name: "last_user".into(), data_type: DataType::Text, - collation: None, options: vec![], }, ], @@ -5445,13 +5429,11 @@ fn parse_varbit_datatype() { ColumnDef { name: "x".into(), data_type: DataType::VarBit(None), - collation: None, options: vec![], }, ColumnDef { name: "y".into(), data_type: DataType::VarBit(Some(42)), - collation: None, options: vec![], } ] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b0c215a5a..76550b8e3 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -346,7 +346,6 @@ fn test_snowflake_create_table_column_comment() { name: None, option: ColumnOption::Comment("some comment".to_string()) }], - collation: None }], columns ) @@ -553,7 +552,6 @@ fn test_snowflake_create_table_with_autoincrement_columns() { ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Identity(IdentityPropertyKind::Autoincrement( @@ -567,7 +565,6 @@ fn test_snowflake_create_table_with_autoincrement_columns() { ColumnDef { name: "b".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Identity(IdentityPropertyKind::Autoincrement( @@ -586,7 +583,6 @@ fn test_snowflake_create_table_with_autoincrement_columns() { ColumnDef { name: "c".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Identity(IdentityPropertyKind::Identity( @@ -600,7 +596,6 @@ fn test_snowflake_create_table_with_autoincrement_columns() { ColumnDef { name: "d".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Identity(IdentityPropertyKind::Identity( @@ -634,8 +629,12 @@ fn test_snowflake_create_table_with_collated_column() { vec![ColumnDef { name: "a".into(), data_type: DataType::Text, - collation: Some(ObjectName::from(vec![Ident::with_quote('\'', "de_DE")])), - options: vec![] + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Collation(ObjectName::from(vec![Ident::with_quote( + '\'', "de_DE" + )])), + }] },] ); } @@ -674,7 +673,6 @@ fn test_snowflake_create_table_with_columns_masking_policy() { vec![ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Policy(ColumnPolicy::MaskingPolicy( @@ -709,7 +707,6 @@ fn test_snowflake_create_table_with_columns_projection_policy() { vec![ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( @@ -747,7 +744,6 @@ fn test_snowflake_create_table_with_columns_tags() { vec![ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ColumnOptionDef { name: None, option: ColumnOption::Tags(TagsColumnOption { @@ -782,7 +778,6 @@ fn test_snowflake_create_table_with_several_column_options() { ColumnDef { name: "a".into(), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -818,8 +813,13 @@ fn test_snowflake_create_table_with_several_column_options() { ColumnDef { name: "b".into(), data_type: DataType::Text, - collation: Some(ObjectName::from(vec![Ident::with_quote('\'', "de_DE")])), options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Collation(ObjectName::from(vec![ + Ident::with_quote('\'', "de_DE") + ])), + }, ColumnOptionDef { name: None, option: ColumnOption::Policy(ColumnPolicy::ProjectionPolicy( diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 3a612f70a..c17743305 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -214,7 +214,6 @@ fn parse_create_table_auto_increment() { vec![ColumnDef { name: "bar".into(), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -243,7 +242,6 @@ fn parse_create_table_primary_key_asc_desc() { let expected_column_def = |kind| ColumnDef { name: "bar".into(), data_type: DataType::Int(None), - collation: None, options: vec![ ColumnOptionDef { name: None, @@ -286,13 +284,11 @@ fn parse_create_sqlite_quote() { ColumnDef { name: Ident::with_quote('"', "KEY"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ColumnDef { name: Ident::with_quote('[', "INDEX"), data_type: DataType::Int(None), - collation: None, options: vec![], }, ], From 339239d0c54fd0fe12e05d8de868340d07030ffa Mon Sep 17 00:00:00 2001 From: benrsatori Date: Thu, 20 Feb 2025 20:50:32 +0200 Subject: [PATCH 733/806] Add support for PostgreSQL/Redshift geometric operators (#1723) --- src/ast/data_type.rs | 36 ++++ src/ast/mod.rs | 12 +- src/ast/operator.rs | 84 +++++++++ src/dialect/mod.rs | 36 +++- src/dialect/postgresql.rs | 4 + src/dialect/redshift.rs | 4 + src/keywords.rs | 6 + src/parser/mod.rs | 107 ++++++++++- src/tokenizer.rs | 184 +++++++++++++++++- tests/sqlparser_common.rs | 358 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 4 - 11 files changed, 809 insertions(+), 26 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index bd46f8bdb..cae8ca8f0 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -386,6 +386,10 @@ pub enum DataType { /// /// [bigquery]: https://cloud.google.com/bigquery/docs/user-defined-functions#templated-sql-udf-parameters AnyType, + /// geometric type + /// + /// [Postgres]: https://www.postgresql.org/docs/9.5/functions-geometry.html + GeometricType(GeometricTypeKind), } impl fmt::Display for DataType { @@ -639,6 +643,7 @@ impl fmt::Display for DataType { DataType::Trigger => write!(f, "TRIGGER"), DataType::AnyType => write!(f, "ANY TYPE"), DataType::Table(fields) => write!(f, "TABLE({})", display_comma_separated(fields)), + DataType::GeometricType(kind) => write!(f, "{}", kind), } } } @@ -915,3 +920,34 @@ pub enum ArrayElemTypeDef { /// `Array(Int64)` Parenthesis(Box), } + +/// 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 +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum GeometricTypeKind { + Point, + Line, + LineSegment, + GeometricBox, + GeometricPath, + Polygon, + Circle, +} + +impl fmt::Display for GeometricTypeKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GeometricTypeKind::Point => write!(f, "point"), + GeometricTypeKind::Line => write!(f, "line"), + GeometricTypeKind::LineSegment => write!(f, "lseg"), + GeometricTypeKind::GeometricBox => write!(f, "box"), + GeometricTypeKind::GeometricPath => write!(f, "path"), + GeometricTypeKind::Polygon => write!(f, "polygon"), + GeometricTypeKind::Circle => write!(f, "circle"), + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index efdad164e..dc50871ce 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -95,6 +95,8 @@ use crate::ast::helpers::stmt_data_loading::{ #[cfg(feature = "visitor")] pub use visitor::*; +pub use self::data_type::GeometricTypeKind; + mod data_type; mod dcl; mod ddl; @@ -1513,7 +1515,15 @@ impl fmt::Display for Expr { Expr::UnaryOp { op, expr } => { if op == &UnaryOperator::PGPostfixFactorial { write!(f, "{expr}{op}") - } else if op == &UnaryOperator::Not { + } else if matches!( + op, + UnaryOperator::Not + | UnaryOperator::Hash + | UnaryOperator::AtDashAt + | UnaryOperator::DoubleAt + | UnaryOperator::QuestionDash + | UnaryOperator::QuestionPipe + ) { write!(f, "{op} {expr}") } else { write!(f, "{op}{expr}") diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 1f9a6b82b..66a35fee5 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -53,6 +53,21 @@ pub enum UnaryOperator { PGAbs, /// Unary logical not operator: e.g. `! false` (Hive-specific) BangNot, + /// `#` Number of points in path or polygon (PostgreSQL/Redshift geometric operator) + /// see + Hash, + /// `@-@` Length or circumference (PostgreSQL/Redshift geometric operator) + /// see + AtDashAt, + /// `@@` Center (PostgreSQL/Redshift geometric operator) + /// see + DoubleAt, + /// `?-` Is horizontal? (PostgreSQL/Redshift geometric operator) + /// see + QuestionDash, + /// `?|` Is vertical? (PostgreSQL/Redshift geometric operator) + /// see + QuestionPipe, } impl fmt::Display for UnaryOperator { @@ -68,6 +83,11 @@ impl fmt::Display for UnaryOperator { UnaryOperator::PGPrefixFactorial => "!!", UnaryOperator::PGAbs => "@", UnaryOperator::BangNot => "!", + UnaryOperator::Hash => "#", + UnaryOperator::AtDashAt => "@-@", + UnaryOperator::DoubleAt => "@@", + UnaryOperator::QuestionDash => "?-", + UnaryOperator::QuestionPipe => "?|", }) } } @@ -253,6 +273,54 @@ pub enum BinaryOperator { /// Specifies a test for an overlap between two datetime periods: /// Overlaps, + /// `##` Point of closest proximity (PostgreSQL/Redshift geometric operator) + /// See + DoubleHash, + /// `<->` Distance between (PostgreSQL/Redshift geometric operator) + /// See + LtDashGt, + /// `&<` Overlaps to left? (PostgreSQL/Redshift geometric operator) + /// See + AndLt, + /// `&>` Overlaps to right? (PostgreSQL/Redshift geometric operator) + /// See + AndGt, + /// `<<|` Is strictly below? (PostgreSQL/Redshift geometric operator) + /// See + LtLtPipe, + /// `|>>` Is strictly above? (PostgreSQL/Redshift geometric operator) + /// See + PipeGtGt, + /// `&<|` Does not extend above? (PostgreSQL/Redshift geometric operator) + /// See + AndLtPipe, + /// `|&>` Does not extend below? (PostgreSQL/Redshift geometric operator) + /// See + PipeAndGt, + /// `<^` Is below? (PostgreSQL/Redshift geometric operator) + /// See + LtCaret, + /// `>^` Is above? (PostgreSQL/Redshift geometric operator) + /// See + GtCaret, + /// `?#` Intersects? (PostgreSQL/Redshift geometric operator) + /// See + QuestionHash, + /// `?-` Is horizontal? (PostgreSQL/Redshift geometric operator) + /// See + QuestionDash, + /// `?-|` Is perpendicular? (PostgreSQL/Redshift geometric operator) + /// See + QuestionDashPipe, + /// `?||` Are Parallel? (PostgreSQL/Redshift geometric operator) + /// See + QuestionDoublePipe, + /// `@` Contained or on? (PostgreSQL/Redshift geometric operator) + /// See + At, + /// `~=` Same as? (PostgreSQL/Redshift geometric operator) + /// See + TildeEq, } impl fmt::Display for BinaryOperator { @@ -310,6 +378,22 @@ impl fmt::Display for BinaryOperator { write!(f, "OPERATOR({})", display_separated(idents, ".")) } BinaryOperator::Overlaps => f.write_str("OVERLAPS"), + BinaryOperator::DoubleHash => f.write_str("##"), + BinaryOperator::LtDashGt => f.write_str("<->"), + BinaryOperator::AndLt => f.write_str("&<"), + BinaryOperator::AndGt => f.write_str("&>"), + BinaryOperator::LtLtPipe => f.write_str("<<|"), + BinaryOperator::PipeGtGt => f.write_str("|>>"), + BinaryOperator::AndLtPipe => f.write_str("&<|"), + BinaryOperator::PipeAndGt => f.write_str("|&>"), + BinaryOperator::LtCaret => f.write_str("<^"), + BinaryOperator::GtCaret => f.write_str(">^"), + BinaryOperator::QuestionHash => f.write_str("?#"), + BinaryOperator::QuestionDash => f.write_str("?-"), + BinaryOperator::QuestionDashPipe => f.write_str("?-|"), + BinaryOperator::QuestionDoublePipe => f.write_str("?||"), + BinaryOperator::At => f.write_str("@"), + BinaryOperator::TildeEq => f.write_str("~="), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index cf267a0f2..86e23c86f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -599,18 +599,34 @@ pub trait Dialect: Debug + Any { | Token::ExclamationMarkDoubleTilde | Token::ExclamationMarkDoubleTildeAsterisk | Token::Spaceship => Ok(p!(Eq)), - Token::Pipe => Ok(p!(Pipe)), + Token::Pipe + | Token::QuestionMarkDash + | Token::DoubleSharp + | Token::Overlap + | Token::AmpersandLeftAngleBracket + | Token::AmpersandRightAngleBracket + | Token::QuestionMarkDashVerticalBar + | Token::AmpersandLeftAngleBracketVerticalBar + | Token::VerticalBarAmpersandRightAngleBracket + | Token::TwoWayArrow + | Token::LeftAngleBracketCaret + | Token::RightAngleBracketCaret + | Token::QuestionMarkSharp + | Token::QuestionMarkDoubleVerticalBar + | Token::QuestionPipe + | Token::TildeEqual + | Token::AtSign + | Token::ShiftLeftVerticalBar + | Token::VerticalBarShiftRight => Ok(p!(Pipe)), Token::Caret | Token::Sharp | Token::ShiftRight | Token::ShiftLeft => Ok(p!(Caret)), Token::Ampersand => Ok(p!(Ampersand)), Token::Plus | Token::Minus => Ok(p!(PlusMinus)), Token::Mul | Token::Div | Token::DuckIntDiv | Token::Mod | Token::StringConcat => { Ok(p!(MulDivModOp)) } - Token::DoubleColon - | Token::ExclamationMark - | Token::LBracket - | Token::Overlap - | Token::CaretAt => Ok(p!(DoubleColon)), + Token::DoubleColon | Token::ExclamationMark | Token::LBracket | Token::CaretAt => { + Ok(p!(DoubleColon)) + } Token::Arrow | Token::LongArrow | Token::HashArrow @@ -622,7 +638,6 @@ pub trait Dialect: Debug + Any { | Token::AtAt | Token::Question | Token::QuestionAnd - | Token::QuestionPipe | Token::CustomBinaryOperator(_) => Ok(p!(PgOther)), _ => Ok(self.prec_unknown()), } @@ -921,6 +936,13 @@ pub trait Dialect: Debug + Any { fn supports_array_typedef_size(&self) -> bool { false } + /// Returns true if the dialect supports geometric types. + /// + /// Postgres: + /// e.g. @@ circle '((0,0),10)' + fn supports_geometric_types(&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 3a3f0e4c3..a20cfac4c 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -250,4 +250,8 @@ impl Dialect for PostgreSqlDialect { fn supports_array_typedef_size(&self) -> bool { true } + + fn supports_geometric_types(&self) -> bool { + true + } } diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index a4522bbf8..3dda762c3 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -113,4 +113,8 @@ impl Dialect for RedshiftSqlDialect { fn supports_string_escape_constant(&self) -> bool { true } + + fn supports_geometric_types(&self) -> bool { + true + } } diff --git a/src/keywords.rs b/src/keywords.rs index a67f6bc76..46c40fbaa 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -141,6 +141,7 @@ define_keywords!( BOOL, BOOLEAN, BOTH, + BOX, BROWSE, BTREE, BUCKET, @@ -175,6 +176,7 @@ define_keywords!( CHARSET, CHAR_LENGTH, CHECK, + CIRCLE, CLEAR, CLOB, CLONE, @@ -478,6 +480,7 @@ define_keywords!( LIKE, LIKE_REGEX, LIMIT, + LINE, LINES, LIST, LISTEN, @@ -499,6 +502,7 @@ define_keywords!( LOWER, LOW_PRIORITY, LS, + LSEG, MACRO, MANAGE, MANAGED, @@ -653,7 +657,9 @@ define_keywords!( PLACING, PLAN, PLANS, + POINT, POLICY, + POLYGON, POOL, PORTION, POSITION, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9c021d918..69268bc51 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1220,7 +1220,17 @@ impl<'a> Parser<'a> { Keyword::MAP if *self.peek_token_ref() == Token::LBrace && self.dialect.support_map_literal_syntax() => { Ok(Some(self.parse_duckdb_map_literal()?)) } - _ => Ok(None) + _ if self.dialect.supports_geometric_types() => match w.keyword { + Keyword::CIRCLE => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Circle)?)), + Keyword::BOX => Ok(Some(self.parse_geometric_type(GeometricTypeKind::GeometricBox)?)), + Keyword::PATH => Ok(Some(self.parse_geometric_type(GeometricTypeKind::GeometricPath)?)), + Keyword::LINE => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Line)?)), + Keyword::LSEG => Ok(Some(self.parse_geometric_type(GeometricTypeKind::LineSegment)?)), + Keyword::POINT => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Point)?)), + Keyword::POLYGON => Ok(Some(self.parse_geometric_type(GeometricTypeKind::Polygon)?)), + _ => Ok(None), + }, + _ => Ok(None), } } @@ -1400,6 +1410,33 @@ impl<'a> Parser<'a> { ), }) } + tok @ Token::Sharp + | tok @ Token::AtDashAt + | tok @ Token::AtAt + | tok @ Token::QuestionMarkDash + | tok @ Token::QuestionPipe + if self.dialect.supports_geometric_types() => + { + let op = match tok { + Token::Sharp => UnaryOperator::Hash, + Token::AtDashAt => UnaryOperator::AtDashAt, + Token::AtAt => UnaryOperator::DoubleAt, + Token::QuestionMarkDash => UnaryOperator::QuestionDash, + Token::QuestionPipe => UnaryOperator::QuestionPipe, + _ => { + return Err(ParserError::ParserError(format!( + "Unexpected token in unary operator parsing: {:?}", + tok + ))) + } + }; + Ok(Expr::UnaryOp { + op, + expr: Box::new( + self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?, + ), + }) + } Token::EscapedStringLiteral(_) if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { self.prev_token(); @@ -1465,6 +1502,14 @@ impl<'a> Parser<'a> { } } + fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result { + let value: Value = self.parse_value()?; + Ok(Expr::TypedString { + data_type: DataType::GeometricType(kind), + value, + }) + } + /// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`. /// If all the fields are `Expr::Identifier`s, return an [Expr::CompoundIdentifier] instead. /// If only the root exists, return the root. @@ -3070,15 +3115,18 @@ impl<'a> Parser<'a> { Token::DuckIntDiv if dialect_is!(dialect is DuckDbDialect | GenericDialect) => { Some(BinaryOperator::DuckIntegerDivide) } - Token::ShiftLeft if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { + Token::ShiftLeft if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect | RedshiftSqlDialect) => { Some(BinaryOperator::PGBitwiseShiftLeft) } - Token::ShiftRight if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect) => { + Token::ShiftRight if dialect_is!(dialect is PostgreSqlDialect | DuckDbDialect | GenericDialect | RedshiftSqlDialect) => { Some(BinaryOperator::PGBitwiseShiftRight) } - Token::Sharp if dialect_is!(dialect is PostgreSqlDialect) => { + Token::Sharp if dialect_is!(dialect is PostgreSqlDialect | RedshiftSqlDialect) => { Some(BinaryOperator::PGBitwiseXor) } + Token::Overlap if dialect_is!(dialect is PostgreSqlDialect | RedshiftSqlDialect) => { + Some(BinaryOperator::PGOverlap) + } Token::Overlap if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => { Some(BinaryOperator::PGOverlap) } @@ -3106,6 +3154,57 @@ impl<'a> Parser<'a> { Token::QuestionAnd => Some(BinaryOperator::QuestionAnd), Token::QuestionPipe => Some(BinaryOperator::QuestionPipe), Token::CustomBinaryOperator(s) => Some(BinaryOperator::Custom(s.clone())), + Token::DoubleSharp if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::DoubleHash) + } + + Token::AmpersandLeftAngleBracket if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::AndLt) + } + Token::AmpersandRightAngleBracket if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::AndGt) + } + Token::QuestionMarkDash if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::QuestionDash) + } + Token::AmpersandLeftAngleBracketVerticalBar + if self.dialect.supports_geometric_types() => + { + Some(BinaryOperator::AndLtPipe) + } + Token::VerticalBarAmpersandRightAngleBracket + if self.dialect.supports_geometric_types() => + { + Some(BinaryOperator::PipeAndGt) + } + Token::TwoWayArrow if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::LtDashGt) + } + Token::LeftAngleBracketCaret if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::LtCaret) + } + Token::RightAngleBracketCaret if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::GtCaret) + } + Token::QuestionMarkSharp if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::QuestionHash) + } + Token::QuestionMarkDoubleVerticalBar if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::QuestionDoublePipe) + } + Token::QuestionMarkDashVerticalBar if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::QuestionDashPipe) + } + Token::TildeEqual if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::TildeEq) + } + Token::ShiftLeftVerticalBar if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::LtLtPipe) + } + Token::VerticalBarShiftRight if self.dialect.supports_geometric_types() => { + Some(BinaryOperator::PipeGtGt) + } + Token::AtSign if self.dialect.supports_geometric_types() => Some(BinaryOperator::At), Token::Word(w) => match w.keyword { Keyword::AND => Some(BinaryOperator::And), diff --git a/src/tokenizer.rs b/src/tokenizer.rs index d4e530c9d..bc0f0efeb 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -170,8 +170,10 @@ pub enum Token { RBrace, /// Right Arrow `=>` RArrow, - /// Sharp `#` used for PostgreSQL Bitwise XOR operator + /// Sharp `#` used for PostgreSQL Bitwise XOR operator, also PostgreSQL/Redshift geometrical unary/binary operator (Number of points in path or polygon/Intersection) Sharp, + /// `##` PostgreSQL/Redshift geometrical binary operator (Point of closest proximity) + DoubleSharp, /// Tilde `~` used for PostgreSQL Bitwise NOT operator or case sensitive match regular expression operator Tilde, /// `~*` , a case insensitive match regular expression operator in PostgreSQL @@ -198,7 +200,7 @@ pub enum Token { ExclamationMark, /// Double Exclamation Mark `!!` used for PostgreSQL prefix factorial operator DoubleExclamationMark, - /// AtSign `@` used for PostgreSQL abs operator + /// AtSign `@` used for PostgreSQL abs operator, also PostgreSQL/Redshift geometrical unary/binary operator (Center, Contained or on) AtSign, /// `^@`, a "starts with" string operator in PostgreSQL CaretAt, @@ -214,6 +216,36 @@ pub enum Token { LongArrow, /// `#>`, extracts JSON sub-object at the specified path HashArrow, + /// `@-@` PostgreSQL/Redshift geometrical unary operator (Length or circumference) + AtDashAt, + /// `?-` PostgreSQL/Redshift geometrical unary/binary operator (Is horizontal?/Are horizontally aligned?) + QuestionMarkDash, + /// `&<` PostgreSQL/Redshift geometrical binary operator (Overlaps to left?) + AmpersandLeftAngleBracket, + /// `&>` PostgreSQL/Redshift geometrical binary operator (Overlaps to right?)` + AmpersandRightAngleBracket, + /// `&<|` PostgreSQL/Redshift geometrical binary operator (Does not extend above?)` + AmpersandLeftAngleBracketVerticalBar, + /// `|&>` PostgreSQL/Redshift geometrical binary operator (Does not extend below?)` + VerticalBarAmpersandRightAngleBracket, + /// `<->` PostgreSQL/Redshift geometrical binary operator (Distance between) + TwoWayArrow, + /// `<^` PostgreSQL/Redshift geometrical binary operator (Is below?) + LeftAngleBracketCaret, + /// `>^` PostgreSQL/Redshift geometrical binary operator (Is above?) + RightAngleBracketCaret, + /// `?#` PostgreSQL/Redshift geometrical binary operator (Intersects or overlaps) + QuestionMarkSharp, + /// `?-|` PostgreSQL/Redshift geometrical binary operator (Is perpendicular?) + QuestionMarkDashVerticalBar, + /// `?||` PostgreSQL/Redshift geometrical binary operator (Are parallel?) + QuestionMarkDoubleVerticalBar, + /// `~=` PostgreSQL/Redshift geometrical binary operator (Same as) + TildeEqual, + /// `<<| PostgreSQL/Redshift geometrical binary operator (Is strictly below?) + ShiftLeftVerticalBar, + /// `|>> PostgreSQL/Redshift geometrical binary operator (Is strictly above?) + VerticalBarShiftRight, /// `#>>`, extracts JSON sub-object at the specified path as text HashLongArrow, /// jsonb @> jsonb -> boolean: Test whether left json contains the right json @@ -303,6 +335,7 @@ impl fmt::Display for Token { Token::RBrace => f.write_str("}"), Token::RArrow => f.write_str("=>"), Token::Sharp => f.write_str("#"), + Token::DoubleSharp => f.write_str("##"), Token::ExclamationMark => f.write_str("!"), Token::DoubleExclamationMark => f.write_str("!!"), Token::Tilde => f.write_str("~"), @@ -320,6 +353,21 @@ impl fmt::Display for Token { Token::Overlap => f.write_str("&&"), Token::PGSquareRoot => f.write_str("|/"), Token::PGCubeRoot => f.write_str("||/"), + Token::AtDashAt => f.write_str("@-@"), + Token::QuestionMarkDash => f.write_str("?-"), + Token::AmpersandLeftAngleBracket => f.write_str("&<"), + Token::AmpersandRightAngleBracket => f.write_str("&>"), + Token::AmpersandLeftAngleBracketVerticalBar => f.write_str("&<|"), + Token::VerticalBarAmpersandRightAngleBracket => f.write_str("|&>"), + Token::TwoWayArrow => f.write_str("<->"), + Token::LeftAngleBracketCaret => f.write_str("<^"), + Token::RightAngleBracketCaret => f.write_str(">^"), + Token::QuestionMarkSharp => f.write_str("?#"), + Token::QuestionMarkDashVerticalBar => f.write_str("?-|"), + Token::QuestionMarkDoubleVerticalBar => f.write_str("?||"), + Token::TildeEqual => f.write_str("~="), + Token::ShiftLeftVerticalBar => f.write_str("<<|"), + Token::VerticalBarShiftRight => f.write_str("|>>"), Token::Placeholder(ref s) => write!(f, "{s}"), Token::Arrow => write!(f, "->"), Token::LongArrow => write!(f, "->>"), @@ -1308,6 +1356,28 @@ impl<'a> Tokenizer<'a> { _ => self.start_binop(chars, "||", Token::StringConcat), } } + Some('&') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('>') => self.consume_for_binop( + chars, + "|&>", + Token::VerticalBarAmpersandRightAngleBracket, + ), + _ => self.start_binop_opt(chars, "|&", None), + } + } + Some('>') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('>') => self.consume_for_binop( + chars, + "|>>", + Token::VerticalBarShiftRight, + ), + _ => self.start_binop_opt(chars, "|>", None), + } + } // Bitshift '|' operator _ => self.start_binop(chars, "|", Token::Pipe), } @@ -1356,8 +1426,34 @@ impl<'a> Tokenizer<'a> { _ => self.start_binop(chars, "<=", Token::LtEq), } } + Some('|') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, "<<|", Token::ShiftLeftVerticalBar) + } Some('>') => self.consume_for_binop(chars, "<>", Token::Neq), + Some('<') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('|') => self.consume_for_binop( + chars, + "<<|", + Token::ShiftLeftVerticalBar, + ), + _ => self.start_binop(chars, "<<", Token::ShiftLeft), + } + } Some('<') => self.consume_for_binop(chars, "<<", Token::ShiftLeft), + Some('-') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('>') => { + self.consume_for_binop(chars, "<->", Token::TwoWayArrow) + } + _ => self.start_binop_opt(chars, "<-", None), + } + } + Some('^') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, "<^", Token::LeftAngleBracketCaret) + } Some('@') => self.consume_for_binop(chars, "<@", Token::ArrowAt), _ => self.start_binop(chars, "<", Token::Lt), } @@ -1367,6 +1463,9 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('=') => self.consume_for_binop(chars, ">=", Token::GtEq), Some('>') => self.consume_for_binop(chars, ">>", Token::ShiftRight), + Some('^') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, ">^", Token::RightAngleBracketCaret) + } _ => self.start_binop(chars, ">", Token::Gt), } } @@ -1385,6 +1484,22 @@ impl<'a> Tokenizer<'a> { '&' => { chars.next(); // consume the '&' match chars.peek() { + Some('>') if self.dialect.supports_geometric_types() => { + chars.next(); + self.consume_and_return(chars, Token::AmpersandRightAngleBracket) + } + Some('<') if self.dialect.supports_geometric_types() => { + chars.next(); // consume + match chars.peek() { + Some('|') => self.consume_and_return( + chars, + Token::AmpersandLeftAngleBracketVerticalBar, + ), + _ => { + self.start_binop(chars, "&<", Token::AmpersandLeftAngleBracket) + } + } + } Some('&') => { chars.next(); // consume the second '&' self.start_binop(chars, "&&", Token::Overlap) @@ -1415,6 +1530,9 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume match chars.peek() { Some('*') => self.consume_for_binop(chars, "~*", Token::TildeAsterisk), + Some('=') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, "~=", Token::TildeEqual) + } Some('~') => { chars.next(); match chars.peek() { @@ -1441,6 +1559,9 @@ impl<'a> Tokenizer<'a> { } } Some(' ') => Ok(Some(Token::Sharp)), + Some('#') if self.dialect.supports_geometric_types() => { + self.consume_for_binop(chars, "##", Token::DoubleSharp) + } Some(sch) if self.dialect.is_identifier_start('#') => { self.tokenize_identifier_or_keyword([ch, *sch], chars) } @@ -1450,6 +1571,16 @@ impl<'a> Tokenizer<'a> { '@' => { chars.next(); match chars.peek() { + Some('@') if self.dialect.supports_geometric_types() => { + self.consume_and_return(chars, Token::AtAt) + } + Some('-') if self.dialect.supports_geometric_types() => { + chars.next(); + match chars.peek() { + Some('@') => self.consume_and_return(chars, Token::AtDashAt), + _ => self.start_binop_opt(chars, "@-", None), + } + } Some('>') => self.consume_and_return(chars, Token::AtArrow), Some('?') => self.consume_and_return(chars, Token::AtQuestion), Some('@') => { @@ -1482,11 +1613,30 @@ impl<'a> Tokenizer<'a> { } } // Postgres uses ? for jsonb operators, not prepared statements - '?' if dialect_of!(self is PostgreSqlDialect) => { - chars.next(); + '?' if self.dialect.supports_geometric_types() => { + chars.next(); // consume match chars.peek() { - Some('|') => self.consume_and_return(chars, Token::QuestionPipe), + Some('|') => { + chars.next(); + match chars.peek() { + Some('|') => self.consume_and_return( + chars, + Token::QuestionMarkDoubleVerticalBar, + ), + _ => Ok(Some(Token::QuestionPipe)), + } + } + Some('&') => self.consume_and_return(chars, Token::QuestionAnd), + Some('-') => { + chars.next(); // consume + match chars.peek() { + Some('|') => self + .consume_and_return(chars, Token::QuestionMarkDashVerticalBar), + _ => Ok(Some(Token::QuestionMarkDash)), + } + } + Some('#') => self.consume_and_return(chars, Token::QuestionMarkSharp), _ => self.consume_and_return(chars, Token::Question), } } @@ -1520,7 +1670,7 @@ impl<'a> Tokenizer<'a> { default: Token, ) -> Result, TokenizerError> { chars.next(); // consume the first char - self.start_binop(chars, prefix, default) + self.start_binop_opt(chars, prefix, Some(default)) } /// parse a custom binary operator @@ -1529,6 +1679,16 @@ impl<'a> Tokenizer<'a> { chars: &mut State, prefix: &str, default: Token, + ) -> Result, TokenizerError> { + self.start_binop_opt(chars, prefix, Some(default)) + } + + /// parse a custom binary operator + fn start_binop_opt( + &self, + chars: &mut State, + prefix: &str, + default: Option, ) -> Result, TokenizerError> { let mut custom = None; while let Some(&ch) = chars.peek() { @@ -1539,10 +1699,14 @@ impl<'a> Tokenizer<'a> { custom.get_or_insert_with(|| prefix.to_string()).push(ch); chars.next(); } - - Ok(Some( - custom.map(Token::CustomBinaryOperator).unwrap_or(default), - )) + match (custom, default) { + (Some(custom), _) => Ok(Token::CustomBinaryOperator(custom).into()), + (None, Some(tok)) => Ok(Some(tok)), + (None, None) => self.tokenizer_error( + chars.location(), + format!("Expected a valid binary operator after '{}'", prefix), + ), + } } /// Tokenize dollar preceded value (i.e: a string/placeholder) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a4e83be06..653142dc6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -13860,3 +13860,361 @@ fn test_select_from_first() { assert_eq!(ast.to_string(), q); } } + +#[test] +fn test_geometric_unary_operators() { + // Number of points in path or polygon + let sql = "# path '((1,0),(0,1),(-1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::Hash, + .. + } + )); + + // Length or circumference + let sql = "@-@ path '((0,0),(1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::AtDashAt, + .. + } + )); + + // Center + let sql = "@@ circle '((0,0),10)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::DoubleAt, + .. + } + )); + // Is horizontal? + let sql = "?- lseg '((-1,0),(1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::QuestionDash, + .. + } + )); + + // Is vertical? + let sql = "?| lseg '((-1,0),(1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::QuestionPipe, + .. + } + )); +} + +#[test] +fn test_geomtery_type() { + let sql = "point '1,2'"; + assert_eq!( + 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()), + } + ); + + let sql = "line '1,2,3,4'"; + assert_eq!( + 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()), + } + ); + + let sql = "path '1,2,3,4'"; + assert_eq!( + 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()), + } + ); + let sql = "box '1,2,3,4'"; + assert_eq!( + 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()), + } + ); + + let sql = "circle '1,2,3'"; + assert_eq!( + 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()), + } + ); + + let sql = "polygon '1,2,3,4'"; + assert_eq!( + 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()), + } + ); + let sql = "lseg '1,2,3,4'"; + assert_eq!( + 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()), + } + ); +} +#[test] +fn test_geometric_binary_operators() { + // Translation plus + let sql = "box '((0,0),(1,1))' + point '(2.0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Plus, + .. + } + )); + // Translation minus + let sql = "box '((0,0),(1,1))' - point '(2.0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Minus, + .. + } + )); + + // Scaling multiply + let sql = "box '((0,0),(1,1))' * point '(2.0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Multiply, + .. + } + )); + + // Scaling divide + let sql = "box '((0,0),(1,1))' / point '(2.0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Divide, + .. + } + )); + + // Intersection + let sql = "'((1,-1),(-1,1))' # '((1,1),(-1,-1))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PGBitwiseXor, + .. + } + )); + + //Point of closest proximity + let sql = "point '(0,0)' ## lseg '((2,0),(0,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::DoubleHash, + .. + } + )); + + // Point of closest proximity + let sql = "box '((0,0),(1,1))' && box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PGOverlap, + .. + } + )); + + // Overlaps to left? + let sql = "box '((0,0),(1,1))' &< box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::AndLt, + .. + } + )); + + // Overlaps to right? + let sql = "box '((0,0),(3,3))' &> box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::AndGt, + .. + } + )); + + // Distance between + let sql = "circle '((0,0),1)' <-> circle '((5,0),1)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::LtDashGt, + .. + } + )); + + // Is left of? + let sql = "circle '((0,0),1)' << circle '((5,0),1)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PGBitwiseShiftLeft, + .. + } + )); + + // Is right of? + let sql = "circle '((5,0),1)' >> circle '((0,0),1)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PGBitwiseShiftRight, + .. + } + )); + + // Is below? + let sql = "circle '((0,0),1)' <^ circle '((0,5),1)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::LtCaret, + .. + } + )); + + // Intersects or overlaps + let sql = "lseg '((-1,0),(1,0))' ?# box '((-2,-2),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionHash, + .. + } + )); + + // Is horizontal? + let sql = "point '(1,0)' ?- point '(0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionDash, + .. + } + )); + + // Is perpendicular? + let sql = "lseg '((0,0),(0,1))' ?-| lseg '((0,0),(1,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionDashPipe, + .. + } + )); + + // Is vertical? + let sql = "point '(0,1)' ?| point '(0,0)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionPipe, + .. + } + )); + + // Are parallel? + let sql = "lseg '((-1,0),(1,0))' ?|| lseg '((-1,2),(1,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::QuestionDoublePipe, + .. + } + )); + + // Contained or on? + let sql = "point '(1,1)' @ circle '((0,0),2)'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::At, + .. + } + )); + + // + // Same as? + let sql = "polygon '((0,0),(1,1))' ~= polygon '((1,1),(0,0))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::TildeEq, + .. + } + )); + + // Is strictly below? + let sql = "box '((0,0),(3,3))' <<| box '((3,4),(5,5))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::LtLtPipe, + .. + } + )); + + // Is strictly above? + let sql = "box '((3,4),(5,5))' |>> box '((0,0),(3,3))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PipeGtGt, + .. + } + )); + + // Does not extend above? + let sql = "box '((0,0),(1,1))' &<| box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::AndLtPipe, + .. + } + )); + + // Does not extend below? + let sql = "box '((0,0),(3,3))' |&> box '((0,0),(2,2))'"; + assert!(matches!( + all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::PipeAndGt, + .. + } + )); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d400792d9..312ce1186 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2066,12 +2066,8 @@ fn parse_pg_custom_binary_ops() { let operators = [ // PostGIS "&&&", // n-D bounding boxes intersect - "&<", // (is strictly to the left of) - "&>", // (is strictly to the right of) "|=|", // distance between A and B trajectories at their closest point of approach "<<#>>", // n-D distance between A and B bounding boxes - "|>>", // A's bounding box is strictly above B's. - "~=", // bounding box is the same // PGroonga "&@", // Full text search by a keyword "&@~", // Full text search by easy to use query language From 28736da235174efa5e08a072a55f368c42271de7 Mon Sep 17 00:00:00 2001 From: Ian Alexander Joiner <14581281+iajoiner@users.noreply.github.com> Date: Fri, 21 Feb 2025 01:18:51 -0500 Subject: [PATCH 734/806] fix: make `serde` feature no_std (#1730) Co-authored-by: Jay White --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 06fed2c68..9caff2512 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ bigdecimal = { version = "0.4.1", features = ["serde"], optional = true } log = "0.4" recursive = { version = "0.1.1", optional = true} -serde = { version = "1.0", features = ["derive"], optional = true } +serde = { version = "1.0", default-features = false, features = ["derive", "alloc"], optional = true } # serde_json is only used in examples/cli, but we have to put it outside # of dev-dependencies because of # https://github.com/rust-lang/cargo/issues/1596 From 3ace97c0efd46f8128590f88392ac30d3e99feae Mon Sep 17 00:00:00 2001 From: Artem Osipov <59066880+osipovartem@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:22:24 +0300 Subject: [PATCH 735/806] Implement SnowFlake ALTER SESSION (#1712) --- src/ast/helpers/key_value_options.rs | 89 ++++++++++++ src/ast/helpers/mod.rs | 1 + src/ast/helpers/stmt_data_loading.rs | 65 +-------- src/ast/mod.rs | 49 +++++-- src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 121 ++++++++++++---- src/keywords.rs | 1 + tests/sqlparser_snowflake.rs | 205 ++++++++++++++------------- 8 files changed, 335 insertions(+), 197 deletions(-) create mode 100644 src/ast/helpers/key_value_options.rs diff --git a/src/ast/helpers/key_value_options.rs b/src/ast/helpers/key_value_options.rs new file mode 100644 index 000000000..06f028dd2 --- /dev/null +++ b/src/ast/helpers/key_value_options.rs @@ -0,0 +1,89 @@ +// 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. + +//! Key-value options for SQL statements. +//! See [this page](https://docs.snowflake.com/en/sql-reference/commands-data-loading) for more details. + +#[cfg(not(feature = "std"))] +use alloc::string::String; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use core::fmt; +use core::fmt::Formatter; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "visitor")] +use sqlparser_derive::{Visit, VisitMut}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct KeyValueOptions { + pub options: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum KeyValueOptionType { + STRING, + BOOLEAN, + ENUM, + NUMBER, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct KeyValueOption { + pub option_name: String, + pub option_type: KeyValueOptionType, + pub value: String, +} + +impl fmt::Display for KeyValueOptions { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if !self.options.is_empty() { + let mut first = false; + for option in &self.options { + if !first { + first = true; + } else { + f.write_str(" ")?; + } + write!(f, "{}", option)?; + } + } + Ok(()) + } +} + +impl fmt::Display for KeyValueOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.option_type { + KeyValueOptionType::STRING => { + write!(f, "{}='{}'", self.option_name, self.value)?; + } + KeyValueOptionType::ENUM | KeyValueOptionType::BOOLEAN | KeyValueOptionType::NUMBER => { + write!(f, "{}={}", self.option_name, self.value)?; + } + } + Ok(()) + } +} diff --git a/src/ast/helpers/mod.rs b/src/ast/helpers/mod.rs index a96bffc51..55831220d 100644 --- a/src/ast/helpers/mod.rs +++ b/src/ast/helpers/mod.rs @@ -15,5 +15,6 @@ // specific language governing permissions and limitations // under the License. pub mod attached_token; +pub mod key_value_options; pub mod stmt_create_table; pub mod stmt_data_loading; diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index 77de5d9ec..cc4fa12fa 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -24,11 +24,11 @@ use alloc::string::String; #[cfg(not(feature = "std"))] use alloc::vec::Vec; use core::fmt; -use core::fmt::Formatter; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::ast::helpers::key_value_options::KeyValueOptions; use crate::ast::{Ident, ObjectName}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; @@ -38,36 +38,10 @@ use sqlparser_derive::{Visit, VisitMut}; #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct StageParamsObject { pub url: Option, - pub encryption: DataLoadingOptions, + pub encryption: KeyValueOptions, pub endpoint: Option, pub storage_integration: Option, - pub credentials: DataLoadingOptions, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct DataLoadingOptions { - pub options: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum DataLoadingOptionType { - STRING, - BOOLEAN, - ENUM, - NUMBER, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct DataLoadingOption { - pub option_name: String, - pub option_type: DataLoadingOptionType, - pub value: String, + pub credentials: KeyValueOptions, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -106,39 +80,6 @@ impl fmt::Display for StageParamsObject { } } -impl fmt::Display for DataLoadingOptions { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if !self.options.is_empty() { - let mut first = false; - for option in &self.options { - if !first { - first = true; - } else { - f.write_str(" ")?; - } - write!(f, "{}", option)?; - } - } - Ok(()) - } -} - -impl fmt::Display for DataLoadingOption { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.option_type { - DataLoadingOptionType::STRING => { - write!(f, "{}='{}'", self.option_name, self.value)?; - } - DataLoadingOptionType::ENUM - | DataLoadingOptionType::BOOLEAN - | DataLoadingOptionType::NUMBER => { - write!(f, "{}={}", self.option_name, self.value)?; - } - } - Ok(()) - } -} - impl fmt::Display for StageLoadSelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.alias.is_some() { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dc50871ce..2b9016d9a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -89,9 +89,8 @@ pub use self::value::{ NormalizationForm, TrimWhereField, Value, }; -use crate::ast::helpers::stmt_data_loading::{ - DataLoadingOptions, StageLoadSelectItem, StageParamsObject, -}; +use crate::ast::helpers::key_value_options::KeyValueOptions; +use crate::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageParamsObject}; #[cfg(feature = "visitor")] pub use visitor::*; @@ -2504,8 +2503,8 @@ pub enum Statement { from_query: Option>, files: Option>, pattern: Option, - file_format: DataLoadingOptions, - copy_options: DataLoadingOptions, + file_format: KeyValueOptions, + copy_options: KeyValueOptions, validation_mode: Option, partition: Option>, }, @@ -2713,6 +2712,17 @@ pub enum Statement { owner: Option, }, /// ```sql + /// ALTER SESSION SET sessionParam + /// ALTER SESSION UNSET [ , , ... ] + /// ``` + /// See + AlterSession { + /// true is to set for the session parameters, false is to unset + set: bool, + /// The session parameters to set or unset + session_params: KeyValueOptions, + }, + /// ```sql /// ATTACH DATABASE 'path/to/file' AS alias /// ``` /// (SQLite-specific) @@ -3240,9 +3250,9 @@ pub enum Statement { if_not_exists: bool, name: ObjectName, stage_params: StageParamsObject, - directory_table_params: DataLoadingOptions, - file_format: DataLoadingOptions, - copy_options: DataLoadingOptions, + directory_table_params: KeyValueOptions, + file_format: KeyValueOptions, + copy_options: KeyValueOptions, comment: Option, }, /// ```sql @@ -4467,6 +4477,29 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::AlterSession { + set, + session_params, + } => { + write!( + f, + "ALTER SESSION {set}", + set = if *set { "SET" } else { "UNSET" } + )?; + if !session_params.options.is_empty() { + if *set { + write!(f, " {}", session_params)?; + } else { + let options = session_params + .options + .iter() + .map(|p| p.option_name.clone()) + .collect::>(); + write!(f, " {}", display_separated(&options, ", "))?; + } + } + Ok(()) + } Statement::Drop { object_type, if_exists, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index de39e50d6..3de41902d 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -430,6 +430,7 @@ impl Spanned for Statement { // These statements need to be implemented Statement::AlterType { .. } => Span::empty(), Statement::AlterRole { .. } => Span::empty(), + Statement::AlterSession { .. } => Span::empty(), Statement::AttachDatabase { .. } => Span::empty(), Statement::AttachDuckDBDatabase { .. } => Span::empty(), Statement::DetachDuckDBDatabase { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 6702b94b2..72252b277 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -17,10 +17,10 @@ #[cfg(not(feature = "std"))] 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::{ - DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, FileStagingCommand, - StageLoadSelectItem, StageParamsObject, + FileStagingCommand, StageLoadSelectItem, StageParamsObject, }; use crate::ast::{ ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, @@ -130,6 +130,16 @@ impl Dialect for SnowflakeDialect { } fn parse_statement(&self, parser: &mut Parser) -> Option> { + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { + // ALTER SESSION + let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { + Some(Keyword::SET) => true, + Some(Keyword::UNSET) => false, + _ => return Some(parser.expected("SET or UNSET", parser.peek_token())), + }; + return Some(parse_alter_session(parser, set)); + } + if parser.parse_keyword(Keyword::CREATE) { // possibly CREATE STAGE //[ OR REPLACE ] @@ -358,6 +368,18 @@ fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result +fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { + let session_options = parse_session_options(parser, set)?; + Ok(Statement::AlterSession { + set, + session_params: KeyValueOptions { + options: session_options, + }, + }) +} + /// Parse snowflake create table statement. /// /// @@ -634,13 +656,13 @@ pub fn parse_create_stage( if_not_exists, name, stage_params, - directory_table_params: DataLoadingOptions { + directory_table_params: KeyValueOptions { options: directory_table_params, }, - file_format: DataLoadingOptions { + file_format: KeyValueOptions { options: file_format, }, - copy_options: DataLoadingOptions { + copy_options: KeyValueOptions { options: copy_options, }, comment, @@ -708,10 +730,10 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { let mut from_stage = None; let mut stage_params = StageParamsObject { url: None, - encryption: DataLoadingOptions { options: vec![] }, + encryption: KeyValueOptions { options: vec![] }, endpoint: None, storage_integration: None, - credentials: DataLoadingOptions { options: vec![] }, + credentials: KeyValueOptions { options: vec![] }, }; let mut from_query = None; let mut partition = None; @@ -818,7 +840,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { Token::Comma => continue, // In `COPY INTO ` the copy options do not have a shared key // like in `COPY INTO
` - Token::Word(key) => copy_options.push(parse_copy_option(parser, key)?), + Token::Word(key) => copy_options.push(parse_option(parser, key)?), _ => return parser.expected("another copy option, ; or EOF'", parser.peek_token()), } } @@ -834,10 +856,10 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { from_query, files: if files.is_empty() { None } else { Some(files) }, pattern, - file_format: DataLoadingOptions { + file_format: KeyValueOptions { options: file_format, }, - copy_options: DataLoadingOptions { + copy_options: KeyValueOptions { options: copy_options, }, validation_mode, @@ -931,8 +953,8 @@ fn parse_select_items_for_data_load( fn parse_stage_params(parser: &mut Parser) -> Result { let (mut url, mut storage_integration, mut endpoint) = (None, None, None); - let mut encryption: DataLoadingOptions = DataLoadingOptions { options: vec![] }; - let mut credentials: DataLoadingOptions = DataLoadingOptions { options: vec![] }; + let mut encryption: KeyValueOptions = KeyValueOptions { options: vec![] }; + let mut credentials: KeyValueOptions = KeyValueOptions { options: vec![] }; // URL if parser.parse_keyword(Keyword::URL) { @@ -961,7 +983,7 @@ fn parse_stage_params(parser: &mut Parser) -> Result Result Result Result, ParserError> { + let mut options: Vec = Vec::new(); + let empty = String::new; + loop { + match parser.next_token().token { + Token::Comma => continue, + Token::Word(key) => { + if set { + let option = parse_option(parser, key)?; + options.push(option); + } else { + options.push(KeyValueOption { + option_name: key.value, + option_type: KeyValueOptionType::STRING, + value: empty(), + }); + } + } + _ => { + if parser.peek_token().token == Token::EOF { + break; + } + return parser.expected("another option", parser.peek_token()); + } + } + } + options + .is_empty() + .then(|| { + Err(ParserError::ParserError( + "expected at least one option".to_string(), + )) + }) + .unwrap_or(Ok(options)) +} + /// Parses options provided within parentheses like: /// ( ENABLE = { TRUE | FALSE } /// [ AUTO_REFRESH = { TRUE | FALSE } ] /// [ REFRESH_ON_CREATE = { TRUE | FALSE } ] /// [ NOTIFICATION_INTEGRATION = '' ] ) /// -fn parse_parentheses_options(parser: &mut Parser) -> Result, ParserError> { - let mut options: Vec = Vec::new(); +fn parse_parentheses_options(parser: &mut Parser) -> Result, ParserError> { + let mut options: Vec = Vec::new(); parser.expect_token(&Token::LParen)?; loop { match parser.next_token().token { Token::RParen => break, Token::Comma => continue, - Token::Word(key) => options.push(parse_copy_option(parser, key)?), + Token::Word(key) => options.push(parse_option(parser, key)?), _ => return parser.expected("another option or ')'", parser.peek_token()), }; } @@ -1004,35 +1069,35 @@ fn parse_parentheses_options(parser: &mut Parser) -> Result Result { +fn parse_option(parser: &mut Parser, key: Word) -> Result { parser.expect_token(&Token::Eq)?; if parser.parse_keyword(Keyword::TRUE) { - Ok(DataLoadingOption { + Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "TRUE".to_string(), }) } else if parser.parse_keyword(Keyword::FALSE) { - Ok(DataLoadingOption { + Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "FALSE".to_string(), }) } else { match parser.next_token().token { - Token::SingleQuotedString(value) => Ok(DataLoadingOption { + Token::SingleQuotedString(value) => Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value, }), - Token::Word(word) => Ok(DataLoadingOption { + Token::Word(word) => Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: word.value, }), - Token::Number(n, _) => Ok(DataLoadingOption { + Token::Number(n, _) => Ok(KeyValueOption { option_name: key.value, - option_type: DataLoadingOptionType::NUMBER, + option_type: KeyValueOptionType::NUMBER, value: n, }), _ => parser.expected("expected option value", parser.peek_token()), diff --git a/src/keywords.rs b/src/keywords.rs index 46c40fbaa..d62a038b8 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -913,6 +913,7 @@ define_keywords!( UNNEST, UNPIVOT, UNSAFE, + UNSET, UNSIGNED, UNTIL, UPDATE, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 76550b8e3..12796bb65 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -19,9 +19,8 @@ //! Test SQL syntax specific to Snowflake. The parser based on the //! generic dialect is also tested (on the inputs it can handle). -use sqlparser::ast::helpers::stmt_data_loading::{ - DataLoadingOption, DataLoadingOptionType, StageLoadSelectItem, -}; +use sqlparser::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType}; +use sqlparser::ast::helpers::stmt_data_loading::StageLoadSelectItem; use sqlparser::ast::*; use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -1914,38 +1913,26 @@ fn test_create_stage_with_stage_params() { "", stage_params.endpoint.unwrap() ); - assert!(stage_params - .credentials - .options - .contains(&DataLoadingOption { - option_name: "AWS_KEY_ID".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "1a2b3c".to_string() - })); - assert!(stage_params - .credentials - .options - .contains(&DataLoadingOption { - option_name: "AWS_SECRET_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "4x5y6z".to_string() - })); - assert!(stage_params - .encryption - .options - .contains(&DataLoadingOption { - option_name: "MASTER_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "key".to_string() - })); - assert!(stage_params - .encryption - .options - .contains(&DataLoadingOption { - option_name: "TYPE".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "AWS_SSE_KMS".to_string() - })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_KEY_ID".to_string(), + option_type: KeyValueOptionType::STRING, + value: "1a2b3c".to_string() + })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_SECRET_KEY".to_string(), + option_type: KeyValueOptionType::STRING, + value: "4x5y6z".to_string() + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "MASTER_KEY".to_string(), + option_type: KeyValueOptionType::STRING, + value: "key".to_string() + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "TYPE".to_string(), + option_type: KeyValueOptionType::STRING, + value: "AWS_SSE_KMS".to_string() + })); } _ => unreachable!(), }; @@ -1966,19 +1953,19 @@ fn test_create_stage_with_directory_table_params() { directory_table_params, .. } => { - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "ENABLE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "TRUE".to_string() })); - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "REFRESH_ON_CREATE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "FALSE".to_string() })); - assert!(directory_table_params.options.contains(&DataLoadingOption { + assert!(directory_table_params.options.contains(&KeyValueOption { option_name: "NOTIFICATION_INTEGRATION".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: "some-string".to_string() })); } @@ -1997,19 +1984,19 @@ fn test_create_stage_with_file_format() { match snowflake_without_unescape().verified_stmt(sql) { Statement::CreateStage { file_format, .. } => { - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "COMPRESSION".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "AUTO".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "BINARY_FORMAT".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "HEX".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "ESCAPE".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: r#"\\"#.to_string() })); } @@ -2030,14 +2017,14 @@ fn test_create_stage_with_copy_options() { ); match snowflake().verified_stmt(sql) { Statement::CreateStage { copy_options, .. } => { - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "ON_ERROR".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "CONTINUE".to_string() })); - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "FORCE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "TRUE".to_string() })); } @@ -2167,38 +2154,26 @@ fn test_copy_into_with_stage_params() { "", stage_params.endpoint.unwrap() ); - assert!(stage_params - .credentials - .options - .contains(&DataLoadingOption { - option_name: "AWS_KEY_ID".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "1a2b3c".to_string() - })); - assert!(stage_params - .credentials - .options - .contains(&DataLoadingOption { - option_name: "AWS_SECRET_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "4x5y6z".to_string() - })); - assert!(stage_params - .encryption - .options - .contains(&DataLoadingOption { - option_name: "MASTER_KEY".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "key".to_string() - })); - assert!(stage_params - .encryption - .options - .contains(&DataLoadingOption { - option_name: "TYPE".to_string(), - option_type: DataLoadingOptionType::STRING, - value: "AWS_SSE_KMS".to_string() - })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_KEY_ID".to_string(), + option_type: KeyValueOptionType::STRING, + value: "1a2b3c".to_string() + })); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_SECRET_KEY".to_string(), + option_type: KeyValueOptionType::STRING, + value: "4x5y6z".to_string() + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "MASTER_KEY".to_string(), + option_type: KeyValueOptionType::STRING, + value: "key".to_string() + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "TYPE".to_string(), + option_type: KeyValueOptionType::STRING, + value: "AWS_SSE_KMS".to_string() + })); } _ => unreachable!(), }; @@ -2326,19 +2301,19 @@ fn test_copy_into_file_format() { match snowflake_without_unescape().verified_stmt(sql) { Statement::CopyIntoSnowflake { file_format, .. } => { - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "COMPRESSION".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "AUTO".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "BINARY_FORMAT".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "HEX".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "ESCAPE".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: r#"\\"#.to_string() })); } @@ -2365,19 +2340,19 @@ fn test_copy_into_file_format() { .unwrap() { Statement::CopyIntoSnowflake { file_format, .. } => { - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "COMPRESSION".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "AUTO".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "BINARY_FORMAT".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "HEX".to_string() })); - assert!(file_format.options.contains(&DataLoadingOption { + assert!(file_format.options.contains(&KeyValueOption { option_name: "ESCAPE".to_string(), - option_type: DataLoadingOptionType::STRING, + option_type: KeyValueOptionType::STRING, value: r#"\\"#.to_string() })); } @@ -2397,14 +2372,14 @@ fn test_copy_into_copy_options() { match snowflake().verified_stmt(sql) { Statement::CopyIntoSnowflake { copy_options, .. } => { - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "ON_ERROR".to_string(), - option_type: DataLoadingOptionType::ENUM, + option_type: KeyValueOptionType::ENUM, value: "CONTINUE".to_string() })); - assert!(copy_options.options.contains(&DataLoadingOption { + assert!(copy_options.options.contains(&KeyValueOption { option_name: "FORCE".to_string(), - option_type: DataLoadingOptionType::BOOLEAN, + option_type: KeyValueOptionType::BOOLEAN, value: "TRUE".to_string() })); } @@ -3475,3 +3450,35 @@ fn test_grant_database_role_to() { snowflake_and_generic().verified_stmt("GRANT DATABASE ROLE r1 TO ROLE r2"); snowflake_and_generic().verified_stmt("GRANT DATABASE ROLE db1.sc1.r1 TO ROLE db1.sc1.r2"); } + +#[test] +fn test_alter_session() { + assert_eq!( + snowflake() + .parse_sql_statements("ALTER SESSION SET") + .unwrap_err() + .to_string(), + "sql parser error: expected at least one option" + ); + assert_eq!( + snowflake() + .parse_sql_statements("ALTER SESSION UNSET") + .unwrap_err() + .to_string(), + "sql parser error: expected at least one option" + ); + + snowflake().verified_stmt("ALTER SESSION SET AUTOCOMMIT=TRUE"); + snowflake().verified_stmt("ALTER SESSION SET AUTOCOMMIT=FALSE QUERY_TAG='tag'"); + snowflake().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT"); + snowflake().verified_stmt("ALTER SESSION UNSET AUTOCOMMIT, QUERY_TAG"); + snowflake().one_statement_parses_to( + "ALTER SESSION SET A=false, B='tag';", + "ALTER SESSION SET A=FALSE B='tag'", + ); + snowflake().one_statement_parses_to( + "ALTER SESSION SET A=true \nB='tag'", + "ALTER SESSION SET A=TRUE B='tag'", + ); + snowflake().one_statement_parses_to("ALTER SESSION UNSET a\nB", "ALTER SESSION UNSET a, B"); +} From 8fc8082e9a0c1c418b3510fae0cedd1e6e241785 Mon Sep 17 00:00:00 2001 From: tomershaniii <65544633+tomershaniii@users.noreply.github.com> Date: Sat, 22 Feb 2025 07:48:39 +0200 Subject: [PATCH 736/806] Extend Visitor trait for Value type (#1725) --- src/ast/value.rs | 7 +++- src/ast/visitor.rs | 99 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index 5798b5404..f2f02754b 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -33,7 +33,12 @@ use sqlparser_derive::{Visit, VisitMut}; /// Primitive SQL values such as number and string #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr( + feature = "visitor", + derive(Visit, VisitMut), + visit(with = "visit_value") +)] + pub enum Value { /// Numeric literal #[cfg(not(feature = "bigdecimal"))] diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 457dbbaed..bb6246498 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -17,7 +17,7 @@ //! Recursive visitors for ast Nodes. See [`Visitor`] for more details. -use crate::ast::{Expr, ObjectName, Query, Statement, TableFactor}; +use crate::ast::{Expr, ObjectName, Query, Statement, TableFactor, Value}; use core::ops::ControlFlow; /// A type that can be visited by a [`Visitor`]. See [`Visitor`] for @@ -233,6 +233,16 @@ pub trait Visitor { fn post_visit_statement(&mut self, _statement: &Statement) -> ControlFlow { ControlFlow::Continue(()) } + + /// Invoked for any Value that appear in the AST before visiting children + fn pre_visit_value(&mut self, _value: &Value) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any Value that appear in the AST after visiting children + fn post_visit_value(&mut self, _value: &Value) -> ControlFlow { + ControlFlow::Continue(()) + } } /// A visitor that can be used to mutate an AST tree. @@ -337,6 +347,16 @@ pub trait VisitorMut { fn post_visit_statement(&mut self, _statement: &mut Statement) -> ControlFlow { ControlFlow::Continue(()) } + + /// Invoked for any value that appear in the AST before visiting children + fn pre_visit_value(&mut self, _value: &mut Value) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any statements that appear in the AST after visiting children + fn post_visit_value(&mut self, _value: &mut Value) -> ControlFlow { + ControlFlow::Continue(()) + } } struct RelationVisitor(F); @@ -647,6 +667,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::ast::Statement; use crate::dialect::GenericDialect; use crate::parser::Parser; use crate::tokenizer::Tokenizer; @@ -720,7 +741,7 @@ mod tests { } } - fn do_visit(sql: &str) -> Vec { + 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) @@ -728,9 +749,8 @@ mod tests { .parse_statement() .unwrap(); - let mut visitor = TestVisitor::default(); - s.visit(&mut visitor); - visitor.visited + s.visit(visitor); + s } #[test] @@ -889,8 +909,9 @@ mod tests { ), ]; for (sql, expected) in tests { - let actual = do_visit(sql); - let actual: Vec<_> = actual.iter().map(|x| x.as_str()).collect(); + let mut visitor = TestVisitor::default(); + let _ = do_visit(sql, &mut visitor); + let actual: Vec<_> = visitor.visited.iter().map(|x| x.as_str()).collect(); assert_eq!(actual, expected) } } @@ -920,3 +941,67 @@ mod tests { s.visit(&mut visitor); } } + +#[cfg(test)] +mod visit_mut_tests { + use crate::ast::{Statement, Value, VisitMut, VisitorMut}; + use crate::dialect::GenericDialect; + use crate::parser::Parser; + use crate::tokenizer::Tokenizer; + use core::ops::ControlFlow; + + #[derive(Default)] + struct MutatorVisitor { + index: u64, + } + + impl VisitorMut for MutatorVisitor { + type Break = (); + + fn pre_visit_value(&mut self, value: &mut Value) -> ControlFlow { + self.index += 1; + *value = Value::SingleQuotedString(format!("REDACTED_{}", self.index)); + ControlFlow::Continue(()) + } + + fn post_visit_value(&mut self, _value: &mut Value) -> ControlFlow { + ControlFlow::Continue(()) + } + } + + 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) + .with_tokens(tokens) + .parse_statement() + .unwrap(); + + s.visit(visitor); + s + } + + #[test] + fn test_value_redact() { + let tests = vec![ + ( + concat!( + "SELECT * FROM monthly_sales ", + "PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ", + "ORDER BY EMPID" + ), + concat!( + "SELECT * FROM monthly_sales ", + "PIVOT(SUM(a.amount) FOR a.MONTH IN ('REDACTED_1', 'REDACTED_2', 'REDACTED_3', 'REDACTED_4')) AS p (c, d) ", + "ORDER BY EMPID" + ), + ), + ]; + + for (sql, expected) in tests { + let mut visitor = MutatorVisitor::default(); + let mutated = do_visit_mut(sql, &mut visitor); + assert_eq!(mutated.to_string(), expected) + } + } +} From 1f1c0693daa031961a05f9a91cb154c2b3db5572 Mon Sep 17 00:00:00 2001 From: SiLe Zhou Date: Sat, 22 Feb 2025 13:52:07 +0800 Subject: [PATCH 737/806] Add support for `ORDER BY ALL` (#1724) --- src/ast/mod.rs | 10 +- src/ast/query.rs | 73 +++++++--- src/ast/spans.rs | 26 ++-- src/dialect/clickhouse.rs | 5 + src/dialect/duckdb.rs | 5 + src/dialect/mod.rs | 8 ++ src/parser/mod.rs | 58 +++++--- tests/sqlparser_clickhouse.rs | 60 +++++--- tests/sqlparser_common.rs | 258 ++++++++++++++++++++++++++++------ tests/sqlparser_hive.rs | 16 ++- tests/sqlparser_mysql.rs | 6 +- 11 files changed, 396 insertions(+), 129 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2b9016d9a..ace752a86 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -68,11 +68,11 @@ pub use self::query::{ JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, - OrderBy, OrderByExpr, 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, + 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, diff --git a/src/ast/query.rs b/src/ast/query.rs index c52a98b63..bed991114 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2218,30 +2218,50 @@ pub enum JoinConstraint { None, } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum OrderByKind { + /// ALL syntax of [DuckDB] and [ClickHouse]. + /// + /// [DuckDB]: + /// [ClickHouse]: + All(OrderByOptions), + + /// Expressions + Expressions(Vec), +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct OrderBy { - pub exprs: Vec, + pub kind: OrderByKind, + /// Optional: `INTERPOLATE` /// Supported by [ClickHouse syntax] - /// - /// [ClickHouse syntax]: pub interpolate: Option, } impl fmt::Display for OrderBy { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "ORDER BY")?; - if !self.exprs.is_empty() { - write!(f, " {}", display_comma_separated(&self.exprs))?; + match &self.kind { + OrderByKind::Expressions(exprs) => { + write!(f, " {}", display_comma_separated(exprs))?; + } + OrderByKind::All(all) => { + write!(f, " ALL{}", all)?; + } } + if let Some(ref interpolate) = self.interpolate { match &interpolate.exprs { Some(exprs) => write!(f, " INTERPOLATE ({})", display_comma_separated(exprs))?, None => write!(f, " INTERPOLATE")?, } } + Ok(()) } } @@ -2252,10 +2272,7 @@ impl fmt::Display for OrderBy { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct OrderByExpr { pub expr: Expr, - /// Optional `ASC` or `DESC` - pub asc: Option, - /// Optional `NULLS FIRST` or `NULLS LAST` - pub nulls_first: Option, + pub options: OrderByOptions, /// Optional: `WITH FILL` /// Supported by [ClickHouse syntax]: pub with_fill: Option, @@ -2263,17 +2280,7 @@ pub struct OrderByExpr { impl fmt::Display for OrderByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.expr)?; - match self.asc { - Some(true) => write!(f, " ASC")?, - Some(false) => write!(f, " DESC")?, - None => (), - } - match self.nulls_first { - Some(true) => write!(f, " NULLS FIRST")?, - Some(false) => write!(f, " NULLS LAST")?, - None => (), - } + write!(f, "{}{}", self.expr, self.options)?; if let Some(ref with_fill) = self.with_fill { write!(f, " {}", with_fill)? } @@ -2339,6 +2346,32 @@ impl fmt::Display for InterpolateExpr { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct OrderByOptions { + /// Optional `ASC` or `DESC` + pub asc: Option, + /// Optional `NULLS FIRST` or `NULLS LAST` + pub nulls_first: Option, +} + +impl fmt::Display for OrderByOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.asc { + Some(true) => write!(f, " ASC")?, + Some(false) => write!(f, " DESC")?, + None => (), + } + match self.nulls_first { + Some(true) => write!(f, " NULLS FIRST")?, + Some(false) => write!(f, " NULLS LAST")?, + None => (), + } + 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/spans.rs b/src/ast/spans.rs index 3de41902d..88c3d8aea 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -30,7 +30,7 @@ use super::{ GroupByExpr, HavingBound, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, - OnConflictAction, OnInsert, OrderBy, OrderByExpr, Partition, PivotValueSource, + 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, @@ -1095,16 +1095,21 @@ impl Spanned for ProjectionSelect { } } +/// # partial span +/// +/// Missing spans: +/// - [OrderByKind::All] impl Spanned for OrderBy { fn span(&self) -> Span { - let OrderBy { exprs, interpolate } = self; - - union_spans( - exprs - .iter() - .map(|i| i.span()) - .chain(interpolate.iter().map(|i| i.span())), - ) + match &self.kind { + OrderByKind::All(_) => Span::empty(), + OrderByKind::Expressions(exprs) => union_spans( + exprs + .iter() + .map(|i| i.span()) + .chain(self.interpolate.iter().map(|i| i.span())), + ), + } } } @@ -1902,8 +1907,7 @@ impl Spanned for OrderByExpr { fn span(&self) -> Span { let OrderByExpr { expr, - asc: _, // bool - nulls_first: _, // bool + options: _, with_fill, } = self; diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 37da2ab5d..f5e70c309 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -80,6 +80,11 @@ impl Dialect for ClickHouseDialect { true } + /// See + fn supports_order_by_all(&self) -> bool { + true + } + // See fn supports_group_by_expr(&self) -> bool { true diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index a2e7fb6c0..3595aa713 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -89,4 +89,9 @@ impl Dialect for DuckDbDialect { fn supports_from_first_select(&self) -> bool { true } + + /// See DuckDB + fn supports_order_by_all(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 86e23c86f..b68adc170 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -943,6 +943,14 @@ pub trait Dialect: Debug + Any { fn supports_geometric_types(&self) -> bool { false } + + /// Returns true if the dialect supports `ORDER BY ALL`. + /// `ALL` which means all columns of the SELECT clause. + /// + /// For example: ```SELECT * FROM addresses ORDER BY ALL;```. + fn supports_order_by_all(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 69268bc51..f80754a3a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9369,17 +9369,26 @@ impl<'a> Parser<'a> { pub fn parse_optional_order_by(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { - let order_by_exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; - let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) { - self.parse_interpolations()? - } else { - None - }; - - Ok(Some(OrderBy { - exprs: order_by_exprs, - interpolate, - })) + let order_by = + if self.dialect.supports_order_by_all() && self.parse_keyword(Keyword::ALL) { + let order_by_options = self.parse_order_by_options()?; + OrderBy { + kind: OrderByKind::All(order_by_options), + interpolate: None, + } + } else { + let exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?; + let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect) { + self.parse_interpolations()? + } else { + None + }; + OrderBy { + kind: OrderByKind::Expressions(exprs), + interpolate, + } + }; + Ok(Some(order_by)) } else { Ok(None) } @@ -13557,15 +13566,7 @@ impl<'a> Parser<'a> { pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parse_expr()?; - let asc = self.parse_asc_desc(); - - let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { - Some(true) - } else if self.parse_keywords(&[Keyword::NULLS, Keyword::LAST]) { - Some(false) - } else { - None - }; + let options = self.parse_order_by_options()?; let with_fill = if dialect_of!(self is ClickHouseDialect | GenericDialect) && self.parse_keywords(&[Keyword::WITH, Keyword::FILL]) @@ -13577,12 +13578,25 @@ impl<'a> Parser<'a> { Ok(OrderByExpr { expr, - asc, - nulls_first, + options, with_fill, }) } + fn parse_order_by_options(&mut self) -> Result { + let asc = self.parse_asc_desc(); + + let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { + Some(true) + } else if self.parse_keywords(&[Keyword::NULLS, Keyword::LAST]) { + Some(false) + } else { + None + }; + + Ok(OrderByOptions { asc, nulls_first }) + } + // Parse a WITH FILL clause (ClickHouse dialect) // that follow the WITH FILL keywords in a ORDER BY clause pub fn parse_with_fill(&mut self) -> Result { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 34f684c69..99e76d45e 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -323,12 +323,14 @@ fn parse_alter_table_add_projection() { vec![] )), order_by: Some(OrderBy { - exprs: vec![OrderByExpr { + kind: OrderByKind::Expressions(vec![OrderByExpr { expr: Identifier(Ident::new("b")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, - }], + }]), interpolate: None, }), } @@ -1068,11 +1070,13 @@ fn parse_select_order_by_with_fill_interpolate() { let select = clickhouse().verified_query(sql); assert_eq!( OrderBy { - exprs: vec![ + kind: OrderByKind::Expressions(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), - asc: Some(true), - nulls_first: Some(true), + options: OrderByOptions { + asc: Some(true), + nulls_first: Some(true), + }, with_fill: Some(WithFill { from: Some(Expr::Value(number("10"))), to: Some(Expr::Value(number("20"))), @@ -1081,15 +1085,17 @@ fn parse_select_order_by_with_fill_interpolate() { }, OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), - asc: Some(false), - nulls_first: Some(false), + options: OrderByOptions { + asc: Some(false), + nulls_first: Some(false), + }, with_fill: Some(WithFill { from: Some(Expr::Value(number("30"))), to: Some(Expr::Value(number("40"))), step: Some(Expr::Value(number("3"))), }), }, - ], + ]), interpolate: Some(Interpolate { exprs: Some(vec![InterpolateExpr { column: Ident::new("col1"), @@ -1147,8 +1153,12 @@ fn parse_with_fill() { from: Some(Expr::Value(number("10"))), to: Some(Expr::Value(number("20"))), step: Some(Expr::Value(number("2"))), - }), - select.order_by.expect("ORDER BY expected").exprs[0].with_fill + }) + .as_ref(), + match select.order_by.expect("ORDER BY expected").kind { + OrderByKind::Expressions(ref exprs) => exprs[0].with_fill.as_ref(), + _ => None, + } ); } @@ -1199,8 +1209,13 @@ fn parse_interpolate_body_with_columns() { }), }, ]) - }), - select.order_by.expect("ORDER BY expected").interpolate + }) + .as_ref(), + select + .order_by + .expect("ORDER BY expected") + .interpolate + .as_ref() ); } @@ -1209,8 +1224,12 @@ fn parse_interpolate_without_body() { let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL INTERPOLATE"; let select = clickhouse().verified_query(sql); assert_eq!( - Some(Interpolate { exprs: None }), - select.order_by.expect("ORDER BY expected").interpolate + Some(Interpolate { exprs: None }).as_ref(), + select + .order_by + .expect("ORDER BY expected") + .interpolate + .as_ref() ); } @@ -1221,8 +1240,13 @@ fn parse_interpolate_with_empty_body() { assert_eq!( Some(Interpolate { exprs: Some(vec![]) - }), - select.order_by.expect("ORDER BY expected").interpolate + }) + .as_ref(), + select + .order_by + .expect("ORDER BY expected") + .interpolate + .as_ref() ); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 653142dc6..c46072d06 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2389,27 +2389,33 @@ fn parse_select_order_by() { fn chk(sql: &str) { let select = verified_query(sql); assert_eq!( - vec![ + OrderByKind::Expressions(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), - asc: Some(true), - nulls_first: None, + options: OrderByOptions { + asc: Some(true), + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("id")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }, - ], - select.order_by.expect("ORDER BY expected").exprs + ]), + select.order_by.expect("ORDER BY expected").kind ); } chk("SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY lname ASC, fname DESC, id"); @@ -2424,46 +2430,186 @@ fn parse_select_order_by_limit() { ORDER BY lname ASC, fname DESC LIMIT 2"; let select = verified_query(sql); assert_eq!( - vec![ + OrderByKind::Expressions(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), - asc: Some(true), - nulls_first: None, + options: OrderByOptions { + asc: Some(true), + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }, - ], - select.order_by.expect("ORDER BY expected").exprs + ]), + select.order_by.expect("ORDER BY expected").kind ); assert_eq!(Some(Expr::Value(number("2"))), select.limit); } +#[test] +fn parse_select_order_by_all() { + fn chk(sql: &str, except_order_by: OrderByKind) { + let dialects = all_dialects_where(|d| d.supports_order_by_all()); + let select = dialects.verified_query(sql); + assert_eq!( + except_order_by, + select.order_by.expect("ORDER BY expected").kind + ); + } + let test_cases = [ + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL", + OrderByKind::All(OrderByOptions { + asc: None, + nulls_first: None, + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL NULLS FIRST", + OrderByKind::All(OrderByOptions { + asc: None, + nulls_first: Some(true), + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL NULLS LAST", + OrderByKind::All(OrderByOptions { + asc: None, + nulls_first: Some(false), + }), + ), + ( + "SELECT id, fname, lname FROM customer ORDER BY ALL ASC", + OrderByKind::All(OrderByOptions { + asc: Some(true), + nulls_first: None, + }), + ), + ( + "SELECT id, fname, lname FROM customer ORDER BY ALL ASC NULLS FIRST", + OrderByKind::All(OrderByOptions { + asc: Some(true), + nulls_first: Some(true), + }), + ), + ( + "SELECT id, fname, lname FROM customer ORDER BY ALL ASC NULLS LAST", + OrderByKind::All(OrderByOptions { + asc: Some(true), + nulls_first: Some(false), + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC", + OrderByKind::All(OrderByOptions { + asc: Some(false), + nulls_first: None, + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC NULLS FIRST", + OrderByKind::All(OrderByOptions { + asc: Some(false), + nulls_first: Some(true), + }), + ), + ( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC NULLS LAST", + OrderByKind::All(OrderByOptions { + asc: Some(false), + nulls_first: Some(false), + }), + ), + ]; + + for (sql, expected_order_by) in test_cases { + chk(sql, expected_order_by); + } +} + +#[test] +fn parse_select_order_by_not_support_all() { + fn chk(sql: &str, except_order_by: OrderByKind) { + let dialects = all_dialects_where(|d| !d.supports_order_by_all()); + let select = dialects.verified_query(sql); + assert_eq!( + except_order_by, + select.order_by.expect("ORDER BY expected").kind + ); + } + let test_cases = [ + ( + "SELECT id, ALL FROM customer WHERE id < 5 ORDER BY ALL", + OrderByKind::Expressions(vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("ALL")), + options: OrderByOptions { + asc: None, + nulls_first: None, + }, + with_fill: None, + }]), + ), + ( + "SELECT id, ALL FROM customer ORDER BY ALL ASC NULLS FIRST", + OrderByKind::Expressions(vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("ALL")), + options: OrderByOptions { + asc: Some(true), + nulls_first: Some(true), + }, + with_fill: None, + }]), + ), + ( + "SELECT id, ALL FROM customer ORDER BY ALL DESC NULLS LAST", + OrderByKind::Expressions(vec![OrderByExpr { + expr: Expr::Identifier(Ident::new("ALL")), + options: OrderByOptions { + asc: Some(false), + nulls_first: Some(false), + }, + with_fill: None, + }]), + ), + ]; + + for (sql, expected_order_by) in test_cases { + chk(sql, expected_order_by); + } +} + #[test] fn parse_select_order_by_nulls_order() { let sql = "SELECT id, fname, lname FROM customer WHERE id < 5 \ ORDER BY lname ASC NULLS FIRST, fname DESC NULLS LAST LIMIT 2"; let select = verified_query(sql); assert_eq!( - vec![ + OrderByKind::Expressions(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), - asc: Some(true), - nulls_first: Some(true), + options: OrderByOptions { + asc: Some(true), + nulls_first: Some(true), + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), - asc: Some(false), - nulls_first: Some(false), + options: OrderByOptions { + asc: Some(false), + nulls_first: Some(false), + }, with_fill: None, }, - ], - select.order_by.expect("ORDER BY expeccted").exprs + ]), + select.order_by.expect("ORDER BY expeccted").kind ); assert_eq!(Some(Expr::Value(number("2"))), select.limit); } @@ -2641,8 +2787,10 @@ fn parse_select_qualify() { partition_by: vec![Expr::Identifier(Ident::new("p"))], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }], window_frame: None, @@ -3065,8 +3213,10 @@ fn parse_listagg() { quote_style: None, span: Span::empty(), }), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }, OrderByExpr { @@ -3075,8 +3225,10 @@ fn parse_listagg() { quote_style: None, span: Span::empty(), }), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }, ] @@ -5172,8 +5324,10 @@ fn parse_window_functions() { partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("dt")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }], window_frame: None, @@ -5386,8 +5540,10 @@ fn test_parse_named_window() { quote_style: None, span: Span::empty(), }), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }], window_frame: None, @@ -8584,14 +8740,18 @@ fn parse_create_index() { let indexed_columns = vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("name")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("age")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }, ]; @@ -8620,14 +8780,18 @@ fn test_create_index_with_using_function() { let indexed_columns = vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("name")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("age")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }, ]; @@ -8664,8 +8828,10 @@ 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")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }]; let with_parameters = vec![ @@ -11345,8 +11511,10 @@ fn test_match_recognize() { partition_by: vec![Expr::Identifier(Ident::new("company"))], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("price_date")), - asc: None, - nulls_first: None, + options: OrderByOptions { + asc: None, + nulls_first: None, + }, with_fill: None, }], measures: vec![ diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 5d710b17d..b2b300aec 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -23,8 +23,8 @@ use sqlparser::ast::{ ClusteredBy, CommentDef, CreateFunction, CreateFunctionBody, CreateFunctionUsing, CreateTable, Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, - OneOrManyWithParens, OrderByExpr, SelectItem, Statement, TableFactor, UnaryOperator, Use, - Value, + OneOrManyWithParens, OrderByExpr, OrderByOptions, SelectItem, Statement, TableFactor, + UnaryOperator, Use, Value, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::ParserError; @@ -171,14 +171,18 @@ fn create_table_with_clustered_by() { sorted_by: Some(vec![ OrderByExpr { expr: Expr::Identifier(Ident::new("a")), - asc: Some(true), - nulls_first: None, + options: OrderByOptions { + asc: Some(true), + nulls_first: None, + }, with_fill: None, }, OrderByExpr { expr: Expr::Identifier(Ident::new("b")), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }, ]), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 44c8350fa..030710743 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2096,8 +2096,10 @@ fn parse_delete_with_order_by() { quote_style: None, span: Span::empty(), }), - asc: Some(false), - nulls_first: None, + options: OrderByOptions { + asc: Some(false), + nulls_first: None, + }, with_fill: None, }], order_by From 7fc37a76e57aa677d8c3958ddda32e9d65a8bb74 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 22 Feb 2025 07:21:45 +0100 Subject: [PATCH 738/806] Parse casting to array using double colon operator in Redshift (#1737) --- src/dialect/duckdb.rs | 2 +- src/dialect/generic.rs | 2 +- src/dialect/mod.rs | 8 +++++--- src/dialect/postgresql.rs | 2 +- src/dialect/redshift.rs | 4 ++++ src/parser/mod.rs | 4 ++-- tests/sqlparser_common.rs | 7 +++++++ 7 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 3595aa713..3366c6705 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -82,7 +82,7 @@ impl Dialect for DuckDbDialect { } // See DuckDB - fn supports_array_typedef_size(&self) -> bool { + fn supports_array_typedef_with_brackets(&self) -> bool { true } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index e04a288d6..041d44bb2 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -148,7 +148,7 @@ impl Dialect for GenericDialect { true } - fn supports_array_typedef_size(&self) -> bool { + fn supports_array_typedef_with_brackets(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b68adc170..1c32bc513 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -931,9 +931,11 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if the dialect supports size definition for array types. - /// For example: ```CREATE TABLE my_table (my_array INT[3])```. - fn supports_array_typedef_size(&self) -> bool { + /// Returns true if the dialect supports array type definition with brackets with + /// an optional size. For example: + /// ```CREATE TABLE my_table (arr1 INT[], arr2 INT[3])``` + /// ```SELECT x::INT[]``` + fn supports_array_typedef_with_brackets(&self) -> bool { false } /// Returns true if the dialect supports geometric types. diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index a20cfac4c..57ed0b684 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -247,7 +247,7 @@ impl Dialect for PostgreSqlDialect { } /// See: - fn supports_array_typedef_size(&self) -> bool { + fn supports_array_typedef_with_brackets(&self) -> bool { true } diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 3dda762c3..25b8f1644 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -117,4 +117,8 @@ impl Dialect for RedshiftSqlDialect { fn supports_geometric_types(&self) -> bool { true } + + fn supports_array_typedef_with_brackets(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f80754a3a..176a7ca13 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9123,9 +9123,9 @@ impl<'a> Parser<'a> { _ => self.expected_at("a data type name", next_token_index), }?; - if self.dialect.supports_array_typedef_size() { - // Parse array data type size + if self.dialect.supports_array_typedef_with_brackets() { while self.consume_token(&Token::LBracket) { + // Parse optional array data type size let size = self.maybe_parse(|p| p.parse_literal_uint())?; self.expect_token(&Token::RBracket)?; data = DataType::Array(ArrayElemTypeDef::SquareBracket(Box::new(data), size)) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c46072d06..d6b8824be 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14386,3 +14386,10 @@ fn test_geometric_binary_operators() { } )); } + +#[test] +fn parse_array_type_def_with_brackets() { + let dialects = all_dialects_where(|d| d.supports_array_typedef_with_brackets()); + dialects.verified_stmt("SELECT x::INT[]"); + dialects.verified_stmt("SELECT STRING_TO_ARRAY('1,2,3', ',')::INT[3]"); +} From 72312ba82af1886c80eb6ff48ea2ae6f9f75205d Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Sat, 22 Feb 2025 07:23:36 +0100 Subject: [PATCH 739/806] Replace parallel condition/result vectors with single CaseWhen vector in Expr::Case (#1733) --- src/ast/mod.rs | 25 ++++++-- src/ast/spans.rs | 6 +- src/parser/mod.rs | 7 +-- tests/sqlparser_common.rs | 107 ++++++++++++++++++++++------------ tests/sqlparser_databricks.rs | 65 +++++++++++++++++++++ 5 files changed, 160 insertions(+), 50 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ace752a86..649b1f79e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -600,6 +600,22 @@ pub enum CeilFloorKind { Scale(Value), } +/// A WHEN clause in a CASE expression containing both +/// the condition and its corresponding result +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CaseWhen { + pub condition: Expr, + pub result: Expr, +} + +impl fmt::Display for CaseWhen { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "WHEN {} THEN {}", self.condition, self.result) + } +} + /// An SQL expression of any type. /// /// # Semantics / Type Checking @@ -918,8 +934,7 @@ pub enum Expr { /// Case { operand: Option>, - conditions: Vec, - results: Vec, + conditions: Vec, else_result: Option>, }, /// An exists expression `[ NOT ] EXISTS(SELECT ...)`, used in expressions like @@ -1621,17 +1636,15 @@ impl fmt::Display for Expr { Expr::Case { operand, conditions, - results, else_result, } => { write!(f, "CASE")?; if let Some(operand) = operand { write!(f, " {operand}")?; } - for (c, r) in conditions.iter().zip(results) { - write!(f, " WHEN {c} THEN {r}")?; + for when in conditions { + write!(f, " {when}")?; } - if let Some(else_result) = else_result { write!(f, " ELSE {else_result}")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 88c3d8aea..a036271cb 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1450,15 +1450,15 @@ impl Spanned for Expr { Expr::Case { operand, conditions, - results, else_result, } => union_spans( operand .as_ref() .map(|i| i.span()) .into_iter() - .chain(conditions.iter().map(|i| i.span())) - .chain(results.iter().map(|i| i.span())) + .chain(conditions.iter().flat_map(|case_when| { + [case_when.condition.span(), case_when.result.span()] + })) .chain(else_result.as_ref().map(|i| i.span())), ), Expr::Exists { subquery, .. } => subquery.span(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 176a7ca13..e40f4d58a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2065,11 +2065,11 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::WHEN)?; } let mut conditions = vec![]; - let mut results = vec![]; loop { - conditions.push(self.parse_expr()?); + let condition = self.parse_expr()?; self.expect_keyword_is(Keyword::THEN)?; - results.push(self.parse_expr()?); + let result = self.parse_expr()?; + conditions.push(CaseWhen { condition, result }); if !self.parse_keyword(Keyword::WHEN) { break; } @@ -2083,7 +2083,6 @@ impl<'a> Parser<'a> { Ok(Expr::Case { operand, conditions, - results, else_result, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d6b8824be..578c42de1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6695,22 +6695,26 @@ fn parse_searched_case_expr() { &Case { operand: None, conditions: vec![ - IsNull(Box::new(Identifier(Ident::new("bar")))), - BinaryOp { - left: Box::new(Identifier(Ident::new("bar"))), - op: Eq, - right: Box::new(Expr::Value(number("0"))), + CaseWhen { + condition: IsNull(Box::new(Identifier(Ident::new("bar")))), + result: Expr::Value(Value::SingleQuotedString("null".to_string())), }, - BinaryOp { - left: Box::new(Identifier(Ident::new("bar"))), - op: GtEq, - right: Box::new(Expr::Value(number("0"))), + CaseWhen { + condition: BinaryOp { + left: Box::new(Identifier(Ident::new("bar"))), + op: Eq, + right: Box::new(Expr::Value(number("0"))), + }, + result: Expr::Value(Value::SingleQuotedString("=0".to_string())), + }, + CaseWhen { + condition: BinaryOp { + left: Box::new(Identifier(Ident::new("bar"))), + op: GtEq, + right: Box::new(Expr::Value(number("0"))), + }, + result: Expr::Value(Value::SingleQuotedString(">=0".to_string())), }, - ], - results: vec![ - Expr::Value(Value::SingleQuotedString("null".to_string())), - Expr::Value(Value::SingleQuotedString("=0".to_string())), - Expr::Value(Value::SingleQuotedString(">=0".to_string())), ], else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( "<0".to_string() @@ -6729,8 +6733,10 @@ fn parse_simple_case_expr() { assert_eq!( &Case { operand: Some(Box::new(Identifier(Ident::new("foo")))), - conditions: vec![Expr::Value(number("1"))], - results: vec![Expr::Value(Value::SingleQuotedString("Y".to_string()))], + conditions: vec![CaseWhen { + condition: Expr::Value(number("1")), + result: Expr::Value(Value::SingleQuotedString("Y".to_string())), + }], else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( "N".to_string() )))), @@ -13902,6 +13908,31 @@ fn test_trailing_commas_in_from() { ); } +#[test] +#[cfg(feature = "visitor")] +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| { + visited.push(expr.to_string()); + core::ops::ControlFlow::<()>::Continue(()) + }); + + assert_eq!( + visited, + [ + "CASE a WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE 5 END", + "a", + "1", + "2", + "3", + "4", + "5" + ] + ); +} + #[test] fn test_lambdas() { let dialects = all_dialects_where(|d| d.supports_lambda_functions()); @@ -13929,28 +13960,30 @@ fn test_lambdas() { body: Box::new(Expr::Case { operand: None, conditions: vec![ - Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("p1"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Identifier(Ident::new("p2"))) + CaseWhen { + condition: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("p1"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("p2"))) + }, + result: Expr::Value(number("0")) }, - Expr::BinaryOp { - left: Box::new(call( - "reverse", - [Expr::Identifier(Ident::new("p1"))] - )), - op: BinaryOperator::Lt, - right: Box::new(call( - "reverse", - [Expr::Identifier(Ident::new("p2"))] - )) - } - ], - results: vec![ - Expr::Value(number("0")), - Expr::UnaryOp { - op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(number("1"))) + CaseWhen { + condition: Expr::BinaryOp { + left: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p1"))] + )), + op: BinaryOperator::Lt, + right: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p2"))] + )) + }, + result: Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(number("1"))) + } } ], else_result: Some(Box::new(Expr::Value(number("1")))) diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 8338a0e71..724bedf47 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -83,6 +83,71 @@ fn test_databricks_exists() { ); } +#[test] +fn test_databricks_lambdas() { + #[rustfmt::skip] + let sql = concat!( + "SELECT array_sort(array('Hello', 'World'), ", + "(p1, p2) -> CASE WHEN p1 = p2 THEN 0 ", + "WHEN reverse(p1) < reverse(p2) THEN -1 ", + "ELSE 1 END)", + ); + pretty_assertions::assert_eq!( + SelectItem::UnnamedExpr(call( + "array_sort", + [ + call( + "array", + [ + Expr::Value(Value::SingleQuotedString("Hello".to_owned())), + Expr::Value(Value::SingleQuotedString("World".to_owned())) + ] + ), + Expr::Lambda(LambdaFunction { + params: OneOrManyWithParens::Many(vec![Ident::new("p1"), Ident::new("p2")]), + body: Box::new(Expr::Case { + operand: None, + conditions: vec![ + CaseWhen { + condition: Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("p1"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("p2"))) + }, + result: Expr::Value(number("0")) + }, + CaseWhen { + condition: Expr::BinaryOp { + left: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p1"))] + )), + op: BinaryOperator::Lt, + right: Box::new(call( + "reverse", + [Expr::Identifier(Ident::new("p2"))] + )), + }, + result: Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(number("1"))) + } + }, + ], + else_result: Some(Box::new(Expr::Value(number("1")))) + }) + }) + ] + )), + databricks().verified_only_select(sql).projection[0] + ); + + databricks().verified_expr( + "map_zip_with(map(1, 'a', 2, 'b'), map(1, 'x', 2, 'y'), (k, v1, v2) -> concat(v1, v2))", + ); + databricks().verified_expr("transform(array(1, 2, 3), x -> x + 1)"); +} + #[test] fn test_values_clause() { let values = Values { From aab12add36bfb4dfd60c2ba38682b503cc248199 Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Mon, 24 Feb 2025 08:34:36 +0100 Subject: [PATCH 740/806] BigQuery: Add support for `BEGIN` (#1718) --- src/ast/mod.rs | 43 ++++++++++++++++++++++++++++ src/dialect/bigquery.rs | 57 +++++++++++++++++++++++++++++++++++-- src/parser/mod.rs | 27 ++++++++++++++++++ tests/sqlparser_bigquery.rs | 46 ++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 33 ++++++++++++--------- tests/sqlparser_sqlite.rs | 17 ----------- 6 files changed, 190 insertions(+), 33 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 649b1f79e..aad122fbf 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3072,6 +3072,28 @@ pub enum Statement { begin: bool, transaction: Option, modifier: Option, + /// List of statements belonging to the `BEGIN` block. + /// Example: + /// ```sql + /// BEGIN + /// SELECT 1; + /// SELECT 2; + /// END; + /// ``` + statements: Vec, + /// Statements of an exception clause. + /// Example: + /// ```sql + /// BEGIN + /// SELECT 1; + /// EXCEPTION WHEN ERROR THEN + /// SELECT 2; + /// SELECT 3; + /// END; + /// + exception_statements: Option>, + /// TRUE if the statement has an `END` keyword. + has_end_keyword: bool, }, /// ```sql /// SET TRANSACTION ... @@ -4815,6 +4837,9 @@ impl fmt::Display for Statement { begin: syntax_begin, transaction, modifier, + statements, + exception_statements, + has_end_keyword, } => { if *syntax_begin { if let Some(modifier) = *modifier { @@ -4831,6 +4856,24 @@ impl fmt::Display for Statement { if !modes.is_empty() { 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, ";")?; + } + 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, ";")?; + } + } + if *has_end_keyword { + write!(f, " END")?; + } Ok(()) } Statement::SetTransaction { diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index b8e7e4cf0..49fb24f19 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -15,9 +15,10 @@ // specific language governing permissions and limitations // under the License. +use crate::ast::Statement; use crate::dialect::Dialect; use crate::keywords::Keyword; -use crate::parser::Parser; +use crate::parser::{Parser, ParserError}; /// These keywords are disallowed as column identifiers. Such that /// `SELECT 5 AS FROM T` is rejected by BigQuery. @@ -44,7 +45,11 @@ const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ pub struct BigQueryDialect; impl Dialect for BigQueryDialect { - // See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers + fn parse_statement(&self, parser: &mut Parser) -> Option> { + self.maybe_parse_statement(parser) + } + + /// See fn is_delimited_identifier_start(&self, ch: char) -> bool { ch == '`' } @@ -60,6 +65,9 @@ impl Dialect for BigQueryDialect { fn is_identifier_start(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' + // BigQuery supports `@@foo.bar` variable syntax in its procedural language. + // https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend + || ch == '@' } fn is_identifier_part(&self, ch: char) -> bool { @@ -129,3 +137,48 @@ impl Dialect for BigQueryDialect { !RESERVED_FOR_COLUMN_ALIAS.contains(kw) } } + +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/parser/mod.rs b/src/parser/mod.rs index e40f4d58a..c08c7049d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4273,6 +4273,27 @@ impl<'a> Parser<'a> { self.parse_comma_separated(f) } + /// Parses 0 or more statements, each followed by a semicolon. + /// If the next token is any of `terminal_keywords` then no more + /// statements will be parsed. + pub(crate) fn parse_statement_list( + &mut self, + terminal_keywords: &[Keyword], + ) -> 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; + } + } + + values.push(self.parse_statement()?); + self.expect_token(&Token::SemiColon)?; + } + Ok(values) + } + /// Default implementation of a predicate that returns true if /// the specified keyword is reserved for column alias. /// See [Dialect::is_column_alias] @@ -13783,6 +13804,9 @@ impl<'a> Parser<'a> { begin: false, transaction: Some(BeginTransactionKind::Transaction), modifier: None, + statements: vec![], + exception_statements: None, + has_end_keyword: false, }) } @@ -13812,6 +13836,9 @@ impl<'a> Parser<'a> { begin: true, transaction, modifier, + statements: vec![], + exception_statements: None, + has_end_keyword: false, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 52aa3b3b4..55e354221 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -236,6 +236,52 @@ fn parse_big_query_non_reserved_column_alias() { bigquery().verified_stmt(sql); } +#[test] +fn parse_at_at_identifier() { + bigquery().verified_stmt("SELECT @@error.stack_trace, @@error.message"); +} + +#[test] +fn parse_begin() { + let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; END"#; + let Statement::StartTransaction { + statements, + exception_statements, + has_end_keyword, + .. + } = bigquery().verified_stmt(sql) + else { + unreachable!(); + }; + assert_eq!(1, statements.len()); + assert_eq!(1, exception_statements.unwrap().len()); + assert!(has_end_keyword); + + bigquery().verified_stmt( + "BEGIN SELECT 1; SELECT 2; EXCEPTION WHEN ERROR THEN SELECT 2; SELECT 4; END", + ); + bigquery() + .verified_stmt("BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT @@error.stack_trace; END"); + bigquery().verified_stmt("BEGIN EXCEPTION WHEN ERROR THEN SELECT 2; END"); + bigquery().verified_stmt("BEGIN SELECT 1; SELECT 2; EXCEPTION WHEN ERROR THEN END"); + bigquery().verified_stmt("BEGIN EXCEPTION WHEN ERROR THEN END"); + bigquery().verified_stmt("BEGIN SELECT 1; SELECT 2; END"); + bigquery().verified_stmt("BEGIN END"); + + assert_eq!( + bigquery() + .parse_sql_statements("BEGIN SELECT 1; SELECT 2 END") + .unwrap_err(), + ParserError::ParserError("Expected: ;, found: END".to_string()) + ); + assert_eq!( + bigquery() + .parse_sql_statements("BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2 END") + .unwrap_err(), + ParserError::ParserError("Expected: ;, found: END".to_string()) + ); +} + #[test] fn parse_delete_statement() { let sql = "DELETE \"table\" WHERE 1"; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 578c42de1..0072baf75 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8343,7 +8343,12 @@ fn lateral_function() { #[test] fn parse_start_transaction() { - match verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { + let dialects = all_dialects_except(|d| + // BigQuery does not support this syntax + d.is::()); + match dialects + .verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") + { Statement::StartTransaction { modes, .. } => assert_eq!( modes, vec![ @@ -8357,7 +8362,7 @@ fn parse_start_transaction() { // For historical reasons, PostgreSQL allows the commas between the modes to // be omitted. - match one_statement_parses_to( + match dialects.one_statement_parses_to( "START TRANSACTION READ ONLY READ WRITE ISOLATION LEVEL SERIALIZABLE", "START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE", ) { @@ -8372,40 +8377,40 @@ fn parse_start_transaction() { _ => unreachable!(), } - verified_stmt("START TRANSACTION"); - verified_stmt("BEGIN"); - verified_stmt("BEGIN WORK"); - verified_stmt("BEGIN TRANSACTION"); + dialects.verified_stmt("START TRANSACTION"); + dialects.verified_stmt("BEGIN"); + dialects.verified_stmt("BEGIN WORK"); + dialects.verified_stmt("BEGIN TRANSACTION"); - verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); - verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED"); - verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ"); - verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"); + dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); + dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED"); + dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ"); + dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"); // Regression test for https://github.com/sqlparser-rs/sqlparser-rs/pull/139, // in which START TRANSACTION would fail to parse if followed by a statement // terminator. assert_eq!( - parse_sql_statements("START TRANSACTION; SELECT 1"), + dialects.parse_sql_statements("START TRANSACTION; SELECT 1"), Ok(vec![ verified_stmt("START TRANSACTION"), verified_stmt("SELECT 1"), ]) ); - let res = parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD"); + let res = dialects.parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD"); assert_eq!( ParserError::ParserError("Expected: isolation level, found: BAD".to_string()), res.unwrap_err() ); - let res = parse_sql_statements("START TRANSACTION BAD"); + let res = dialects.parse_sql_statements("START TRANSACTION BAD"); assert_eq!( ParserError::ParserError("Expected: end of statement, found: BAD".to_string()), res.unwrap_err() ); - let res = parse_sql_statements("START TRANSACTION READ ONLY,"); + let res = dialects.parse_sql_statements("START TRANSACTION READ ONLY,"); assert_eq!( ParserError::ParserError("Expected: transaction mode, found: EOF".to_string()), res.unwrap_err() diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index c17743305..17dcfed85 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -518,23 +518,6 @@ fn parse_start_transaction_with_modifier() { sqlite_and_generic().verified_stmt("BEGIN DEFERRED"); sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE"); sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE"); - - let unsupported_dialects = all_dialects_except(|d| d.supports_start_transaction_modifier()); - let res = unsupported_dialects.parse_sql_statements("BEGIN DEFERRED"); - assert_eq!( - ParserError::ParserError("Expected: end of statement, found: DEFERRED".to_string()), - res.unwrap_err(), - ); - let res = unsupported_dialects.parse_sql_statements("BEGIN IMMEDIATE"); - assert_eq!( - ParserError::ParserError("Expected: end of statement, found: IMMEDIATE".to_string()), - res.unwrap_err(), - ); - let res = unsupported_dialects.parse_sql_statements("BEGIN EXCLUSIVE"); - assert_eq!( - ParserError::ParserError("Expected: end of statement, found: EXCLUSIVE".to_string()), - res.unwrap_err(), - ); } #[test] From c335c8883be2bf9407166a07c0b55ee9d8195a4d Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 25 Feb 2025 07:33:57 +0100 Subject: [PATCH 741/806] Store spans for Value expressions (#1738) --- src/ast/mod.rs | 21 +- src/ast/spans.rs | 41 +- src/ast/value.rs | 101 +++- src/ast/visitor.rs | 2 +- src/parser/mod.rs | 134 +++-- tests/sqlparser_bigquery.rs | 473 ++++++++-------- tests/sqlparser_clickhouse.rs | 97 ++-- tests/sqlparser_common.rs | 902 ++++++++++++++++++------------ tests/sqlparser_custom_dialect.rs | 2 +- tests/sqlparser_databricks.rs | 41 +- tests/sqlparser_duckdb.rs | 43 +- tests/sqlparser_hive.rs | 3 +- tests/sqlparser_mssql.rs | 137 +++-- tests/sqlparser_mysql.rs | 136 +++-- tests/sqlparser_postgres.rs | 431 ++++++++------ tests/sqlparser_redshift.rs | 24 +- tests/sqlparser_snowflake.rs | 46 +- tests/sqlparser_sqlite.rs | 28 +- 18 files changed, 1620 insertions(+), 1042 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index aad122fbf..afe3e77d2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -86,7 +86,7 @@ pub use self::trigger::{ pub use self::value::{ escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString, - NormalizationForm, TrimWhereField, Value, + NormalizationForm, TrimWhereField, Value, ValueWithSpan, }; use crate::ast::helpers::key_value_options::KeyValueOptions; @@ -908,7 +908,7 @@ pub enum Expr { /// Nested expression e.g. `(foo > bar)` or `(1)` Nested(Box), /// A literal value, such as string, number, date or NULL - Value(Value), + Value(ValueWithSpan), /// IntroducedString { introducer: String, @@ -1051,6 +1051,13 @@ pub enum Expr { Lambda(LambdaFunction), } +impl Expr { + /// Creates a new [`Expr::Value`] + pub fn value(value: impl Into) -> Self { + Expr::Value(value.into()) + } +} + /// The contents inside the `[` and `]` in a subscript expression. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -8789,9 +8796,9 @@ mod tests { #[test] fn test_interval_display() { let interval = Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "123:45.67", - )))), + value: Box::new(Expr::Value( + Value::SingleQuotedString(String::from("123:45.67")).with_empty_span(), + )), leading_field: Some(DateTimeField::Minute), leading_precision: Some(10), last_field: Some(DateTimeField::Second), @@ -8803,7 +8810,9 @@ mod tests { ); let interval = Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("5")))), + value: Box::new(Expr::Value( + Value::SingleQuotedString(String::from("5")).with_empty_span(), + )), leading_field: Some(DateTimeField::Second), leading_precision: Some(1), last_field: None, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a036271cb..cfc3eb633 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -21,21 +21,21 @@ use core::iter; use crate::tokenizer::Span; use super::{ - dcl::SecondaryRoles, 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, 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, + 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, + 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. @@ -1978,10 +1978,13 @@ impl Spanned for TableAliasColumnDef { } } -/// # missing span -/// -/// The span of a `Value` is currently not implemented, as doing so -/// requires a breaking changes, which may be done in a future release. +impl Spanned for ValueWithSpan { + fn span(&self) -> Span { + self.span + } +} + +/// The span is stored in the `ValueWrapper` struct impl Spanned for Value { fn span(&self) -> Span { Span::empty() // # todo: Value needs to store spans before this is possible diff --git a/src/ast/value.rs b/src/ast/value.rs index f2f02754b..77e2e0e81 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -26,10 +26,88 @@ use bigdecimal::BigDecimal; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::ast::Ident; +use crate::{ast::Ident, tokenizer::Span}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +/// Wraps a primitive SQL [`Value`] with its [`Span`] location +/// +/// # Example: create a `ValueWithSpan` from a `Value` +/// ``` +/// # use sqlparser::ast::{Value, ValueWithSpan}; +/// # use sqlparser::tokenizer::{Location, Span}; +/// let value = Value::SingleQuotedString(String::from("endpoint")); +/// // from line 1, column 1 to line 1, column 7 +/// let span = Span::new(Location::new(1, 1), Location::new(1, 7)); +/// let value_with_span = value.with_span(span); +/// ``` +/// +/// # Example: create a `ValueWithSpan` from a `Value` with an empty span +/// +/// You can call [`Value::with_empty_span`] to create a `ValueWithSpan` with an empty span +/// ``` +/// # use sqlparser::ast::{Value, ValueWithSpan}; +/// # use sqlparser::tokenizer::{Location, Span}; +/// let value = Value::SingleQuotedString(String::from("endpoint")); +/// let value_with_span = value.with_empty_span(); +/// assert_eq!(value_with_span.span, Span::empty()); +/// ``` +/// +/// You can also use the [`From`] trait to convert `ValueWithSpan` to/from `Value`s +/// ``` +/// # use sqlparser::ast::{Value, ValueWithSpan}; +/// # use sqlparser::tokenizer::{Location, Span}; +/// let value = Value::SingleQuotedString(String::from("endpoint")); +/// // converting `Value` to `ValueWithSpan` results in an empty span +/// let value_with_span: ValueWithSpan = value.into(); +/// assert_eq!(value_with_span.span, Span::empty()); +/// // convert back to `Value` +/// let value: Value = value_with_span.into(); +/// ``` +#[derive(Debug, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ValueWithSpan { + pub value: Value, + pub span: Span, +} + +impl PartialEq for ValueWithSpan { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Ord for ValueWithSpan { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.value.cmp(&other.value) + } +} + +impl PartialOrd for ValueWithSpan { + fn partial_cmp(&self, other: &Self) -> Option { + Some(Ord::cmp(self, other)) + } +} + +impl core::hash::Hash for ValueWithSpan { + fn hash(&self, state: &mut H) { + self.value.hash(state); + } +} + +impl From for ValueWithSpan { + fn from(value: Value) -> Self { + value.with_empty_span() + } +} + +impl From for Value { + fn from(value: ValueWithSpan) -> Self { + value.value + } +} + /// Primitive SQL values such as number and string #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -102,6 +180,13 @@ pub enum Value { Placeholder(String), } +impl ValueWithSpan { + /// If the underlying literal is a string, regardless of quote style, returns the associated string value + pub fn into_string(self) -> Option { + self.value.into_string() + } +} + impl Value { /// If the underlying literal is a string, regardless of quote style, returns the associated string value pub fn into_string(self) -> Option { @@ -126,6 +211,20 @@ impl Value { _ => None, } } + + pub fn with_span(self, span: Span) -> ValueWithSpan { + ValueWithSpan { value: self, span } + } + + pub fn with_empty_span(self) -> ValueWithSpan { + self.with_span(Span::empty()) + } +} + +impl fmt::Display for ValueWithSpan { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.value) + } } impl fmt::Display for Value { diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index bb6246498..a5d355fe4 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -547,7 +547,7 @@ where /// /// visit_expressions_mut(&mut statements, |expr| { /// if matches!(expr, Expr::Identifier(col_name) if col_name.value == "x") { -/// let old_expr = std::mem::replace(expr, Expr::Value(Value::Null)); +/// let old_expr = std::mem::replace(expr, Expr::value(Value::Null)); /// *expr = Expr::Function(Function { /// name: ObjectName::from(vec![Ident::new("f")]), /// uses_odbc_syntax: false, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c08c7049d..72e4567cb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1315,7 +1315,7 @@ impl<'a> Parser<'a> { DataType::Custom(..) => parser_err!("dummy", loc), data_type => Ok(Expr::TypedString { data_type, - value: parser.parse_value()?, + value: parser.parse_value()?.value, }), } })?; @@ -1503,7 +1503,7 @@ impl<'a> Parser<'a> { } fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result { - let value: Value = self.parse_value()?; + let value: Value = self.parse_value()?.value; Ok(Expr::TypedString { data_type: DataType::GeometricType(kind), value, @@ -2089,7 +2089,7 @@ impl<'a> Parser<'a> { pub fn parse_optional_cast_format(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::FORMAT) { - let value = self.parse_value()?; + let value = self.parse_value()?.value; match self.parse_optional_time_zone()? { Some(tz) => Ok(Some(CastFormat::ValueAtTimeZone(value, tz))), None => Ok(Some(CastFormat::Value(value))), @@ -2101,7 +2101,7 @@ impl<'a> Parser<'a> { pub fn parse_optional_time_zone(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::AT, Keyword::TIME, Keyword::ZONE]) { - self.parse_value().map(Some) + self.parse_value().map(|v| Some(v.value)) } else { Ok(None) } @@ -2230,7 +2230,7 @@ impl<'a> Parser<'a> { CeilFloorKind::DateTimeField(self.parse_date_time_field()?) } else if self.consume_token(&Token::Comma) { // Parse `CEIL/FLOOR(expr, scale)` - match self.parse_value()? { + match self.parse_value()?.value { Value::Number(n, s) => CeilFloorKind::Scale(Value::Number(n, s)), _ => { return Err(ParserError::ParserError( @@ -2566,7 +2566,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; // MySQL is too permissive about the value, IMO we can't validate it perfectly on syntax level. - let match_value = self.parse_value()?; + let match_value = self.parse_value()?.value; let in_natural_language_mode_keywords = &[ Keyword::IN, @@ -6324,11 +6324,11 @@ impl<'a> Parser<'a> { FetchDirection::Last } else if self.parse_keyword(Keyword::ABSOLUTE) { FetchDirection::Absolute { - limit: self.parse_number_value()?, + limit: self.parse_number_value()?.value, } } else if self.parse_keyword(Keyword::RELATIVE) { FetchDirection::Relative { - limit: self.parse_number_value()?, + limit: self.parse_number_value()?.value, } } else if self.parse_keyword(Keyword::FORWARD) { if self.parse_keyword(Keyword::ALL) { @@ -6336,7 +6336,7 @@ impl<'a> Parser<'a> { } else { FetchDirection::Forward { // TODO: Support optional - limit: Some(self.parse_number_value()?), + limit: Some(self.parse_number_value()?.value), } } } else if self.parse_keyword(Keyword::BACKWARD) { @@ -6345,14 +6345,14 @@ impl<'a> Parser<'a> { } else { FetchDirection::Backward { // TODO: Support optional - limit: Some(self.parse_number_value()?), + limit: Some(self.parse_number_value()?.value), } } } else if self.parse_keyword(Keyword::ALL) { FetchDirection::All } else { FetchDirection::Count { - limit: self.parse_number_value()?, + limit: self.parse_number_value()?.value, } }; @@ -7345,7 +7345,7 @@ impl<'a> Parser<'a> { }; self.expect_keyword_is(Keyword::INTO)?; - let num_buckets = self.parse_number_value()?; + let num_buckets = self.parse_number_value()?.value; self.expect_keyword_is(Keyword::BUCKETS)?; Some(ClusteredBy { columns, @@ -8579,21 +8579,22 @@ impl<'a> Parser<'a> { } /// Parse a literal value (numbers, strings, date/time, booleans) - pub fn parse_value(&mut self) -> Result { + pub fn parse_value(&mut self) -> Result { let next_token = self.next_token(); let span = next_token.span; + let ok_value = |value: Value| Ok(value.with_span(span)); match next_token.token { Token::Word(w) => match w.keyword { Keyword::TRUE if self.dialect.supports_boolean_literals() => { - Ok(Value::Boolean(true)) + ok_value(Value::Boolean(true)) } Keyword::FALSE if self.dialect.supports_boolean_literals() => { - Ok(Value::Boolean(false)) + ok_value(Value::Boolean(false)) } - Keyword::NULL => Ok(Value::Null), + Keyword::NULL => ok_value(Value::Null), Keyword::NoKeyword if w.quote_style.is_some() => match w.quote_style { - Some('"') => Ok(Value::DoubleQuotedString(w.value)), - Some('\'') => Ok(Value::SingleQuotedString(w.value)), + Some('"') => ok_value(Value::DoubleQuotedString(w.value)), + Some('\'') => ok_value(Value::SingleQuotedString(w.value)), _ => self.expected( "A value?", TokenWithSpan { @@ -8613,45 +8614,51 @@ impl<'a> Parser<'a> { // The call to n.parse() returns a bigdecimal when the // bigdecimal feature is enabled, and is otherwise a no-op // (i.e., it returns the input string). - Token::Number(n, l) => Ok(Value::Number(Self::parse(n, span.start)?, l)), - Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())), - Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())), + Token::Number(n, l) => ok_value(Value::Number(Self::parse(n, span.start)?, l)), + Token::SingleQuotedString(ref s) => ok_value(Value::SingleQuotedString(s.to_string())), + Token::DoubleQuotedString(ref s) => ok_value(Value::DoubleQuotedString(s.to_string())), Token::TripleSingleQuotedString(ref s) => { - Ok(Value::TripleSingleQuotedString(s.to_string())) + ok_value(Value::TripleSingleQuotedString(s.to_string())) } Token::TripleDoubleQuotedString(ref s) => { - Ok(Value::TripleDoubleQuotedString(s.to_string())) + ok_value(Value::TripleDoubleQuotedString(s.to_string())) } - Token::DollarQuotedString(ref s) => Ok(Value::DollarQuotedString(s.clone())), + Token::DollarQuotedString(ref s) => ok_value(Value::DollarQuotedString(s.clone())), Token::SingleQuotedByteStringLiteral(ref s) => { - Ok(Value::SingleQuotedByteStringLiteral(s.clone())) + ok_value(Value::SingleQuotedByteStringLiteral(s.clone())) } Token::DoubleQuotedByteStringLiteral(ref s) => { - Ok(Value::DoubleQuotedByteStringLiteral(s.clone())) + ok_value(Value::DoubleQuotedByteStringLiteral(s.clone())) } Token::TripleSingleQuotedByteStringLiteral(ref s) => { - Ok(Value::TripleSingleQuotedByteStringLiteral(s.clone())) + ok_value(Value::TripleSingleQuotedByteStringLiteral(s.clone())) } Token::TripleDoubleQuotedByteStringLiteral(ref s) => { - Ok(Value::TripleDoubleQuotedByteStringLiteral(s.clone())) + ok_value(Value::TripleDoubleQuotedByteStringLiteral(s.clone())) } Token::SingleQuotedRawStringLiteral(ref s) => { - Ok(Value::SingleQuotedRawStringLiteral(s.clone())) + ok_value(Value::SingleQuotedRawStringLiteral(s.clone())) } Token::DoubleQuotedRawStringLiteral(ref s) => { - Ok(Value::DoubleQuotedRawStringLiteral(s.clone())) + ok_value(Value::DoubleQuotedRawStringLiteral(s.clone())) } Token::TripleSingleQuotedRawStringLiteral(ref s) => { - Ok(Value::TripleSingleQuotedRawStringLiteral(s.clone())) + ok_value(Value::TripleSingleQuotedRawStringLiteral(s.clone())) } Token::TripleDoubleQuotedRawStringLiteral(ref s) => { - Ok(Value::TripleDoubleQuotedRawStringLiteral(s.clone())) + ok_value(Value::TripleDoubleQuotedRawStringLiteral(s.clone())) } - Token::NationalStringLiteral(ref s) => Ok(Value::NationalStringLiteral(s.to_string())), - Token::EscapedStringLiteral(ref s) => Ok(Value::EscapedStringLiteral(s.to_string())), - Token::UnicodeStringLiteral(ref s) => Ok(Value::UnicodeStringLiteral(s.to_string())), - Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())), - Token::Placeholder(ref s) => Ok(Value::Placeholder(s.to_string())), + Token::NationalStringLiteral(ref s) => { + ok_value(Value::NationalStringLiteral(s.to_string())) + } + Token::EscapedStringLiteral(ref s) => { + ok_value(Value::EscapedStringLiteral(s.to_string())) + } + Token::UnicodeStringLiteral(ref s) => { + ok_value(Value::UnicodeStringLiteral(s.to_string())) + } + Token::HexStringLiteral(ref s) => ok_value(Value::HexStringLiteral(s.to_string())), + Token::Placeholder(ref s) => ok_value(Value::Placeholder(s.to_string())), tok @ Token::Colon | tok @ Token::AtSign => { // Not calling self.parse_identifier(false)? because only in placeholder we want to check numbers as idfentifies // This because snowflake allows numbers as placeholders @@ -8662,7 +8669,7 @@ impl<'a> Parser<'a> { _ => self.expected("placeholder", next_token), }?; let placeholder = tok.to_string() + &ident.value; - Ok(Value::Placeholder(placeholder)) + ok_value(Value::Placeholder(placeholder)) } unexpected => self.expected( "a value", @@ -8675,10 +8682,11 @@ impl<'a> Parser<'a> { } /// Parse an unsigned numeric literal - pub fn parse_number_value(&mut self) -> Result { - match self.parse_value()? { - v @ Value::Number(_, _) => Ok(v), - v @ Value::Placeholder(_) => Ok(v), + pub fn parse_number_value(&mut self) -> Result { + let value_wrapper = self.parse_value()?; + match &value_wrapper.value { + Value::Number(_, _) => Ok(value_wrapper), + Value::Placeholder(_) => Ok(value_wrapper), _ => { self.prev_token(); self.expected("literal number", self.peek_token()) @@ -8736,15 +8744,16 @@ impl<'a> Parser<'a> { /// e.g. `CREATE FUNCTION ... AS $$ body $$`. fn parse_create_function_body_string(&mut self) -> Result { let peek_token = self.peek_token(); + let span = peek_token.span; match peek_token.token { Token::DollarQuotedString(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { self.next_token(); - Ok(Expr::Value(Value::DollarQuotedString(s))) + Ok(Expr::Value(Value::DollarQuotedString(s).with_span(span))) } - _ => Ok(Expr::Value(Value::SingleQuotedString( - self.parse_literal_string()?, - ))), + _ => Ok(Expr::Value( + Value::SingleQuotedString(self.parse_literal_string()?).with_span(span), + )), } } @@ -10268,7 +10277,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()?; + let value = p.parse_value()?.value; Ok(Setting { key, value }) })?; Some(key_values) @@ -10990,7 +10999,7 @@ impl<'a> Parser<'a> { }) } else if variable.to_string() == "TRANSACTION" && modifier.is_none() { if self.parse_keyword(Keyword::SNAPSHOT) { - let snapshot_id = self.parse_value()?; + let snapshot_id = self.parse_value()?.value; return Ok(Statement::SetTransaction { modes: vec![], snapshot: Some(snapshot_id), @@ -11655,7 +11664,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword_with_tokens(Keyword::JSON_TABLE, &[Token::LParen]) { let json_expr = self.parse_expr()?; self.expect_token(&Token::Comma)?; - let json_path = self.parse_value()?; + let json_path = self.parse_value()?.value; self.expect_keyword_is(Keyword::COLUMNS)?; self.expect_token(&Token::LParen)?; let columns = self.parse_comma_separated(Parser::parse_json_table_column_def)?; @@ -11790,9 +11799,9 @@ impl<'a> Parser<'a> { let parenthesized = self.consume_token(&Token::LParen); let (quantity, bucket) = if parenthesized && self.parse_keyword(Keyword::BUCKET) { - let selected_bucket = self.parse_number_value()?; + let selected_bucket = self.parse_number_value()?.value; self.expect_keywords(&[Keyword::OUT, Keyword::OF])?; - let total = self.parse_number_value()?; + let total = self.parse_number_value()?.value; let on = if self.parse_keyword(Keyword::ON) { Some(self.parse_expr()?) } else { @@ -11810,8 +11819,9 @@ impl<'a> Parser<'a> { let value = match self.maybe_parse(|p| p.parse_expr())? { Some(num) => num, None => { - if let Token::Word(w) = self.next_token().token { - Expr::Value(Value::Placeholder(w.value)) + let next_token = self.next_token(); + if let Token::Word(w) = next_token.token { + Expr::Value(Value::Placeholder(w.value).with_span(next_token.span)) } else { return parser_err!( "Expecting number or byte length e.g. 100M", @@ -11869,7 +11879,7 @@ impl<'a> Parser<'a> { modifier: TableSampleSeedModifier, ) -> Result { self.expect_token(&Token::LParen)?; - let value = self.parse_number_value()?; + let value = self.parse_number_value()?.value; self.expect_token(&Token::RParen)?; Ok(TableSampleSeed { modifier, value }) } @@ -11880,7 +11890,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let json_expr = self.parse_expr()?; let json_path = if self.consume_token(&Token::Comma) { - Some(self.parse_value()?) + Some(self.parse_value()?.value) } else { None }; @@ -12149,7 +12159,7 @@ impl<'a> Parser<'a> { pub fn parse_json_table_column_def(&mut self) -> Result { if self.parse_keyword(Keyword::NESTED) { let _has_path_keyword = self.parse_keyword(Keyword::PATH); - let path = self.parse_value()?; + let path = self.parse_value()?.value; self.expect_keyword_is(Keyword::COLUMNS)?; let columns = self.parse_parenthesized(|p| { p.parse_comma_separated(Self::parse_json_table_column_def) @@ -12167,7 +12177,7 @@ impl<'a> Parser<'a> { let r#type = self.parse_data_type()?; let exists = self.parse_keyword(Keyword::EXISTS); self.expect_keyword_is(Keyword::PATH)?; - let path = self.parse_value()?; + let path = self.parse_value()?.value; let mut on_empty = None; let mut on_error = None; while let Some(error_handling) = self.parse_json_table_column_error_handling()? { @@ -12224,7 +12234,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::ERROR) { JsonTableColumnErrorHandling::Error } else if self.parse_keyword(Keyword::DEFAULT) { - JsonTableColumnErrorHandling::Default(self.parse_value()?) + JsonTableColumnErrorHandling::Default(self.parse_value()?.value) } else { return Ok(None); }; @@ -13299,7 +13309,7 @@ impl<'a> Parser<'a> { if dialect_of!(self is GenericDialect | MySqlDialect) && self.parse_keyword(Keyword::SEPARATOR) { - clauses.push(FunctionArgumentClause::Separator(self.parse_value()?)); + clauses.push(FunctionArgumentClause::Separator(self.parse_value()?.value)); } if let Some(on_overflow) = self.parse_listagg_on_overflow()? { @@ -14183,7 +14193,7 @@ impl<'a> Parser<'a> { } fn parse_pragma_value(&mut self) -> Result { - match self.parse_value()? { + match self.parse_value()?.value { v @ Value::SingleQuotedString(_) => Ok(v), v @ Value::DoubleQuotedString(_) => Ok(v), v @ Value::Number(_, _) => Ok(v), @@ -14643,7 +14653,7 @@ impl<'a> Parser<'a> { fn maybe_parse_show_stmt_starts_with(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::STARTS, Keyword::WITH]) { - Ok(Some(self.parse_value()?)) + Ok(Some(self.parse_value()?.value)) } else { Ok(None) } @@ -14659,7 +14669,7 @@ impl<'a> Parser<'a> { fn maybe_parse_show_stmt_from(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::FROM) { - Ok(Some(self.parse_value()?)) + Ok(Some(self.parse_value()?.value)) } else { Ok(None) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 55e354221..3037d4ae5 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -29,19 +29,19 @@ use test_utils::*; #[test] fn parse_literal_string() { let sql = concat!( - "SELECT ", - "'single', ", - r#""double", "#, - "'''triple-single''', ", - r#""""triple-double""", "#, - r#"'single\'escaped', "#, - r#"'''triple-single\'escaped''', "#, - r#"'''triple-single'unescaped''', "#, - r#""double\"escaped", "#, - r#""""triple-double\"escaped""", "#, - r#""""triple-double"unescaped""", "#, - r#""""triple-double'unescaped""", "#, - r#"'''triple-single"unescaped'''"#, + "SELECT ", // line 1, column 1 + "'single', ", // line 1, column 7 + r#""double", "#, // line 1, column 14 + "'''triple-single''', ", // line 1, column 22 + r#""""triple-double""", "#, // line 1, column 33 + r#"'single\'escaped', "#, // line 1, column 43 + r#"'''triple-single\'escaped''', "#, // line 1, column 55 + r#"'''triple-single'unescaped''', "#, // line 1, column 68 + r#""double\"escaped", "#, // line 1, column 83 + r#""""triple-double\"escaped""", "#, // line 1, column 92 + r#""""triple-double"unescaped""", "#, // line 1, column 105 + r#""""triple-double'unescaped""", "#, // line 1, column 118 + r#"'''triple-single"unescaped'''"#, // line 1, column 131 ); let dialect = TestedDialects::new_with_options( vec![Box::new(BigQueryDialect {})], @@ -50,63 +50,67 @@ fn parse_literal_string() { let select = dialect.verified_only_select(sql); assert_eq!(12, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedString("single".into())), + &Expr::Value(Value::SingleQuotedString("single".into()).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedString("double".into())), + &Expr::Value(Value::DoubleQuotedString("double".into()).with_empty_span()), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedString("triple-single".into())), + &Expr::Value(Value::TripleSingleQuotedString("triple-single".into()).with_empty_span()), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedString("triple-double".into())), + &Expr::Value(Value::TripleDoubleQuotedString("triple-double".into()).with_empty_span()), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::SingleQuotedString(r#"single\'escaped"#.into())), + &Expr::Value(Value::SingleQuotedString(r#"single\'escaped"#.into()).with_empty_span()), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedString( - r#"triple-single\'escaped"#.into() - )), + &Expr::Value( + Value::TripleSingleQuotedString(r#"triple-single\'escaped"#.into()).with_empty_span() + ), expr_from_projection(&select.projection[5]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedString( - r#"triple-single'unescaped"#.into() - )), + &Expr::Value( + Value::TripleSingleQuotedString(r#"triple-single'unescaped"#.into()).with_empty_span() + ), expr_from_projection(&select.projection[6]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedString(r#"double\"escaped"#.to_string())), + &Expr::Value(Value::DoubleQuotedString(r#"double\"escaped"#.to_string()).with_empty_span()), expr_from_projection(&select.projection[7]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedString( - r#"triple-double\"escaped"#.to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedString(r#"triple-double\"escaped"#.to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[8]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedString( - r#"triple-double"unescaped"#.to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedString(r#"triple-double"unescaped"#.to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[9]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedString( - r#"triple-double'unescaped"#.to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedString(r#"triple-double'unescaped"#.to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[10]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedString( - r#"triple-single"unescaped"#.to_string() - )), + &Expr::Value( + Value::TripleSingleQuotedString(r#"triple-single"unescaped"#.to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[11]) ); } @@ -114,48 +118,56 @@ fn parse_literal_string() { #[test] fn parse_byte_literal() { let sql = concat!( - "SELECT ", - "B'abc', ", - r#"B"abc", "#, - r#"B'f\(abc,(.*),def\)', "#, - r#"B"f\(abc,(.*),def\)", "#, - r#"B'''abc''', "#, - r#"B"""abc""""#, + "SELECT ", // line 1, column 1 + "B'abc', ", // line 1, column 8 + r#"B"abc", "#, // line 1, column 15 + r#"B'f\(abc,(.*),def\)', "#, // line 1, column 22 + r#"B"f\(abc,(.*),def\)", "#, // line 1, column 42 + r#"B'''abc''', "#, // line 1, column 62 + r#"B"""abc""""#, // line 1, column 74 ); let stmt = bigquery().verified_stmt(sql); if let Statement::Query(query) = stmt { if let SetExpr::Select(select) = *query.body { assert_eq!(6, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedByteStringLiteral("abc".to_string())), + &Expr::Value( + Value::SingleQuotedByteStringLiteral("abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedByteStringLiteral("abc".to_string())), + &Expr::Value( + Value::DoubleQuotedByteStringLiteral("abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::SingleQuotedByteStringLiteral( - r"f\(abc,(.*),def\)".to_string() - )), + &Expr::Value( + Value::SingleQuotedByteStringLiteral(r"f\(abc,(.*),def\)".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedByteStringLiteral( - r"f\(abc,(.*),def\)".to_string() - )), + &Expr::Value( + Value::DoubleQuotedByteStringLiteral(r"f\(abc,(.*),def\)".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedByteStringLiteral( - r"abc".to_string() - )), + &Expr::Value( + Value::TripleSingleQuotedByteStringLiteral(r"abc".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedByteStringLiteral( - r"abc".to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedByteStringLiteral(r"abc".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[5]) ); } @@ -172,48 +184,54 @@ fn parse_byte_literal() { #[test] fn parse_raw_literal() { let sql = concat!( - "SELECT ", - "R'abc', ", - r#"R"abc", "#, - r#"R'f\(abc,(.*),def\)', "#, - r#"R"f\(abc,(.*),def\)", "#, - r#"R'''abc''', "#, - r#"R"""abc""""#, + "SELECT ", // line 1, column 1 + "R'abc', ", // line 1, column 8 + r#"R"abc", "#, // line 1, column 15 + r#"R'f\(abc,(.*),def\)', "#, // line 1, column 22 + r#"R"f\(abc,(.*),def\)", "#, // line 1, column 42 + r#"R'''abc''', "#, // line 1, column 62 + r#"R"""abc""""#, // line 1, column 74 ); let stmt = bigquery().verified_stmt(sql); if let Statement::Query(query) = stmt { if let SetExpr::Select(select) = *query.body { assert_eq!(6, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedRawStringLiteral("abc".to_string())), + &Expr::Value( + Value::SingleQuotedRawStringLiteral("abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedRawStringLiteral("abc".to_string())), + &Expr::Value( + Value::DoubleQuotedRawStringLiteral("abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::SingleQuotedRawStringLiteral( - r"f\(abc,(.*),def\)".to_string() - )), + &Expr::Value( + Value::SingleQuotedRawStringLiteral(r"f\(abc,(.*),def\)".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedRawStringLiteral( - r"f\(abc,(.*),def\)".to_string() - )), + &Expr::Value( + Value::DoubleQuotedRawStringLiteral(r"f\(abc,(.*),def\)".to_string()) + .with_empty_span() + ), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::TripleSingleQuotedRawStringLiteral( - r"abc".to_string() - )), + &Expr::Value( + Value::TripleSingleQuotedRawStringLiteral(r"abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::TripleDoubleQuotedRawStringLiteral( - r"abc".to_string() - )), + &Expr::Value( + Value::TripleDoubleQuotedRawStringLiteral(r"abc".to_string()).with_empty_span() + ), expr_from_projection(&select.projection[5]) ); } @@ -336,7 +354,11 @@ fn parse_create_view_with_options() { data_type: None, options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString("field age".to_string())), + value: Expr::Value( + Value::DoubleQuotedString("field age".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), }])]), }, ], @@ -356,9 +378,10 @@ fn parse_create_view_with_options() { assert_eq!( &SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString( - "a view that expires in 2 days".to_string() - )), + value: Expr::Value( + Value::DoubleQuotedString("a view that expires in 2 days".to_string()) + .with_empty_span() + ), }, &options[2], ); @@ -366,6 +389,7 @@ fn parse_create_view_with_options() { _ => unreachable!(), } } + #[test] fn parse_create_view_if_not_exists() { let sql = "CREATE VIEW IF NOT EXISTS mydataset.newview AS SELECT foo FROM bar"; @@ -481,9 +505,11 @@ fn parse_create_table_with_options() { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString( - "field x".to_string() - )), + value: Expr::Value( + Value::DoubleQuotedString("field x".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), },]) }, ] @@ -495,9 +521,11 @@ fn parse_create_table_with_options() { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString( - "field y".to_string() - )), + value: Expr::Value( + Value::DoubleQuotedString("field y".to_string()).with_span( + Span::new(Location::new(1, 42), Location::new(1, 52)) + ) + ), },]) }] }, @@ -514,13 +542,22 @@ fn parse_create_table_with_options() { Some(vec![ SqlOption::KeyValue { key: Ident::new("partition_expiration_days"), - value: Expr::Value(number("1")), + value: Expr::Value( + number("1").with_span(Span::new( + Location::new(1, 42), + Location::new(1, 43) + )) + ), }, SqlOption::KeyValue { key: Ident::new("description"), - value: Expr::Value(Value::DoubleQuotedString( - "table option description".to_string() - )), + value: Expr::Value( + Value::DoubleQuotedString("table option description".to_string()) + .with_span(Span::new( + Location::new(1, 42), + Location::new(1, 52) + )) + ), }, ]) ), @@ -628,23 +665,23 @@ fn parse_invalid_brackets() { fn parse_tuple_struct_literal() { // tuple syntax: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#tuple_syntax // syntax: (expr1, expr2 [, ... ]) - let sql = "SELECT (1, 2, 3), (1, 1.0, '123', true)"; + let sql = "SELECT (1, 2, 3), (1, 1.0, '123', true)"; // line 1, column 1 let select = bigquery().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Tuple(vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ]), expr_from_projection(&select.projection[0]) ); assert_eq!( &Expr::Tuple(vec![ - Expr::Value(number("1")), - Expr::Value(number("1.0")), - Expr::Value(Value::SingleQuotedString("123".into())), - Expr::Value(Value::Boolean(true)) + Expr::value(number("1")), + Expr::value(number("1.0")), + Expr::Value(Value::SingleQuotedString("123".into()).with_empty_span()), + Expr::Value(Value::Boolean(true).with_empty_span()) ]), expr_from_projection(&select.projection[1]) ); @@ -660,9 +697,9 @@ fn parse_typeless_struct_syntax() { assert_eq!( &Expr::Struct { values: vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ], fields: Default::default() }, @@ -671,30 +708,35 @@ fn parse_typeless_struct_syntax() { assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("abc".into())),], + values: vec![Expr::Value( + Value::SingleQuotedString("abc".into()).with_empty_span() + )], fields: Default::default() }, expr_from_projection(&select.projection[1]) ); + assert_eq!( &Expr::Struct { values: vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::CompoundIdentifier(vec![Ident::from("t"), Ident::from("str_col")]), ], fields: Default::default() }, expr_from_projection(&select.projection[2]) ); + assert_eq!( &Expr::Struct { values: vec![ Expr::Named { - expr: Expr::Value(number("1")).into(), + expr: Expr::value(number("1")).into(), name: Ident::from("a") }, Expr::Named { - expr: Expr::Value(Value::SingleQuotedString("abc".into())).into(), + expr: Expr::Value(Value::SingleQuotedString("abc".into()).with_empty_span()) + .into(), name: Ident::from("b") }, ], @@ -702,6 +744,7 @@ fn parse_typeless_struct_syntax() { }, expr_from_projection(&select.projection[3]) ); + assert_eq!( &Expr::Struct { values: vec![Expr::Named { @@ -724,7 +767,7 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")),], + values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: None, field_type: DataType::Int64, @@ -735,7 +778,7 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!( &Expr::Struct { values: vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::CompoundIdentifier(vec![ Ident { value: "t".into(), @@ -776,7 +819,7 @@ fn parse_typed_struct_syntax_bigquery() { value: "nested_col".into(), quote_style: None, span: Span::empty(), - }),], + })], fields: vec![ StructField { field_name: Some("arr".into()), @@ -808,7 +851,7 @@ fn parse_typed_struct_syntax_bigquery() { value: "nested_col".into(), quote_style: None, span: Span::empty(), - }),], + })], fields: vec![ StructField { field_name: Some("x".into()), @@ -833,7 +876,7 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::Boolean(true)),], + values: vec![Expr::Value(Value::Boolean(true).with_empty_span())], fields: vec![StructField { field_name: None, field_type: DataType::Bool @@ -843,9 +886,9 @@ fn parse_typed_struct_syntax_bigquery() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedByteStringLiteral( - "abc".into() - )),], + values: vec![Expr::Value( + Value::SingleQuotedByteStringLiteral("abc".into()).with_empty_span() + )], fields: vec![StructField { field_name: None, field_type: DataType::Bytes(Some(42)) @@ -859,7 +902,9 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(4, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString("2011-05-05".into())),], + values: vec![Expr::Value( + Value::DoubleQuotedString("2011-05-05".into()).with_empty_span() + )], fields: vec![StructField { field_name: None, field_type: DataType::Date @@ -872,7 +917,7 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::TypedString { data_type: DataType::Datetime(None), value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::Datetime(None) @@ -882,7 +927,7 @@ fn parse_typed_struct_syntax_bigquery() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5.0")),], + values: vec![Expr::value(number("5.0"))], fields: vec![StructField { field_name: None, field_type: DataType::Float64 @@ -892,7 +937,7 @@ fn parse_typed_struct_syntax_bigquery() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("1")),], + values: vec![Expr::value(number("1"))], fields: vec![StructField { field_name: None, field_type: DataType::Int64 @@ -907,12 +952,14 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!( &Expr::Struct { values: vec![Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString("2".into()))), + value: Box::new(Expr::Value( + Value::SingleQuotedString("2".into()).with_empty_span() + )), leading_field: Some(DateTimeField::Hour), leading_precision: None, last_field: None, fractional_seconds_precision: None - }),], + })], fields: vec![StructField { field_name: None, field_type: DataType::Interval @@ -927,7 +974,7 @@ fn parse_typed_struct_syntax_bigquery() { value: Value::SingleQuotedString( r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() ) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::JSON @@ -941,7 +988,9 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString("foo".into())),], + values: vec![Expr::Value( + Value::DoubleQuotedString("foo".into()).with_empty_span() + )], fields: vec![StructField { field_name: None, field_type: DataType::String(Some(42)) @@ -954,7 +1003,7 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::Timestamp(None, TimezoneInfo::None) @@ -968,7 +1017,7 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), value: Value::SingleQuotedString("15:30:00".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::Time(None, TimezoneInfo::None) @@ -985,7 +1034,7 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), value: Value::SingleQuotedString("1".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::Numeric(ExactNumberInfo::None) @@ -998,7 +1047,7 @@ fn parse_typed_struct_syntax_bigquery() { values: vec![Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), value: Value::SingleQuotedString("1".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::BigNumeric(ExactNumberInfo::None) @@ -1013,7 +1062,7 @@ fn parse_typed_struct_syntax_bigquery() { assert_eq!(1, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("1")), Expr::Value(number("2")),], + values: vec![Expr::value(number("1")), Expr::value(number("2")),], fields: vec![ StructField { field_name: Some("key".into()), @@ -1039,7 +1088,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")),], + values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: None, field_type: DataType::Int64, @@ -1050,7 +1099,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!( &Expr::Struct { values: vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::CompoundIdentifier(vec![ Ident { value: "t".into(), @@ -1085,34 +1134,6 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { }, expr_from_projection(&select.projection[1]) ); - assert_eq!( - &Expr::Struct { - values: vec![Expr::Identifier(Ident { - value: "nested_col".into(), - quote_style: None, - span: Span::empty(), - }),], - fields: vec![ - StructField { - field_name: Some("arr".into()), - field_type: DataType::Array(ArrayElemTypeDef::AngleBracket(Box::new( - DataType::Float64 - ))) - }, - StructField { - field_name: Some("str".into()), - field_type: DataType::Struct( - vec![StructField { - field_name: None, - field_type: DataType::Bool - }], - StructBracketKind::AngleBrackets - ) - }, - ] - }, - expr_from_projection(&select.projection[2]) - ); let sql = r#"SELECT STRUCT>(nested_col)"#; let select = bigquery_and_generic().verified_only_select(sql); @@ -1123,7 +1144,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { value: "nested_col".into(), quote_style: None, span: Span::empty(), - }),], + })], fields: vec![ StructField { field_name: Some("x".into()), @@ -1148,7 +1169,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::Boolean(true)),], + values: vec![Expr::Value(Value::Boolean(true).with_empty_span())], fields: vec![StructField { field_name: None, field_type: DataType::Bool @@ -1158,9 +1179,9 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedByteStringLiteral( - "abc".into() - )),], + values: vec![Expr::Value( + Value::SingleQuotedByteStringLiteral("abc".into()).with_empty_span() + )], fields: vec![StructField { field_name: None, field_type: DataType::Bytes(Some(42)) @@ -1174,7 +1195,9 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(4, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("2011-05-05".into())),], + values: vec![Expr::Value( + Value::SingleQuotedString("2011-05-05".into()).with_empty_span() + )], fields: vec![StructField { field_name: None, field_type: DataType::Date @@ -1187,7 +1210,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::TypedString { data_type: DataType::Datetime(None), value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::Datetime(None) @@ -1197,7 +1220,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5.0")),], + values: vec![Expr::value(number("5.0"))], fields: vec![StructField { field_name: None, field_type: DataType::Float64 @@ -1207,7 +1230,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("1")),], + values: vec![Expr::value(number("1"))], fields: vec![StructField { field_name: None, field_type: DataType::Int64 @@ -1222,12 +1245,14 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!( &Expr::Struct { values: vec![Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString("1".into()))), + value: Box::new(Expr::Value( + Value::SingleQuotedString("1".into()).with_empty_span() + )), leading_field: Some(DateTimeField::Month), leading_precision: None, last_field: None, fractional_seconds_precision: None - }),], + })], fields: vec![StructField { field_name: None, field_type: DataType::Interval @@ -1242,7 +1267,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { value: Value::SingleQuotedString( r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() ) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::JSON @@ -1256,7 +1281,9 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { assert_eq!(3, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("foo".into())),], + values: vec![Expr::Value( + Value::SingleQuotedString("foo".into()).with_empty_span() + )], fields: vec![StructField { field_name: None, field_type: DataType::String(Some(42)) @@ -1269,7 +1296,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::TypedString { data_type: DataType::Timestamp(None, TimezoneInfo::None), value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::Timestamp(None, TimezoneInfo::None) @@ -1283,7 +1310,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::TypedString { data_type: DataType::Time(None, TimezoneInfo::None), value: Value::SingleQuotedString("15:30:00".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::Time(None, TimezoneInfo::None) @@ -1300,7 +1327,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::TypedString { data_type: DataType::Numeric(ExactNumberInfo::None), value: Value::SingleQuotedString("1".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::Numeric(ExactNumberInfo::None) @@ -1313,7 +1340,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { values: vec![Expr::TypedString { data_type: DataType::BigNumeric(ExactNumberInfo::None), value: Value::SingleQuotedString("1".into()) - },], + }], fields: vec![StructField { field_name: None, field_type: DataType::BigNumeric(ExactNumberInfo::None) @@ -1325,12 +1352,12 @@ fn parse_typed_struct_syntax_bigquery_and_generic() { #[test] fn parse_typed_struct_with_field_name_bigquery() { - let sql = r#"SELECT STRUCT(5), STRUCT("foo")"#; + let sql = r#"SELECT STRUCT(5), STRUCT("foo")"#; // line 1, column 1 let select = bigquery().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")),], + values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: Some(Ident::from("x")), field_type: DataType::Int64 @@ -1340,7 +1367,9 @@ fn parse_typed_struct_with_field_name_bigquery() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::DoubleQuotedString("foo".into())),], + values: vec![Expr::Value( + Value::DoubleQuotedString("foo".into()).with_empty_span() + )], fields: vec![StructField { field_name: Some(Ident::from("y")), field_type: DataType::String(None) @@ -1349,12 +1378,12 @@ fn parse_typed_struct_with_field_name_bigquery() { expr_from_projection(&select.projection[1]) ); - let sql = r#"SELECT STRUCT(5, 5)"#; + let sql = r#"SELECT STRUCT(5, 5)"#; // line 1, column 1 let select = bigquery().verified_only_select(sql); assert_eq!(1, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")), Expr::Value(number("5")),], + values: vec![Expr::value(number("5")), Expr::value(number("5")),], fields: vec![ StructField { field_name: Some(Ident::from("x")), @@ -1372,12 +1401,12 @@ fn parse_typed_struct_with_field_name_bigquery() { #[test] fn parse_typed_struct_with_field_name_bigquery_and_generic() { - let sql = r#"SELECT STRUCT(5), STRUCT('foo')"#; + let sql = r#"SELECT STRUCT(5), STRUCT('foo')"#; // line 1, column 1 let select = bigquery().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")),], + values: vec![Expr::value(number("5"))], fields: vec![StructField { field_name: Some(Ident::from("x")), field_type: DataType::Int64 @@ -1387,7 +1416,9 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { ); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(Value::SingleQuotedString("foo".into())),], + values: vec![Expr::Value( + Value::SingleQuotedString("foo".into()).with_empty_span() + )], fields: vec![StructField { field_name: Some(Ident::from("y")), field_type: DataType::String(None) @@ -1396,12 +1427,12 @@ fn parse_typed_struct_with_field_name_bigquery_and_generic() { expr_from_projection(&select.projection[1]) ); - let sql = r#"SELECT STRUCT(5, 5)"#; + let sql = r#"SELECT STRUCT(5, 5)"#; // line 1, column 1 let select = bigquery_and_generic().verified_only_select(sql); assert_eq!(1, select.projection.len()); assert_eq!( &Expr::Struct { - values: vec![Expr::Value(number("5")), Expr::Value(number("5")),], + values: vec![Expr::value(number("5")), Expr::value(number("5")),], fields: vec![ StructField { field_name: Some(Ident::from("x")), @@ -1609,7 +1640,7 @@ fn parse_hyphenated_table_identifiers() { #[test] fn parse_table_time_travel() { let version = "2023-08-18 23:08:18".to_string(); - let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); + let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); // line 1, column 1 let select = bigquery().verified_only_select(&sql); assert_eq!( select.from, @@ -1620,7 +1651,7 @@ fn parse_table_time_travel() { args: None, with_hints: vec![], version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( - Value::SingleQuotedString(version) + Value::SingleQuotedString(version).with_empty_span() ))), partitions: vec![], with_ordinality: false, @@ -1689,18 +1720,18 @@ fn parse_merge() { columns: vec![Ident::new("product"), Ident::new("quantity")], kind: MergeInsertKind::Values(Values { explicit_row: false, - rows: vec![vec![Expr::Value(number("1")), Expr::Value(number("2"))]], + rows: vec![vec![Expr::value(number("1")), Expr::value(number("2"))]], }), }); let update_action = MergeAction::Update { assignments: vec![ Assignment { target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("a")])), - value: Expr::Value(number("1")), + value: Expr::value(number("1")), }, Assignment { target: AssignmentTarget::ColumnName(ObjectName::from(vec![Ident::new("b")])), - value: Expr::Value(number("2")), + value: Expr::value(number("2")), }, ], }; @@ -1749,17 +1780,17 @@ fn parse_merge() { }, source ); - assert_eq!(Expr::Value(Value::Boolean(false)), *on); + assert_eq!(Expr::Value(Value::Boolean(false).with_empty_span()), *on); assert_eq!( vec![ MergeClause { clause_kind: MergeClauseKind::NotMatched, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: insert_action.clone(), }, MergeClause { clause_kind: MergeClauseKind::NotMatchedByTarget, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: insert_action.clone(), }, MergeClause { @@ -1769,7 +1800,7 @@ fn parse_merge() { }, MergeClause { clause_kind: MergeClauseKind::NotMatchedBySource, - predicate: Some(Expr::Value(number("2"))), + predicate: Some(Expr::value(number("2"))), action: MergeAction::Delete }, MergeClause { @@ -1779,12 +1810,12 @@ fn parse_merge() { }, MergeClause { clause_kind: MergeClauseKind::NotMatchedBySource, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: update_action.clone(), }, MergeClause { clause_kind: MergeClauseKind::NotMatched, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: MergeAction::Insert(MergeInsertExpr { columns: vec![Ident::new("product"), Ident::new("quantity"),], kind: MergeInsertKind::Row, @@ -1800,7 +1831,7 @@ fn parse_merge() { }, MergeClause { clause_kind: MergeClauseKind::NotMatched, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: MergeAction::Insert(MergeInsertExpr { columns: vec![], kind: MergeInsertKind::Row @@ -1816,7 +1847,7 @@ fn parse_merge() { }, MergeClause { clause_kind: MergeClauseKind::Matched, - predicate: Some(Expr::Value(number("1"))), + predicate: Some(Expr::value(number("1"))), action: MergeAction::Delete, }, MergeClause { @@ -1832,7 +1863,7 @@ fn parse_merge() { kind: MergeInsertKind::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::Identifier(Ident::new("DEFAULT")), ]] }) @@ -1846,7 +1877,7 @@ fn parse_merge() { kind: MergeInsertKind::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Value(number("1")), + Expr::value(number("1")), Expr::Identifier(Ident::new("DEFAULT")), ]] }) @@ -1962,7 +1993,7 @@ fn parse_array_agg_func() { fn parse_big_query_declare() { for (sql, expected_names, expected_data_type, expected_assigned_expr) in [ ( - "DECLARE x INT64", + "DECLARE x INT64", // line 1, column 1 vec![Ident::new("x")], Some(DataType::Int64), None, @@ -1971,25 +2002,25 @@ fn parse_big_query_declare() { "DECLARE x INT64 DEFAULT 42", vec![Ident::new("x")], Some(DataType::Int64), - Some(DeclareAssignment::Default(Box::new(Expr::Value(number( - "42", - ))))), + Some(DeclareAssignment::Default(Box::new(Expr::Value( + number("42").with_empty_span(), + )))), ), ( "DECLARE x, y, z INT64 DEFAULT 42", vec![Ident::new("x"), Ident::new("y"), Ident::new("z")], Some(DataType::Int64), - Some(DeclareAssignment::Default(Box::new(Expr::Value(number( - "42", - ))))), + Some(DeclareAssignment::Default(Box::new(Expr::Value( + number("42").with_empty_span(), + )))), ), ( "DECLARE x DEFAULT 42", vec![Ident::new("x")], None, - Some(DeclareAssignment::Default(Box::new(Expr::Value(number( - "42", - ))))), + Some(DeclareAssignment::Default(Box::new(Expr::Value( + number("42").with_empty_span(), + )))), ), ] { match bigquery().verified_stmt(sql) { @@ -2047,7 +2078,7 @@ fn parse_map_access_expr() { AccessExpr::Subscript(Subscript::Index { index: Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Expr::Value(number("1")).into(), + expr: Expr::value(number("1")).into(), }, }), AccessExpr::Subscript(Subscript::Index { @@ -2060,7 +2091,7 @@ fn parse_map_access_expr() { args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - number("2"), + number("2").with_empty_span(), )))], clauses: vec![], }), @@ -2111,12 +2142,12 @@ fn test_bigquery_create_function() { ]), args: Some(vec![OperateFunctionArg::with_name("x", DataType::Float64),]), return_type: Some(DataType::Float64), - function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value(number( - "42" - )))), + function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value( + number("42").with_empty_span() + ))), options: Some(vec![SqlOption::KeyValue { key: Ident::new("x"), - value: Expr::Value(Value::SingleQuotedString("y".into())), + value: Expr::Value(Value::SingleQuotedString("y".into()).with_empty_span()), }]), behavior: None, using: None, @@ -2235,10 +2266,14 @@ fn test_bigquery_trim() { let select = bigquery().verified_only_select(sql_only_select); assert_eq!( &Expr::Trim { - expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + 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()))]), + trim_characters: Some(vec![Expr::Value( + Value::SingleQuotedString("a".to_owned()).with_empty_span() + )]), }, expr_from_projection(only(&select.projection)) ); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 99e76d45e..98a1aef62 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -55,7 +55,10 @@ fn parse_map_access_expr() { "indexOf", [ Expr::Identifier(Ident::new("string_names")), - Expr::Value(Value::SingleQuotedString("endpoint".to_string())) + Expr::Value( + (Value::SingleQuotedString("endpoint".to_string())) + .with_empty_span() + ) ] ), })], @@ -71,7 +74,9 @@ fn parse_map_access_expr() { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString("test".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("test".to_string())).with_empty_span() + )), }), op: BinaryOperator::And, right: Box::new(BinaryOp { @@ -82,13 +87,18 @@ fn parse_map_access_expr() { "indexOf", [ Expr::Identifier(Ident::new("string_name")), - Expr::Value(Value::SingleQuotedString("app".to_string())) + Expr::Value( + (Value::SingleQuotedString("app".to_string())) + .with_empty_span() + ) ] ), })], }), op: BinaryOperator::NotEq, - right: Box::new(Expr::Value(Value::SingleQuotedString("foo".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("foo".to_string())).with_empty_span() + )), }), }), group_by: GroupByExpr::Expressions(vec![], vec![]), @@ -114,8 +124,8 @@ fn parse_array_expr() { assert_eq!( &Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("1".to_string())), - Expr::Value(Value::SingleQuotedString("2".to_string())), + Expr::Value((Value::SingleQuotedString("1".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("2".to_string())).with_empty_span()), ], named: false, }), @@ -1016,17 +1026,15 @@ fn parse_select_parametric_function() { assert_eq!(parameters.args.len(), 2); assert_eq!( parameters.args[0], - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Number( - "0.5".parse().unwrap(), - false - )))) + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (Value::Number("0.5".parse().unwrap(), false)).with_empty_span() + ))) ); assert_eq!( parameters.args[1], - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Number( - "0.6".parse().unwrap(), - false - )))) + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (Value::Number("0.6".parse().unwrap(), false)).with_empty_span() + ))) ); } _ => unreachable!(), @@ -1078,9 +1086,9 @@ fn parse_select_order_by_with_fill_interpolate() { nulls_first: Some(true), }, with_fill: Some(WithFill { - from: Some(Expr::Value(number("10"))), - to: Some(Expr::Value(number("20"))), - step: Some(Expr::Value(number("2"))), + from: Some(Expr::value(number("10"))), + to: Some(Expr::value(number("20"))), + step: Some(Expr::value(number("2"))), }), }, OrderByExpr { @@ -1090,9 +1098,9 @@ fn parse_select_order_by_with_fill_interpolate() { nulls_first: Some(false), }, with_fill: Some(WithFill { - from: Some(Expr::Value(number("30"))), - to: Some(Expr::Value(number("40"))), - step: Some(Expr::Value(number("3"))), + from: Some(Expr::value(number("30"))), + to: Some(Expr::value(number("40"))), + step: Some(Expr::value(number("3"))), }), }, ]), @@ -1102,14 +1110,14 @@ fn parse_select_order_by_with_fill_interpolate() { expr: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("col1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), }]) }) }, select.order_by.expect("ORDER BY expected") ); - assert_eq!(Some(Expr::Value(number("2"))), select.limit); + assert_eq!(Some(Expr::value(number("2"))), select.limit); } #[test] @@ -1150,9 +1158,9 @@ fn parse_with_fill() { let select = clickhouse().verified_query(sql); assert_eq!( Some(WithFill { - from: Some(Expr::Value(number("10"))), - to: Some(Expr::Value(number("20"))), - step: Some(Expr::Value(number("2"))), + from: Some(Expr::value(number("10"))), + to: Some(Expr::value(number("20"))), + step: Some(Expr::value(number("2"))), }) .as_ref(), match select.order_by.expect("ORDER BY expected").kind { @@ -1193,7 +1201,7 @@ fn parse_interpolate_body_with_columns() { expr: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("col1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), }, InterpolateExpr { @@ -1205,7 +1213,7 @@ fn parse_interpolate_body_with_columns() { expr: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("col4"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("4"))), + right: Box::new(Expr::value(number("4"))), }), }, ]) @@ -1260,7 +1268,9 @@ fn test_prewhere() { Some(&BinaryOp { left: Box::new(Identifier(Ident::new("x"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), }) ); let selection = query.as_ref().body.as_select().unwrap().selection.as_ref(); @@ -1269,7 +1279,9 @@ fn test_prewhere() { Some(&BinaryOp { left: Box::new(Identifier(Ident::new("y"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("2".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("2".parse().unwrap(), false)).with_empty_span() + )), }) ); } @@ -1285,13 +1297,17 @@ fn test_prewhere() { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("x"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), }), op: BinaryOperator::And, right: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("y"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("2".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("2".parse().unwrap(), false)).with_empty_span() + )), }), }) ); @@ -1399,10 +1415,9 @@ fn parse_create_table_on_commit_and_as_query() { assert_eq!(on_commit, Some(OnCommit::PreserveRows)); assert_eq!( query.unwrap().body.as_select().unwrap().projection, - vec![UnnamedExpr(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - )))] + vec![UnnamedExpr(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + ))] ); } _ => unreachable!(), @@ -1415,9 +1430,9 @@ fn parse_freeze_and_unfreeze_partition() { for operation_name in &["FREEZE", "UNFREEZE"] { let sql = format!("ALTER TABLE t {operation_name} PARTITION '2024-08-14'"); - let expected_partition = Partition::Expr(Expr::Value(Value::SingleQuotedString( - "2024-08-14".to_string(), - ))); + let expected_partition = Partition::Expr(Expr::Value( + Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(), + )); match clickhouse_and_generic().verified_stmt(&sql) { Statement::AlterTable { operations, .. } => { assert_eq!(operations.len(), 1); @@ -1445,9 +1460,9 @@ fn parse_freeze_and_unfreeze_partition() { match clickhouse_and_generic().verified_stmt(&sql) { Statement::AlterTable { operations, .. } => { assert_eq!(operations.len(), 1); - let expected_partition = Partition::Expr(Expr::Value(Value::SingleQuotedString( - "2024-08-14".to_string(), - ))); + let expected_partition = Partition::Expr(Expr::Value( + Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(), + )); let expected_operation = if operation_name == &"FREEZE" { AlterTableOperation::FreezePartition { partition: expected_partition, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0072baf75..0a68d31e8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -69,7 +69,9 @@ fn parse_numeric_literal_underscore() { assert_eq!( select.projection, - vec![UnnamedExpr(Expr::Value(number("10_000")))] + vec![UnnamedExpr(Expr::Value( + (number("10_000")).with_empty_span() + ))] ); } @@ -93,9 +95,9 @@ fn parse_function_object_name() { #[test] fn parse_insert_values() { let row = vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ]; let rows1 = vec![row.clone()]; let rows2 = vec![row.clone(), row]; @@ -384,15 +386,15 @@ fn parse_update() { vec![ Assignment { target: AssignmentTarget::ColumnName(ObjectName::from(vec!["a".into()])), - value: Expr::Value(number("1")), + value: Expr::value(number("1")), }, Assignment { target: AssignmentTarget::ColumnName(ObjectName::from(vec!["b".into()])), - value: Expr::Value(number("2")), + value: Expr::value(number("2")), }, Assignment { target: AssignmentTarget::ColumnName(ObjectName::from(vec!["c".into()])), - value: Expr::Value(number("3")), + value: Expr::value(number("3")), }, ] ); @@ -556,7 +558,9 @@ fn parse_update_with_table_alias() { Ident::new("u"), Ident::new("username") ])), - value: Expr::Value(Value::SingleQuotedString("new_user".to_string())), + value: Expr::Value( + (Value::SingleQuotedString("new_user".to_string())).with_empty_span() + ), }], assignments ); @@ -567,9 +571,9 @@ fn parse_update_with_table_alias() { Ident::new("username"), ])), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "old_user".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("old_user".to_string())).with_empty_span() + )), }), selection ); @@ -797,7 +801,7 @@ fn parse_where_delete_statement() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("name"))), op: Eq, - right: Box::new(Expr::Value(number("5"))), + right: Box::new(Expr::value(number("5"))), }, selection.unwrap(), ); @@ -896,7 +900,7 @@ 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); + assert_eq!(Some(Expr::value(number("5"))), select.limit); } #[test] @@ -908,10 +912,10 @@ fn parse_limit() { 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); + assert_eq!(Some(Expr::value(number("1"))), ast.limit); let ast = verified_query("SELECT 1 LIMIT 5"); - assert_eq!(Some(Expr::Value(number("5"))), ast.limit); + assert_eq!(Some(Expr::value(number("5"))), ast.limit); } #[test] @@ -1129,7 +1133,7 @@ fn parse_column_aliases() { } = only(&select.projection) { assert_eq!(&BinaryOperator::Plus, op); - assert_eq!(&Expr::Value(number("1")), right.as_ref()); + assert_eq!(&Expr::value(number("1")), right.as_ref()); assert_eq!(&Ident::new("newname"), alias); } else { panic!("Expected: ExprWithAlias") @@ -1356,7 +1360,7 @@ fn parse_null_in_select() { let sql = "SELECT NULL"; let select = verified_only_select(sql); assert_eq!( - &Expr::Value(Value::Null), + &Expr::Value((Value::Null).with_empty_span()), expr_from_projection(only(&select.projection)), ); } @@ -1392,18 +1396,18 @@ fn parse_exponent_in_select() -> Result<(), ParserError> { assert_eq!( &vec![ - SelectItem::UnnamedExpr(Expr::Value(number("10e-20"))), - SelectItem::UnnamedExpr(Expr::Value(number("1e3"))), - SelectItem::UnnamedExpr(Expr::Value(number("1e+3"))), + SelectItem::UnnamedExpr(Expr::Value((number("10e-20")).with_empty_span())), + SelectItem::UnnamedExpr(Expr::value(number("1e3"))), + SelectItem::UnnamedExpr(Expr::Value((number("1e+3")).with_empty_span())), SelectItem::ExprWithAlias { - expr: Expr::Value(number("1e3")), + expr: Expr::value(number("1e3")), alias: Ident::new("a") }, SelectItem::ExprWithAlias { - expr: Expr::Value(number("1")), + expr: Expr::value(number("1")), alias: Ident::new("e") }, - SelectItem::UnnamedExpr(Expr::Value(number("0.5e2"))), + SelectItem::UnnamedExpr(Expr::value(number("0.5e2"))), ], &select.projection ); @@ -1437,9 +1441,9 @@ fn parse_escaped_single_quote_string_predicate_with_escape() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("salary"))), op: NotEq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "Jim's salary".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("Jim's salary".to_string())).with_empty_span() + )), }), ast.selection, ); @@ -1463,9 +1467,9 @@ fn parse_escaped_single_quote_string_predicate_with_no_escape() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("salary"))), op: NotEq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "Jim''s salary".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("Jim''s salary".to_string())).with_empty_span() + )), }), ast.selection, ); @@ -1478,11 +1482,14 @@ fn parse_number() { #[cfg(feature = "bigdecimal")] assert_eq!( expr, - Expr::Value(Value::Number(bigdecimal::BigDecimal::from(1), false)) + Expr::Value((Value::Number(bigdecimal::BigDecimal::from(1), false)).with_empty_span()) ); #[cfg(not(feature = "bigdecimal"))] - assert_eq!(expr, Expr::Value(Value::Number("1.0".into(), false))); + assert_eq!( + expr, + Expr::Value((Value::Number("1.0".into(), false)).with_empty_span()) + ); } #[test] @@ -1634,15 +1641,15 @@ fn parse_json_object() { }) => assert_eq!( &[ FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), + name: Expr::Value((Value::SingleQuotedString("name".into())).with_empty_span()), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("value".into())).with_empty_span() + )), operator: FunctionArgOperator::Colon }, FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("type".into())), - arg: FunctionArgExpr::Expr(Expr::Value(number("1"))), + name: Expr::Value((Value::SingleQuotedString("type".into())).with_empty_span()), + arg: FunctionArgExpr::Expr(Expr::value(number("1"))), operator: FunctionArgOperator::Colon } ], @@ -1660,15 +1667,19 @@ fn parse_json_object() { assert_eq!( &[ FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), + name: Expr::Value( + (Value::SingleQuotedString("name".into())).with_empty_span() + ), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("value".into())).with_empty_span() + )), operator: FunctionArgOperator::Colon }, FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("type".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::Null)), + name: Expr::Value( + (Value::SingleQuotedString("type".into())).with_empty_span() + ), + arg: FunctionArgExpr::Expr(Expr::Value((Value::Null).with_empty_span())), operator: FunctionArgOperator::Colon } ], @@ -1725,10 +1736,10 @@ fn parse_json_object() { }) => { assert_eq!( &FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), + name: Expr::Value((Value::SingleQuotedString("name".into())).with_empty_span()), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("value".into())).with_empty_span() + )), operator: FunctionArgOperator::Colon }, &args[0] @@ -1736,7 +1747,10 @@ fn parse_json_object() { assert!(matches!( args[1], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::Function(_)), operator: FunctionArgOperator::Colon } @@ -1760,10 +1774,10 @@ fn parse_json_object() { }) => { assert_eq!( &FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString("name".into())), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "value".into() - ))), + name: Expr::Value((Value::SingleQuotedString("name".into())).with_empty_span()), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("value".into())).with_empty_span() + )), operator: FunctionArgOperator::Colon }, &args[0] @@ -1771,7 +1785,10 @@ fn parse_json_object() { assert!(matches!( args[1], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::Function(_)), operator: FunctionArgOperator::Colon } @@ -1880,9 +1897,9 @@ fn parse_not_precedence() { Expr::UnaryOp { op: UnaryOperator::Not, expr: Box::new(Expr::Between { - expr: Box::new(Expr::Value(number("1"))), - low: Box::new(Expr::Value(number("1"))), - high: Box::new(Expr::Value(number("2"))), + expr: Box::new(Expr::value(number("1"))), + low: Box::new(Expr::value(number("1"))), + high: Box::new(Expr::value(number("2"))), negated: true, }), }, @@ -1895,9 +1912,13 @@ fn parse_not_precedence() { Expr::UnaryOp { op: UnaryOperator::Not, expr: Box::new(Expr::Like { - expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("a".into())).with_empty_span() + )), negated: true, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("b".into())).with_empty_span() + )), escape_char: None, any: false, }), @@ -1912,7 +1933,9 @@ fn parse_not_precedence() { op: UnaryOperator::Not, expr: Box::new(Expr::InList { expr: Box::new(Expr::Identifier("a".into())), - list: vec![Expr::Value(Value::SingleQuotedString("a".into()))], + list: vec![Expr::Value( + (Value::SingleQuotedString("a".into())).with_empty_span() + )], negated: true, }), }, @@ -1932,7 +1955,7 @@ fn parse_null_like() { expr: Box::new(Expr::Identifier(Ident::new("column1"))), any: false, negated: false, - pattern: Box::new(Expr::Value(Value::Null)), + pattern: Box::new(Expr::Value((Value::Null).with_empty_span())), escape_char: None, }, alias: Ident { @@ -1946,7 +1969,7 @@ fn parse_null_like() { assert_eq!( SelectItem::ExprWithAlias { expr: Expr::Like { - expr: Box::new(Expr::Value(Value::Null)), + expr: Box::new(Expr::Value((Value::Null).with_empty_span())), any: false, negated: false, pattern: Box::new(Expr::Identifier(Ident::new("column1"))), @@ -1974,7 +1997,9 @@ fn parse_ilike() { Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, any: false, }, @@ -1991,7 +2016,9 @@ fn parse_ilike() { Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: Some('^'.to_string()), any: false, }, @@ -2009,7 +2036,9 @@ fn parse_ilike() { Expr::IsNull(Box::new(Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, any: false, })), @@ -2032,7 +2061,9 @@ fn parse_like() { Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, any: false, }, @@ -2049,7 +2080,9 @@ fn parse_like() { Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: Some('^'.to_string()), any: false, }, @@ -2067,7 +2100,9 @@ fn parse_like() { Expr::IsNull(Box::new(Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, any: false, })), @@ -2090,7 +2125,9 @@ fn parse_similar_to() { Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: None, }, select.selection.unwrap() @@ -2106,7 +2143,9 @@ fn parse_similar_to() { Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: Some('^'.to_string()), }, select.selection.unwrap() @@ -2122,7 +2161,9 @@ fn parse_similar_to() { Expr::IsNull(Box::new(Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("%a".to_string())).with_empty_span() + )), escape_char: Some('^'.to_string()), })), select.selection.unwrap() @@ -2144,8 +2185,8 @@ fn parse_in_list() { Expr::InList { expr: Box::new(Expr::Identifier(Ident::new("segment"))), list: vec![ - Expr::Value(Value::SingleQuotedString("HIGH".to_string())), - Expr::Value(Value::SingleQuotedString("MED".to_string())), + Expr::Value((Value::SingleQuotedString("HIGH".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("MED".to_string())).with_empty_span()), ], negated, }, @@ -2287,8 +2328,8 @@ fn parse_between() { assert_eq!( Expr::Between { expr: Box::new(Expr::Identifier(Ident::new("age"))), - low: Box::new(Expr::Value(number("25"))), - high: Box::new(Expr::Value(number("32"))), + low: Box::new(Expr::value(number("25"))), + high: Box::new(Expr::value(number("32"))), negated, }, select.selection.unwrap() @@ -2305,16 +2346,16 @@ fn parse_between_with_expr() { let select = verified_only_select(sql); assert_eq!( Expr::IsNull(Box::new(Expr::Between { - expr: Box::new(Expr::Value(number("1"))), + expr: Box::new(Expr::value(number("1"))), low: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: Plus, - right: Box::new(Expr::Value(number("2"))), + right: Box::new(Expr::value(number("2"))), }), high: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("3"))), + left: Box::new(Expr::value(number("3"))), op: Plus, - right: Box::new(Expr::Value(number("4"))), + right: Box::new(Expr::value(number("4"))), }), negated: false, })), @@ -2326,19 +2367,19 @@ fn parse_between_with_expr() { assert_eq!( Expr::BinaryOp { left: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), op: BinaryOperator::And, right: Box::new(Expr::Between { expr: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Plus, right: Box::new(Expr::Identifier(Ident::new("x"))), }), - low: Box::new(Expr::Value(number("1"))), - high: Box::new(Expr::Value(number("2"))), + low: Box::new(Expr::value(number("1"))), + high: Box::new(Expr::value(number("2"))), negated: false, }), }, @@ -2353,13 +2394,15 @@ fn parse_tuples() { assert_eq!( vec![ SelectItem::UnnamedExpr(Expr::Tuple(vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), + Expr::value(number("1")), + Expr::value(number("2")), ])), - SelectItem::UnnamedExpr(Expr::Nested(Box::new(Expr::Value(number("1"))))), + SelectItem::UnnamedExpr(Expr::Nested(Box::new(Expr::Value( + (number("1")).with_empty_span() + )))), SelectItem::UnnamedExpr(Expr::Tuple(vec![ - Expr::Value(Value::SingleQuotedString("foo".into())), - Expr::Value(number("3")), + Expr::Value((Value::SingleQuotedString("foo".into())).with_empty_span()), + Expr::value(number("3")), Expr::Identifier(Ident::new("baz")), ])), ], @@ -2450,7 +2493,7 @@ fn parse_select_order_by_limit() { ]), select.order_by.expect("ORDER BY expected").kind ); - assert_eq!(Some(Expr::Value(number("2"))), select.limit); + assert_eq!(Some(Expr::value(number("2"))), select.limit); } #[test] @@ -2611,7 +2654,7 @@ fn parse_select_order_by_nulls_order() { ]), select.order_by.expect("ORDER BY expeccted").kind ); - assert_eq!(Some(Expr::Value(number("2"))), select.limit); + assert_eq!(Some(Expr::value(number("2"))), select.limit); } #[test] @@ -2755,7 +2798,7 @@ fn parse_select_having() { within_group: vec![] })), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), select.having ); @@ -2798,7 +2841,7 @@ fn parse_select_qualify() { within_group: vec![] })), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), select.qualify ); @@ -2809,7 +2852,7 @@ fn parse_select_qualify() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("row_num"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), select.qualify ); @@ -3191,14 +3234,14 @@ fn parse_listagg() { "dateid" )))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(", ".to_owned()) + (Value::SingleQuotedString(", ".to_owned())).with_empty_span() ))) ], clauses: vec![FunctionArgumentClause::OnOverflow( ListAggOnOverflow::Truncate { - filler: Some(Box::new(Expr::Value(Value::SingleQuotedString( - "%".to_string(), - )))), + filler: Some(Box::new(Expr::Value( + (Value::SingleQuotedString("%".to_string(),)).with_empty_span() + ))), with_count: false, } )], @@ -3454,13 +3497,13 @@ fn gen_number_case(value: &str) -> (Vec, Vec) { format!("{} AS col_alias", value), ]; let expected = vec![ - SelectItem::UnnamedExpr(Expr::Value(number(value))), + SelectItem::UnnamedExpr(Expr::value(number(value))), SelectItem::ExprWithAlias { - expr: Expr::Value(number(value)), + expr: Expr::value(number(value)), alias: Ident::new("col_alias"), }, SelectItem::ExprWithAlias { - expr: Expr::Value(number(value)), + expr: Expr::value(number(value)), alias: Ident::new("col_alias"), }, ]; @@ -3481,19 +3524,19 @@ fn gen_sign_number_case(value: &str, op: UnaryOperator) -> (Vec, Vec { match message { - Expr::Value(Value::SingleQuotedString(s)) => assert_eq!(s, "No rows in my_table"), + Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(s), + span: _, + }) => assert_eq!(s, "No rows in my_table"), _ => unreachable!(), }; } @@ -4324,11 +4370,13 @@ fn parse_create_table_with_options() { vec![ SqlOption::KeyValue { key: "foo".into(), - value: Expr::Value(Value::SingleQuotedString("bar".into())), + value: Expr::Value( + (Value::SingleQuotedString("bar".into())).with_empty_span() + ), }, SqlOption::KeyValue { key: "a".into(), - value: Expr::Value(number("123")), + value: Expr::value(number("123")), }, ], with_options @@ -4554,7 +4602,9 @@ fn parse_alter_table() { quote_style: Some('\''), span: Span::empty(), }, - value: Expr::Value(Value::SingleQuotedString("parquet".to_string())), + value: Expr::Value( + (Value::SingleQuotedString("parquet".to_string())).with_empty_span() + ), }], ); } @@ -4698,11 +4748,13 @@ fn parse_alter_view_with_options() { vec![ SqlOption::KeyValue { key: "foo".into(), - value: Expr::Value(Value::SingleQuotedString("bar".into())), + value: Expr::Value( + (Value::SingleQuotedString("bar".into())).with_empty_span() + ), }, SqlOption::KeyValue { key: "a".into(), - value: Expr::Value(number("123")), + value: Expr::value(number("123")), }, ], with_options @@ -4868,7 +4920,7 @@ fn parse_alter_table_alter_column() { assert_eq!( op, AlterColumnOperation::SetDefault { - value: Expr::Value(test_utils::number("0")) + value: Expr::Value((test_utils::number("0")).with_empty_span()) } ); } @@ -5202,16 +5254,16 @@ fn parse_named_argument_function() { args: vec![ FunctionArg::Named { name: Ident::new("a"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "1".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("1".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::RightArrow }, FunctionArg::Named { name: Ident::new("b"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "2".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("2".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::RightArrow }, ], @@ -5242,16 +5294,16 @@ fn parse_named_argument_function_with_eq_operator() { args: vec![ FunctionArg::Named { name: Ident::new("a"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "1".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("1".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::Equals }, FunctionArg::Named { name: Ident::new("b"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "2".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("2".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::Equals }, ], @@ -5275,7 +5327,7 @@ fn parse_named_argument_function_with_eq_operator() { [Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("bar"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("42"))), + right: Box::new(Expr::value(number("42"))), }] ), ); @@ -5625,14 +5677,14 @@ fn parse_literal_integer() { let select = verified_only_select(sql); assert_eq!(3, select.projection.len()); assert_eq!( - &Expr::Value(number("1")), + &Expr::value(number("1")), expr_from_projection(&select.projection[0]), ); // negative literal is parsed as a - and expr assert_eq!( &UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(number("10"))) + expr: Box::new(Expr::value(number("10"))) }, expr_from_projection(&select.projection[1]), ); @@ -5640,7 +5692,7 @@ fn parse_literal_integer() { assert_eq!( &UnaryOp { op: UnaryOperator::Plus, - expr: Box::new(Expr::Value(number("20"))) + expr: Box::new(Expr::value(number("20"))) }, expr_from_projection(&select.projection[2]), ) @@ -5654,11 +5706,11 @@ fn parse_literal_decimal() { let select = verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( - &Expr::Value(number("0.300000000000000004")), + &Expr::value(number("0.300000000000000004")), expr_from_projection(&select.projection[0]), ); assert_eq!( - &Expr::Value(number("9007199254740993.0")), + &Expr::value(number("9007199254740993.0")), expr_from_projection(&select.projection[1]), ) } @@ -5669,15 +5721,17 @@ fn parse_literal_string() { let select = verified_only_select(sql); assert_eq!(3, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedString("one".to_string())), + &Expr::Value((Value::SingleQuotedString("one".to_string())).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::NationalStringLiteral("national string".to_string())), + &Expr::Value( + (Value::NationalStringLiteral("national string".to_string())).with_empty_span() + ), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::HexStringLiteral("deadBEEF".to_string())), + &Expr::Value((Value::HexStringLiteral("deadBEEF".to_string())).with_empty_span()), expr_from_projection(&select.projection[2]) ); @@ -5762,7 +5816,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("1-1")))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1-1"))).with_empty_span() + )), leading_field: Some(DateTimeField::Year), leading_precision: None, last_field: Some(DateTimeField::Month), @@ -5775,9 +5831,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "01:01.01" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("01:01.01"))).with_empty_span() + )), leading_field: Some(DateTimeField::Minute), leading_precision: Some(5), last_field: Some(DateTimeField::Second), @@ -5790,7 +5846,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("1")))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1"))).with_empty_span() + )), leading_field: Some(DateTimeField::Second), leading_precision: Some(5), last_field: None, @@ -5803,7 +5861,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("10")))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("10"))).with_empty_span() + )), leading_field: Some(DateTimeField::Hour), leading_precision: None, last_field: None, @@ -5816,7 +5876,7 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(number("5"))), + value: Box::new(Expr::value(number("5"))), leading_field: Some(DateTimeField::Day), leading_precision: None, last_field: None, @@ -5829,7 +5889,7 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(number("5"))), + value: Box::new(Expr::value(number("5"))), leading_field: Some(DateTimeField::Days), leading_precision: None, last_field: None, @@ -5842,7 +5902,9 @@ fn parse_interval_all() { let select = verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from("10")))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("10"))).with_empty_span() + )), leading_field: Some(DateTimeField::Hour), leading_precision: Some(1), last_field: None, @@ -5911,9 +5973,9 @@ fn parse_interval_dont_require_unit() { let select = dialects.verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 DAY" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1 DAY"))).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -5950,9 +6012,9 @@ fn parse_interval_require_qualifier() { expr_from_projection(only(&select.projection)), &Expr::Interval(Interval { value: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), leading_field: Some(DateTimeField::Day), leading_precision: None, @@ -5967,9 +6029,13 @@ fn parse_interval_require_qualifier() { expr_from_projection(only(&select.projection)), &Expr::Interval(Interval { value: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + left: Box::new(Expr::Value( + (Value::SingleQuotedString("1".to_string())).with_empty_span() + )), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("1".to_string())).with_empty_span() + )), }), leading_field: Some(DateTimeField::Day), leading_precision: None, @@ -5985,12 +6051,18 @@ fn parse_interval_require_qualifier() { &Expr::Interval(Interval { value: Box::new(Expr::BinaryOp { left: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))), + left: Box::new(Expr::Value( + (Value::SingleQuotedString("1".to_string())).with_empty_span() + )), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(Value::SingleQuotedString("2".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("2".to_string())).with_empty_span() + )), }), op: BinaryOperator::Minus, - right: Box::new(Expr::Value(Value::SingleQuotedString("3".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("3".to_string())).with_empty_span() + )), }), leading_field: Some(DateTimeField::Day), leading_precision: None, @@ -6009,9 +6081,9 @@ fn parse_interval_disallow_interval_expr() { assert_eq!( expr_from_projection(only(&select.projection)), &Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 DAY" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1 DAY"))).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -6032,9 +6104,9 @@ fn parse_interval_disallow_interval_expr() { expr_from_projection(only(&select.projection)), &Expr::BinaryOp { left: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 DAY" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1 DAY"))).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -6042,9 +6114,9 @@ fn parse_interval_disallow_interval_expr() { })), op: BinaryOperator::Gt, right: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString(String::from( - "1 SECOND" - )))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString(String::from("1 SECOND"))).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -6062,9 +6134,9 @@ fn interval_disallow_interval_expr_gt() { expr, Expr::BinaryOp { left: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "1 second".to_string() - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("1 second".to_string())).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -6089,9 +6161,9 @@ fn interval_disallow_interval_expr_double_colon() { Expr::Cast { kind: CastKind::DoubleColon, expr: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "1 second".to_string() - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("1 second".to_string())).with_empty_span() + )), leading_field: None, leading_precision: None, last_field: None, @@ -6151,9 +6223,9 @@ fn parse_interval_and_or_xor() { })), op: BinaryOperator::Plus, right: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "5 days".to_string(), - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("5 days".to_string())).with_empty_span(), + )), leading_field: None, leading_precision: None, last_field: None, @@ -6177,9 +6249,9 @@ fn parse_interval_and_or_xor() { })), op: BinaryOperator::Plus, right: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "3 days".to_string(), - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("3 days".to_string())).with_empty_span(), + )), leading_field: None, leading_precision: None, last_field: None, @@ -6234,15 +6306,15 @@ fn parse_interval_and_or_xor() { #[test] fn parse_at_timezone() { - let zero = Expr::Value(number("0")); + let zero = Expr::value(number("0")); let sql = "SELECT FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00' FROM t"; let select = verified_only_select(sql); assert_eq!( &Expr::AtTimeZone { timestamp: Box::new(call("FROM_UNIXTIME", [zero.clone()])), - time_zone: Box::new(Expr::Value(Value::SingleQuotedString( - "UTC-06:00".to_string() - ))), + time_zone: Box::new(Expr::Value( + (Value::SingleQuotedString("UTC-06:00".to_string())).with_empty_span() + )), }, expr_from_projection(only(&select.projection)), ); @@ -6256,11 +6328,13 @@ fn parse_at_timezone() { [ Expr::AtTimeZone { timestamp: Box::new(call("FROM_UNIXTIME", [zero])), - time_zone: Box::new(Expr::Value(Value::SingleQuotedString( - "UTC-06:00".to_string() - ))), + time_zone: Box::new(Expr::Value( + (Value::SingleQuotedString("UTC-06:00".to_string())).with_empty_span() + )), }, - Expr::Value(Value::SingleQuotedString("%Y-%m-%dT%H".to_string()),) + Expr::Value( + (Value::SingleQuotedString("%Y-%m-%dT%H".to_string())).with_empty_span() + ) ] ), alias: Ident { @@ -6434,7 +6508,9 @@ fn parse_table_function() { assert_eq!( call( "FUN", - [Expr::Value(Value::SingleQuotedString("1".to_owned()))], + [Expr::Value( + (Value::SingleQuotedString("1".to_owned())).with_empty_span() + )], ), expr ); @@ -6617,9 +6693,9 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![call( "make_array", [ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ], )], with_offset: false, @@ -6643,14 +6719,14 @@ fn parse_unnest_in_from_clause() { call( "make_array", [ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ], ), call( "make_array", - [Expr::Value(number("5")), Expr::Value(number("6"))], + [Expr::value(number("5")), Expr::value(number("6"))], ), ], with_offset: false, @@ -6697,26 +6773,26 @@ fn parse_searched_case_expr() { conditions: vec![ CaseWhen { condition: IsNull(Box::new(Identifier(Ident::new("bar")))), - result: Expr::Value(Value::SingleQuotedString("null".to_string())), + result: Expr::value(Value::SingleQuotedString("null".to_string())), }, CaseWhen { condition: BinaryOp { left: Box::new(Identifier(Ident::new("bar"))), op: Eq, - right: Box::new(Expr::Value(number("0"))), + right: Box::new(Expr::value(number("0"))), }, - result: Expr::Value(Value::SingleQuotedString("=0".to_string())), + result: Expr::value(Value::SingleQuotedString("=0".to_string())), }, CaseWhen { condition: BinaryOp { left: Box::new(Identifier(Ident::new("bar"))), op: GtEq, - right: Box::new(Expr::Value(number("0"))), + right: Box::new(Expr::value(number("0"))), }, - result: Expr::Value(Value::SingleQuotedString(">=0".to_string())), + result: Expr::value(Value::SingleQuotedString(">=0".to_string())), }, ], - else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( + else_result: Some(Box::new(Expr::value(Value::SingleQuotedString( "<0".to_string() )))), }, @@ -6734,10 +6810,10 @@ fn parse_simple_case_expr() { &Case { operand: Some(Box::new(Identifier(Ident::new("foo")))), conditions: vec![CaseWhen { - condition: Expr::Value(number("1")), - result: Expr::Value(Value::SingleQuotedString("Y".to_string())), + condition: Expr::value(number("1")), + result: Expr::value(Value::SingleQuotedString("Y".to_string())), }], - else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( + else_result: Some(Box::new(Expr::value(Value::SingleQuotedString( "N".to_string() )))), }, @@ -7488,13 +7564,15 @@ fn parse_overlay() { let select = verified_only_select(sql); assert_eq!( &Expr::Overlay { - expr: Box::new(Expr::Value(Value::SingleQuotedString("abcdef".to_string()))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("abcdef".to_string())).with_empty_span() + )), overlay_what: Box::new(Expr::Identifier(Ident::new("name"))), - overlay_from: Box::new(Expr::Value(number("3"))), + overlay_from: Box::new(Expr::value(number("3"))), overlay_for: Some(Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), })), }, expr_from_projection(only(&select.projection)) @@ -7722,11 +7800,13 @@ fn parse_create_view_with_options() { CreateTableOptions::With(vec![ SqlOption::KeyValue { key: "foo".into(), - value: Expr::Value(Value::SingleQuotedString("bar".into())), + value: Expr::Value( + (Value::SingleQuotedString("bar".into())).with_empty_span() + ), }, SqlOption::KeyValue { key: "a".into(), - value: Expr::Value(number("123")), + value: Expr::value(number("123")), }, ]), options @@ -8069,7 +8149,7 @@ fn parse_offset() { all_dialects_where(|d| !d.is_column_alias(&Keyword::OFFSET, &mut Parser::new(d))); let expect = Some(Offset { - value: Expr::Value(number("2")), + value: Expr::value(number("2")), rows: OffsetRows::Rows, }); let ast = dialects.verified_query("SELECT foo FROM bar OFFSET 2 ROWS"); @@ -8097,7 +8177,7 @@ fn parse_offset() { assert_eq!( ast.offset, Some(Offset { - value: Expr::Value(number("0")), + value: Expr::value(number("0")), rows: OffsetRows::Rows, }) ); @@ -8105,7 +8185,7 @@ fn parse_offset() { assert_eq!( ast.offset, Some(Offset { - value: Expr::Value(number("1")), + value: Expr::value(number("1")), rows: OffsetRows::Row, }) ); @@ -8113,7 +8193,7 @@ fn parse_offset() { assert_eq!( ast.offset, Some(Offset { - value: Expr::Value(number("1")), + value: Expr::value(number("1")), rows: OffsetRows::None, }) ); @@ -8124,7 +8204,7 @@ fn parse_fetch() { let fetch_first_two_rows_only = Some(Fetch { with_ties: false, percent: false, - quantity: Some(Expr::Value(number("2"))), + quantity: Some(Expr::value(number("2"))), }); let ast = verified_query("SELECT foo FROM bar FETCH FIRST 2 ROWS ONLY"); assert_eq!(ast.fetch, fetch_first_two_rows_only); @@ -8151,7 +8231,7 @@ fn parse_fetch() { Some(Fetch { with_ties: true, percent: false, - quantity: Some(Expr::Value(number("2"))), + quantity: Some(Expr::value(number("2"))), }) ); let ast = verified_query("SELECT foo FROM bar FETCH FIRST 50 PERCENT ROWS ONLY"); @@ -8160,7 +8240,7 @@ fn parse_fetch() { Some(Fetch { with_ties: false, percent: true, - quantity: Some(Expr::Value(number("50"))), + quantity: Some(Expr::value(number("50"))), }) ); let ast = verified_query( @@ -8169,7 +8249,7 @@ fn parse_fetch() { assert_eq!( ast.offset, Some(Offset { - value: Expr::Value(number("2")), + value: Expr::value(number("2")), rows: OffsetRows::Rows, }) ); @@ -8191,7 +8271,7 @@ fn parse_fetch() { assert_eq!( ast.offset, Some(Offset { - value: Expr::Value(number("2")), + value: Expr::value(number("2")), rows: OffsetRows::Rows, }) ); @@ -8202,7 +8282,7 @@ fn parse_fetch() { assert_eq!( subquery.offset, Some(Offset { - value: Expr::Value(number("2")), + value: Expr::value(number("2")), rows: OffsetRows::Rows, }) ); @@ -8252,7 +8332,9 @@ fn lateral_derived() { let join = &from.joins[0]; assert_eq!( join.join_operator, - JoinOperator::Left(JoinConstraint::On(Expr::Value(test_utils::number("1")))) + JoinOperator::Left(JoinConstraint::On(Expr::Value( + (test_utils::number("1")).with_empty_span() + ))) ); if let TableFactor::Derived { lateral, @@ -8312,7 +8394,9 @@ fn lateral_function() { lateral: true, name: ObjectName::from(vec!["generate_series".into()]), args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("1")).with_empty_span(), + ))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::CompoundIdentifier( vec![Ident::new("customer"), Ident::new("id")], ))), @@ -8481,7 +8565,9 @@ fn parse_set_variable() { ); assert_eq!( value, - vec![Expr::Value(Value::SingleQuotedString("1".into()))] + vec![Expr::Value( + (Value::SingleQuotedString("1".into())).with_empty_span() + )] ); } _ => unreachable!(), @@ -8509,9 +8595,9 @@ fn parse_set_variable() { assert_eq!( value, vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")), + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")), ] ); } @@ -8581,7 +8667,9 @@ fn parse_set_role_as_variable() { ); assert_eq!( value, - vec![Expr::Value(Value::SingleQuotedString("foobar".into()))] + vec![Expr::Value( + (Value::SingleQuotedString("foobar".into())).with_empty_span() + )] ); } _ => unreachable!(), @@ -8597,15 +8685,16 @@ fn parse_double_colon_cast_at_timezone() { &Expr::AtTimeZone { timestamp: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2001-01-01T00:00:00.000Z".to_string() - ),)), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2001-01-01T00:00:00.000Z".to_string())) + .with_empty_span() + )), data_type: DataType::Timestamp(None, TimezoneInfo::None), format: None }), - time_zone: Box::new(Expr::Value(Value::SingleQuotedString( - "Europe/Brussels".to_string() - ))), + time_zone: Box::new(Expr::Value( + (Value::SingleQuotedString("Europe/Brussels".to_string())).with_empty_span() + )), }, expr_from_projection(only(&select.projection)), ); @@ -8628,7 +8717,9 @@ fn parse_set_time_zone() { ); assert_eq!( value, - vec![Expr::Value(Value::SingleQuotedString("UTC".into()))] + vec![Expr::Value( + (Value::SingleQuotedString("UTC".into())).with_empty_span() + )] ); } _ => unreachable!(), @@ -8642,7 +8733,10 @@ 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()))); + assert_eq!( + value, + Expr::Value((Value::SingleQuotedString("UTC".into())).with_empty_span()) + ); } _ => unreachable!(), } @@ -8849,7 +8943,7 @@ fn test_create_index_with_with_clause() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("fillfactor"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("70"))), + right: Box::new(Expr::value(number("70"))), }, Expr::Identifier(Ident::new("single_param")), ]; @@ -9338,9 +9432,9 @@ fn parse_merge() { Ident::new("A"), ])), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "a".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("a".to_string())).with_empty_span() + )), }), action: MergeAction::Update { assignments: vec![ @@ -9566,7 +9660,9 @@ fn test_placeholder() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Placeholder("$Id1".into()))), + right: Box::new(Expr::Value( + (Value::Placeholder("$Id1".into())).with_empty_span() + )), }) ); @@ -9574,12 +9670,14 @@ fn test_placeholder() { let ast = dialects.verified_query(sql); assert_eq!( ast.limit, - Some(Expr::Value(Value::Placeholder("$1".into()))) + Some(Expr::Value( + (Value::Placeholder("$1".into())).with_empty_span() + )) ); assert_eq!( ast.offset, Some(Offset { - value: Expr::Value(Value::Placeholder("$2".into())), + value: Expr::Value((Value::Placeholder("$2".into())).with_empty_span()), rows: OffsetRows::None, }), ); @@ -9603,7 +9701,9 @@ fn test_placeholder() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Placeholder("?".into()))), + right: Box::new(Expr::Value( + (Value::Placeholder("?".into())).with_empty_span() + )), }) ); @@ -9612,9 +9712,15 @@ fn test_placeholder() { assert_eq!( ast.projection, vec![ - UnnamedExpr(Expr::Value(Value::Placeholder("$fromage_français".into()))), - UnnamedExpr(Expr::Value(Value::Placeholder(":x".into()))), - UnnamedExpr(Expr::Value(Value::Placeholder("?123".into()))), + UnnamedExpr(Expr::Value( + (Value::Placeholder("$fromage_français".into())).with_empty_span() + )), + UnnamedExpr(Expr::Value( + (Value::Placeholder(":x".into())).with_empty_span() + )), + UnnamedExpr(Expr::Value( + (Value::Placeholder("?123".into())).with_empty_span() + )), ] ); } @@ -9655,12 +9761,12 @@ fn verified_expr(query: &str) -> Expr { fn parse_offset_and_limit() { let sql = "SELECT foo FROM bar LIMIT 1 OFFSET 2"; let expect = Some(Offset { - value: Expr::Value(number("2")), + value: Expr::value(number("2")), rows: OffsetRows::None, }); let ast = verified_query(sql); assert_eq!(ast.offset, expect); - assert_eq!(ast.limit, Some(Expr::Value(number("1")))); + assert_eq!(ast.limit, Some(Expr::value(number("1")))); // different order is OK one_statement_parses_to("SELECT foo FROM bar OFFSET 2 LIMIT 1", sql); @@ -9680,18 +9786,18 @@ fn parse_offset_and_limit() { assert_eq!( ast.limit, Some(Expr::BinaryOp { - left: Box::new(Expr::Value(number("1"))), + left: Box::new(Expr::value(number("1"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("2"))), + right: Box::new(Expr::value(number("2"))), }), ); assert_eq!( ast.offset, Some(Offset { value: Expr::BinaryOp { - left: Box::new(Expr::Value(number("3"))), + left: Box::new(Expr::value(number("3"))), op: BinaryOperator::Multiply, - right: Box::new(Expr::Value(number("4"))), + right: Box::new(Expr::value(number("4"))), }, rows: OffsetRows::None, }), @@ -9762,7 +9868,9 @@ fn parse_time_functions() { fn parse_position() { assert_eq!( Expr::Position { - expr: Box::new(Expr::Value(Value::SingleQuotedString("@".to_string()))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("@".to_string())).with_empty_span() + )), r#in: Box::new(Expr::Identifier(Ident::new("field"))), }, verified_expr("POSITION('@' IN field)"), @@ -9773,9 +9881,9 @@ fn parse_position() { call( "position", [ - Expr::Value(Value::SingleQuotedString("an".to_owned())), - Expr::Value(Value::SingleQuotedString("banana".to_owned())), - Expr::Value(number("1")), + Expr::Value((Value::SingleQuotedString("an".to_owned())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("banana".to_owned())).with_empty_span()), + Expr::value(number("1")), ] ), verified_expr("position('an', 'banana', 1)") @@ -10028,11 +10136,11 @@ fn parse_cache_table() { options: vec![ SqlOption::KeyValue { key: Ident::with_quote('\'', "K1"), - value: Expr::Value(Value::SingleQuotedString("V1".into())), + value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { key: Ident::with_quote('\'', "K2"), - value: Expr::Value(number("0.88")), + value: Expr::value(number("0.88")), }, ], query: None, @@ -10053,11 +10161,11 @@ fn parse_cache_table() { options: vec![ SqlOption::KeyValue { key: Ident::with_quote('\'', "K1"), - value: Expr::Value(Value::SingleQuotedString("V1".into())), + value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { key: Ident::with_quote('\'', "K2"), - value: Expr::Value(number("0.88")), + value: Expr::value(number("0.88")), }, ], query: Some(query.clone().into()), @@ -10078,11 +10186,11 @@ fn parse_cache_table() { options: vec![ SqlOption::KeyValue { key: Ident::with_quote('\'', "K1"), - value: Expr::Value(Value::SingleQuotedString("V1".into())), + value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { key: Ident::with_quote('\'', "K2"), - value: Expr::Value(number("0.88")), + value: Expr::value(number("0.88")), }, ], query: Some(query.clone().into()), @@ -10293,7 +10401,9 @@ fn parse_escaped_string_with_unescape() { let expr = expr_from_projection(only(&value.projection)); assert_eq!( *expr, - Expr::Value(Value::SingleQuotedString(quoted.to_string())) + Expr::Value( + (Value::SingleQuotedString(quoted.to_string())).with_empty_span() + ) ); } _ => unreachable!(), @@ -10333,7 +10443,9 @@ fn parse_escaped_string_without_unescape() { let expr = expr_from_projection(only(&value.projection)); assert_eq!( *expr, - Expr::Value(Value::SingleQuotedString(quoted.to_string())) + Expr::Value( + (Value::SingleQuotedString(quoted.to_string())).with_empty_span() + ) ); } _ => unreachable!(), @@ -10404,11 +10516,13 @@ fn parse_pivot_table() { value_column: vec![Ident::new("a"), Ident::new("MONTH")], value_source: PivotValueSource::List(vec![ ExprWithAlias { - expr: Expr::Value(number("1")), + expr: Expr::value(number("1")), alias: Some(Ident::new("x")) }, ExprWithAlias { - expr: Expr::Value(Value::SingleQuotedString("two".to_string())), + expr: Expr::Value( + (Value::SingleQuotedString("two".to_string())).with_empty_span() + ), alias: None }, ExprWithAlias { @@ -10639,11 +10753,17 @@ fn parse_pivot_unpivot_table() { value_column: vec![Ident::new("year")], value_source: PivotValueSource::List(vec![ ExprWithAlias { - expr: Expr::Value(Value::SingleQuotedString("population_2000".to_string())), + expr: Expr::Value( + (Value::SingleQuotedString("population_2000".to_string())) + .with_empty_span() + ), alias: None }, ExprWithAlias { - expr: Expr::Value(Value::SingleQuotedString("population_2010".to_string())), + expr: Expr::Value( + (Value::SingleQuotedString("population_2010".to_string())) + .with_empty_span() + ), alias: None }, ]), @@ -10890,7 +11010,7 @@ fn parse_call() { args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".to_string()) + (Value::SingleQuotedString("a".to_string())).with_empty_span() )))], clauses: vec![], }), @@ -10919,8 +11039,8 @@ fn parse_execute_stored_procedure() { }, ])), parameters: vec![ - Expr::Value(Value::NationalStringLiteral("param1".to_string())), - Expr::Value(Value::NationalStringLiteral("param2".to_string())), + Expr::Value((Value::NationalStringLiteral("param1".to_string())).with_empty_span()), + Expr::Value((Value::NationalStringLiteral("param2".to_string())).with_empty_span()), ], has_parentheses: false, immediate: false, @@ -10947,12 +11067,12 @@ fn parse_execute_immediate() { let dialects = all_dialects_where(|d| d.supports_execute_immediate()); let expected = Statement::Execute { - parameters: vec![Expr::Value(Value::SingleQuotedString( - "SELECT 1".to_string(), - ))], + parameters: vec![Expr::Value( + (Value::SingleQuotedString("SELECT 1".to_string())).with_empty_span(), + )], immediate: true, using: vec![ExprWithAlias { - expr: Expr::Value(number("1")), + expr: Expr::value(number("1")), alias: Some(Ident::new("b")), }], into: vec![Ident::new("a")], @@ -11102,7 +11222,9 @@ fn parse_unload() { quote_style: None, span: Span::empty(), }, - value: Expr::Value(Value::SingleQuotedString("AVRO".to_string())) + value: Expr::Value( + (Value::SingleQuotedString("AVRO".to_string())).with_empty_span() + ) }] } ); @@ -11210,7 +11332,7 @@ fn parse_map_access_expr() { AccessExpr::Subscript(Subscript::Index { index: Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Expr::Value(number("1")).into(), + expr: Expr::value(number("1")).into(), }, }), AccessExpr::Subscript(Subscript::Index { @@ -11223,7 +11345,7 @@ fn parse_map_access_expr() { args: FunctionArguments::List(FunctionArgumentList { duplicate_treatment: None, args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - number("2"), + (number("2")).with_empty_span(), )))], clauses: vec![], }), @@ -11276,9 +11398,9 @@ fn parse_connect_by() { condition: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "president".to_owned(), - ))), + right: Box::new(Expr::Value( + Value::SingleQuotedString("president".to_owned()).with_empty_span(), + )), }, relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), @@ -11346,7 +11468,7 @@ fn parse_connect_by() { selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("employee_id"))), op: BinaryOperator::NotEq, - right: Box::new(Expr::Value(number("42"))), + right: Box::new(Expr::value(number("42"))), }), group_by: GroupByExpr::Expressions(vec![], vec![]), cluster_by: vec![], @@ -11361,9 +11483,9 @@ fn parse_connect_by() { condition: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "president".to_owned(), - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("president".to_owned(),)).with_empty_span() + )), }, relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), @@ -11446,7 +11568,9 @@ fn test_selective_aggregation() { filter: Some(Box::new(Expr::Like { negated: false, expr: Box::new(Expr::Identifier(Ident::new("name"))), - pattern: Box::new(Expr::Value(Value::SingleQuotedString("a%".to_owned()))), + pattern: Box::new(Expr::Value( + (Value::SingleQuotedString("a%".to_owned())).with_empty_span() + )), escape_char: None, any: false, })), @@ -11804,7 +11928,9 @@ fn test_select_wildcard_with_replace() { let expected = SelectItem::Wildcard(WildcardAdditionalOptions { opt_replace: Some(ReplaceSelectItem { items: vec![Box::new(ReplaceSelectElement { - expr: Expr::Value(Value::SingleQuotedString("widget".to_owned())), + expr: Expr::Value( + (Value::SingleQuotedString("widget".to_owned())).with_empty_span(), + ), column_name: Ident::new("item_name"), as_keyword: true, })], @@ -11823,13 +11949,13 @@ fn test_select_wildcard_with_replace() { expr: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("quantity"))), op: BinaryOperator::Divide, - right: Box::new(Expr::Value(number("2"))), + right: Box::new(Expr::value(number("2"))), }, column_name: Ident::new("quantity"), as_keyword: true, }), Box::new(ReplaceSelectElement { - expr: Expr::Value(number("3")), + expr: Expr::value(number("3")), column_name: Ident::new("order_id"), as_keyword: true, }), @@ -11912,15 +12038,15 @@ fn test_dictionary_syntax() { Expr::Dictionary(vec![ DictionaryField { key: Ident::with_quote('\'', "Alberta"), - value: Box::new(Expr::Value(Value::SingleQuotedString( - "Edmonton".to_owned(), - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("Edmonton".to_owned())).with_empty_span(), + )), }, DictionaryField { key: Ident::with_quote('\'', "Manitoba"), - value: Box::new(Expr::Value(Value::SingleQuotedString( - "Winnipeg".to_owned(), - ))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("Winnipeg".to_owned())).with_empty_span(), + )), }, ]), ); @@ -11932,9 +12058,9 @@ fn test_dictionary_syntax() { key: Ident::with_quote('\'', "start"), value: Box::new(Expr::Cast { kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2023-04-01".to_owned(), - ))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2023-04-01".to_owned())).with_empty_span(), + )), data_type: DataType::Timestamp(None, TimezoneInfo::None), format: None, }), @@ -11943,9 +12069,9 @@ fn test_dictionary_syntax() { key: Ident::with_quote('\'', "end"), value: Box::new(Expr::Cast { kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2023-04-05".to_owned(), - ))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2023-04-05".to_owned())).with_empty_span(), + )), data_type: DataType::Timestamp(None, TimezoneInfo::None), format: None, }), @@ -11968,25 +12094,27 @@ fn test_map_syntax() { Expr::Map(Map { entries: vec![ MapEntry { - key: Box::new(Expr::Value(Value::SingleQuotedString("Alberta".to_owned()))), - value: Box::new(Expr::Value(Value::SingleQuotedString( - "Edmonton".to_owned(), - ))), + key: Box::new(Expr::Value( + (Value::SingleQuotedString("Alberta".to_owned())).with_empty_span(), + )), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("Edmonton".to_owned())).with_empty_span(), + )), }, MapEntry { - key: Box::new(Expr::Value(Value::SingleQuotedString( - "Manitoba".to_owned(), - ))), - value: Box::new(Expr::Value(Value::SingleQuotedString( - "Winnipeg".to_owned(), - ))), + key: Box::new(Expr::Value( + (Value::SingleQuotedString("Manitoba".to_owned())).with_empty_span(), + )), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("Winnipeg".to_owned())).with_empty_span(), + )), }, ], }), ); fn number_expr(s: &str) -> Expr { - Expr::Value(number(s)) + Expr::value(number(s)) } check( @@ -12014,14 +12142,14 @@ fn test_map_syntax() { elem: vec![number_expr("1"), number_expr("2"), number_expr("3")], named: false, })), - value: Box::new(Expr::Value(number("10.0"))), + value: Box::new(Expr::value(number("10.0"))), }, MapEntry { key: Box::new(Expr::Array(Array { elem: vec![number_expr("4"), number_expr("5"), number_expr("6")], named: false, })), - value: Box::new(Expr::Value(number("20.0"))), + value: Box::new(Expr::value(number("20.0"))), }, ], }), @@ -12033,17 +12161,21 @@ fn test_map_syntax() { root: Box::new(Expr::Map(Map { entries: vec![ MapEntry { - key: Box::new(Expr::Value(Value::SingleQuotedString("a".to_owned()))), + key: Box::new(Expr::Value( + (Value::SingleQuotedString("a".to_owned())).with_empty_span(), + )), value: Box::new(number_expr("10")), }, MapEntry { - key: Box::new(Expr::Value(Value::SingleQuotedString("b".to_owned()))), + key: Box::new(Expr::Value( + (Value::SingleQuotedString("b".to_owned())).with_empty_span(), + )), value: Box::new(number_expr("20")), }, ], })), access_chain: vec![AccessExpr::Subscript(Subscript::Index { - index: Expr::Value(Value::SingleQuotedString("a".to_owned())), + index: Expr::Value((Value::SingleQuotedString("a".to_owned())).with_empty_span()), })], }, ); @@ -12192,9 +12324,9 @@ fn test_extract_seconds_ok() { syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2 seconds".to_string() - ))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span() + )), data_type: DataType::Interval, format: None, }), @@ -12217,9 +12349,9 @@ fn test_extract_seconds_ok() { syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2 seconds".to_string(), - ))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span(), + )), data_type: DataType::Interval, format: None, }), @@ -12271,9 +12403,9 @@ fn test_extract_seconds_single_quote_ok() { syntax: ExtractSyntax::From, expr: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "2 seconds".to_string() - ))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("2 seconds".to_string())).with_empty_span() + )), data_type: DataType::Interval, format: None, }), @@ -12324,11 +12456,11 @@ fn parse_explain_with_option_list() { Some(vec![ UtilityOption { name: Ident::new("ANALYZE"), - arg: Some(Expr::Value(Value::Boolean(false))), + arg: Some(Expr::Value((Value::Boolean(false)).with_empty_span())), }, UtilityOption { name: Ident::new("VERBOSE"), - arg: Some(Expr::Value(Value::Boolean(true))), + arg: Some(Expr::Value((Value::Boolean(true)).with_empty_span())), }, ]), ); @@ -12364,7 +12496,9 @@ fn parse_explain_with_option_list() { }, UtilityOption { name: Ident::new("FORMAT2"), - arg: Some(Expr::Value(Value::SingleQuotedString("JSON".to_string()))), + arg: Some(Expr::Value( + (Value::SingleQuotedString("JSON".to_string())).with_empty_span(), + )), }, UtilityOption { name: Ident::new("FORMAT3"), @@ -12386,20 +12520,26 @@ fn parse_explain_with_option_list() { Some(vec![ UtilityOption { name: Ident::new("NUM1"), - arg: Some(Expr::Value(Value::Number("10".parse().unwrap(), false))), + arg: Some(Expr::Value( + (Value::Number("10".parse().unwrap(), false)).with_empty_span(), + )), }, UtilityOption { name: Ident::new("NUM2"), arg: Some(Expr::UnaryOp { op: UnaryOperator::Plus, - expr: Box::new(Expr::Value(Value::Number("10.1".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Value::Number("10.1".parse().unwrap(), false)).with_empty_span(), + )), }), }, UtilityOption { name: Ident::new("NUM3"), arg: Some(Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(Value::Number("10.2".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Value::Number("10.2".parse().unwrap(), false)).with_empty_span(), + )), }), }, ]), @@ -12412,7 +12552,7 @@ fn parse_explain_with_option_list() { }, UtilityOption { name: Ident::new("VERBOSE"), - arg: Some(Expr::Value(Value::Boolean(true))), + arg: Some(Expr::Value((Value::Boolean(true)).with_empty_span())), }, UtilityOption { name: Ident::new("WAL"), @@ -12426,7 +12566,9 @@ fn parse_explain_with_option_list() { name: Ident::new("USER_DEF_NUM"), arg: Some(Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(Value::Number("100.1".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Value::Number("100.1".parse().unwrap(), false)).with_empty_span(), + )), }), }, ]; @@ -12471,15 +12613,21 @@ fn test_create_policy() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("c0"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), }) ); assert_eq!( with_check, Some(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + left: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), }) ); } @@ -12692,11 +12840,15 @@ fn test_create_connector() { Some(vec![ SqlOption::KeyValue { key: Ident::with_quote('\'', "user"), - value: Expr::Value(Value::SingleQuotedString("root".to_string())) + value: Expr::Value( + (Value::SingleQuotedString("root".to_string())).with_empty_span() + ) }, SqlOption::KeyValue { key: Ident::with_quote('\'', "password"), - value: Expr::Value(Value::SingleQuotedString("password".to_string())) + value: Expr::Value( + (Value::SingleQuotedString("password".to_string())).with_empty_span() + ) } ]) ); @@ -12759,11 +12911,15 @@ fn test_alter_connector() { Some(vec![ SqlOption::KeyValue { key: Ident::with_quote('\'', "user"), - value: Expr::Value(Value::SingleQuotedString("root".to_string())) + value: Expr::Value( + (Value::SingleQuotedString("root".to_string())).with_empty_span() + ) }, SqlOption::KeyValue { key: Ident::with_quote('\'', "password"), - value: Expr::Value(Value::SingleQuotedString("password".to_string())) + value: Expr::Value( + (Value::SingleQuotedString("password".to_string())).with_empty_span() + ) } ]) ); @@ -13230,12 +13386,16 @@ fn parse_load_data() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("year"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("2024".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("2024".parse().unwrap(), false)).with_empty_span() + )), }, Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("month"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("11".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("11".parse().unwrap(), false)).with_empty_span() + )), } ]), partitioned @@ -13268,24 +13428,34 @@ fn parse_load_data() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("year"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("2024".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("2024".parse().unwrap(), false)).with_empty_span() + )), }, Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("month"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Number("11".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("11".parse().unwrap(), false)).with_empty_span() + )), } ]), partitioned ); assert_eq!( Some(HiveLoadDataFormat { - serde: Expr::Value(Value::SingleQuotedString( - "org.apache.hadoop.hive.serde2.OpenCSVSerde".to_string() - )), - input_format: Expr::Value(Value::SingleQuotedString( - "org.apache.hadoop.mapred.TextInputFormat".to_string() - )) + serde: Expr::Value( + (Value::SingleQuotedString( + "org.apache.hadoop.hive.serde2.OpenCSVSerde".to_string() + )) + .with_empty_span() + ), + input_format: Expr::Value( + (Value::SingleQuotedString( + "org.apache.hadoop.mapred.TextInputFormat".to_string() + )) + .with_empty_span() + ) }), table_format ); @@ -13363,7 +13533,9 @@ fn parse_bang_not() { Box::new(Expr::Nested(Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("b"))), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(Value::Number("3".parse().unwrap(), false))), + right: Box::new(Expr::Value( + Value::Number("3".parse().unwrap(), false).with_empty_span(), + )), }))), ] .into_iter() @@ -13705,11 +13877,13 @@ fn parse_composite_access_expr() { values: vec![ Expr::Named { name: Ident::new("a"), - expr: Box::new(Expr::Value(Number("1".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Number("1".parse().unwrap(), false)).with_empty_span(), + )), }, Expr::Named { name: Ident::new("b"), - expr: Box::new(Expr::Value(Value::Null)), + expr: Box::new(Expr::Value((Value::Null).with_empty_span())), }, ], fields: vec![], @@ -13717,7 +13891,7 @@ fn parse_composite_access_expr() { }, Expr::Named { name: Ident::new("d"), - expr: Box::new(Expr::Value(Value::Null)), + expr: Box::new(Expr::Value((Value::Null).with_empty_span())), }, ], fields: vec![], @@ -13744,11 +13918,15 @@ fn parse_create_table_with_enum_types() { vec![ EnumMember::NamedValue( "a".to_string(), - Expr::Value(Number("1".parse().unwrap(), false)) + Expr::Value( + (Number("1".parse().unwrap(), false)).with_empty_span() + ) ), EnumMember::NamedValue( "b".to_string(), - Expr::Value(Number("2".parse().unwrap(), false)) + Expr::Value( + (Number("2".parse().unwrap(), false)).with_empty_span() + ) ) ], Some(8) @@ -13761,11 +13939,15 @@ fn parse_create_table_with_enum_types() { vec![ EnumMember::NamedValue( "a".to_string(), - Expr::Value(Number("1".parse().unwrap(), false)) + Expr::Value( + (Number("1".parse().unwrap(), false)).with_empty_span() + ) ), EnumMember::NamedValue( "b".to_string(), - Expr::Value(Number("2".parse().unwrap(), false)) + Expr::Value( + (Number("2".parse().unwrap(), false)).with_empty_span() + ) ) ], Some(16) @@ -13956,8 +14138,12 @@ fn test_lambdas() { call( "array", [ - Expr::Value(Value::SingleQuotedString("Hello".to_owned())), - Expr::Value(Value::SingleQuotedString("World".to_owned())) + Expr::Value( + (Value::SingleQuotedString("Hello".to_owned())).with_empty_span() + ), + Expr::Value( + (Value::SingleQuotedString("World".to_owned())).with_empty_span() + ) ] ), Expr::Lambda(LambdaFunction { @@ -13971,7 +14157,7 @@ fn test_lambdas() { op: BinaryOperator::Eq, right: Box::new(Expr::Identifier(Ident::new("p2"))) }, - result: Expr::Value(number("0")) + result: Expr::value(number("0")), }, CaseWhen { condition: Expr::BinaryOp { @@ -13983,15 +14169,15 @@ fn test_lambdas() { right: Box::new(call( "reverse", [Expr::Identifier(Ident::new("p2"))] - )) + )), }, result: Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(number("1"))) + expr: Box::new(Expr::value(number("1"))) } - } + }, ], - else_result: Some(Box::new(Expr::Value(number("1")))) + else_result: Some(Box::new(Expr::value(number("1")))), }) }) ] diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index 61874fc27..cee604aca 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -41,7 +41,7 @@ fn custom_prefix_parser() -> Result<(), ParserError> { fn parse_prefix(&self, parser: &mut Parser) -> Option> { if parser.consume_token(&Token::Number("1".to_string(), false)) { - Some(Ok(Expr::Value(Value::Null))) + Some(Ok(Expr::Value(Value::Null.with_empty_span()))) } else { None } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 724bedf47..3b36d7a13 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -47,7 +47,9 @@ fn test_databricks_identifiers() { databricks() .verified_only_select(r#"SELECT "Ä""#) .projection[0], - SelectItem::UnnamedExpr(Expr::Value(Value::DoubleQuotedString("Ä".to_owned()))) + SelectItem::UnnamedExpr(Expr::Value( + (Value::DoubleQuotedString("Ä".to_owned())).with_empty_span() + )) ); } @@ -62,9 +64,9 @@ fn test_databricks_exists() { call( "array", [ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")) + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")) ] ), Expr::Lambda(LambdaFunction { @@ -99,8 +101,8 @@ fn test_databricks_lambdas() { call( "array", [ - Expr::Value(Value::SingleQuotedString("Hello".to_owned())), - Expr::Value(Value::SingleQuotedString("World".to_owned())) + Expr::value(Value::SingleQuotedString("Hello".to_owned())), + Expr::value(Value::SingleQuotedString("World".to_owned())) ] ), Expr::Lambda(LambdaFunction { @@ -114,7 +116,7 @@ fn test_databricks_lambdas() { op: BinaryOperator::Eq, right: Box::new(Expr::Identifier(Ident::new("p2"))) }, - result: Expr::Value(number("0")) + result: Expr::value(number("0")) }, CaseWhen { condition: Expr::BinaryOp { @@ -130,11 +132,11 @@ fn test_databricks_lambdas() { }, result: Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(number("1"))) + expr: Box::new(Expr::value(number("1"))) } }, ], - else_result: Some(Box::new(Expr::Value(number("1")))) + else_result: Some(Box::new(Expr::value(number("1")))) }) }) ] @@ -154,12 +156,12 @@ fn test_values_clause() { explicit_row: false, rows: vec![ vec![ - Expr::Value(Value::DoubleQuotedString("one".to_owned())), - Expr::Value(number("1")), + Expr::Value((Value::DoubleQuotedString("one".to_owned())).with_empty_span()), + Expr::value(number("1")), ], vec![ - Expr::Value(Value::SingleQuotedString("two".to_owned())), - Expr::Value(number("2")), + Expr::Value((Value::SingleQuotedString("two".to_owned())).with_empty_span()), + Expr::value(number("2")), ], ], }; @@ -286,8 +288,8 @@ fn parse_databricks_struct_function() { .projection[0], SelectItem::UnnamedExpr(Expr::Struct { values: vec![ - Expr::Value(number("1")), - Expr::Value(Value::SingleQuotedString("foo".to_string())) + Expr::value(number("1")), + Expr::Value((Value::SingleQuotedString("foo".to_string())).with_empty_span()) ], fields: vec![] }) @@ -299,14 +301,17 @@ fn parse_databricks_struct_function() { SelectItem::UnnamedExpr(Expr::Struct { values: vec![ Expr::Named { - expr: Expr::Value(number("1")).into(), + expr: Expr::value(number("1")).into(), name: Ident::new("one") }, Expr::Named { - expr: Expr::Value(Value::SingleQuotedString("foo".to_string())).into(), + expr: Expr::Value( + (Value::SingleQuotedString("foo".to_string())).with_empty_span() + ) + .into(), name: Ident::new("foo") }, - Expr::Value(Value::Boolean(false)) + Expr::Value((Value::Boolean(false)).with_empty_span()) ], fields: vec![] }) diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 05e60d556..bed02428d 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -210,7 +210,7 @@ fn test_create_macro_default_args() { MacroArg::new("a"), MacroArg { name: Ident::new("b"), - default_expr: Some(Expr::Value(number("5"))), + default_expr: Some(Expr::value(number("5"))), }, ]), definition: MacroDefinition::Expr(Expr::BinaryOp { @@ -363,15 +363,15 @@ fn test_duckdb_struct_literal() { &Expr::Dictionary(vec![ DictionaryField { key: Ident::with_quote('\'', "a"), - value: Box::new(Expr::Value(number("1"))), + value: Box::new(Expr::value(number("1"))), }, DictionaryField { key: Ident::with_quote('\'', "b"), - value: Box::new(Expr::Value(number("2"))), + value: Box::new(Expr::value(number("2"))), }, DictionaryField { key: Ident::with_quote('\'', "c"), - value: Box::new(Expr::Value(number("3"))), + value: Box::new(Expr::value(number("3"))), }, ],), expr_from_projection(&select.projection[0]) @@ -381,7 +381,9 @@ fn test_duckdb_struct_literal() { &Expr::Array(Array { elem: vec![Expr::Dictionary(vec![DictionaryField { key: Ident::with_quote('\'', "a"), - value: Box::new(Expr::Value(Value::SingleQuotedString("abc".to_string()))), + value: Box::new(Expr::Value( + (Value::SingleQuotedString("abc".to_string())).with_empty_span() + )), },],)], named: false }), @@ -391,7 +393,7 @@ fn test_duckdb_struct_literal() { &Expr::Dictionary(vec![ DictionaryField { key: Ident::with_quote('\'', "a"), - value: Box::new(Expr::Value(number("1"))), + value: Box::new(Expr::value(number("1"))), }, DictionaryField { key: Ident::with_quote('\'', "b"), @@ -410,11 +412,14 @@ fn test_duckdb_struct_literal() { &Expr::Dictionary(vec![ DictionaryField { key: Ident::with_quote('\'', "a"), - value: Expr::Value(number("1")).into(), + value: Expr::value(number("1")).into(), }, DictionaryField { key: Ident::with_quote('\'', "b"), - value: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(), + value: Expr::Value( + (Value::SingleQuotedString("abc".to_string())).with_empty_span() + ) + .into(), }, ],), expr_from_projection(&select.projection[3]) @@ -431,7 +436,7 @@ fn test_duckdb_struct_literal() { key: Ident::with_quote('\'', "a"), value: Expr::Dictionary(vec![DictionaryField { key: Ident::with_quote('\'', "aa"), - value: Expr::Value(number("1")).into(), + value: Expr::value(number("1")).into(), }],) .into(), }],), @@ -594,16 +599,16 @@ fn test_duckdb_named_argument_function_with_assignment_operator() { args: vec![ FunctionArg::Named { name: Ident::new("a"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "1".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("1".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::Assignment }, FunctionArg::Named { name: Ident::new("b"), - arg: FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString( - "2".to_owned() - ))), + arg: FunctionArgExpr::Expr(Expr::Value( + (Value::SingleQuotedString("2".to_owned())).with_empty_span() + )), operator: FunctionArgOperator::Assignment }, ], @@ -632,14 +637,14 @@ fn test_array_index() { &Expr::CompoundFieldAccess { root: Box::new(Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("a".to_owned())), - Expr::Value(Value::SingleQuotedString("b".to_owned())), - Expr::Value(Value::SingleQuotedString("c".to_owned())) + Expr::Value((Value::SingleQuotedString("a".to_owned())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("b".to_owned())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("c".to_owned())).with_empty_span()) ], named: false })), access_chain: vec![AccessExpr::Subscript(Subscript::Index { - index: Expr::Value(number("3")) + index: Expr::value(number("3")) })] }, expr diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index b2b300aec..d7f3c014b 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -409,7 +409,8 @@ fn parse_create_function() { assert_eq!( function_body, Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - Value::SingleQuotedString("org.random.class.Name".to_string()) + (Value::SingleQuotedString("org.random.class.Name".to_string())) + .with_empty_span() ))) ); assert_eq!( diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 7b1277599..ec565e50e 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -68,7 +68,7 @@ fn parse_table_time_travel() { args: None, with_hints: vec![], version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( - Value::SingleQuotedString(version) + (Value::SingleQuotedString(version)).with_empty_span() ))), partitions: vec![], with_ordinality: false, @@ -121,7 +121,9 @@ fn parse_create_procedure() { distinct: None, top: None, top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))], + projection: vec![SelectItem::UnnamedExpr(Expr::Value( + (number("1")).with_empty_span() + ))], into: None, from: vec![], lateral_views: vec![], @@ -473,7 +475,9 @@ fn parse_mssql_top_paren() { let select = ms_and_generic().verified_only_select(sql); let top = select.top.unwrap(); assert_eq!( - Some(TopQuantity::Expr(Expr::Value(number("5")))), + Some(TopQuantity::Expr(Expr::Value( + (number("5")).with_empty_span() + ))), top.quantity ); assert!(!top.percent); @@ -485,7 +489,9 @@ fn parse_mssql_top_percent() { let select = ms_and_generic().verified_only_select(sql); let top = select.top.unwrap(); assert_eq!( - Some(TopQuantity::Expr(Expr::Value(number("5")))), + Some(TopQuantity::Expr(Expr::Value( + (number("5")).with_empty_span() + ))), top.quantity ); assert!(top.percent); @@ -497,7 +503,9 @@ fn parse_mssql_top_with_ties() { let select = ms_and_generic().verified_only_select(sql); let top = select.top.unwrap(); assert_eq!( - Some(TopQuantity::Expr(Expr::Value(number("5")))), + Some(TopQuantity::Expr(Expr::Value( + (number("5")).with_empty_span() + ))), top.quantity ); assert!(top.with_ties); @@ -509,7 +517,9 @@ fn parse_mssql_top_percent_with_ties() { let select = ms_and_generic().verified_only_select(sql); let top = select.top.unwrap(); assert_eq!( - Some(TopQuantity::Expr(Expr::Value(number("10")))), + Some(TopQuantity::Expr(Expr::Value( + (number("10")).with_empty_span() + ))), top.quantity ); assert!(top.percent); @@ -746,7 +756,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[0], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::Function(_)), operator: FunctionArgOperator::Colon } @@ -762,7 +775,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[2], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::Subquery(_)), operator: FunctionArgOperator::Colon } @@ -793,7 +809,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[0], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), operator: FunctionArgOperator::Colon } @@ -801,7 +820,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[1], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), operator: FunctionArgOperator::Colon } @@ -809,7 +831,10 @@ fn parse_mssql_json_object() { assert!(matches!( args[2], FunctionArg::ExprNamed { - name: Expr::Value(Value::SingleQuotedString(_)), + name: Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(_), + span: _ + }), arg: FunctionArgExpr::Expr(Expr::CompoundIdentifier(_)), operator: FunctionArgOperator::Colon } @@ -830,11 +855,17 @@ fn parse_mssql_json_array() { assert_eq!( &[ FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".into()) + (Value::SingleQuotedString("a".into())).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("1")).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (Value::Null).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("2")).with_empty_span() ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Null))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), ], &args[..] ); @@ -856,11 +887,17 @@ fn parse_mssql_json_array() { assert_eq!( &[ FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".into()) + (Value::SingleQuotedString("a".into())).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("1")).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (Value::Null).with_empty_span() + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("2")).with_empty_span() ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::Null))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), ], &args[..] ); @@ -915,7 +952,7 @@ fn parse_mssql_json_array() { }) => { assert_eq!( &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".into()) + (Value::SingleQuotedString("a".into())).with_empty_span() ))), &args[0] ); @@ -942,7 +979,7 @@ fn parse_mssql_json_array() { }) => { assert_eq!( &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("a".into()) + (Value::SingleQuotedString("a".into())).with_empty_span() ))), &args[0] ); @@ -964,7 +1001,9 @@ fn parse_mssql_json_array() { .. }) => { assert_eq!( - &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + &FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + (number("1")).with_empty_span() + ))), &args[0] ); assert!(matches!( @@ -1042,15 +1081,15 @@ fn parse_convert() { unreachable!() }; assert!(!is_try); - assert_eq!(Expr::Value(number("1")), *expr); + assert_eq!(Expr::value(number("1")), *expr); assert_eq!(Some(DataType::Int(None)), data_type); assert!(charset.is_none()); assert!(target_before_value); assert_eq!( vec![ - Expr::Value(number("2")), - Expr::Value(number("3")), - Expr::Value(Value::Null), + Expr::value(number("2")), + Expr::value(number("3")), + Expr::Value((Value::Null).with_empty_span()), ], styles ); @@ -1089,8 +1128,12 @@ fn parse_substring_in_select() { quote_style: None, span: Span::empty(), })), - substring_from: Some(Box::new(Expr::Value(number("0")))), - substring_for: Some(Box::new(Expr::Value(number("1")))), + substring_from: Some(Box::new(Expr::Value( + (number("0")).with_empty_span() + ))), + substring_for: Some(Box::new(Expr::Value( + (number("1")).with_empty_span() + ))), special: true, })], into: None, @@ -1179,9 +1222,9 @@ fn parse_mssql_declare() { span: Span::empty(), }], data_type: Some(Text), - assignment: Some(MsSqlAssignment(Box::new(Expr::Value(SingleQuotedString( - "foobar".to_string() - ))))), + assignment: Some(MsSqlAssignment(Box::new(Expr::Value( + (SingleQuotedString("foobar".to_string())).with_empty_span() + )))), declare_type: None, binary: None, sensitive: None, @@ -1215,7 +1258,9 @@ fn parse_mssql_declare() { local: false, hivevar: false, variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("@bar")])), - value: vec![Expr::Value(Value::Number("2".parse().unwrap(), false))], + value: vec![Expr::Value( + (Value::Number("2".parse().unwrap(), false)).with_empty_span() + )], }, Statement::Query(Box::new(Query { with: None, @@ -1236,7 +1281,9 @@ fn parse_mssql_declare() { projection: vec![SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("@bar"))), op: BinaryOperator::Multiply, - right: Box::new(Expr::Value(Value::Number("4".parse().unwrap(), false))), + right: Box::new(Expr::Value( + (Value::Number("4".parse().unwrap(), false)).with_empty_span() + )), })], into: None, from: vec![], @@ -1268,11 +1315,15 @@ fn test_parse_raiserror() { assert_eq!( s, Statement::RaisError { - message: Box::new(Expr::Value(Value::SingleQuotedString( - "This is a test".to_string() - ))), - severity: Box::new(Expr::Value(Value::Number("16".parse().unwrap(), false))), - state: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))), + message: Box::new(Expr::Value( + (Value::SingleQuotedString("This is a test".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("1".parse().unwrap(), false)).with_empty_span() + )), arguments: vec![], options: vec![], } @@ -1347,7 +1398,7 @@ fn parse_create_table_with_valid_options() { SqlOption::Partition { column_name: "column_a".into(), range_direction: None, - for_values: vec![Expr::Value(test_utils::number("10")), Expr::Value(test_utils::number("11"))] , + for_values: vec![Expr::Value((test_utils::number("10")).with_empty_span()), Expr::Value((test_utils::number("11")).with_empty_span())] , }, ], ), @@ -1358,8 +1409,8 @@ fn parse_create_table_with_valid_options() { column_name: "column_a".into(), range_direction: Some(PartitionRangeDirection::Left), for_values: vec![ - Expr::Value(test_utils::number("10")), - Expr::Value(test_utils::number("11")), + Expr::Value((test_utils::number("10")).with_empty_span()), + Expr::Value((test_utils::number("11")).with_empty_span()), ], } ], @@ -1630,8 +1681,8 @@ fn parse_create_table_with_identity_column() { IdentityProperty { parameters: Some(IdentityPropertyFormatKind::FunctionCall( IdentityParameters { - seed: Expr::Value(number("1")), - increment: Expr::Value(number("1")), + seed: Expr::value(number("1")), + increment: Expr::value(number("1")), }, )), order: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 030710743..ad2987b28 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -52,11 +52,11 @@ fn parse_literal_string() { let select = mysql().verified_only_select(sql); assert_eq!(2, select.projection.len()); assert_eq!( - &Expr::Value(Value::SingleQuotedString("single".to_string())), + &Expr::Value((Value::SingleQuotedString("single".to_string())).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::DoubleQuotedString("double".to_string())), + &Expr::Value((Value::DoubleQuotedString("double".to_string())).with_empty_span()), expr_from_projection(&select.projection[1]) ); } @@ -621,7 +621,7 @@ fn parse_set_variables() { local: true, hivevar: false, variables: OneOrManyWithParens::One(ObjectName::from(vec!["autocommit".into()])), - value: vec![Expr::Value(number("1"))], + value: vec![Expr::value(number("1"))], } ); } @@ -986,7 +986,9 @@ fn parse_create_table_both_options_and_as_query() { assert_eq!(collation, Some("utf8mb4_0900_ai_ci".to_string())); assert_eq!( query.unwrap().body.as_select().unwrap().projection, - vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))] + vec![SelectItem::UnnamedExpr(Expr::Value( + (number("1")).with_empty_span() + ))] ); } _ => unreachable!(), @@ -1413,18 +1415,25 @@ fn parse_simple_insert() { explicit_row: false, rows: vec![ vec![ - Expr::Value(Value::SingleQuotedString( - "Test Some Inserts".to_string() - )), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ], vec![ - Expr::Value(Value::SingleQuotedString("Test Entry 2".to_string())), - Expr::Value(number("2")) + Expr::Value( + (Value::SingleQuotedString("Test Entry 2".to_string())) + .with_empty_span() + ), + Expr::value(number("2")) ], vec![ - Expr::Value(Value::SingleQuotedString("Test Entry 3".to_string())), - Expr::Value(number("3")) + Expr::Value( + (Value::SingleQuotedString("Test Entry 3".to_string())) + .with_empty_span() + ), + Expr::value(number("3")) ] ] })), @@ -1471,8 +1480,11 @@ fn parse_ignore_insert() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ]] })), order_by: None, @@ -1518,8 +1530,11 @@ fn parse_priority_insert() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ]] })), order_by: None, @@ -1562,8 +1577,11 @@ fn parse_priority_insert() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ]] })), order_by: None, @@ -1611,9 +1629,9 @@ fn parse_insert_as() { with: None, body: Box::new(SetExpr::Values(Values { explicit_row: false, - rows: vec![vec![Expr::Value(Value::SingleQuotedString( - "2024-01-01".to_string() - ))]] + rows: vec![vec![Expr::Value( + (Value::SingleQuotedString("2024-01-01".to_string())).with_empty_span() + )]] })), order_by: None, limit: None, @@ -1672,8 +1690,11 @@ fn parse_insert_as() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Value(number("1")), - Expr::Value(Value::SingleQuotedString("2024-01-01".to_string())) + Expr::value(number("1")), + Expr::Value( + (Value::SingleQuotedString("2024-01-01".to_string())) + .with_empty_span() + ) ]] })), order_by: None, @@ -1720,8 +1741,11 @@ fn parse_replace_insert() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), - Expr::Value(number("1")) + Expr::Value( + (Value::SingleQuotedString("Test Some Inserts".to_string())) + .with_empty_span() + ), + Expr::value(number("1")) ]] })), order_by: None, @@ -1816,16 +1840,20 @@ fn parse_insert_with_on_duplicate_update() { body: Box::new(SetExpr::Values(Values { explicit_row: false, rows: vec![vec![ - Expr::Value(Value::SingleQuotedString( - "accounting_manager".to_string() - )), - Expr::Value(Value::SingleQuotedString( - "Some description about the group".to_string() - )), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), - Expr::Value(Value::Boolean(true)), + Expr::Value( + (Value::SingleQuotedString("accounting_manager".to_string())) + .with_empty_span() + ), + Expr::Value( + (Value::SingleQuotedString( + "Some description about the group".to_string() + )) + .with_empty_span() + ), + Expr::Value((Value::Boolean(true)).with_empty_span()), + Expr::Value((Value::Boolean(true)).with_empty_span()), + Expr::Value((Value::Boolean(true)).with_empty_span()), + Expr::Value((Value::Boolean(true)).with_empty_span()), ]] })), order_by: None, @@ -1946,7 +1974,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { top: None, top_before_distinct: false, projection: vec![ - SelectItem::UnnamedExpr(Expr::Value(number("123e4"))), + SelectItem::UnnamedExpr(Expr::value(number("123e4"))), SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("123col_$@123abc"))) ], into: None, @@ -2063,7 +2091,7 @@ fn parse_update_with_joins() { Ident::new("o"), Ident::new("completed") ])), - value: Expr::Value(Value::Boolean(true)) + value: Expr::Value((Value::Boolean(true)).with_empty_span()) }], assignments ); @@ -2074,7 +2102,9 @@ fn parse_update_with_joins() { Ident::new("firstname") ])), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString("Peter".to_string()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("Peter".to_string())).with_empty_span() + )) }), selection ); @@ -2114,7 +2144,7 @@ fn parse_delete_with_limit() { let sql = "DELETE FROM customers LIMIT 100"; match mysql().verified_stmt(sql) { Statement::Delete(Delete { limit, .. }) => { - assert_eq!(Some(Expr::Value(number("100"))), limit); + assert_eq!(Some(Expr::value(number("100"))), limit); } _ => unreachable!(), } @@ -2462,8 +2492,12 @@ fn parse_substring_in_select() { quote_style: None, span: Span::empty(), })), - substring_from: Some(Box::new(Expr::Value(number("0")))), - substring_for: Some(Box::new(Expr::Value(number("1")))), + substring_from: Some(Box::new(Expr::Value( + (number("0")).with_empty_span() + ))), + substring_for: Some(Box::new(Expr::Value( + (number("1")).with_empty_span() + ))), special: true, })], into: None, @@ -2937,7 +2971,7 @@ fn parse_json_table() { .from[0] .relation, TableFactor::JsonTable { - json_expr: Expr::Value(Value::SingleQuotedString("[1,2]".to_string())), + json_expr: Expr::Value((Value::SingleQuotedString("[1,2]".to_string())).with_empty_span()), json_path: Value::SingleQuotedString("$[*]".to_string()), columns: vec![ JsonTableColumn::Named(JsonTableNamedColumn { @@ -2975,33 +3009,33 @@ fn parse_logical_xor() { let select = mysql_and_generic().verified_only_select(sql); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(true))), + left: Box::new(Expr::Value((Value::Boolean(true)).with_empty_span())), op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(true))), + right: Box::new(Expr::Value((Value::Boolean(true)).with_empty_span())), }), select.projection[0] ); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(false))), + left: Box::new(Expr::Value((Value::Boolean(false)).with_empty_span())), op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(false))), + right: Box::new(Expr::Value((Value::Boolean(false)).with_empty_span())), }), select.projection[1] ); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(true))), + left: Box::new(Expr::Value((Value::Boolean(true)).with_empty_span())), op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(false))), + right: Box::new(Expr::Value((Value::Boolean(false)).with_empty_span())), }), select.projection[2] ); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::Boolean(false))), + left: Box::new(Expr::Value((Value::Boolean(false)).with_empty_span())), op: BinaryOperator::Xor, - right: Box::new(Expr::Value(Value::Boolean(true))), + right: Box::new(Expr::Value((Value::Boolean(true)).with_empty_span())), }), select.projection[3] ); @@ -3013,7 +3047,7 @@ fn parse_bitstring_literal() { assert_eq!( select.projection, vec![SelectItem::UnnamedExpr(Expr::Value( - Value::SingleQuotedByteStringLiteral("111".to_string()) + (Value::SingleQuotedByteStringLiteral("111".to_string())).with_empty_span() ))] ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 312ce1186..22558329d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -436,7 +436,9 @@ fn parse_create_table_with_defaults() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Default(Expr::Value(Value::Boolean(true))), + option: ColumnOption::Default(Expr::Value( + (Value::Boolean(true)).with_empty_span() + )), }, ColumnOptionDef { name: None, @@ -488,15 +490,15 @@ fn parse_create_table_with_defaults() { vec![ SqlOption::KeyValue { key: "fillfactor".into(), - value: Expr::Value(number("20")) + value: Expr::value(number("20")) }, SqlOption::KeyValue { key: "user_catalog_table".into(), - value: Expr::Value(Value::Boolean(true)) + value: Expr::Value((Value::Boolean(true)).with_empty_span()) }, SqlOption::KeyValue { key: "autovacuum_vacuum_threshold".into(), - value: Expr::Value(number("100")) + value: Expr::value(number("100")) }, ] ); @@ -768,7 +770,8 @@ fn parse_alter_table_alter_column() { ) { AlterTableOperation::AlterColumn { column_name, op } => { assert_eq!("is_active", column_name.to_string()); - let using_expr = Expr::Value(Value::SingleQuotedString("text".to_string())); + let using_expr = + Expr::Value(Value::SingleQuotedString("text".to_string()).with_empty_span()); assert_eq!( op, AlterColumnOperation::SetDataType { @@ -1280,7 +1283,7 @@ fn parse_copy_to() { top_before_distinct: false, projection: vec![ SelectItem::ExprWithAlias { - expr: Expr::Value(number("42")), + expr: Expr::value(number("42")), alias: Ident { value: "a".into(), quote_style: None, @@ -1288,7 +1291,9 @@ fn parse_copy_to() { }, }, SelectItem::ExprWithAlias { - expr: Expr::Value(Value::SingleQuotedString("hello".into())), + expr: Expr::Value( + (Value::SingleQuotedString("hello".into())).with_empty_span() + ), alias: Ident { value: "b".into(), quote_style: None, @@ -1446,7 +1451,9 @@ fn parse_set() { local: false, hivevar: false, variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), - value: vec![Expr::Value(Value::SingleQuotedString("b".into()))], + value: vec![Expr::Value( + (Value::SingleQuotedString("b".into())).with_empty_span() + )], } ); @@ -1457,7 +1464,7 @@ fn parse_set() { local: false, hivevar: false, variables: OneOrManyWithParens::One(ObjectName::from(vec![Ident::new("a")])), - value: vec![Expr::Value(number("0"))], + value: vec![Expr::value(number("0"))], } ); @@ -1518,7 +1525,7 @@ fn parse_set() { Ident::new("reducer"), Ident::new("parallelism") ])), - value: vec![Expr::Value(Value::Boolean(false))], + value: vec![Expr::Value((Value::Boolean(false)).with_empty_span())], } ); @@ -1670,8 +1677,8 @@ fn parse_execute() { Statement::Execute { name: Some(ObjectName::from(vec!["a".into()])), parameters: vec![ - Expr::Value(number("1")), - Expr::Value(Value::SingleQuotedString("t".to_string())) + Expr::value(number("1")), + Expr::Value((Value::SingleQuotedString("t".to_string())).with_empty_span()) ], has_parentheses: true, using: vec![], @@ -1692,7 +1699,9 @@ fn parse_execute() { ExprWithAlias { expr: Expr::Cast { kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::Number("1337".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Value::Number("1337".parse().unwrap(), false)).with_empty_span() + )), data_type: DataType::SmallInt(None), format: None }, @@ -1701,7 +1710,9 @@ fn parse_execute() { ExprWithAlias { expr: Expr::Cast { kind: CastKind::Cast, - expr: Box::new(Expr::Value(Value::Number("7331".parse().unwrap(), false))), + expr: Box::new(Expr::Value( + (Value::Number("7331".parse().unwrap(), false)).with_empty_span() + )), data_type: DataType::SmallInt(None), format: None }, @@ -1899,7 +1910,9 @@ fn parse_pg_on_conflict() { target: AssignmentTarget::ColumnName(ObjectName::from( vec!["dname".into()] )), - value: Expr::Value(Value::Placeholder("$1".to_string())) + value: Expr::Value( + (Value::Placeholder("$1".to_string())).with_empty_span() + ) },], selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { @@ -1908,7 +1921,9 @@ fn parse_pg_on_conflict() { span: Span::empty(), })), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(Value::Placeholder("$2".to_string()))) + right: Box::new(Expr::Value( + (Value::Placeholder("$2".to_string())).with_empty_span() + )) }) }), action @@ -1942,7 +1957,9 @@ fn parse_pg_on_conflict() { target: AssignmentTarget::ColumnName(ObjectName::from( vec!["dname".into()] )), - value: Expr::Value(Value::Placeholder("$1".to_string())) + value: Expr::Value( + (Value::Placeholder("$1".to_string())).with_empty_span() + ) },], selection: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident { @@ -1951,7 +1968,9 @@ fn parse_pg_on_conflict() { span: Span::empty(), })), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(Value::Placeholder("$2".to_string()))) + right: Box::new(Expr::Value( + (Value::Placeholder("$2".to_string())).with_empty_span() + )) }) }), action @@ -2167,9 +2186,13 @@ fn parse_pg_regex_match_ops() { let select = pg().verified_only_select(&format!("SELECT 'abc' {} '^a'", &str_op)); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString("abc".into()))), + left: Box::new(Expr::Value( + (Value::SingleQuotedString("abc".into())).with_empty_span() + )), op: op.clone(), - right: Box::new(Expr::Value(Value::SingleQuotedString("^a".into()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("^a".into())).with_empty_span() + )), }), select.projection[0] ); @@ -2189,9 +2212,13 @@ fn parse_pg_like_match_ops() { let select = pg().verified_only_select(&format!("SELECT 'abc' {} 'a_c%'", &str_op)); assert_eq!( SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString("abc".into()))), + left: Box::new(Expr::Value( + (Value::SingleQuotedString("abc".into())).with_empty_span() + )), op: op.clone(), - right: Box::new(Expr::Value(Value::SingleQuotedString("a_c%".into()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("a_c%".into())).with_empty_span() + )), }), select.projection[0] ); @@ -2201,7 +2228,7 @@ fn parse_pg_like_match_ops() { #[test] fn parse_array_index_expr() { let num: Vec = (0..=10) - .map(|s| Expr::Value(number(&s.to_string()))) + .map(|s| Expr::Value(number(&s.to_string()).with_empty_span())) .collect(); let sql = "SELECT foo[0] FROM foos"; @@ -2312,7 +2339,7 @@ fn parse_array_subscript() { ( "(ARRAY[1, 2, 3, 4, 5, 6])[2]", Subscript::Index { - index: Expr::Value(number("2")), + index: Expr::value(number("2")), }, ), ( @@ -2324,17 +2351,17 @@ fn parse_array_subscript() { ( "(ARRAY[1, 2, 3, 4, 5, 6])[2:5]", Subscript::Slice { - lower_bound: Some(Expr::Value(number("2"))), - upper_bound: Some(Expr::Value(number("5"))), + lower_bound: Some(Expr::value(number("2"))), + upper_bound: Some(Expr::value(number("5"))), stride: None, }, ), ( "(ARRAY[1, 2, 3, 4, 5, 6])[2:5:3]", Subscript::Slice { - lower_bound: Some(Expr::Value(number("2"))), - upper_bound: Some(Expr::Value(number("5"))), - stride: Some(Expr::Value(number("3"))), + lower_bound: Some(Expr::value(number("2"))), + upper_bound: Some(Expr::value(number("5"))), + stride: Some(Expr::value(number("3"))), }, ), ( @@ -2343,12 +2370,12 @@ fn parse_array_subscript() { lower_bound: Some(Expr::BinaryOp { left: Box::new(call("array_length", [Expr::Identifier(Ident::new("arr"))])), op: BinaryOperator::Minus, - right: Box::new(Expr::Value(number("3"))), + right: Box::new(Expr::value(number("3"))), }), upper_bound: Some(Expr::BinaryOp { left: Box::new(call("array_length", [Expr::Identifier(Ident::new("arr"))])), op: BinaryOperator::Minus, - right: Box::new(Expr::Value(number("1"))), + right: Box::new(Expr::value(number("1"))), }), stride: None, }, @@ -2357,14 +2384,14 @@ fn parse_array_subscript() { "(ARRAY[1, 2, 3, 4, 5, 6])[:5]", Subscript::Slice { lower_bound: None, - upper_bound: Some(Expr::Value(number("5"))), + upper_bound: Some(Expr::value(number("5"))), stride: None, }, ), ( "(ARRAY[1, 2, 3, 4, 5, 6])[2:]", Subscript::Slice { - lower_bound: Some(Expr::Value(number("2"))), + lower_bound: Some(Expr::value(number("2"))), upper_bound: None, stride: None, }, @@ -2400,19 +2427,19 @@ fn parse_array_multi_subscript() { root: Box::new(call( "make_array", vec![ - Expr::Value(number("1")), - Expr::Value(number("2")), - Expr::Value(number("3")) + Expr::value(number("1")), + Expr::value(number("2")), + Expr::value(number("3")) ] )), access_chain: vec![ AccessExpr::Subscript(Subscript::Slice { - lower_bound: Some(Expr::Value(number("1"))), - upper_bound: Some(Expr::Value(number("2"))), + lower_bound: Some(Expr::value(number("1"))), + upper_bound: Some(Expr::value(number("2"))), stride: None, }), AccessExpr::Subscript(Subscript::Index { - index: Expr::Value(number("2")), + index: Expr::value(number("2")), }), ], }, @@ -2655,7 +2682,9 @@ fn parse_array_subquery_expr() { distinct: None, top: None, top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("1")))], + projection: vec![SelectItem::UnnamedExpr(Expr::Value( + (number("1")).with_empty_span() + ))], into: None, from: vec![], lateral_views: vec![], @@ -2678,7 +2707,9 @@ fn parse_array_subquery_expr() { distinct: None, top: None, top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr(Expr::Value(number("2")))], + projection: vec![SelectItem::UnnamedExpr(Expr::Value( + (number("2")).with_empty_span() + ))], into: None, from: vec![], lateral_views: vec![], @@ -2750,7 +2781,9 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("params"))), op: BinaryOperator::LongArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString("name".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("name".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2761,7 +2794,9 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("params"))), op: BinaryOperator::Arrow, - right: Box::new(Expr::Value(Value::SingleQuotedString("name".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("name".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2773,12 +2808,14 @@ fn test_json() { left: Box::new(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::Arrow, - right: Box::new(Expr::Value(Value::SingleQuotedString("items".to_string()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("items".to_string())).with_empty_span() + )) }), op: BinaryOperator::LongArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "product".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("product".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2790,7 +2827,7 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("obj"))), op: BinaryOperator::Arrow, - right: Box::new(Expr::Value(number("42"))), + right: Box::new(Expr::value(number("42"))), }), select.projection[0] ); @@ -2815,9 +2852,9 @@ fn test_json() { left: Box::new(Expr::Identifier(Ident::new("obj"))), op: BinaryOperator::Arrow, right: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(number("3"))), + left: Box::new(Expr::value(number("3"))), op: BinaryOperator::Multiply, - right: Box::new(Expr::Value(number("2"))), + right: Box::new(Expr::value(number("2"))), }), }), select.projection[0] @@ -2829,9 +2866,9 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::HashArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "{a,b,c}".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("{a,b,c}".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2842,9 +2879,9 @@ fn test_json() { SelectItem::UnnamedExpr(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::HashLongArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "{a,b,c}".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("{a,b,c}".to_string())).with_empty_span() + )), }), select.projection[0] ); @@ -2855,9 +2892,9 @@ fn test_json() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::AtArrow, - right: Box::new(Expr::Value(Value::SingleQuotedString( - "{\"a\": 1}".to_string() - ))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("{\"a\": 1}".to_string())).with_empty_span() + )), }, select.selection.unwrap(), ); @@ -2866,9 +2903,9 @@ fn test_json() { let select = pg().verified_only_select(sql); assert_eq!( Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString( - "{\"a\": 1}".to_string() - ))), + left: Box::new(Expr::Value( + (Value::SingleQuotedString("{\"a\": 1}".to_string())).with_empty_span() + )), op: BinaryOperator::ArrowAt, right: Box::new(Expr::Identifier(Ident::new("info"))), }, @@ -2883,8 +2920,8 @@ fn test_json() { op: BinaryOperator::HashMinus, right: Box::new(Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("a".to_string())), - Expr::Value(Value::SingleQuotedString("b".to_string())), + Expr::Value((Value::SingleQuotedString("a".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), ], named: true, })), @@ -2898,7 +2935,9 @@ fn test_json() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::from("info"))), op: BinaryOperator::AtQuestion, - right: Box::new(Expr::Value(Value::SingleQuotedString("$.a".to_string())),), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("$.a".to_string())).with_empty_span() + ),), }, select.selection.unwrap(), ); @@ -2909,7 +2948,9 @@ fn test_json() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::from("info"))), op: BinaryOperator::AtAt, - right: Box::new(Expr::Value(Value::SingleQuotedString("$.a".to_string())),), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("$.a".to_string())).with_empty_span() + ),), }, select.selection.unwrap(), ); @@ -2920,7 +2961,9 @@ fn test_json() { Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("info"))), op: BinaryOperator::Question, - right: Box::new(Expr::Value(Value::SingleQuotedString("b".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("b".to_string())).with_empty_span() + )), }, select.selection.unwrap(), ); @@ -2933,8 +2976,8 @@ fn test_json() { op: BinaryOperator::QuestionAnd, right: Box::new(Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("b".to_string())), - Expr::Value(Value::SingleQuotedString("c".to_string())) + Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("c".to_string())).with_empty_span()) ], named: true })) @@ -2950,8 +2993,8 @@ fn test_json() { op: BinaryOperator::QuestionPipe, right: Box::new(Expr::Array(Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("b".to_string())), - Expr::Value(Value::SingleQuotedString("c".to_string())) + Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), + Expr::Value((Value::SingleQuotedString("c".to_string())).with_empty_span()) ], named: true })) @@ -3025,7 +3068,7 @@ fn test_composite_value() { access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("price")))] }), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(number("9"))) + right: Box::new(Expr::value(number("9"))) } ); @@ -3045,8 +3088,12 @@ fn test_composite_value() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Array( Array { elem: vec![ - Expr::Value(Value::SingleQuotedString("i".to_string())), - Expr::Value(Value::SingleQuotedString("i".to_string())), + Expr::Value( + (Value::SingleQuotedString("i".to_string())).with_empty_span() + ), + Expr::Value( + (Value::SingleQuotedString("i".to_string())).with_empty_span() + ), ], named: true } @@ -3106,27 +3153,27 @@ fn parse_escaped_literal_string() { let select = pg_and_generic().verified_only_select(sql); assert_eq!(6, select.projection.len()); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("s1 \n s1".to_string())), + &Expr::Value((Value::EscapedStringLiteral("s1 \n s1".to_string())).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("s2 \\n s2".to_string())), + &Expr::Value((Value::EscapedStringLiteral("s2 \\n s2".to_string())).with_empty_span()), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("s3 \\\n s3".to_string())), + &Expr::Value((Value::EscapedStringLiteral("s3 \\\n s3".to_string())).with_empty_span()), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("s4 \\\\n s4".to_string())), + &Expr::Value((Value::EscapedStringLiteral("s4 \\\\n s4".to_string())).with_empty_span()), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("'".to_string())), + &Expr::Value((Value::EscapedStringLiteral("'".to_string())).with_empty_span()), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("foo \\".to_string())), + &Expr::Value((Value::EscapedStringLiteral("foo \\".to_string())).with_empty_span()), expr_from_projection(&select.projection[5]) ); @@ -3144,31 +3191,31 @@ fn parse_escaped_literal_string() { let select = pg_and_generic().verified_only_select_with_canonical(sql, canonical); assert_eq!(7, select.projection.len()); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("\u{0001}".to_string())), + &Expr::Value((Value::EscapedStringLiteral("\u{0001}".to_string())).with_empty_span()), expr_from_projection(&select.projection[0]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("\u{10ffff}".to_string())), + &Expr::Value((Value::EscapedStringLiteral("\u{10ffff}".to_string())).with_empty_span()), expr_from_projection(&select.projection[1]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("\u{000c}".to_string())), + &Expr::Value((Value::EscapedStringLiteral("\u{000c}".to_string())).with_empty_span()), expr_from_projection(&select.projection[2]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("%".to_string())), + &Expr::Value((Value::EscapedStringLiteral("%".to_string())).with_empty_span()), expr_from_projection(&select.projection[3]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("\u{0002}".to_string())), + &Expr::Value((Value::EscapedStringLiteral("\u{0002}".to_string())).with_empty_span()), expr_from_projection(&select.projection[4]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("%".to_string())), + &Expr::Value((Value::EscapedStringLiteral("%".to_string())).with_empty_span()), expr_from_projection(&select.projection[5]) ); assert_eq!( - &Expr::Value(Value::EscapedStringLiteral("%".to_string())), + &Expr::Value((Value::EscapedStringLiteral("%".to_string())).with_empty_span()), expr_from_projection(&select.projection[6]) ); @@ -3310,7 +3357,9 @@ fn parse_custom_operator() { "pg_catalog".into(), "~".into() ]), - right: Box::new(Expr::Value(Value::SingleQuotedString("^(table)$".into()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("^(table)$".into())).with_empty_span() + )) }) ); @@ -3326,7 +3375,9 @@ fn parse_custom_operator() { span: Span::empty(), })), op: BinaryOperator::PGCustomBinaryOperator(vec!["pg_catalog".into(), "~".into()]), - right: Box::new(Expr::Value(Value::SingleQuotedString("^(table)$".into()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("^(table)$".into())).with_empty_span() + )) }) ); @@ -3342,7 +3393,9 @@ fn parse_custom_operator() { span: Span::empty(), })), op: BinaryOperator::PGCustomBinaryOperator(vec!["~".into()]), - right: Box::new(Expr::Value(Value::SingleQuotedString("^(table)$".into()))) + right: Box::new(Expr::Value( + (Value::SingleQuotedString("^(table)$".into())).with_empty_span() + )) }) ); } @@ -3428,9 +3481,9 @@ fn parse_create_role() { assert_eq!(*bypassrls, Some(true)); assert_eq!( *password, - Some(Password::Password(Expr::Value(Value::SingleQuotedString( - "abcdef".into() - )))) + Some(Password::Password(Expr::Value( + (Value::SingleQuotedString("abcdef".into())).with_empty_span() + ))) ); assert_eq!(*superuser, Some(true)); assert_eq!(*create_db, Some(false)); @@ -3439,7 +3492,9 @@ fn parse_create_role() { assert_eq!(*connection_limit, None); assert_eq!( *valid_until, - Some(Expr::Value(Value::SingleQuotedString("2025-01-01".into()))) + Some(Expr::Value( + (Value::SingleQuotedString("2025-01-01".into())).with_empty_span() + )) ); assert_eq_vec(&["role1", "role2"], in_role); assert!(in_group.is_empty()); @@ -3521,13 +3576,15 @@ fn parse_alter_role() { RoleOption::Login(true), RoleOption::Replication(true), RoleOption::BypassRLS(true), - RoleOption::ConnectionLimit(Expr::Value(number("100"))), + RoleOption::ConnectionLimit(Expr::value(number("100"))), RoleOption::Password({ - Password::Password(Expr::Value(Value::SingleQuotedString("abcdef".into()))) + Password::Password(Expr::Value( + (Value::SingleQuotedString("abcdef".into())).with_empty_span(), + )) }), - RoleOption::ValidUntil(Expr::Value(Value::SingleQuotedString( - "2025-01-01".into(), - ))) + RoleOption::ValidUntil(Expr::Value( + (Value::SingleQuotedString("2025-01-01".into(),)).with_empty_span() + )) ] }, } @@ -3593,7 +3650,9 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }]), - config_value: SetConfigValue::Value(Expr::Value(number("100000"))), + config_value: SetConfigValue::Value(Expr::Value( + (number("100000")).with_empty_span() + )), in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, @@ -3618,7 +3677,9 @@ fn parse_alter_role() { quote_style: None, span: Span::empty(), }]), - config_value: SetConfigValue::Value(Expr::Value(number("100000"))), + config_value: SetConfigValue::Value(Expr::Value( + (number("100000")).with_empty_span() + )), in_database: Some(ObjectName::from(vec![Ident { value: "database_name".into(), quote_style: None, @@ -3799,7 +3860,7 @@ fn parse_create_function() { called_on_null: Some(FunctionCalledOnNull::Strict), parallel: Some(FunctionParallel::Safe), function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - Value::SingleQuotedString("select $1 + $2;".into()) + (Value::SingleQuotedString("select $1 + $2;".into())).with_empty_span() ))), if_not_exists: false, using: None, @@ -3862,7 +3923,9 @@ fn parse_drop_function() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), }], @@ -3888,10 +3951,9 @@ fn parse_drop_function() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - ))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), }, @@ -3907,10 +3969,9 @@ fn parse_drop_function() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - ))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), } @@ -3956,7 +4017,9 @@ fn parse_drop_procedure() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number("1".parse().unwrap(), false))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), }], @@ -3982,10 +4045,9 @@ fn parse_drop_procedure() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - ))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), }, @@ -4001,10 +4063,9 @@ fn parse_drop_procedure() { mode: Some(ArgMode::In), name: Some("b".into()), data_type: DataType::Integer(None), - default_expr: Some(Expr::Value(Value::Number( - "1".parse().unwrap(), - false - ))), + default_expr: Some(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), } ]), } @@ -4041,36 +4102,48 @@ fn parse_dollar_quoted_string() { }; assert_eq!( - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: None, - value: "hello".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: None, + value: "hello".into() + })) + .with_empty_span() + ), expr_from_projection(&projection[0]) ); assert_eq!( - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: Some("tag_name".into()), - value: "world".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: Some("tag_name".into()), + value: "world".into() + })) + .with_empty_span() + ), expr_from_projection(&projection[1]) ); assert_eq!( - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: None, - value: "Foo$Bar".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: None, + value: "Foo$Bar".into() + })) + .with_empty_span() + ), expr_from_projection(&projection[2]) ); assert_eq!( projection[3], SelectItem::ExprWithAlias { - expr: Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: None, - value: "Foo$Bar".into(), - })), + expr: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: None, + value: "Foo$Bar".into(), + })) + .with_empty_span() + ), alias: Ident { value: "col_name".into(), quote_style: None, @@ -4081,18 +4154,24 @@ fn parse_dollar_quoted_string() { assert_eq!( expr_from_projection(&projection[4]), - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: None, - value: "".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: None, + value: "".into() + })) + .with_empty_span() + ), ); assert_eq!( expr_from_projection(&projection[5]), - &Expr::Value(Value::DollarQuotedString(DollarQuotedString { - tag: Some("tag_name".into()), - value: "".into() - })), + &Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + tag: Some("tag_name".into()), + value: "".into() + })) + .with_empty_span() + ), ); } @@ -4438,7 +4517,7 @@ fn test_simple_postgres_insert_with_alias() { explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), - Expr::Value(Value::Number("123".to_string(), false)) + Expr::Value((Value::Number("123".to_string(), false)).with_empty_span()) ]] })), order_by: None, @@ -4508,10 +4587,10 @@ fn test_simple_postgres_insert_with_alias() { explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), - Expr::Value(Value::Number( - bigdecimal::BigDecimal::new(123.into(), 0), - false - )) + Expr::Value( + (Value::Number(bigdecimal::BigDecimal::new(123.into(), 0), false)) + .with_empty_span() + ) ]] })), order_by: None, @@ -4580,7 +4659,9 @@ fn test_simple_insert_with_quoted_alias() { explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), - Expr::Value(Value::SingleQuotedString("0123".to_string())) + Expr::Value( + (Value::SingleQuotedString("0123".to_string())).with_empty_span() + ) ]] })), order_by: None, @@ -4650,18 +4731,18 @@ fn parse_at_time_zone() { }), time_zone: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString( - "America/Los_Angeles".to_owned(), - ))), + expr: Box::new(Expr::Value( + Value::SingleQuotedString("America/Los_Angeles".to_owned()).with_empty_span(), + )), data_type: DataType::Text, format: None, }), }), op: BinaryOperator::Plus, right: Box::new(Expr::Interval(Interval { - value: Box::new(Expr::Value(Value::SingleQuotedString( - "23 hours".to_owned(), - ))), + value: Box::new(Expr::Value( + Value::SingleQuotedString("23 hours".to_owned()).with_empty_span(), + )), leading_field: None, leading_precision: None, last_field: None, @@ -4685,11 +4766,13 @@ fn parse_create_table_with_options() { vec![ SqlOption::KeyValue { key: "foo".into(), - value: Expr::Value(Value::SingleQuotedString("bar".into())), + value: Expr::Value( + (Value::SingleQuotedString("bar".into())).with_empty_span() + ), }, SqlOption::KeyValue { key: "a".into(), - value: Expr::Value(number("123")), + value: Expr::value(number("123")), }, ], with_options @@ -4735,7 +4818,10 @@ fn test_table_unnest_with_ordinality() { #[test] fn test_escaped_string_literal() { match pg().verified_expr(r#"E'\n'"#) { - Expr::Value(Value::EscapedStringLiteral(s)) => { + Expr::Value(ValueWithSpan { + value: Value::EscapedStringLiteral(s), + span: _, + }) => { assert_eq!("\n", s); } _ => unreachable!(), @@ -4790,7 +4876,7 @@ fn parse_create_after_update_trigger_with_condition() { Ident::new("balance"), ])), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(number("10000"))), + right: Box::new(Expr::value(number("10000"))), }))), exec_body: TriggerExecBody { exec_type: TriggerExecBodyType::Function, @@ -5155,7 +5241,7 @@ fn parse_trigger_related_functions() { return_type: Some(DataType::Trigger), function_body: Some( CreateFunctionBody::AsBeforeOptions( - Expr::Value( + Expr::Value(( Value::DollarQuotedString( DollarQuotedString { value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n\n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n\n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(), @@ -5163,8 +5249,8 @@ fn parse_trigger_related_functions() { "emp_stamp".to_owned(), ), }, - ), - ), + ) + ).with_empty_span()), ), ), behavior: None, @@ -5231,7 +5317,10 @@ fn test_unicode_string_literal() { ]; for (input, expected) in pairs { match pg_and_generic().verified_expr(input) { - Expr::Value(Value::UnicodeStringLiteral(s)) => { + Expr::Value(ValueWithSpan { + value: Value::UnicodeStringLiteral(s), + span: _, + }) => { assert_eq!(expected, s); } _ => unreachable!(), @@ -5250,10 +5339,14 @@ fn check_arrow_precedence(sql: &str, arrow_operator: BinaryOperator) { span: Span::empty(), })), op: arrow_operator, - right: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("bar".to_string())).with_empty_span() + )), }), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::SingleQuotedString("spam".to_string()))), + right: Box::new(Expr::Value( + (Value::SingleQuotedString("spam".to_string())).with_empty_span() + )), } ) } @@ -5283,7 +5376,9 @@ fn arrow_cast_precedence() { op: BinaryOperator::Arrow, right: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - expr: Box::new(Expr::Value(Value::SingleQuotedString("bar".to_string()))), + expr: Box::new(Expr::Value( + (Value::SingleQuotedString("bar".to_string())).with_empty_span() + )), data_type: DataType::Text, format: None, }), @@ -5410,7 +5505,7 @@ fn parse_bitstring_literal() { assert_eq!( select.projection, vec![SelectItem::UnnamedExpr(Expr::Value( - Value::SingleQuotedByteStringLiteral("111".to_string()) + (Value::SingleQuotedByteStringLiteral("111".to_string())).with_empty_span() ))] ); } diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index c4b897f01..7736735cb 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -208,7 +208,7 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Dot { key: "o_orderkey".to_string(), @@ -231,10 +231,12 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Bracket { - key: Expr::Value(Value::SingleQuotedString("id".to_owned())) + key: Expr::Value( + (Value::SingleQuotedString("id".to_owned())).with_empty_span() + ) } ] } @@ -255,10 +257,12 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Bracket { - key: Expr::Value(Value::SingleQuotedString("id".to_owned())) + key: Expr::Value( + (Value::SingleQuotedString("id".to_owned())).with_empty_span() + ) } ] } @@ -279,7 +283,7 @@ fn test_redshift_json_path() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Dot { key: "id".to_string(), @@ -306,7 +310,7 @@ fn test_parse_json_path_from() { &Some(JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Dot { key: "a".to_string(), @@ -330,14 +334,16 @@ fn test_parse_json_path_from() { &Some(JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")) + key: Expr::value(number("0")) }, JsonPathElem::Dot { key: "a".to_string(), quoted: false }, JsonPathElem::Bracket { - key: Expr::Value(Value::Number("1".parse().unwrap(), false)) + key: Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + ) }, JsonPathElem::Dot { key: "b".to_string(), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 12796bb65..b1d31e6dd 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -570,8 +570,8 @@ fn test_snowflake_create_table_with_autoincrement_columns() { IdentityProperty { parameters: Some(IdentityPropertyFormatKind::FunctionCall( IdentityParameters { - seed: Expr::Value(number("100")), - increment: Expr::Value(number("1")), + seed: Expr::value(number("100")), + increment: Expr::value(number("1")), } )), order: Some(IdentityPropertyOrder::NoOrder), @@ -602,8 +602,12 @@ fn test_snowflake_create_table_with_autoincrement_columns() { parameters: Some( IdentityPropertyFormatKind::StartAndIncrement( IdentityParameters { - seed: Expr::Value(number("100")), - increment: Expr::Value(number("1")), + seed: Expr::Value( + (number("100")).with_empty_span() + ), + increment: Expr::Value( + (number("1")).with_empty_span() + ), } ) ), @@ -1108,9 +1112,9 @@ fn parse_semi_structured_data_traversal() { path: JsonPath { path: vec![JsonPathElem::Bracket { key: Expr::BinaryOp { - left: Box::new(Expr::Value(number("2"))), + left: Box::new(Expr::value(number("2"))), op: BinaryOperator::Plus, - right: Box::new(Expr::Value(number("2"))) + right: Box::new(Expr::value(number("2"))) }, }] }, @@ -1188,7 +1192,7 @@ fn parse_semi_structured_data_traversal() { quoted: false, }, JsonPathElem::Bracket { - key: Expr::Value(number("0")), + key: Expr::value(number("0")), }, JsonPathElem::Dot { key: "bar".to_owned(), @@ -1210,7 +1214,7 @@ fn parse_semi_structured_data_traversal() { path: JsonPath { path: vec![ JsonPathElem::Bracket { - key: Expr::Value(number("0")), + key: Expr::value(number("0")), }, JsonPathElem::Dot { key: "foo".to_owned(), @@ -1276,7 +1280,7 @@ fn parse_semi_structured_data_traversal() { }), path: JsonPath { path: vec![JsonPathElem::Bracket { - key: Expr::Value(number("1")) + key: Expr::value(number("1")) }] } } @@ -1661,13 +1665,13 @@ fn parse_snowflake_declare_result_set() { ( "DECLARE res RESULTSET DEFAULT 42", "res", - Some(DeclareAssignment::Default(Expr::Value(number("42")).into())), + Some(DeclareAssignment::Default(Expr::value(number("42")).into())), ), ( "DECLARE res RESULTSET := 42", "res", Some(DeclareAssignment::DuckAssignment( - Expr::Value(number("42")).into(), + Expr::value(number("42")).into(), )), ), ("DECLARE res RESULTSET", "res", None), @@ -1717,8 +1721,8 @@ fn parse_snowflake_declare_exception() { "ex", Some(DeclareAssignment::Expr( Expr::Tuple(vec![ - Expr::Value(number("42")), - Expr::Value(Value::SingleQuotedString("ERROR".to_string())), + Expr::value(number("42")), + Expr::Value((Value::SingleQuotedString("ERROR".to_string())).with_empty_span()), ]) .into(), )), @@ -1754,13 +1758,13 @@ fn parse_snowflake_declare_variable() { "DECLARE profit TEXT DEFAULT 42", "profit", Some(DataType::Text), - Some(DeclareAssignment::Default(Expr::Value(number("42")).into())), + Some(DeclareAssignment::Default(Expr::value(number("42")).into())), ), ( "DECLARE profit DEFAULT 42", "profit", None, - Some(DeclareAssignment::Default(Expr::Value(number("42")).into())), + Some(DeclareAssignment::Default(Expr::value(number("42")).into())), ), ("DECLARE profit TEXT", "profit", Some(DataType::Text), None), ("DECLARE profit", "profit", None, None), @@ -2509,10 +2513,14 @@ fn test_snowflake_trim() { let select = snowflake().verified_only_select(sql_only_select); assert_eq!( &Expr::Trim { - expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + 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()))]), + trim_characters: Some(vec![Expr::Value( + (Value::SingleQuotedString("a".to_owned())).with_empty_span() + )]), }, expr_from_projection(only(&select.projection)) ); @@ -2530,7 +2538,7 @@ fn test_number_placeholder() { let sql_only_select = "SELECT :1"; let select = snowflake().verified_only_select(sql_only_select); assert_eq!( - &Expr::Value(Value::Placeholder(":1".into())), + &Expr::Value((Value::Placeholder(":1".into())).with_empty_span()), expr_from_projection(only(&select.projection)) ); @@ -2676,7 +2684,7 @@ fn parse_comma_outer_join() { "myudf", [Expr::UnaryOp { op: UnaryOperator::Plus, - expr: Box::new(Expr::Value(number("42"))) + expr: Box::new(Expr::value(number("42"))) }] )), }) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 17dcfed85..361c9b051 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -369,7 +369,9 @@ fn test_placeholder() { let ast = sqlite().verified_only_select(sql); assert_eq!( ast.projection[0], - UnnamedExpr(Expr::Value(Value::Placeholder("@xxx".into()))), + UnnamedExpr(Expr::Value( + (Value::Placeholder("@xxx".into())).with_empty_span() + )), ); } @@ -446,7 +448,11 @@ fn parse_attach_database() { match verified_stmt { Statement::AttachDatabase { schema_name, - database_file_name: Expr::Value(Value::SingleQuotedString(literal_name)), + database_file_name: + Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(literal_name), + span: _, + }), database: true, } => { assert_eq!(schema_name.value, "test"); @@ -469,8 +475,8 @@ fn parse_update_tuple_row_values() { ObjectName::from(vec![Ident::new("b"),]), ]), value: Expr::Tuple(vec![ - Expr::Value(Value::Number("1".parse().unwrap(), false)), - Expr::Value(Value::Number("2".parse().unwrap(), false)) + Expr::Value((Value::Number("1".parse().unwrap(), false)).with_empty_span()), + Expr::Value((Value::Number("2".parse().unwrap(), false)).with_empty_span()) ]) }], selection: None, @@ -530,7 +536,12 @@ fn test_dollar_identifier_as_placeholder() { Expr::BinaryOp { op, left, right } => { assert_eq!(op, BinaryOperator::Eq); assert_eq!(left, Box::new(Expr::Identifier(Ident::new("id")))); - assert_eq!(right, Box::new(Expr::Value(Placeholder("$id".to_string())))); + assert_eq!( + right, + Box::new(Expr::Value( + (Placeholder("$id".to_string())).with_empty_span() + )) + ); } _ => unreachable!(), } @@ -540,7 +551,12 @@ fn test_dollar_identifier_as_placeholder() { Expr::BinaryOp { op, left, right } => { assert_eq!(op, BinaryOperator::Eq); assert_eq!(left, Box::new(Expr::Identifier(Ident::new("id")))); - assert_eq!(right, Box::new(Expr::Value(Placeholder("$$".to_string())))); + assert_eq!( + right, + Box::new(Expr::Value( + (Placeholder("$$".to_string())).with_empty_span() + )) + ); } _ => unreachable!(), } From 648efd7057d63c65b53eddc3d05cc89d5697d85c Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 25 Feb 2025 08:50:29 +0200 Subject: [PATCH 742/806] feat: adjust create and drop trigger for mysql dialect (#1734) --- src/ast/mod.rs | 7 ++++-- src/parser/mod.rs | 11 +++++---- tests/sqlparser_mysql.rs | 48 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 4 ++-- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index afe3e77d2..5263bfc3c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3256,7 +3256,7 @@ pub enum Statement { DropTrigger { if_exists: bool, trigger_name: ObjectName, - table_name: ObjectName, + table_name: Option, /// `CASCADE` or `RESTRICT` option: Option, }, @@ -4062,7 +4062,10 @@ impl fmt::Display for Statement { if *if_exists { write!(f, " IF EXISTS")?; } - write!(f, " {trigger_name} ON {table_name}")?; + match &table_name { + Some(table_name) => write!(f, " {trigger_name} ON {table_name}")?, + None => write!(f, " {trigger_name}")?, + }; if let Some(option) = option { write!(f, " {option}")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 72e4567cb..2d64ff3e1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4990,14 +4990,17 @@ 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) { + if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) { self.prev_token(); return self.expected("an object type after DROP", self.peek_token()); } let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let trigger_name = self.parse_object_name(false)?; - self.expect_keyword_is(Keyword::ON)?; - let table_name = self.parse_object_name(false)?; + let table_name = if self.parse_keyword(Keyword::ON) { + Some(self.parse_object_name(false)?) + } else { + None + }; let option = self .parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) .map(|keyword| match keyword { @@ -5018,7 +5021,7 @@ impl<'a> Parser<'a> { or_replace: bool, is_constraint: bool, ) -> Result { - if !dialect_of!(self is PostgreSqlDialect | GenericDialect) { + if !dialect_of!(self is PostgreSqlDialect | GenericDialect | MySqlDialect) { self.prev_token(); return self.expected("an object type after CREATE", self.peek_token()); } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ad2987b28..861f782c6 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3291,3 +3291,51 @@ fn parse_looks_like_single_line_comment() { "UPDATE account SET balance = balance WHERE account_id = 5752", ); } + +#[test] +fn parse_create_trigger() { + let sql_create_trigger = r#" + CREATE TRIGGER emp_stamp BEFORE INSERT ON emp + FOR EACH ROW EXECUTE FUNCTION emp_stamp(); + "#; + let create_stmt = mysql().one_statement_parses_to(sql_create_trigger, ""); + assert_eq!( + create_stmt, + Statement::CreateTrigger { + or_replace: false, + is_constraint: false, + name: ObjectName::from(vec![Ident::new("emp_stamp")]), + period: TriggerPeriod::Before, + events: vec![TriggerEvent::Insert], + table_name: ObjectName::from(vec![Ident::new("emp")]), + referenced_table_name: None, + referencing: vec![], + trigger_object: TriggerObject::Row, + include_each: true, + condition: None, + exec_body: TriggerExecBody { + exec_type: TriggerExecBodyType::Function, + func_desc: FunctionDesc { + name: ObjectName::from(vec![Ident::new("emp_stamp")]), + args: None, + } + }, + characteristics: None, + } + ); +} + +#[test] +fn parse_drop_trigger() { + let sql_drop_trigger = "DROP TRIGGER emp_stamp;"; + let drop_stmt = mysql().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, + } + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 22558329d..7508218f9 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5044,7 +5044,7 @@ fn parse_drop_trigger() { Statement::DropTrigger { if_exists, trigger_name: ObjectName::from(vec![Ident::new("check_update")]), - table_name: ObjectName::from(vec![Ident::new("table_name")]), + table_name: Some(ObjectName::from(vec![Ident::new("table_name")])), option } ); @@ -5297,7 +5297,7 @@ fn parse_trigger_related_functions() { Statement::DropTrigger { if_exists: false, trigger_name: ObjectName::from(vec![Ident::new("emp_stamp")]), - table_name: ObjectName::from(vec![Ident::new("emp")]), + table_name: Some(ObjectName::from(vec![Ident::new("emp")])), option: None } ); From de4dbc5b1d5bb91f3887478c8e32e739ab701e84 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Tue, 25 Feb 2025 22:00:08 -0800 Subject: [PATCH 743/806] Parse SIGNED INTEGER type in MySQL CAST (#1739) --- src/ast/data_type.rs | 70 +++++++++++++++++++++++++++++----------- src/ast/mod.rs | 5 +-- src/keywords.rs | 1 + src/parser/mod.rs | 32 ++++++++++++------ tests/sqlparser_mysql.rs | 28 +++++++++++++--- 5 files changed, 101 insertions(+), 35 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index cae8ca8f0..57bc67441 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -132,19 +132,19 @@ pub enum DataType { /// 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 - UnsignedTinyInt(Option), + 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) /// /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html Int2(Option), - /// Unsigned Int2 with optional display width e.g. INT2 Unsigned or INT2(5) Unsigned - UnsignedInt2(Option), + /// 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) SmallInt(Option), /// Unsigned small integer with optional display width e.g. SMALLINT UNSIGNED or SMALLINT(5) UNSIGNED - UnsignedSmallInt(Option), + SmallIntUnsigned(Option), /// 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 @@ -152,7 +152,7 @@ pub enum DataType { /// 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 - UnsignedMediumInt(Option), + MediumIntUnsigned(Option), /// Int with optional display width e.g. INT or INT(11) Int(Option), /// Int4 as alias for Integer in [postgresql] @@ -197,11 +197,11 @@ pub enum DataType { /// 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 - UnsignedInt(Option), + IntUnsigned(Option), /// Unsigned int4 with optional display width e.g. INT4 UNSIGNED or INT4(11) UNSIGNED - UnsignedInt4(Option), + Int4Unsigned(Option), /// Unsigned integer with optional display width e.g. INTEGER UNSIGNED or INTEGER(11) UNSIGNED - UnsignedInteger(Option), + IntegerUnsigned(Option), /// Unsigned integer type in [clickhouse] /// Note: UInt8 mean 8 bits in [clickhouse] /// @@ -235,9 +235,29 @@ pub enum DataType { /// 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 - UnsignedBigInt(Option), + BigIntUnsigned(Option), /// Unsigned Int8 with optional display width e.g. INT8 UNSIGNED or INT8(11) UNSIGNED - UnsignedInt8(Option), + Int8Unsigned(Option), + /// Signed integer as used in [MySQL CAST] target types, without optional `INTEGER` suffix: + /// `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` + /// + /// [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` + /// + /// [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` + /// + /// [MySQL CAST]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html + UnsignedInteger, /// Float4 as alias for Real in [postgresql] /// /// [postgresql]: https://www.postgresql.org/docs/15/datatype.html @@ -433,29 +453,29 @@ impl fmt::Display for DataType { DataType::TinyInt(zerofill) => { format_type_with_optional_length(f, "TINYINT", zerofill, false) } - DataType::UnsignedTinyInt(zerofill) => { + DataType::TinyIntUnsigned(zerofill) => { format_type_with_optional_length(f, "TINYINT", zerofill, true) } DataType::Int2(zerofill) => { format_type_with_optional_length(f, "INT2", zerofill, false) } - DataType::UnsignedInt2(zerofill) => { + DataType::Int2Unsigned(zerofill) => { format_type_with_optional_length(f, "INT2", zerofill, true) } DataType::SmallInt(zerofill) => { format_type_with_optional_length(f, "SMALLINT", zerofill, false) } - DataType::UnsignedSmallInt(zerofill) => { + DataType::SmallIntUnsigned(zerofill) => { format_type_with_optional_length(f, "SMALLINT", zerofill, true) } DataType::MediumInt(zerofill) => { format_type_with_optional_length(f, "MEDIUMINT", zerofill, false) } - DataType::UnsignedMediumInt(zerofill) => { + DataType::MediumIntUnsigned(zerofill) => { format_type_with_optional_length(f, "MEDIUMINT", zerofill, true) } DataType::Int(zerofill) => format_type_with_optional_length(f, "INT", zerofill, false), - DataType::UnsignedInt(zerofill) => { + DataType::IntUnsigned(zerofill) => { format_type_with_optional_length(f, "INT", zerofill, true) } DataType::Int4(zerofill) => { @@ -479,22 +499,22 @@ impl fmt::Display for DataType { DataType::Int256 => { write!(f, "Int256") } - DataType::UnsignedInt4(zerofill) => { + DataType::Int4Unsigned(zerofill) => { format_type_with_optional_length(f, "INT4", zerofill, true) } DataType::Integer(zerofill) => { format_type_with_optional_length(f, "INTEGER", zerofill, false) } - DataType::UnsignedInteger(zerofill) => { + DataType::IntegerUnsigned(zerofill) => { format_type_with_optional_length(f, "INTEGER", zerofill, true) } DataType::BigInt(zerofill) => { format_type_with_optional_length(f, "BIGINT", zerofill, false) } - DataType::UnsignedBigInt(zerofill) => { + DataType::BigIntUnsigned(zerofill) => { format_type_with_optional_length(f, "BIGINT", zerofill, true) } - DataType::UnsignedInt8(zerofill) => { + DataType::Int8Unsigned(zerofill) => { format_type_with_optional_length(f, "INT8", zerofill, true) } DataType::UInt8 => { @@ -515,6 +535,18 @@ impl fmt::Display for DataType { DataType::UInt256 => { write!(f, "UInt256") } + DataType::Signed => { + write!(f, "SIGNED") + } + DataType::SignedInteger => { + write!(f, "SIGNED INTEGER") + } + DataType::Unsigned => { + write!(f, "UNSIGNED") + } + DataType::UnsignedInteger => { + write!(f, "UNSIGNED INTEGER") + } DataType::Real => write!(f, "REAL"), DataType::Float4 => write!(f, "FLOAT4"), DataType::Float32 => write!(f, "Float32"), diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5263bfc3c..85d3ed91c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -814,8 +814,9 @@ pub enum Expr { kind: CastKind, expr: Box, data_type: DataType, - // Optional CAST(string_expression AS type FORMAT format_string_expression) as used by BigQuery - // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax + /// Optional CAST(string_expression AS type FORMAT format_string_expression) as used by [BigQuery] + /// + /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax format: Option, }, /// AT a timestamp to a different timezone e.g. `FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00'` diff --git a/src/keywords.rs b/src/keywords.rs index d62a038b8..020b404ed 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -790,6 +790,7 @@ define_keywords!( SHARE, SHARING, SHOW, + SIGNED, SIMILAR, SKIP, SLOW, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2d64ff3e1..ddcb6055f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8867,7 +8867,7 @@ impl<'a> Parser<'a> { Keyword::TINYINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedTinyInt(optional_precision?)) + Ok(DataType::TinyIntUnsigned(optional_precision?)) } else { Ok(DataType::TinyInt(optional_precision?)) } @@ -8875,7 +8875,7 @@ impl<'a> Parser<'a> { Keyword::INT2 => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt2(optional_precision?)) + Ok(DataType::Int2Unsigned(optional_precision?)) } else { Ok(DataType::Int2(optional_precision?)) } @@ -8883,7 +8883,7 @@ impl<'a> Parser<'a> { Keyword::SMALLINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedSmallInt(optional_precision?)) + Ok(DataType::SmallIntUnsigned(optional_precision?)) } else { Ok(DataType::SmallInt(optional_precision?)) } @@ -8891,7 +8891,7 @@ impl<'a> Parser<'a> { Keyword::MEDIUMINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedMediumInt(optional_precision?)) + Ok(DataType::MediumIntUnsigned(optional_precision?)) } else { Ok(DataType::MediumInt(optional_precision?)) } @@ -8899,7 +8899,7 @@ impl<'a> Parser<'a> { Keyword::INT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt(optional_precision?)) + Ok(DataType::IntUnsigned(optional_precision?)) } else { Ok(DataType::Int(optional_precision?)) } @@ -8907,7 +8907,7 @@ impl<'a> Parser<'a> { Keyword::INT4 => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt4(optional_precision?)) + Ok(DataType::Int4Unsigned(optional_precision?)) } else { Ok(DataType::Int4(optional_precision?)) } @@ -8915,7 +8915,7 @@ impl<'a> Parser<'a> { Keyword::INT8 => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInt8(optional_precision?)) + Ok(DataType::Int8Unsigned(optional_precision?)) } else { Ok(DataType::Int8(optional_precision?)) } @@ -8928,7 +8928,7 @@ impl<'a> Parser<'a> { Keyword::INTEGER => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedInteger(optional_precision?)) + Ok(DataType::IntegerUnsigned(optional_precision?)) } else { Ok(DataType::Integer(optional_precision?)) } @@ -8936,7 +8936,7 @@ impl<'a> Parser<'a> { Keyword::BIGINT => { let optional_precision = self.parse_optional_precision(); if self.parse_keyword(Keyword::UNSIGNED) { - Ok(DataType::UnsignedBigInt(optional_precision?)) + Ok(DataType::BigIntUnsigned(optional_precision?)) } else { Ok(DataType::BigInt(optional_precision?)) } @@ -9142,6 +9142,20 @@ impl<'a> Parser<'a> { let columns = self.parse_returns_table_columns()?; Ok(DataType::Table(columns)) } + Keyword::SIGNED => { + if self.parse_keyword(Keyword::INTEGER) { + Ok(DataType::SignedInteger) + } else { + Ok(DataType::Signed) + } + } + Keyword::UNSIGNED => { + if self.parse_keyword(Keyword::INTEGER) { + Ok(DataType::UnsignedInteger) + } else { + Ok(DataType::Unsigned) + } + } _ => { self.prev_token(); let type_name = self.parse_object_name(false)?; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 861f782c6..4856bd894 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1359,27 +1359,27 @@ fn parse_create_table_unsigned() { vec![ ColumnDef { name: Ident::new("bar_tinyint"), - data_type: DataType::UnsignedTinyInt(Some(3)), + data_type: DataType::TinyIntUnsigned(Some(3)), options: vec![], }, ColumnDef { name: Ident::new("bar_smallint"), - data_type: DataType::UnsignedSmallInt(Some(5)), + data_type: DataType::SmallIntUnsigned(Some(5)), options: vec![], }, ColumnDef { name: Ident::new("bar_mediumint"), - data_type: DataType::UnsignedMediumInt(Some(13)), + data_type: DataType::MediumIntUnsigned(Some(13)), options: vec![], }, ColumnDef { name: Ident::new("bar_int"), - data_type: DataType::UnsignedInt(Some(11)), + data_type: DataType::IntUnsigned(Some(11)), options: vec![], }, ColumnDef { name: Ident::new("bar_bigint"), - data_type: DataType::UnsignedBigInt(Some(20)), + data_type: DataType::BigIntUnsigned(Some(20)), options: vec![], }, ], @@ -3339,3 +3339,21 @@ fn parse_drop_trigger() { } ); } + +#[test] +fn parse_cast_integers() { + mysql().verified_expr("CAST(foo AS UNSIGNED)"); + mysql().verified_expr("CAST(foo AS SIGNED)"); + mysql().verified_expr("CAST(foo AS UNSIGNED INTEGER)"); + mysql().verified_expr("CAST(foo AS SIGNED INTEGER)"); + + mysql() + .run_parser_method("CAST(foo AS UNSIGNED(3))", |p| p.parse_expr()) + .expect_err("CAST doesn't allow display width"); + mysql() + .run_parser_method("CAST(foo AS UNSIGNED(3) INTEGER)", |p| p.parse_expr()) + .expect_err("CAST doesn't allow display width"); + mysql() + .run_parser_method("CAST(foo AS UNSIGNED INTEGER(3))", |p| p.parse_expr()) + .expect_err("CAST doesn't allow display width"); +} From 3adc746b11411746f78f5c2506e1bf72e1caf583 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Tue, 25 Feb 2025 22:03:38 -0800 Subject: [PATCH 744/806] Parse MySQL ALTER TABLE ALGORITHM option (#1745) --- src/ast/ddl.rs | 45 +++++++++++++++++++++++++++++++++---- src/ast/mod.rs | 18 +++++++-------- src/ast/spans.rs | 1 + src/keywords.rs | 2 ++ src/parser/mod.rs | 18 +++++++++++++++ tests/sqlparser_mysql.rs | 48 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 13 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1fbc45603..372a75adb 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -65,7 +65,6 @@ pub enum AlterTableOperation { name: Ident, select: ProjectionSelect, }, - /// `DROP PROJECTION [IF EXISTS] name` /// /// Note: this is a ClickHouse-specific operation. @@ -74,7 +73,6 @@ pub enum AlterTableOperation { if_exists: bool, name: Ident, }, - /// `MATERIALIZE PROJECTION [IF EXISTS] name [IN PARTITION partition_name]` /// /// Note: this is a ClickHouse-specific operation. @@ -84,7 +82,6 @@ pub enum AlterTableOperation { name: Ident, partition: Option, }, - /// `CLEAR PROJECTION [IF EXISTS] name [IN PARTITION partition_name]` /// /// Note: this is a ClickHouse-specific operation. @@ -94,7 +91,6 @@ pub enum AlterTableOperation { name: Ident, partition: Option, }, - /// `DISABLE ROW LEVEL SECURITY` /// /// Note: this is a PostgreSQL-specific operation. @@ -272,6 +268,15 @@ pub enum AlterTableOperation { DropClusteringKey, SuspendRecluster, ResumeRecluster, + /// `ALGORITHM [=] { DEFAULT | INSTANT | INPLACE | COPY }` + /// + /// [MySQL]-specific table alter algorithm. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + Algorithm { + equals: bool, + algorithm: AlterTableAlgorithm, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -317,6 +322,30 @@ impl fmt::Display for AlterPolicyOperation { } } +/// [MySQL] `ALTER TABLE` algorithm. +/// +/// [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 AlterTableAlgorithm { + Default, + Instant, + Inplace, + Copy, +} + +impl fmt::Display for AlterTableAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Self::Default => "DEFAULT", + Self::Instant => "INSTANT", + Self::Inplace => "INPLACE", + Self::Copy => "COPY", + }) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -407,6 +436,14 @@ impl fmt::Display for AlterTableOperation { } write!(f, " {} ({})", name, query) } + AlterTableOperation::Algorithm { equals, algorithm } => { + write!( + f, + "ALGORITHM {}{}", + if *equals { "= " } else { "" }, + algorithm + ) + } AlterTableOperation::DropProjection { if_exists, name } => { write!(f, "DROP PROJECTION")?; if *if_exists { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 85d3ed91c..72be3ff6c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -48,15 +48,15 @@ pub use self::dcl::{ }; pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, - 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, + AlterTableAlgorithm, 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, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index cfc3eb633..7cb5ddfed 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1062,6 +1062,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::DropClusteringKey => Span::empty(), AlterTableOperation::SuspendRecluster => Span::empty(), AlterTableOperation::ResumeRecluster => Span::empty(), + AlterTableOperation::Algorithm { .. } => Span::empty(), } } } diff --git a/src/keywords.rs b/src/keywords.rs index 020b404ed..a6854f073 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -427,11 +427,13 @@ define_keywords!( INNER, INOUT, INPATH, + INPLACE, INPUT, INPUTFORMAT, INSENSITIVE, INSERT, INSTALL, + INSTANT, INSTEAD, INT, INT128, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ddcb6055f..86a86824d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8163,6 +8163,24 @@ impl<'a> Parser<'a> { AlterTableOperation::SuspendRecluster } else if self.parse_keywords(&[Keyword::RESUME, Keyword::RECLUSTER]) { AlterTableOperation::ResumeRecluster + } else if self.parse_keyword(Keyword::ALGORITHM) { + let equals = self.consume_token(&Token::Eq); + let algorithm = match self.parse_one_of_keywords(&[ + Keyword::DEFAULT, + Keyword::INSTANT, + Keyword::INPLACE, + Keyword::COPY, + ]) { + Some(Keyword::DEFAULT) => AlterTableAlgorithm::Default, + Some(Keyword::INSTANT) => AlterTableAlgorithm::Instant, + Some(Keyword::INPLACE) => AlterTableAlgorithm::Inplace, + Some(Keyword::COPY) => AlterTableAlgorithm::Copy, + _ => self.expected( + "DEFAULT, INSTANT, INPLACE, or COPY after ALGORITHM [=]", + self.peek_token(), + )?, + }; + AlterTableOperation::Algorithm { equals, algorithm } } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 4856bd894..5e98d3f41 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2422,6 +2422,54 @@ fn parse_alter_table_modify_column() { assert_eq!(expected_operation, operation); } +#[test] +fn parse_alter_table_with_algorithm() { + let sql = "ALTER TABLE tab ALGORITHM = COPY"; + let expected_operation = AlterTableOperation::Algorithm { + equals: true, + algorithm: AlterTableAlgorithm::Copy, + }; + let operation = alter_table_op(mysql_and_generic().verified_stmt(sql)); + assert_eq!(expected_operation, operation); + + // Check order doesn't matter + let sql = + "ALTER TABLE users DROP COLUMN password_digest, ALGORITHM = COPY, 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::Algorithm { + equals: true, + algorithm: AlterTableAlgorithm::Copy, + }, + 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` ALGORITHM DEFAULT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM INSTANT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM INPLACE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM COPY"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = DEFAULT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = INSTANT"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = INPLACE"); + mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = COPY"); +} + #[test] fn parse_alter_table_modify_column_with_column_position() { let expected_name = ObjectName::from(vec![Ident::new("orders")]); From 5b3500139a9bb76959e2c880b2c60fbd7fb9738e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 27 Feb 2025 00:33:08 -0500 Subject: [PATCH 745/806] Random test cleanups use Expr::value (#1749) --- tests/sqlparser_clickhouse.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 98a1aef62..72a64a488 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -55,10 +55,7 @@ fn parse_map_access_expr() { "indexOf", [ Expr::Identifier(Ident::new("string_names")), - Expr::Value( - (Value::SingleQuotedString("endpoint".to_string())) - .with_empty_span() - ) + Expr::value(Value::SingleQuotedString("endpoint".to_string())) ] ), })], @@ -74,9 +71,7 @@ fn parse_map_access_expr() { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value( - (Value::SingleQuotedString("test".to_string())).with_empty_span() - )), + right: Box::new(Expr::value(Value::SingleQuotedString("test".to_string()))), }), op: BinaryOperator::And, right: Box::new(BinaryOp { @@ -87,18 +82,13 @@ fn parse_map_access_expr() { "indexOf", [ Expr::Identifier(Ident::new("string_name")), - Expr::Value( - (Value::SingleQuotedString("app".to_string())) - .with_empty_span() - ) + Expr::value(Value::SingleQuotedString("app".to_string())) ] ), })], }), op: BinaryOperator::NotEq, - right: Box::new(Expr::Value( - (Value::SingleQuotedString("foo".to_string())).with_empty_span() - )), + right: Box::new(Expr::value(Value::SingleQuotedString("foo".to_string()))), }), }), group_by: GroupByExpr::Expressions(vec![], vec![]), @@ -124,8 +114,8 @@ fn parse_array_expr() { assert_eq!( &Expr::Array(Array { elem: vec![ - Expr::Value((Value::SingleQuotedString("1".to_string())).with_empty_span()), - Expr::Value((Value::SingleQuotedString("2".to_string())).with_empty_span()), + Expr::value(Value::SingleQuotedString("1".to_string())), + Expr::value(Value::SingleQuotedString("2".to_string())), ], named: false, }), From c2914f82e10b0e45d83ca5e218ad5a1a760edde6 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Wed, 26 Feb 2025 21:40:30 -0800 Subject: [PATCH 746/806] Parse ALTER TABLE AUTO_INCREMENT operation for MySQL (#1748) --- src/ast/ddl.rs | 18 ++++++++++++++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 4 ++++ tests/sqlparser_mysql.rs | 13 +++++++++++++ 4 files changed, 36 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 372a75adb..bb85eb06c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -34,6 +34,7 @@ use crate::ast::{ CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, + ValueWithSpan, }; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -277,6 +278,15 @@ pub enum AlterTableOperation { equals: bool, algorithm: AlterTableAlgorithm, }, + /// `AUTO_INCREMENT [=] ` + /// + /// [MySQL]-specific table option for raising current auto increment value. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + AutoIncrement { + equals: bool, + value: ValueWithSpan, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -663,6 +673,14 @@ impl fmt::Display for AlterTableOperation { write!(f, "RESUME RECLUSTER")?; Ok(()) } + AlterTableOperation::AutoIncrement { equals, value } => { + write!( + f, + "AUTO_INCREMENT {}{}", + if *equals { "= " } else { "" }, + value + ) + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7cb5ddfed..38e9e258e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1063,6 +1063,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::SuspendRecluster => Span::empty(), AlterTableOperation::ResumeRecluster => Span::empty(), AlterTableOperation::Algorithm { .. } => Span::empty(), + AlterTableOperation::AutoIncrement { value, .. } => value.span(), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 86a86824d..f234fcc07 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8181,6 +8181,10 @@ impl<'a> Parser<'a> { )?, }; AlterTableOperation::Algorithm { equals, algorithm } + } else if self.parse_keyword(Keyword::AUTO_INCREMENT) { + let equals = self.consume_token(&Token::Eq); + let value = self.parse_number_value()?; + AlterTableOperation::AutoIncrement { equals, value } } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5e98d3f41..15f79b4c2 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2470,6 +2470,19 @@ fn parse_alter_table_with_algorithm() { mysql_and_generic().verified_stmt("ALTER TABLE `users` ALGORITHM = COPY"); } +#[test] +fn parse_alter_table_auto_increment() { + let sql = "ALTER TABLE tab AUTO_INCREMENT = 42"; + let expected_operation = AlterTableOperation::AutoIncrement { + equals: true, + value: number("42").with_empty_span(), + }; + let operation = alter_table_op(mysql().verified_stmt(sql)); + assert_eq!(expected_operation, operation); + + mysql_and_generic().verified_stmt("ALTER TABLE `users` AUTO_INCREMENT 42"); +} + #[test] fn parse_alter_table_modify_column_with_column_position() { let expected_name = ObjectName::from(vec![Ident::new("orders")]); From ed416548dcfe4a73a3240bbf625fb9010a4925c8 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 27 Feb 2025 12:29:59 -0500 Subject: [PATCH 747/806] Prepare for 55.0.0 release: Version and CHANGELOG (#1750) Co-authored-by: Ophir LOJKINE --- CHANGELOG.md | 1 + Cargo.toml | 2 +- changelog/0.55.0.md | 173 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 changelog/0.55.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c55b285..362a637d9 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.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) - `0.52.0`: [changelog/0.52.0.md](changelog/0.52.0.md) diff --git a/Cargo.toml b/Cargo.toml index 9caff2512..99bfdc241 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.54.0" +version = "0.55.0" authors = ["Apache DataFusion "] homepage = "https://github.com/apache/datafusion-sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/changelog/0.55.0.md b/changelog/0.55.0.md new file mode 100644 index 000000000..046bf22bc --- /dev/null +++ b/changelog/0.55.0.md @@ -0,0 +1,173 @@ + + +# sqlparser-rs 0.55.0 Changelog + +This release consists of 55 commits from 25 contributors. See credits at the end of this changelog for more information. + +## Migrating usages of `Expr::Value` + +In v0.55 of sqlparser the `Expr::Value` enum variant contains a `ValueWithSpan` instead of a `Value`. Here is how to migrate. + +### When pattern matching + +```diff +- Expr::Value(Value::SingleQuotedString(my_string)) => { ... } ++ Expr::Value(ValueWithSpan{ value: Value::SingleQuotedString(my_string), span: _ }) => { ... } +``` + +### When creating an `Expr` + +Use the new `Expr::value` method (notice the lowercase `v`), which will create a `ValueWithSpan` containing an empty span: + +```diff +- Expr::Value(Value::SingleQuotedString(my_string)) ++ Expr::value(Value::SingleQuotedString(my_string)) +``` + +## Migrating usages of `ObjectName` + +In v0.55 of sqlparser, the `ObjectName` structure has been changed as shown below. Here is now to migrate. + +```diff +- pub struct ObjectName(pub Vec); ++ pub struct ObjectName(pub Vec) +``` + +### When constructing `ObjectName` + +Use the `From` impl: + +```diff +- name: ObjectName(vec![Ident::new("f")]), ++ name: ObjectName::from(vec![Ident::new("f")]), +``` + +### Accessing Spans + +Use the `span()` function + +```diff +- name.span ++ name.span() +``` + + + +**Breaking changes:** + +- Enhance object name path segments [#1539](https://github.com/apache/datafusion-sqlparser-rs/pull/1539) (ayman-sigma) +- Store spans for Value expressions [#1738](https://github.com/apache/datafusion-sqlparser-rs/pull/1738) (lovasoa) + +**Implemented enhancements:** + +- feat: adjust create and drop trigger for mysql dialect [#1734](https://github.com/apache/datafusion-sqlparser-rs/pull/1734) (invm) + +**Fixed bugs:** + +- fix: make `serde` feature no_std [#1730](https://github.com/apache/datafusion-sqlparser-rs/pull/1730) (iajoiner) + +**Other:** + +- Update rat_exclude_file.txt [#1670](https://github.com/apache/datafusion-sqlparser-rs/pull/1670) (alamb) +- Add support for Snowflake account privileges [#1666](https://github.com/apache/datafusion-sqlparser-rs/pull/1666) (yoavcloud) +- Add support for Create Iceberg Table statement for Snowflake parser [#1664](https://github.com/apache/datafusion-sqlparser-rs/pull/1664) (Vedin) +- National strings: check if dialect supports backslash escape [#1672](https://github.com/apache/datafusion-sqlparser-rs/pull/1672) (hansott) +- Only support escape literals for Postgres, Redshift and generic dialect [#1674](https://github.com/apache/datafusion-sqlparser-rs/pull/1674) (hansott) +- BigQuery: Support trailing commas in column definitions list [#1682](https://github.com/apache/datafusion-sqlparser-rs/pull/1682) (iffyio) +- Enable GROUP BY exp for Snowflake dialect [#1683](https://github.com/apache/datafusion-sqlparser-rs/pull/1683) (yoavcloud) +- Add support for parsing empty dictionary expressions [#1684](https://github.com/apache/datafusion-sqlparser-rs/pull/1684) (yoavcloud) +- Support multiple tables in `UPDATE FROM` clause [#1681](https://github.com/apache/datafusion-sqlparser-rs/pull/1681) (iffyio) +- Add support for mysql table hints [#1675](https://github.com/apache/datafusion-sqlparser-rs/pull/1675) (AvivDavid-Satori) +- BigQuery: Add support for select expr star [#1680](https://github.com/apache/datafusion-sqlparser-rs/pull/1680) (iffyio) +- Support underscore separators in numbers for Clickhouse. Fixes #1659 [#1677](https://github.com/apache/datafusion-sqlparser-rs/pull/1677) (graup) +- BigQuery: Fix column identifier reserved keywords list [#1678](https://github.com/apache/datafusion-sqlparser-rs/pull/1678) (iffyio) +- Fix bug when parsing a Snowflake stage with `;` suffix [#1688](https://github.com/apache/datafusion-sqlparser-rs/pull/1688) (yoavcloud) +- Allow plain JOIN without turning it into INNER [#1692](https://github.com/apache/datafusion-sqlparser-rs/pull/1692) (mvzink) +- Fix DDL generation in case of an empty arguments function. [#1690](https://github.com/apache/datafusion-sqlparser-rs/pull/1690) (remysaissy) +- Fix `CREATE FUNCTION` round trip for Hive dialect [#1693](https://github.com/apache/datafusion-sqlparser-rs/pull/1693) (iffyio) +- Make numeric literal underscore test dialect agnostic [#1685](https://github.com/apache/datafusion-sqlparser-rs/pull/1685) (iffyio) +- Extend lambda support for ClickHouse and DuckDB dialects [#1686](https://github.com/apache/datafusion-sqlparser-rs/pull/1686) (gstvg) +- Make TypedString preserve quote style [#1679](https://github.com/apache/datafusion-sqlparser-rs/pull/1679) (graup) +- Do not parse ASOF and MATCH_CONDITION as table factor aliases [#1698](https://github.com/apache/datafusion-sqlparser-rs/pull/1698) (yoavcloud) +- Add support for GRANT on some common Snowflake objects [#1699](https://github.com/apache/datafusion-sqlparser-rs/pull/1699) (yoavcloud) +- Add RETURNS TABLE() support for CREATE FUNCTION in Postgresql [#1687](https://github.com/apache/datafusion-sqlparser-rs/pull/1687) (remysaissy) +- Add parsing for GRANT ROLE and GRANT DATABASE ROLE in Snowflake dialect [#1689](https://github.com/apache/datafusion-sqlparser-rs/pull/1689) (yoavcloud) +- Add support for `CREATE/ALTER/DROP CONNECTOR` syntax [#1701](https://github.com/apache/datafusion-sqlparser-rs/pull/1701) (wugeer) +- Parse Snowflake COPY INTO [#1669](https://github.com/apache/datafusion-sqlparser-rs/pull/1669) (yoavcloud) +- Require space after -- to start single line comment in MySQL [#1705](https://github.com/apache/datafusion-sqlparser-rs/pull/1705) (hansott) +- Add suppport for Show Objects statement for the Snowflake parser [#1702](https://github.com/apache/datafusion-sqlparser-rs/pull/1702) (DanCodedThis) +- Fix incorrect parsing of JsonAccess bracket notation after cast in Snowflake [#1708](https://github.com/apache/datafusion-sqlparser-rs/pull/1708) (yoavcloud) +- Parse Postgres VARBIT datatype [#1703](https://github.com/apache/datafusion-sqlparser-rs/pull/1703) (mvzink) +- Implement FROM-first selects [#1713](https://github.com/apache/datafusion-sqlparser-rs/pull/1713) (mitsuhiko) +- Enable custom dialects to support `MATCH() AGAINST()` [#1719](https://github.com/apache/datafusion-sqlparser-rs/pull/1719) (joocer) +- Support group by cube/rollup etc in BigQuery [#1720](https://github.com/apache/datafusion-sqlparser-rs/pull/1720) (Groennbeck) +- Add support for MS Varbinary(MAX) (#1714) [#1715](https://github.com/apache/datafusion-sqlparser-rs/pull/1715) (TylerBrinks) +- Add supports for Hive's `SELECT ... GROUP BY .. GROUPING SETS` syntax [#1653](https://github.com/apache/datafusion-sqlparser-rs/pull/1653) (wugeer) +- Differentiate LEFT JOIN from LEFT OUTER JOIN [#1726](https://github.com/apache/datafusion-sqlparser-rs/pull/1726) (mvzink) +- Add support for Postgres `ALTER TYPE` [#1727](https://github.com/apache/datafusion-sqlparser-rs/pull/1727) (jvatic) +- Replace `Method` and `CompositeAccess` with `CompoundFieldAccess` [#1716](https://github.com/apache/datafusion-sqlparser-rs/pull/1716) (iffyio) +- Add support for `EXECUTE IMMEDIATE` [#1717](https://github.com/apache/datafusion-sqlparser-rs/pull/1717) (iffyio) +- Treat COLLATE like any other column option [#1731](https://github.com/apache/datafusion-sqlparser-rs/pull/1731) (mvzink) +- Add support for PostgreSQL/Redshift geometric operators [#1723](https://github.com/apache/datafusion-sqlparser-rs/pull/1723) (benrsatori) +- Implement SnowFlake ALTER SESSION [#1712](https://github.com/apache/datafusion-sqlparser-rs/pull/1712) (osipovartem) +- Extend Visitor trait for Value type [#1725](https://github.com/apache/datafusion-sqlparser-rs/pull/1725) (tomershaniii) +- Add support for `ORDER BY ALL` [#1724](https://github.com/apache/datafusion-sqlparser-rs/pull/1724) (PokIsemaine) +- Parse casting to array using double colon operator in Redshift [#1737](https://github.com/apache/datafusion-sqlparser-rs/pull/1737) (yoavcloud) +- Replace parallel condition/result vectors with single CaseWhen vector in Expr::Case. This fixes the iteration order when using the `Visitor` trait. Expressions are now visited in the same order as they appear in the sql source. [#1733](https://github.com/apache/datafusion-sqlparser-rs/pull/1733) (lovasoa) +- BigQuery: Add support for `BEGIN` [#1718](https://github.com/apache/datafusion-sqlparser-rs/pull/1718) (iffyio) +- Parse SIGNED INTEGER type in MySQL CAST [#1739](https://github.com/apache/datafusion-sqlparser-rs/pull/1739) (mvzink) +- Parse MySQL ALTER TABLE ALGORITHM option [#1745](https://github.com/apache/datafusion-sqlparser-rs/pull/1745) (mvzink) +- Random test cleanups use Expr::value [#1749](https://github.com/apache/datafusion-sqlparser-rs/pull/1749) (alamb) +- Parse ALTER TABLE AUTO_INCREMENT operation for MySQL [#1748](https://github.com/apache/datafusion-sqlparser-rs/pull/1748) (mvzink) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 10 Yoav Cohen + 9 Ifeanyi Ubah + 7 Michael Victor Zink + 3 Hans Ott + 2 Andrew Lamb + 2 Ophir LOJKINE + 2 Paul Grau + 2 Rémy SAISSY + 2 wugeer + 1 Armin Ronacher + 1 Artem Osipov + 1 AvivDavid-Satori + 1 Ayman Elkfrawy + 1 DanCodedThis + 1 Denys Tsomenko + 1 Emil + 1 Ian Alexander Joiner + 1 Jesse Stuart + 1 Justin Joyce + 1 Michael + 1 SiLe Zhou + 1 Tyler Brinks + 1 benrsatori + 1 gstvg + 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 a629ddf89b0fbfb189d07b3130bc34aad9b9faf3 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 28 Feb 2025 22:07:39 -0800 Subject: [PATCH 748/806] 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 1c32bc513..1cea6bc2b 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 8a0da87e4..cb86f2b47 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 bc0f0efeb..d33a7d8af 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 0a68d31e8..3c43ed61f 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 15f79b4c2..f0774fcf5 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 749/806] 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 72be3ff6c..554ec19b7 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 041d44bb2..c13d5aa69 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 1cea6bc2b..aeb097cfd 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 cb86f2b47..0bdfc9bf3 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 57ed0b684..9b08b8f32 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 f234fcc07..b11e57791 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 3c43ed61f..2c35c2438 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 f0774fcf5..8d89ce4eb 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 750/806] 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 980f5ec3f..aeed1eb79 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 ec565e50e..3f313af4f 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 751/806] 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 | 92 ++++++++++++--- tests/sqlparser_common.rs | 83 +++++++------ tests/sqlparser_postgres.rs | 230 ++++++++++++++++++++++++++++++++++++ 8 files changed, 405 insertions(+), 61 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index bb85eb06c..619631432 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 8cfc67414..ccea7fbcb 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 554ec19b7..e5e4aef05 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 38e9e258e..0a64fb8ea 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 a6854f073..bda817df9 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 b11e57791..b34415388 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) - } else { - self.expected("index type {BTREE | HASH}", self.peek_token()) - } + 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 { + 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 2c35c2438..a8ccd70a7 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 7508218f9..0dfcc24ea 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 752/806] 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 619631432..99d8521cd 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 0a64fb8ea..62ca9dc0a 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 b34415388..2b95d6740 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 8d89ce4eb..560ea9dae 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 753/806] 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 bed991114..12f72932f 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 62ca9dc0a..8c3eff3c3 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 2b95d6740..400a9480f 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 a8ccd70a7..8225d3672 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 754/806] 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 | 294 +++++++++++++++++++---------- 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, 562 insertions(+), 373 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e5e4aef05..8ab3fc0f1 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, - }, - /// ```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, + Discard { + object_type: DiscardObject, }, - /// ```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 8c3eff3c3..fb0fc3f33 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 aeb097cfd..8d4557e2f 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 aeed1eb79..3db34748e 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 0bdfc9bf3..2077ea195 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 bda817df9..195bbb172 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 400a9480f..32a7bccd2 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)?; + } + + 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); } + } - 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() + 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()); + } + + 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 names && self.dialect.supports_set_names() { + 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(Statement::SetNamesDefault {}); + 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() + } + .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 { - self.expected("equals sign or TO", self.peek_token()) + 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 8225d3672..c7bf287cd 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 d7f3c014b..56fe22a0d 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 3f313af4f..386bd1788 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 560ea9dae..13a8a6cc1 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 0dfcc24ea..a65c4fa38 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 755/806] 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 8ab3fc0f1..139850e86 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 12f72932f..1b30dcf17 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 fb0fc3f33..a4f5eb46c 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 a5d355fe4..50985a3e7 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 32a7bccd2..d3c48a6e7 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 72a64a488..c56f98860 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 c7bf287cd..b5d42ea6c 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 386bd1788..af71d2523 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 13a8a6cc1..a56335934 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 a65c4fa38..1a98870f1 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 756/806] 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 139850e86..4c0ffea9e 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 d3c48a6e7..60e1c1463 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 b5d42ea6c..d0edafb7c 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 757/806] 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 4c0ffea9e..66fd4c6fb 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 a4f5eb46c..0ee11f23f 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 195bbb172..47da10096 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 60e1c1463..3adfe55e4 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 d0edafb7c..8c9cae831 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 758/806] 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 66fd4c6fb..8c4079213 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 3adfe55e4..864fd579f 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 8c9cae831..c65cc6b53 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 1a98870f1..e62f23597 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 759/806] 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 72252b277..09a0e57ce 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 864fd579f..e4c170edd 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 b1d31e6dd..f37b657e5 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 760/806] 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 99d8521cd..39e43ef15 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 8c4079213..69f2e57f1 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 0ee11f23f..783248a78 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 47da10096..974230f19 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 e4c170edd..0d8edc05c 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 a56335934..b6287d92d 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 761/806] 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 69f2e57f1..6dc4f5b20 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 783248a78..65d43c108 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 974230f19..7b9c8bf29 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 0d8edc05c..ee50cd04c 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 c65cc6b53..c8df7cab3 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 762/806] 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 6dc4f5b20..9f895ee64 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 ee50cd04c..dcf7a4a8f 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 c8df7cab3..4ba8df7fb 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 56fe22a0d..a9549cb60 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 af71d2523..d4e5fa719 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 b6287d92d..884351491 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 e62f23597..cf66af74e 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 763/806] 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 9f895ee64..b1bce3e97 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 65d43c108..11770d1bc 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 7b9c8bf29..349c9ffb0 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 dcf7a4a8f..a43be5c68 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 4ba8df7fb..cf95e2ae9 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 d4e5fa719..4ea42efc7 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 884351491..2767a78c8 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 764/806] SET statements: scope modifier for multiple assignments (#1772) --- src/ast/mod.rs | 28 ++++++--- src/dialect/generic.rs | 4 ++ src/parser/mod.rs | 121 +++++++++++++++++------------------- 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, 134 insertions(+), 95 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b1bce3e97..3264cf038 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 c13d5aa69..92cfca8fd 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 a43be5c68..7d8417f36 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, - ]); - - if let Some(Keyword::HIVEVAR) = modifier { + let hivevar = self.parse_keyword(Keyword::HIVEVAR); + + // 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 cf95e2ae9..a137291ac 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 a9549cb60..2af93db7d 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 4ea42efc7..ca59b164e 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 2767a78c8..d52619d54 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 cf66af74e..a6d65ec75 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 765/806] 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 3264cf038..f187df995 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 7d8417f36..65d536f7e 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 d52619d54..3d318f701 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 766/806] 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 66a35fee5..73fe9cf42 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 8d4557e2f..79184dd70 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 65d536f7e..adaae2862 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 3d318f701..1d4fd6a0d 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 767/806] 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 d18a76b50..5e8e460f6 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 768/806] 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 09a0e57ce..2b04f9e9a 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 349c9ffb0..a0c556d2d 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 a137291ac..795dae4b3 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 769/806] 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 57bc67441..dc523696a 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 a0c556d2d..8609ec43d 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 adaae2862..2b61529ff 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 3b36d7a13..88aae499a 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 770/806] 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 3db34748e..18a963a4b 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 ca59b164e..2bfc38a6a 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 771/806] 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 2b04f9e9a..d1a696a0b 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 f37b657e5..097af3466 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 772/806] 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 5e8e460f6..6acfbcef7 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 773/806] 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 f187df995..9456991ed 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 8609ec43d..1aa2190c0 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 2b61529ff..40d6b0acd 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 097af3466..62e52e2d1 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 774/806] 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 39e43ef15..6a649b73b 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 9456991ed..07a22798e 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 d1a696a0b..f303f8218 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 1aa2190c0..bf1206f6a 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 795dae4b3..9fe6eae7a 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 775/806] 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 25b8f1644..d90eb6e7d 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 7736735cb..c75abe16f 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 776/806] 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 07a22798e..8537bd858 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 bf1206f6a..a1b4c0c3a 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 40d6b0acd..50a9aae84 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 3037d4ae5..5eb30d15c 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 9fe6eae7a..365332170 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 2bfc38a6a..5d76fd019 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 c75abe16f..060e3853d 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 777/806] 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 dc523696a..52919de8a 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] - /// - /// [clickhouse]: https://clickhouse.com/docs/en/sql-reference/data-types/int-uint + /// 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 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 type, see [PostgreSQL]. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype-bit.html Bytea, - /// Bit string, e.g. [Postgres], [MySQL], or [MSSQL] + /// Bit string, see [PostgreSQL], [MySQL], or [MSSQL]. /// - /// [Postgres]: https://www.postgresql.org/docs/current/datatype-bit.html + /// [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 6a649b73b..000ab3a4f 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 8537bd858..a6dc682d5 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 79184dd70..e41964f45 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 a1b4c0c3a..73c244261 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 50a9aae84..549549973 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 bed02428d..a421154ad 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 778/806] Add support for MSSQL IF/ELSE statements. (#1791) Co-authored-by: Roman Borschel --- src/ast/mod.rs | 166 +++++++++++++++++++++++++------------- 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, 525 insertions(+), 129 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a6dc682d5..3ee19043e 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,82 +2201,128 @@ 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 fmt::Display for 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 ConditionalStatements { - condition: expr, - statements, - kind, + let ConditionalStatementBlock { + start_token: AttachedToken(start_token), + condition, + then_token, + conditional_statements, } = self; - let kind = match kind { - ConditionalStatementKind::When => "WHEN", - ConditionalStatementKind::If => "IF", - ConditionalStatementKind::ElseIf => "ELSEIF", - }; + write!(f, "{start_token}")?; + + if let Some(condition) = condition { + write!(f, " {condition}")?; + } - write!(f, "{kind} {expr} THEN")?; + if then_token.is_some() { + write!(f, " THEN")?; + } - if !statements.is_empty() { - write!(f, " ")?; - format_statement_list(f, statements)?; + 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 { + 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") + } + } + } +} + /// A `RAISE` statement. /// /// Examples: diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 11770d1bc..d253f8914 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 18a963a4b..d86d68a20 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 549549973..b9076bb77 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 365332170..14716dde9 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 5d76fd019..bcaf527c0 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 779/806] 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 d90eb6e7d..feccca5dd 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 060e3853d..be2b67223 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 780/806] 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 1b30dcf17..abc115a0d 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 d253f8914..9ff83b760 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 73c244261..0b947b61c 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 b9076bb77..0ccf10d7a 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 1d4fd6a0d..c60936ca8 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 781/806] 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 | 164 ++++++++++++++------------- tests/sqlparser_snowflake.rs | 36 ++++-- 5 files changed, 146 insertions(+), 90 deletions(-) diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index cc4fa12fa..e960bb05b 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 3ee19043e..4031936e9 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 9ff83b760..31a036b1a 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 f303f8218..8b279c7cc 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()), - }?)); - } - _ => { - // element not present move back - parser.prev_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()), + }?)); } - - // 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()), - }?); + _ => { + // element not present move back + parser.prev_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; - } - } + // 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(Some(select_items)) + + 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 62e52e2d1..4b6694143 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 782/806] Fix tokenization of qualified identifiers with numeric prefix. (#1803) Co-authored-by: Roman Borschel --- src/tokenizer.rs | 76 ++++++++++++++++++++---- tests/sqlparser_mysql.rs | 122 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 12 deletions(-) diff --git a/src/tokenizer.rs b/src/tokenizer.rs index d33a7d8af..13bce0c0d 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 !word.is_empty() { - s += word.as_str(); + // 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(); + 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 c60936ca8..3a3d8f002 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 783/806] 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 ccea7fbcb..9cdb1ca86 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 344e9dec9..1c50cb843 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 31a036b1a..230341518 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 0b947b61c..fb2734096 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 0ccf10d7a..c4bec1d5c 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 a421154ad..320583248 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 bcaf527c0..44fd01f17 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 a6d65ec75..098d4b1c4 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 784/806] 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 c4bec1d5c..adf8d86d6 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 44fd01f17..644589b58 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 785/806] 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 adf8d86d6..bb989fef1 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 4b6694143..84c08874e 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 786/806] 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 4031936e9..72fb99977 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 bb989fef1..d43e5d02f 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 14716dde9..be848a603 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 787/806] 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 2077ea195..f69e42436 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 3a3d8f002..9d8d12b57 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 788/806] 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 72fb99977..ab3be35c1 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 230341518..a241fdf4d 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 fb2734096..a5400a5b0 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 d43e5d02f..a9ddd1837 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 644589b58..2786384b3 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 789/806] 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 92cfca8fd..8f57e487f 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 be848a603..1c03a0fa4 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 790/806] 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 ab3be35c1..74e8cb55c 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 abc115a0d..982985ec3 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 a241fdf4d..27d52c26f 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 a5400a5b0..4eaad7ed2 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 a9ddd1837..77466b97e 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 1c03a0fa4..466b65ec6 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 791/806] 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 000ab3a4f..c1c113b32 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 74e8cb55c..c4bb3fa17 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 27d52c26f..16ff660dc 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 d86d68a20..31e324f06 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 77466b97e..9b519fe84 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 5eb30d15c..416d2e435 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 466b65ec6..66800b811 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 2af93db7d..9b0430947 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 2786384b3..b86e1a7d4 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 098d4b1c4..27fc7fa17 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 792/806] 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 c4bb3fa17..b60ade78b 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 16ff660dc..93de5fff2 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 9b519fe84..fe81b5999 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 66800b811..fa2346c2c 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 793/806] 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 b60ade78b..45924579b 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 fe81b5999..0546548af 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 b86e1a7d4..ef6103474 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 794/806] 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 52fd2e548..6f2b7c41c 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 795/806] 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 45924579b..d65889810 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 93de5fff2..28d479f30 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 e41964f45..b2dff065e 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 8b279c7cc..c4d6a5ad5 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 4eaad7ed2..15a6f91ad 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 0546548af..03ea91faf 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 9d8d12b57..f74248b86 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 84c08874e..aa974115d 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 796/806] 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 362a637d9..a5511a053 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 99bfdc241..d746775e4 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 000000000..9e6289e88 --- /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 797/806] 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 9e6289e88..b3c8a67aa 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 798/806] 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 d65889810..b496403c4 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 982985ec3..a90b61668 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 28d479f30..661cd03da 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 49fb24f19..68ca1390a 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 b2dff065e..b754a04f1 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 15a6f91ad..d2ccbb2c6 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 03ea91faf..2e37f1bc9 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 13bce0c0d..4fad54624 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 fa2346c2c..6d99929d5 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 ef6103474..b2d5210c2 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 f74248b86..990107b25 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 27fc7fa17..4ad8e00cc 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 799/806] 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 b496403c4..b14392665 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 661cd03da..33bc07392 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 d2ccbb2c6..32612ccd5 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 2e37f1bc9..0d74235b2 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 4ad8e00cc..6c008c84d 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 800/806] 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 b14392665..582922a3e 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 33bc07392..836f229a2 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 32612ccd5..bf8a1915d 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 0d74235b2..cbd464c34 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 6270ac42b..3c22fa911 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 6d99929d5..1ddf3f92e 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 b2d5210c2..88e7a1f17 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 801/806] 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 9cdb1ca86..7ed17be9b 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(_) => (), - } - } - if let Some(auto_increment_offset) = self.auto_increment_offset { - write!(f, " AUTO_INCREMENT {auto_increment_offset}")?; + match &self.table_options { + options @ CreateTableOptions::With(_) + | options @ CreateTableOptions::Plain(_) + | options @ CreateTableOptions::TableProperties(_) => write!(f, " {}", options)?, + _ => (), } + 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 1c50cb843..542d30ea9 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 e960bb05b..92a727279 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 582922a3e..d74d197e3 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 836f229a2..3f703ffaf 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 c4d6a5ad5..ccce16198 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 bf8a1915d..ddb786650 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 cbd464c34..a347f3d4d 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 416d2e435..8f54f3c97 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 c56f98860..d0218b6c3 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 1ddf3f92e..7a8b8bdaa 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 320583248..8e4983655 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 9b0430947..14dcbffd1 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 88e7a1f17..8cc5758fd 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 990107b25..4bb1063dd 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"; + + 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!(), + } +} - for sql in [canonical, with_equal] { - match mysql().one_statement_parses_to(sql, canonical) { +#[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 6c008c84d..1fb7432a4 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 aa974115d..52be31435 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 802/806] 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 | 43 ++++++++++----- tests/sqlparser_mssql.rs | 105 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_mysql.rs | 6 ++- tests/sqlparser_postgres.rs | 38 ++++++++----- 7 files changed, 251 insertions(+), 37 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d74d197e3..c3009743d 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 cf1c8c466..2c64e4239 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 31e324f06..647e82a2a 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 a347f3d4d..2011d31e3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -745,26 +745,38 @@ impl<'a> Parser<'a> { } }; + let conditional_statements = self.parse_conditional_statements(terminal_keywords)?; + + Ok(ConditionalStatementBlock { + start_token: AttachedToken(start_token), + condition, + then_token, + conditional_statements, + }) + } + + /// 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 { - let statements = self.parse_statement_list(terminal_keywords)?; - ConditionalStatements::Sequence { statements } + ConditionalStatements::Sequence { + statements: self.parse_statement_list(terminal_keywords)?, + } }; - - Ok(ConditionalStatementBlock { - start_token: AttachedToken(start_token), - condition, - then_token, - conditional_statements, - }) + Ok(conditional_statements) } /// Parse a `RAISE` statement. @@ -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 8cc5758fd..9ff55198f 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 4bb1063dd..27c60b052 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 1fb7432a4..008d0670a 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 803/806] 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 c1c113b32..a457a0655 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 c3009743d..a72cb8add 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 3f703ffaf..ff2a61cf3 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 2011d31e3..fc6f44376 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 008d0670a..859eca453 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 804/806] 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 a72cb8add..6b7ba12d9 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 fc6f44376..d18c7f694 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 9ff55198f..1c0a00b16 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 805/806] 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 73fe9cf42..d0bb05e3c 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 b754a04f1..6fbbc7a23 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 138c4692c..847e0d135 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 361c9b051..b759065f3 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 806/806] 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 4fad54624..afe1e35c7 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:?}"); + } + } }