From d7149616007775717a96beb464ee78834c362b1d Mon Sep 17 00:00:00 2001 From: kysshsy Date: Fri, 13 Sep 2024 23:59:54 +0800 Subject: [PATCH 01/10] feat: support explain options --- src/ast/mod.rs | 21 ++++++++++++++++ src/parser/mod.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4e3cbcdba..641e5d3bb 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 + options: Option)>>, }, /// ```sql /// SAVEPOINT @@ -3219,6 +3221,7 @@ impl fmt::Display for Statement { analyze, statement, format, + options, } => { write!(f, "{describe_alias} ")?; @@ -3234,6 +3237,24 @@ impl fmt::Display for Statement { write!(f, "FORMAT {format} ")?; } + if let Some(options) = options { + write!(f, "( ")?; + + let mut iter = options.iter().peekable(); + while let Some((name, arg)) = iter.next() { + if let Some(ref value) = arg { + write!(f, "{} {}", name, value)?; + } else { + write!(f, "{}", name)?; + } + if iter.peek().is_some() { + write!(f, ", ")?; + } + } + + write!(f, " ) ")?; + } + write!(f, "{statement}") } Statement::Query(s) => write!(f, "{s}"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4e5ca2b5a..ed67b8edc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1277,6 +1277,50 @@ impl<'a> Parser<'a> { } } + pub fn parse_utility_options(&mut self) -> Result)>, ParserError> { + self.expect_token(&Token::LParen)?; + let mut options = vec![]; + loop { + options.push(self.parse_utility_option()?); + + if self.peek_token() != Token::Comma { + break; + } + self.next_token(); + } + + self.expect_token(&Token::RParen)?; + + Ok(options) + } + + fn parse_utility_option(&mut self) -> Result<(Ident, Option), ParserError> { + let name = self.parse_identifier(false)?; + + let next_token = self.peek_token(); + if next_token == Token::Comma || next_token == Token::RParen { + return Ok((name, None)); + } + let arg = match next_token.token { + Token::Number(s, _) => { + self.next_token(); + s + } + // OFF is also accepted in parse_literal_string + Token::Word(s) + if s.keyword == Keyword::TRUE + || s.keyword == Keyword::FALSE + || s.keyword == Keyword::ON => + { + self.next_token(); + s.value + } + _ => self.parse_literal_string()?, + }; + + Ok((name, Some(arg))) + } + fn try_parse_expr_sub_query(&mut self) -> Result, ParserError> { if self .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) @@ -8464,11 +8508,22 @@ 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; + + if dialect_of!(self is PostgreSqlDialect | DuckDbDialect ) + && 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); + format = None; + if self.parse_keyword(Keyword::FORMAT) { + format = Some(self.parse_analyze_format()?); + } } match self.maybe_parse(|parser| parser.parse_statement()) { @@ -8481,6 +8536,7 @@ impl<'a> Parser<'a> { verbose, statement: Box::new(statement), format, + options, }), _ => { let hive_format = From 67e903ba76c7819419c36fb3e894198cdb3a02c7 Mon Sep 17 00:00:00 2001 From: kysshsy Date: Sun, 15 Sep 2024 00:09:14 +0800 Subject: [PATCH 02/10] feat: use nameed struct which is more representative and add Doc --- src/ast/mod.rs | 77 ++++++++++++++++++++++++++++++++++++----------- src/parser/mod.rs | 31 ++++++++++++++----- 2 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 641e5d3bb..c4e0fe5cc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3032,8 +3032,8 @@ pub enum Statement { statement: Box, /// Optional output format of explain format: Option, - /// Postgres style utility options - options: Option)>>, + /// Postgres style utility options, `(analyze, verbose true)` + options: Option, }, /// ```sql /// SAVEPOINT @@ -3238,21 +3238,7 @@ impl fmt::Display for Statement { } if let Some(options) = options { - write!(f, "( ")?; - - let mut iter = options.iter().peekable(); - while let Some((name, arg)) = iter.next() { - if let Some(ref value) = arg { - write!(f, "{} {}", name, value)?; - } else { - write!(f, "{}", name)?; - } - if iter.peek().is_some() { - write!(f, ", ")?; - } - } - - write!(f, " ) ")?; + write!(f, "{options}")?; } write!(f, "{statement}") @@ -7146,6 +7132,63 @@ where } } +/// Represents a list of options used in various PostgreSQL utility statements such as +/// `EXPLAIN`, `VACUUM`, and `CLUSTER`. +/// +/// For example, the `EXPLAIN` statement with options might look like this: +/// +/// ```sql +/// EXPLAIN (ANALYZE, VERBOSE true) SELECT * FROM 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 UtilityOptionList { + pub options: Vec, +} + +impl Display for UtilityOptionList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "( ")?; + + let mut iter = self.options.iter().peekable(); + while let Some(option) = iter.next() { + write!(f, "{}", option)?; + if iter.peek().is_some() { + write!(f, ", ")?; + } + } + + write!(f, " ) ") + } +} + +/// 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 `-`) +/// - A non-keyword string +/// - keyword: `true`, `false`, `on` (`off` is also accept) +/// - Empty +#[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/parser/mod.rs b/src/parser/mod.rs index ed67b8edc..bc280e77e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1277,7 +1277,7 @@ impl<'a> Parser<'a> { } } - pub fn parse_utility_options(&mut self) -> Result)>, ParserError> { + pub fn parse_utility_options(&mut self) -> Result { self.expect_token(&Token::LParen)?; let mut options = vec![]; loop { @@ -1291,20 +1291,34 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; - Ok(options) + Ok(UtilityOptionList { options }) } - fn parse_utility_option(&mut self) -> Result<(Ident, Option), ParserError> { + 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((name, None)); + return Ok(UtilityOption { name, arg: None }); } let arg = match next_token.token { - Token::Number(s, _) => { + sign @ Token::Plus | sign @ Token::Minus => { self.next_token(); - s + let cur_token = self.next_token(); + match cur_token.token { + Token::Number(num, _) => { + if sign == Token::Minus { + format!("-{num}") + } else { + num + } + } + _ => return self.expected("literal number", cur_token), + } + } + Token::Number(num, _) => { + self.next_token(); + num } // OFF is also accepted in parse_literal_string Token::Word(s) @@ -1318,7 +1332,10 @@ impl<'a> Parser<'a> { _ => self.parse_literal_string()?, }; - Ok((name, Some(arg))) + Ok(UtilityOption { + name, + arg: Some(arg), + }) } fn try_parse_expr_sub_query(&mut self) -> Result, ParserError> { From cde72eab452c370fc661eb5bd337a425499e47a1 Mon Sep 17 00:00:00 2001 From: kysshsy Date: Sun, 15 Sep 2024 10:30:43 +0800 Subject: [PATCH 03/10] fix: handle keyword JSON and XML but not non-reserved Postgres, and add Explain describe_alias check --- src/parser/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bc280e77e..6791eaf72 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1324,7 +1324,10 @@ impl<'a> Parser<'a> { Token::Word(s) if s.keyword == Keyword::TRUE || s.keyword == Keyword::FALSE - || s.keyword == Keyword::ON => + || s.keyword == Keyword::ON + || s.keyword == Keyword::TEXT + || s.keyword == Keyword::JSON + || s.keyword == Keyword::XML => { self.next_token(); s.value @@ -8530,7 +8533,8 @@ impl<'a> Parser<'a> { let mut format = None; let mut options = None; - if dialect_of!(self is PostgreSqlDialect | DuckDbDialect ) + if describe_alias == DescribeAlias::Explain + && dialect_of!(self is PostgreSqlDialect | DuckDbDialect ) && self.peek_token().token == Token::LParen { options = Some(self.parse_utility_options()?) From 1c34d7b750d13508cad979cd7e0ddae564bb5944 Mon Sep 17 00:00:00 2001 From: kysshsy Date: Sun, 15 Sep 2024 10:32:09 +0800 Subject: [PATCH 04/10] test: add explain with option list test --- src/test_utils.rs | 6 ++ tests/sqlparser_common.rs | 19 +++- tests/sqlparser_postgres.rs | 169 +++++++++++++++++++++++++++++++++++- 3 files changed, 191 insertions(+), 3 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 5c05ec996..be7182ada 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -148,6 +148,12 @@ impl TestedDialects { self.one_statement_parses_to(sql, sql) } + /// Ensures that `sql` parses as a single [Statement] and that the re-serialization of the parsed + /// result matches the given `canonical` SQL string. + pub fn verified_stmt_with_canonical(&self, sql: &str, canonical: &str) -> Statement { + self.one_statement_parses_to(sql, canonical) + } + /// 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). diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c4a52938d..ffa6be88e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4272,6 +4272,7 @@ fn run_explain_analyze( expected_verbose: bool, expected_analyze: bool, expected_format: Option, + exepcted_options: Option, ) { match verified_stmt(query) { Statement::Explain { @@ -4280,10 +4281,12 @@ fn run_explain_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,26 +4331,35 @@ 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( + "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("EXPLAIN SELECT sqrt(id) FROM foo", false, false, None, None); run_explain_analyze( "EXPLAIN VERBOSE SELECT sqrt(id) FROM foo", true, false, None, + None, ); run_explain_analyze( "EXPLAIN ANALYZE SELECT sqrt(id) FROM foo", false, true, None, + None, ); run_explain_analyze( "EXPLAIN ANALYZE VERBOSE SELECT sqrt(id) FROM foo", true, true, None, + None, ); run_explain_analyze( @@ -4355,6 +4367,7 @@ fn parse_explain_analyze_with_simple_select() { false, true, Some(AnalyzeFormat::GRAPHVIZ), + None, ); run_explain_analyze( @@ -4362,6 +4375,7 @@ fn parse_explain_analyze_with_simple_select() { true, true, Some(AnalyzeFormat::JSON), + None, ); run_explain_analyze( @@ -4369,6 +4383,7 @@ fn parse_explain_analyze_with_simple_select() { true, false, Some(AnalyzeFormat::TEXT), + None, ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 069b61d05..b78af47c1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -19,7 +19,7 @@ mod test_utils; use test_utils::*; use sqlparser::ast::*; -use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; +use sqlparser::dialect::{Dialect, GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; #[test] @@ -5108,3 +5108,170 @@ fn arrow_cast_precedence() { } ) } + +#[test] +fn parse_explain_with_option_list() { + run_explain_analyze_with_postgres_dialect( + "EXPLAIN ( ANALYZE FALSE, VERBOSE TRUE ) SELECT sqrt(id) FROM foo", + None, + false, + false, + None, + Some(UtilityOptionList { + options: vec![ + UtilityOption { + name: Ident::new("ANALYZE"), + arg: Some("FALSE".to_string()), + }, + UtilityOption { + name: Ident::new("VERBOSE"), + arg: Some("TRUE".to_string()), + }, + ], + }), + ); + + run_explain_analyze_with_postgres_dialect( + "EXPLAIN ( ANALYZE ON, VERBOSE OFF ) SELECT sqrt(id) FROM foo", + None, + false, + false, + None, + Some(UtilityOptionList { + options: vec![ + UtilityOption { + name: Ident::new("ANALYZE"), + arg: Some("ON".to_string()), + }, + UtilityOption { + name: Ident::new("VERBOSE"), + arg: Some("OFF".to_string()), + }, + ], + }), + ); + + run_explain_analyze_with_postgres_dialect( + r#"EXPLAIN ( FORMAT1 TEXT, FORMAT2 'JSON', FORMAT3 "XML", FORMAT4 YAML ) SELECT sqrt(id) FROM foo"#, + Some( + r#"EXPLAIN ( FORMAT1 TEXT, FORMAT2 JSON, FORMAT3 XML, FORMAT4 YAML ) SELECT sqrt(id) FROM foo"#, + ), + false, + false, + None, + Some(UtilityOptionList { + options: vec![ + UtilityOption { + name: Ident::new("FORMAT1"), + arg: Some("TEXT".to_string()), + }, + UtilityOption { + name: Ident::new("FORMAT2"), + arg: Some("JSON".to_string()), + }, + UtilityOption { + name: Ident::new("FORMAT3"), + arg: Some("XML".to_string()), + }, + UtilityOption { + name: Ident::new("FORMAT4"), + arg: Some("YAML".to_string()), + }, + ], + }), + ); + + run_explain_analyze_with_postgres_dialect( + r#"EXPLAIN ( NUM1 10, NUM2 +10.1, NUM3 -10.2 ) SELECT sqrt(id) FROM foo"#, + Some(r#"EXPLAIN ( NUM1 10, NUM2 10.1, NUM3 -10.2 ) SELECT sqrt(id) FROM foo"#), + false, + false, + None, + Some(UtilityOptionList { + options: vec![ + UtilityOption { + name: Ident::new("NUM1"), + arg: Some("10".to_string()), + }, + UtilityOption { + name: Ident::new("NUM2"), + arg: Some("10.1".to_string()), + }, + UtilityOption { + name: Ident::new("NUM3"), + arg: Some("-10.2".to_string()), + }, + ], + }), + ); + + let utility_option_list = UtilityOptionList { + options: vec![ + UtilityOption { + name: Ident::new("ANALYZE"), + arg: None, + }, + UtilityOption { + name: Ident::new("VERBOSE"), + arg: Some("TRUE".to_string()), + }, + UtilityOption { + name: Ident::new("WAL"), + arg: Some("OFF".to_string()), + }, + UtilityOption { + name: Ident::new("FORMAT"), + arg: Some("YAML".to_string()), + }, + UtilityOption { + name: Ident::new("USER_DEF_NUM"), + arg: Some("-100.1".to_string()), + }, + ], + }; + run_explain_analyze_with_postgres_dialect( + "EXPLAIN ( ANALYZE, VERBOSE TRUE, WAL OFF, FORMAT YAML, USER_DEF_NUM -100.1 ) SELECT sqrt(id) FROM foo", + None, + false, + false, + None, + Some(utility_option_list), + ); +} + +fn run_explain_analyze_with_postgres_dialect( + query: &str, + expcted_canonical: Option<&str>, + expected_verbose: bool, + expected_analyze: bool, + expected_format: Option, + expcted_options: Option, +) { + let test_dialect = TestedDialects { + dialects: vec![Box::new(PostgreSqlDialect {}) as Box], + options: None, + }; + let stmt = if let Some(canonical) = expcted_canonical { + test_dialect.verified_stmt_with_canonical(query, canonical) + } else { + test_dialect.verified_stmt(query) + }; + + match stmt { + 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, expcted_options); + assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); + } + _ => panic!("Unexpected Statement, must be Explain"), + } +} From 900bc283601bd9ef27fa542c342b9d9f92490eeb Mon Sep 17 00:00:00 2001 From: kysshsy Date: Mon, 16 Sep 2024 17:03:35 +0800 Subject: [PATCH 05/10] feat: parse arg as expr --- src/ast/mod.rs | 27 ++++-- src/parser/mod.rs | 53 ++--------- tests/sqlparser_common.rs | 157 ++++++++++++++++++++++++++++++++- tests/sqlparser_postgres.rs | 169 +----------------------------------- 4 files changed, 183 insertions(+), 223 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c4e0fe5cc..bf3223e36 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7133,12 +7133,18 @@ where } /// Represents a list of options used in various PostgreSQL utility statements such as -/// `EXPLAIN`, `VACUUM`, and `CLUSTER`. +/// CLUSTER, EXPLAIN, `VACUUM`, and `REINDEX`. It takes the form `( option [, ...] )`. /// -/// For example, the `EXPLAIN` statement with options might look like this: +/// [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) SELECT * FROM my_table; +/// 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))] @@ -7167,16 +7173,21 @@ impl Display for UtilityOptionList { /// /// 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 `-`) -/// - A non-keyword string -/// - keyword: `true`, `false`, `on` (`off` is also accept) -/// - Empty +/// - 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) +/// ```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, + pub arg: Option, } impl Display for UtilityOption { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6791eaf72..108c98668 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1277,17 +1277,9 @@ impl<'a> Parser<'a> { } } - pub fn parse_utility_options(&mut self) -> Result { + pub fn parse_utility_option_list(&mut self) -> Result { self.expect_token(&Token::LParen)?; - let mut options = vec![]; - loop { - options.push(self.parse_utility_option()?); - - if self.peek_token() != Token::Comma { - break; - } - self.next_token(); - } + let options = self.parse_comma_separated(Self::parse_utility_option)?; self.expect_token(&Token::RParen)?; @@ -1301,39 +1293,7 @@ impl<'a> Parser<'a> { if next_token == Token::Comma || next_token == Token::RParen { return Ok(UtilityOption { name, arg: None }); } - let arg = match next_token.token { - sign @ Token::Plus | sign @ Token::Minus => { - self.next_token(); - let cur_token = self.next_token(); - match cur_token.token { - Token::Number(num, _) => { - if sign == Token::Minus { - format!("-{num}") - } else { - num - } - } - _ => return self.expected("literal number", cur_token), - } - } - Token::Number(num, _) => { - self.next_token(); - num - } - // OFF is also accepted in parse_literal_string - Token::Word(s) - if s.keyword == Keyword::TRUE - || s.keyword == Keyword::FALSE - || s.keyword == Keyword::ON - || s.keyword == Keyword::TEXT - || s.keyword == Keyword::JSON - || s.keyword == Keyword::XML => - { - self.next_token(); - s.value - } - _ => self.parse_literal_string()?, - }; + let arg = self.parse_expr()?; Ok(UtilityOption { name, @@ -8533,15 +8493,16 @@ impl<'a> Parser<'a> { let mut format = None; 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 - && dialect_of!(self is PostgreSqlDialect | DuckDbDialect ) + && dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) && self.peek_token().token == Token::LParen { - options = Some(self.parse_utility_options()?) + options = Some(self.parse_utility_option_list()?) } else { analyze = self.parse_keyword(Keyword::ANALYZE); verbose = self.parse_keyword(Keyword::VERBOSE); - format = None; if self.parse_keyword(Keyword::FORMAT) { format = Some(self.parse_analyze_format()?); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ffa6be88e..2a8ee0b89 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4274,7 +4274,25 @@ fn run_explain_analyze( expected_format: Option, exepcted_options: Option, ) { - match verified_stmt(query) { + run_explain_analyze_with_specified_dialect( + |_d| true, + query, + expected_verbose, + expected_analyze, + expected_format, + exepcted_options, + ) +} + +fn run_explain_analyze_with_specified_dialect bool>( + dialect_predicate: T, + query: &str, + expected_verbose: bool, + expected_analyze: bool, + expected_format: Option, + exepcted_options: Option, +) { + match all_dialects_where(dialect_predicate).verified_stmt(query) { Statement::Explain { describe_alias: _, analyze, @@ -10840,3 +10858,140 @@ fn test_truncate_table_with_on_cluster() { .unwrap_err() ); } + +#[test] +fn parse_explain_with_option_list() { + run_explain_analyze_with_specified_dialect( + |d| d.is::() || d.is::() || d.is::(), + "EXPLAIN ( ANALYZE false, VERBOSE true ) SELECT sqrt(id) FROM foo", + false, + false, + None, + Some(UtilityOptionList { + options: 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_with_specified_dialect( + |d| d.is::() || d.is::() || d.is::(), + "EXPLAIN ( ANALYZE ON, VERBOSE OFF ) SELECT sqrt(id) FROM foo", + false, + false, + None, + Some(UtilityOptionList { + options: 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_with_specified_dialect( + |d| d.is::() || d.is::() || d.is::(), + r#"EXPLAIN ( FORMAT1 TEXT, FORMAT2 'JSON', FORMAT3 "XML", FORMAT4 YAML ) SELECT sqrt(id) FROM foo"#, + false, + false, + None, + Some(UtilityOptionList { + options: 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_with_specified_dialect( + |d| d.is::() || d.is::() || d.is::(), + r#"EXPLAIN ( NUM1 10, NUM2 +10.1, NUM3 -10.2 ) SELECT sqrt(id) FROM foo"#, + false, + false, + None, + Some(UtilityOptionList { + options: vec![ + UtilityOption { + name: Ident::new("NUM1"), + arg: Some(Expr::Value(Value::Number("10".to_string(), false))), + }, + UtilityOption { + name: Ident::new("NUM2"), + arg: Some(Expr::UnaryOp { + op: UnaryOperator::Plus, + expr: Box::new(Expr::Value(Value::Number("10.1".to_string(), false))), + }), + }, + UtilityOption { + name: Ident::new("NUM3"), + arg: Some(Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(Value::Number("10.2".to_string(), false))), + }), + }, + ], + }), + ); + + let utility_option_list = UtilityOptionList { + 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".to_string(), false))), + }), + }, + ], + }; + run_explain_analyze_with_specified_dialect( + |d| d.is::() || d.is::() || d.is::(), + "EXPLAIN ( ANALYZE, VERBOSE true, WAL OFF, FORMAT YAML, USER_DEF_NUM -100.1 ) SELECT sqrt(id) FROM foo", + false, + false, + None, + Some(utility_option_list), + ); +} \ No newline at end of file diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b78af47c1..069b61d05 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -19,7 +19,7 @@ mod test_utils; use test_utils::*; use sqlparser::ast::*; -use sqlparser::dialect::{Dialect, GenericDialect, PostgreSqlDialect}; +use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; #[test] @@ -5108,170 +5108,3 @@ fn arrow_cast_precedence() { } ) } - -#[test] -fn parse_explain_with_option_list() { - run_explain_analyze_with_postgres_dialect( - "EXPLAIN ( ANALYZE FALSE, VERBOSE TRUE ) SELECT sqrt(id) FROM foo", - None, - false, - false, - None, - Some(UtilityOptionList { - options: vec![ - UtilityOption { - name: Ident::new("ANALYZE"), - arg: Some("FALSE".to_string()), - }, - UtilityOption { - name: Ident::new("VERBOSE"), - arg: Some("TRUE".to_string()), - }, - ], - }), - ); - - run_explain_analyze_with_postgres_dialect( - "EXPLAIN ( ANALYZE ON, VERBOSE OFF ) SELECT sqrt(id) FROM foo", - None, - false, - false, - None, - Some(UtilityOptionList { - options: vec![ - UtilityOption { - name: Ident::new("ANALYZE"), - arg: Some("ON".to_string()), - }, - UtilityOption { - name: Ident::new("VERBOSE"), - arg: Some("OFF".to_string()), - }, - ], - }), - ); - - run_explain_analyze_with_postgres_dialect( - r#"EXPLAIN ( FORMAT1 TEXT, FORMAT2 'JSON', FORMAT3 "XML", FORMAT4 YAML ) SELECT sqrt(id) FROM foo"#, - Some( - r#"EXPLAIN ( FORMAT1 TEXT, FORMAT2 JSON, FORMAT3 XML, FORMAT4 YAML ) SELECT sqrt(id) FROM foo"#, - ), - false, - false, - None, - Some(UtilityOptionList { - options: vec![ - UtilityOption { - name: Ident::new("FORMAT1"), - arg: Some("TEXT".to_string()), - }, - UtilityOption { - name: Ident::new("FORMAT2"), - arg: Some("JSON".to_string()), - }, - UtilityOption { - name: Ident::new("FORMAT3"), - arg: Some("XML".to_string()), - }, - UtilityOption { - name: Ident::new("FORMAT4"), - arg: Some("YAML".to_string()), - }, - ], - }), - ); - - run_explain_analyze_with_postgres_dialect( - r#"EXPLAIN ( NUM1 10, NUM2 +10.1, NUM3 -10.2 ) SELECT sqrt(id) FROM foo"#, - Some(r#"EXPLAIN ( NUM1 10, NUM2 10.1, NUM3 -10.2 ) SELECT sqrt(id) FROM foo"#), - false, - false, - None, - Some(UtilityOptionList { - options: vec![ - UtilityOption { - name: Ident::new("NUM1"), - arg: Some("10".to_string()), - }, - UtilityOption { - name: Ident::new("NUM2"), - arg: Some("10.1".to_string()), - }, - UtilityOption { - name: Ident::new("NUM3"), - arg: Some("-10.2".to_string()), - }, - ], - }), - ); - - let utility_option_list = UtilityOptionList { - options: vec![ - UtilityOption { - name: Ident::new("ANALYZE"), - arg: None, - }, - UtilityOption { - name: Ident::new("VERBOSE"), - arg: Some("TRUE".to_string()), - }, - UtilityOption { - name: Ident::new("WAL"), - arg: Some("OFF".to_string()), - }, - UtilityOption { - name: Ident::new("FORMAT"), - arg: Some("YAML".to_string()), - }, - UtilityOption { - name: Ident::new("USER_DEF_NUM"), - arg: Some("-100.1".to_string()), - }, - ], - }; - run_explain_analyze_with_postgres_dialect( - "EXPLAIN ( ANALYZE, VERBOSE TRUE, WAL OFF, FORMAT YAML, USER_DEF_NUM -100.1 ) SELECT sqrt(id) FROM foo", - None, - false, - false, - None, - Some(utility_option_list), - ); -} - -fn run_explain_analyze_with_postgres_dialect( - query: &str, - expcted_canonical: Option<&str>, - expected_verbose: bool, - expected_analyze: bool, - expected_format: Option, - expcted_options: Option, -) { - let test_dialect = TestedDialects { - dialects: vec![Box::new(PostgreSqlDialect {}) as Box], - options: None, - }; - let stmt = if let Some(canonical) = expcted_canonical { - test_dialect.verified_stmt_with_canonical(query, canonical) - } else { - test_dialect.verified_stmt(query) - }; - - match stmt { - 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, expcted_options); - assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); - } - _ => panic!("Unexpected Statement, must be Explain"), - } -} From 381f12916b256b12824bd3e1044f28b3810178a8 Mon Sep 17 00:00:00 2001 From: kysshsy Date: Mon, 16 Sep 2024 17:15:45 +0800 Subject: [PATCH 06/10] test: add dialect flag --- src/dialect/duckdb.rs | 6 ++++++ src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 4 ++++ src/dialect/postgresql.rs | 5 +++++ tests/sqlparser_common.rs | 24 ++++++++++++------------ 5 files changed, 31 insertions(+), 12 deletions(-) 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/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2a8ee0b89..31f154254 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4274,7 +4274,7 @@ fn run_explain_analyze( expected_format: Option, exepcted_options: Option, ) { - run_explain_analyze_with_specified_dialect( + run_explain_analyze_with_specific_dialect( |_d| true, query, expected_verbose, @@ -4284,7 +4284,7 @@ fn run_explain_analyze( ) } -fn run_explain_analyze_with_specified_dialect bool>( +fn run_explain_analyze_with_specific_dialect bool>( dialect_predicate: T, query: &str, expected_verbose: bool, @@ -10861,8 +10861,8 @@ fn test_truncate_table_with_on_cluster() { #[test] fn parse_explain_with_option_list() { - run_explain_analyze_with_specified_dialect( - |d| d.is::() || d.is::() || d.is::(), + run_explain_analyze_with_specific_dialect( + |d| d.supports_explain_with_utility_options(), "EXPLAIN ( ANALYZE false, VERBOSE true ) SELECT sqrt(id) FROM foo", false, false, @@ -10881,8 +10881,8 @@ fn parse_explain_with_option_list() { }), ); - run_explain_analyze_with_specified_dialect( - |d| d.is::() || d.is::() || d.is::(), + run_explain_analyze_with_specific_dialect( + |d| d.supports_explain_with_utility_options(), "EXPLAIN ( ANALYZE ON, VERBOSE OFF ) SELECT sqrt(id) FROM foo", false, false, @@ -10901,8 +10901,8 @@ fn parse_explain_with_option_list() { }), ); - run_explain_analyze_with_specified_dialect( - |d| d.is::() || d.is::() || d.is::(), + run_explain_analyze_with_specific_dialect( + |d| d.supports_explain_with_utility_options(), r#"EXPLAIN ( FORMAT1 TEXT, FORMAT2 'JSON', FORMAT3 "XML", FORMAT4 YAML ) SELECT sqrt(id) FROM foo"#, false, false, @@ -10929,8 +10929,8 @@ fn parse_explain_with_option_list() { }), ); - run_explain_analyze_with_specified_dialect( - |d| d.is::() || d.is::() || d.is::(), + run_explain_analyze_with_specific_dialect( + |d| d.supports_explain_with_utility_options(), r#"EXPLAIN ( NUM1 10, NUM2 +10.1, NUM3 -10.2 ) SELECT sqrt(id) FROM foo"#, false, false, @@ -10986,8 +10986,8 @@ fn parse_explain_with_option_list() { }, ], }; - run_explain_analyze_with_specified_dialect( - |d| d.is::() || d.is::() || d.is::(), + run_explain_analyze_with_specific_dialect( + |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, From 5c75a9e38a57f3439a1e949c8ea063dcfa5a0a87 Mon Sep 17 00:00:00 2001 From: kysshsy Date: Mon, 16 Sep 2024 19:16:35 +0800 Subject: [PATCH 07/10] feat: remove UtilityOptionList --- src/ast/mod.rs | 51 +++-------- src/parser/mod.rs | 5 +- tests/sqlparser_common.rs | 176 ++++++++++++++++++-------------------- 3 files changed, 97 insertions(+), 135 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bf3223e36..1e1064d5a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3033,7 +3033,7 @@ pub enum Statement { /// Optional output format of explain format: Option, /// Postgres style utility options, `(analyze, verbose true)` - options: Option, + options: Option>, }, /// ```sql /// SAVEPOINT @@ -3238,7 +3238,7 @@ impl fmt::Display for Statement { } if let Some(options) = options { - write!(f, "{options}")?; + write!(f, "({}) ",display_comma_separated(options))?; } write!(f, "{statement}") @@ -7132,43 +7132,6 @@ where } } -/// Represents a list of options used in various PostgreSQL utility statements such as -/// CLUSTER, EXPLAIN, `VACUUM`, and `REINDEX`. It takes the form `( 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 UtilityOptionList { - pub options: Vec, -} - -impl Display for UtilityOptionList { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "( ")?; - - let mut iter = self.options.iter().peekable(); - while let Some(option) = iter.next() { - write!(f, "{}", option)?; - if iter.peek().is_some() { - write!(f, ", ")?; - } - } - - write!(f, " ) ") - } -} - /// Represents a single PostgreSQL utility option. /// /// A utility option is a key-value pair where the key is an identifier (IDENT) and the value @@ -7177,6 +7140,16 @@ impl Display for UtilityOptionList { /// - 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; /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 108c98668..7f203a68c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1277,13 +1277,12 @@ impl<'a> Parser<'a> { } } - pub fn parse_utility_option_list(&mut self) -> Result { + pub fn parse_utility_option_list(&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(UtilityOptionList { options }) + Ok(options) } fn parse_utility_option(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 31f154254..71befd062 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4272,7 +4272,7 @@ fn run_explain_analyze( expected_verbose: bool, expected_analyze: bool, expected_format: Option, - exepcted_options: Option, + exepcted_options: Option>, ) { run_explain_analyze_with_specific_dialect( |_d| true, @@ -4290,7 +4290,7 @@ fn run_explain_analyze_with_specific_dialect bool>( expected_verbose: bool, expected_analyze: bool, expected_format: Option, - exepcted_options: Option, + exepcted_options: Option>, ) { match all_dialects_where(dialect_predicate).verified_stmt(query) { Statement::Explain { @@ -10863,132 +10863,122 @@ fn test_truncate_table_with_on_cluster() { fn parse_explain_with_option_list() { run_explain_analyze_with_specific_dialect( |d| d.supports_explain_with_utility_options(), - "EXPLAIN ( ANALYZE false, VERBOSE true ) SELECT sqrt(id) FROM foo", + "EXPLAIN (ANALYZE false, VERBOSE true) SELECT sqrt(id) FROM foo", false, false, None, - Some(UtilityOptionList { - options: 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))), - }, - ], - }), + 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_with_specific_dialect( |d| d.supports_explain_with_utility_options(), - "EXPLAIN ( ANALYZE ON, VERBOSE OFF ) SELECT sqrt(id) FROM foo", + "EXPLAIN (ANALYZE ON, VERBOSE OFF) SELECT sqrt(id) FROM foo", false, false, None, - Some(UtilityOptionList { - options: 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"))), - }, - ], - }), + 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_with_specific_dialect( |d| d.supports_explain_with_utility_options(), - r#"EXPLAIN ( FORMAT1 TEXT, FORMAT2 'JSON', FORMAT3 "XML", FORMAT4 YAML ) SELECT sqrt(id) FROM foo"#, + r#"EXPLAIN (FORMAT1 TEXT, FORMAT2 'JSON', FORMAT3 "XML", FORMAT4 YAML) SELECT sqrt(id) FROM foo"#, false, false, None, - Some(UtilityOptionList { - options: 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"))), - }, - ], - }), + 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_with_specific_dialect( |d| d.supports_explain_with_utility_options(), - r#"EXPLAIN ( NUM1 10, NUM2 +10.1, NUM3 -10.2 ) SELECT sqrt(id) FROM foo"#, + r#"EXPLAIN (NUM1 10, NUM2 +10.1, NUM3 -10.2) SELECT sqrt(id) FROM foo"#, false, false, None, - Some(UtilityOptionList { - options: vec![ - UtilityOption { - name: Ident::new("NUM1"), - arg: Some(Expr::Value(Value::Number("10".to_string(), false))), - }, - UtilityOption { - name: Ident::new("NUM2"), - arg: Some(Expr::UnaryOp { - op: UnaryOperator::Plus, - expr: Box::new(Expr::Value(Value::Number("10.1".to_string(), false))), - }), - }, - UtilityOption { - name: Ident::new("NUM3"), - arg: Some(Expr::UnaryOp { - op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(Value::Number("10.2".to_string(), false))), - }), - }, - ], - }), - ); - - let utility_option_list = UtilityOptionList { - options: vec![ - UtilityOption { - name: Ident::new("ANALYZE"), - arg: None, - }, + Some(vec![ UtilityOption { - name: Ident::new("VERBOSE"), - arg: Some(Expr::Value(Value::Boolean(true))), + name: Ident::new("NUM1"), + arg: Some(Expr::Value(Value::Number("10".to_string(), false))), }, UtilityOption { - name: Ident::new("WAL"), - arg: Some(Expr::Identifier(Ident::new("OFF"))), - }, - UtilityOption { - name: Ident::new("FORMAT"), - arg: Some(Expr::Identifier(Ident::new("YAML"))), + name: Ident::new("NUM2"), + arg: Some(Expr::UnaryOp { + op: UnaryOperator::Plus, + expr: Box::new(Expr::Value(Value::Number("10.1".to_string(), false))), + }), }, UtilityOption { - name: Ident::new("USER_DEF_NUM"), + name: Ident::new("NUM3"), arg: Some(Expr::UnaryOp { op: UnaryOperator::Minus, - expr: Box::new(Expr::Value(Value::Number("100.1".to_string(), false))), + expr: Box::new(Expr::Value(Value::Number("10.2".to_string(), false))), }), }, - ], - }; + ]), + ); + + let utility_option_list = 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".to_string(), false))), + }), + }, + ]; run_explain_analyze_with_specific_dialect( |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", + "EXPLAIN (ANALYZE, VERBOSE true, WAL OFF, FORMAT YAML, USER_DEF_NUM -100.1) SELECT sqrt(id) FROM foo", false, false, None, From 406ce849a2584173180d2328c8e4f2ec8ffe14b8 Mon Sep 17 00:00:00 2001 From: kysshsy Date: Wed, 18 Sep 2024 20:48:40 +0800 Subject: [PATCH 08/10] update base on comment --- src/ast/mod.rs | 2 +- src/parser/mod.rs | 6 ++-- src/test_utils.rs | 6 ---- tests/sqlparser_common.rs | 61 +++++++++++++++++++-------------------- 4 files changed, 33 insertions(+), 42 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1e1064d5a..883398fc5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3238,7 +3238,7 @@ impl fmt::Display for Statement { } if let Some(options) = options { - write!(f, "({}) ",display_comma_separated(options))?; + write!(f, "({}) ", display_comma_separated(options))?; } write!(f, "{statement}") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7f203a68c..751101fea 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1277,7 +1277,7 @@ impl<'a> Parser<'a> { } } - pub fn parse_utility_option_list(&mut self) -> Result, ParserError> { + 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)?; @@ -8495,10 +8495,10 @@ impl<'a> Parser<'a> { // Note: DuckDB is compatible with PostgreSQL syntax for this statement, // although not all features may be implemented. if describe_alias == DescribeAlias::Explain - && dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) + && self.dialect.supports_explain_with_utility_options() && self.peek_token().token == Token::LParen { - options = Some(self.parse_utility_option_list()?) + options = Some(self.parse_utility_options()?) } else { analyze = self.parse_keyword(Keyword::ANALYZE); verbose = self.parse_keyword(Keyword::VERBOSE); diff --git a/src/test_utils.rs b/src/test_utils.rs index be7182ada..5c05ec996 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -148,12 +148,6 @@ impl TestedDialects { self.one_statement_parses_to(sql, sql) } - /// Ensures that `sql` parses as a single [Statement] and that the re-serialization of the parsed - /// result matches the given `canonical` SQL string. - pub fn verified_stmt_with_canonical(&self, sql: &str, canonical: &str) -> Statement { - self.one_statement_parses_to(sql, canonical) - } - /// 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). diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 71befd062..24484c68d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4268,31 +4268,14 @@ 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>, ) { - run_explain_analyze_with_specific_dialect( - |_d| true, - query, - expected_verbose, - expected_analyze, - expected_format, - exepcted_options, - ) -} - -fn run_explain_analyze_with_specific_dialect bool>( - dialect_predicate: T, - query: &str, - expected_verbose: bool, - expected_analyze: bool, - expected_format: Option, - exepcted_options: Option>, -) { - match all_dialects_where(dialect_predicate).verified_stmt(query) { + match dialect.verified_stmt(query) { Statement::Explain { describe_alias: _, analyze, @@ -4350,6 +4333,7 @@ fn explain_desc() { fn parse_explain_analyze_with_simple_select() { // Describe is an alias for EXPLAIN run_explain_analyze( + all_dialects(), "DESCRIBE SELECT sqrt(id) FROM foo", false, false, @@ -4357,8 +4341,16 @@ fn parse_explain_analyze_with_simple_select() { None, ); - run_explain_analyze("EXPLAIN SELECT sqrt(id) FROM foo", false, false, None, 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, @@ -4366,6 +4358,7 @@ fn parse_explain_analyze_with_simple_select() { None, ); run_explain_analyze( + all_dialects(), "EXPLAIN ANALYZE SELECT sqrt(id) FROM foo", false, true, @@ -4373,6 +4366,7 @@ fn parse_explain_analyze_with_simple_select() { None, ); run_explain_analyze( + all_dialects(), "EXPLAIN ANALYZE VERBOSE SELECT sqrt(id) FROM foo", true, true, @@ -4381,6 +4375,7 @@ fn parse_explain_analyze_with_simple_select() { ); run_explain_analyze( + all_dialects(), "EXPLAIN ANALYZE FORMAT GRAPHVIZ SELECT sqrt(id) FROM foo", false, true, @@ -4389,6 +4384,7 @@ fn parse_explain_analyze_with_simple_select() { ); run_explain_analyze( + all_dialects(), "EXPLAIN ANALYZE VERBOSE FORMAT JSON SELECT sqrt(id) FROM foo", true, true, @@ -4397,6 +4393,7 @@ fn parse_explain_analyze_with_simple_select() { ); run_explain_analyze( + all_dialects(), "EXPLAIN VERBOSE FORMAT TEXT SELECT sqrt(id) FROM foo", true, false, @@ -10861,8 +10858,8 @@ fn test_truncate_table_with_on_cluster() { #[test] fn parse_explain_with_option_list() { - run_explain_analyze_with_specific_dialect( - |d| d.supports_explain_with_utility_options(), + 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, @@ -10879,8 +10876,8 @@ fn parse_explain_with_option_list() { ]), ); - run_explain_analyze_with_specific_dialect( - |d| d.supports_explain_with_utility_options(), + 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, @@ -10897,8 +10894,8 @@ fn parse_explain_with_option_list() { ]), ); - run_explain_analyze_with_specific_dialect( - |d| d.supports_explain_with_utility_options(), + 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, @@ -10923,8 +10920,8 @@ fn parse_explain_with_option_list() { ]), ); - run_explain_analyze_with_specific_dialect( - |d| d.supports_explain_with_utility_options(), + 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, @@ -10951,7 +10948,7 @@ fn parse_explain_with_option_list() { ]), ); - let utility_option_list = vec![ + let utility_options = vec![ UtilityOption { name: Ident::new("ANALYZE"), arg: None, @@ -10976,12 +10973,12 @@ fn parse_explain_with_option_list() { }), }, ]; - run_explain_analyze_with_specific_dialect( - |d| d.supports_explain_with_utility_options(), + 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_option_list), + Some(utility_options), ); } \ No newline at end of file From b5bb4f265e7814b5de261e89380e3addf07cf6aa Mon Sep 17 00:00:00 2001 From: kysshsy Date: Thu, 19 Sep 2024 21:26:48 +0800 Subject: [PATCH 09/10] fix BigDecimal feature in test --- tests/sqlparser_common.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 24484c68d..68125c624 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10929,20 +10929,20 @@ fn parse_explain_with_option_list() { Some(vec![ UtilityOption { name: Ident::new("NUM1"), - arg: Some(Expr::Value(Value::Number("10".to_string(), false))), + 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".to_string(), false))), + 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".to_string(), false))), + expr: Box::new(Expr::Value(Value::Number("10.2".parse().unwrap(), false))), }), }, ]), @@ -10969,7 +10969,7 @@ 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".to_string(), false))), + expr: Box::new(Expr::Value(Value::Number("100.1".parse().unwrap(), false))), }), }, ]; From f537053ed99c4e21183041a3696ed2741a0f1dca Mon Sep 17 00:00:00 2001 From: kysshsy Date: Thu, 19 Sep 2024 21:54:29 +0800 Subject: [PATCH 10/10] fix doc links --- src/ast/mod.rs | 8 ++++---- tests/sqlparser_common.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 883398fc5..94dabb059 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7144,10 +7144,10 @@ where /// 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 +/// [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 diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 68125c624..2f001e17e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10981,4 +10981,4 @@ fn parse_explain_with_option_list() { None, Some(utility_options), ); -} \ No newline at end of file +}