mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-08-25 00:14:06 +00:00
feat: support explain options (#1426)
This commit is contained in:
parent
1c505ce736
commit
04a53e5753
7 changed files with 269 additions and 8 deletions
|
@ -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::*;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue