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), + ); +}