feat: support explain options (#1426)

This commit is contained in:
Siyuan Huang 2024-09-19 23:28:02 +08:00 committed by GitHub
parent 1c505ce736
commit 04a53e5753
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 269 additions and 8 deletions

View file

@ -3032,6 +3032,8 @@ pub enum Statement {
statement: Box<Statement>,
/// Optional output format of explain
format: Option<AnalyzeFormat>,
/// Postgres style utility options, `(analyze, verbose true)`
options: Option<Vec<UtilityOption>>,
},
/// ```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<Expr>,
}
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::*;

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -166,6 +166,11 @@ impl Dialect for PostgreSqlDialect {
fn supports_create_index_with_clause(&self) -> bool {
true
}
/// see <https://www.postgresql.org/docs/current/sql-explain.html>
fn supports_explain_with_utility_options(&self) -> bool {
true
}
}
pub fn parse_comment(parser: &mut Parser) -> Result<Statement, ParserError> {

View file

@ -1277,6 +1277,29 @@ impl<'a> Parser<'a> {
}
}
pub fn parse_utility_options(&mut self) -> Result<Vec<UtilityOption>, 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<UtilityOption, ParserError> {
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<Option<Expr>, 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<Statement, ParserError> {
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 =

View file

@ -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<AnalyzeFormat>,
exepcted_options: Option<Vec<UtilityOption>>,
) {
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("EXPLAIN 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(
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),
);
}