From 05eda1a4e7f95a2b84746ccbc0ec7d901cdcad7d Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Tue, 22 Dec 2020 19:10:09 +0300 Subject: [PATCH 1/3] feat: Introduce support for EXPLAIN/ANALYZE --- src/ast/mod.rs | 15 ++++++++++ src/dialect/keywords.rs | 6 ++++ src/parser.rs | 8 +++++ src/tokenizer.rs | 62 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 26 ++++++++++++++++ 5 files changed, 117 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a726b299d..948dd22e6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -431,6 +431,12 @@ impl fmt::Display for WindowFrameBound { #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Statement { + // + Explain { + analyze: bool, + /// A SQL query that specifies what to explain + query: Box, + }, /// SELECT Query(Box), /// INSERT @@ -591,6 +597,15 @@ impl fmt::Display for Statement { #[allow(clippy::cognitive_complexity)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + Statement::Explain { analyze, query } => { + write!(f, "EXPLAIN ")?; + + if *analyze { + write!(f, "ANALYZE ")?; + } + + write!(f, "{}", query) + } Statement::Query(s) => write!(f, "{}", s), Statement::Insert { table_name, diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index d14534881..b58dfe736 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -72,6 +72,7 @@ define_keywords!( ALL, ALLOCATE, ALTER, + ANALYZE, AND, ANY, APPLY, @@ -190,6 +191,7 @@ define_keywords!( EXECUTE, EXISTS, EXP, + EXPLAIN, EXTENDED, EXTERNAL, EXTRACT, @@ -465,6 +467,8 @@ define_keywords!( pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ // Reserved as both a table and a column alias: Keyword::WITH, + Keyword::EXPLAIN, + Keyword::ANALYZE, Keyword::SELECT, Keyword::WHERE, Keyword::GROUP, @@ -496,6 +500,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ // Reserved as both a table and a column alias: Keyword::WITH, + Keyword::EXPLAIN, + Keyword::ANALYZE, Keyword::SELECT, Keyword::WHERE, Keyword::GROUP, diff --git a/src/parser.rs b/src/parser.rs index b40e94de7..effddbb64 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -131,6 +131,7 @@ impl<'a> Parser<'a> { pub fn parse_statement(&mut self) -> Result { match self.next_token() { Token::Word(w) => match w.keyword { + Keyword::EXPLAIN => Ok(self.parse_explain()?), Keyword::SELECT | Keyword::WITH | Keyword::VALUES => { self.prev_token(); Ok(Statement::Query(Box::new(self.parse_query()?))) @@ -1790,6 +1791,13 @@ impl<'a> Parser<'a> { }) } + pub fn parse_explain(&mut self) -> Result { + let analyze = self.parse_keyword(Keyword::ANALYZE); + let query = Box::new(self.parse_query()?); + + Ok(Statement::Explain { analyze, query }) + } + /// Parse a query expression, i.e. a `SELECT` statement optionally /// preceeded with some `WITH` CTE declarations and optionally followed /// by `ORDER BY`. Unlike some other parse_... methods, this one doesn't diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 70587f18b..bbad1a4c4 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -734,6 +734,68 @@ mod tests { compare(expected, tokens); } + #[test] + 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 expected = vec![ + Token::make_keyword("EXPLAIN"), + Token::Whitespace(Whitespace::Space), + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Mult, + Token::Whitespace(Whitespace::Space), + Token::make_keyword("FROM"), + Token::Whitespace(Whitespace::Space), + Token::make_word("customer", None), + Token::Whitespace(Whitespace::Space), + Token::make_keyword("WHERE"), + Token::Whitespace(Whitespace::Space), + Token::make_word("id", None), + Token::Whitespace(Whitespace::Space), + Token::Eq, + Token::Whitespace(Whitespace::Space), + Token::Number(String::from("1")), + ]; + + compare(expected, tokens); + } + + #[test] + 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 expected = vec![ + Token::make_keyword("EXPLAIN"), + Token::Whitespace(Whitespace::Space), + Token::make_keyword("ANALYZE"), + Token::Whitespace(Whitespace::Space), + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::Mult, + Token::Whitespace(Whitespace::Space), + Token::make_keyword("FROM"), + Token::Whitespace(Whitespace::Space), + Token::make_word("customer", None), + Token::Whitespace(Whitespace::Space), + Token::make_keyword("WHERE"), + Token::Whitespace(Whitespace::Space), + Token::make_word("id", None), + Token::Whitespace(Whitespace::Space), + Token::Eq, + Token::Whitespace(Whitespace::Space), + Token::Number(String::from("1")), + ]; + + compare(expected, tokens); + } + #[test] fn tokenize_string_predicate() { let sql = String::from("SELECT * FROM customer WHERE salary != 'Not Provided'"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7fba5dcb9..7b42e05bc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1606,6 +1606,32 @@ fn parse_scalar_function_in_projection() { ); } +#[test] +fn parse_explain_with_simple_select() { + let sql = "EXPLAIN SELECT sqrt(id) FROM foo"; + + match verified_stmt(sql) { + Statement::Explain { analyze, query } => { + assert_eq!(analyze, false); + assert_eq!("SELECT sqrt(id) FROM foo", query.to_string()); + } + _ => panic!("Expected EXPLAIN"), + } +} + +#[test] +fn parse_explain_analyze_with_simple_select() { + let sql = "EXPLAIN ANALYZE SELECT sqrt(id) FROM foo"; + + match verified_stmt(sql) { + Statement::Explain { analyze, query } => { + assert_eq!(analyze, true); + assert_eq!("SELECT sqrt(id) FROM foo", query.to_string()); + } + _ => panic!("Expected EXPLAIN"), + } +} + #[test] fn parse_named_argument_function() { let sql = "SELECT FUN(a => '1', b => '2') FROM foo"; From 8cecd2c9f3c97def0059bedc74b1a87549a8393d Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Tue, 22 Dec 2020 19:13:04 +0300 Subject: [PATCH 2/3] misc: run cargo +nightly fmt --- tests/sqlparser_common.rs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7b42e05bc..64f919527 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -543,17 +543,23 @@ fn parse_is_not_null() { 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"; - assert_matches!(verified_expr(sql), Expr::BinaryOp { - op: BinaryOperator::Or, - .. - }); + assert_matches!( + verified_expr(sql), + Expr::BinaryOp { + op: BinaryOperator::Or, + .. + } + ); // But NOT has lower precedence than comparison operators, so the following parses as NOT (a IS NULL) let sql = "NOT a IS NULL"; - assert_matches!(verified_expr(sql), Expr::UnaryOp { - op: UnaryOperator::Not, - .. - }); + assert_matches!( + verified_expr(sql), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + ); // NOT has lower precedence than BETWEEN, so the following parses as NOT (1 NOT BETWEEN 1 AND 2) let sql = "NOT 1 NOT BETWEEN 1 AND 2"; @@ -1463,7 +1469,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 { .. }); } #[test] @@ -2580,11 +2586,14 @@ fn parse_multiple_statements() { #[test] fn parse_scalar_subqueries() { let sql = "(SELECT 1) + (SELECT 2)"; - assert_matches!(verified_expr(sql), Expr::BinaryOp { + assert_matches!( + verified_expr(sql), + Expr::BinaryOp { op: BinaryOperator::Plus, .. //left: box Subquery { .. }, //right: box Subquery { .. }, - }); + } + ); } #[test] From f0801db98c7bdbdc2ec23248733f5fc110d72fc7 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Fri, 25 Dec 2020 00:42:15 +0300 Subject: [PATCH 3/3] feat: Support level for Explain, support statements --- src/ast/mod.rs | 19 +++++++++++++++---- src/dialect/keywords.rs | 1 + src/parser.rs | 10 ++++++++-- tests/sqlparser_common.rs | 37 +++++++++++++++++++------------------ 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 948dd22e6..2d63bbfab 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -431,11 +431,14 @@ impl fmt::Display for WindowFrameBound { #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Statement { - // + // EXPLAIN Explain { + // Carry out the command and show actual run times and other statistics. analyze: bool, + // Display additional information regarding the plan. + verbose: bool, /// A SQL query that specifies what to explain - query: Box, + statement: Box, }, /// SELECT Query(Box), @@ -597,14 +600,22 @@ impl fmt::Display for Statement { #[allow(clippy::cognitive_complexity)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Statement::Explain { analyze, query } => { + Statement::Explain { + verbose, + analyze, + statement, + } => { write!(f, "EXPLAIN ")?; if *analyze { write!(f, "ANALYZE ")?; } - write!(f, "{}", query) + if *verbose { + write!(f, "VERBOSE ")?; + } + + write!(f, "{}", statement) } Statement::Query(s) => write!(f, "{}", s), Statement::Insert { diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index b58dfe736..6e7065043 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -445,6 +445,7 @@ define_keywords!( VARYING, VAR_POP, VAR_SAMP, + VERBOSE, VERSIONING, VIEW, VIRTUAL, diff --git a/src/parser.rs b/src/parser.rs index effddbb64..0db093f93 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1793,9 +1793,15 @@ impl<'a> Parser<'a> { pub fn parse_explain(&mut self) -> Result { let analyze = self.parse_keyword(Keyword::ANALYZE); - let query = Box::new(self.parse_query()?); + let verbose = self.parse_keyword(Keyword::VERBOSE); + + let statement = Box::new(self.parse_statement()?); - Ok(Statement::Explain { analyze, query }) + Ok(Statement::Explain { + analyze, + verbose, + statement, + }) } /// Parse a query expression, i.e. a `SELECT` statement optionally diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 64f919527..a311dd267 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1612,30 +1612,31 @@ fn parse_scalar_function_in_projection() { ); } -#[test] -fn parse_explain_with_simple_select() { - let sql = "EXPLAIN SELECT sqrt(id) FROM foo"; - - match verified_stmt(sql) { - Statement::Explain { analyze, query } => { - assert_eq!(analyze, false); - assert_eq!("SELECT sqrt(id) FROM foo", query.to_string()); +fn run_explain_analyze(query: &str, expected_verbose: bool, expected_analyze: bool) { + match verified_stmt(query) { + Statement::Explain { + analyze, + verbose, + statement, + } => { + assert_eq!(verbose, expected_verbose); + assert_eq!(analyze, expected_analyze); + assert_eq!("SELECT sqrt(id) FROM foo", statement.to_string()); } - _ => panic!("Expected EXPLAIN"), + _ => panic!("Unexpected Statement, must be Explain"), } } #[test] fn parse_explain_analyze_with_simple_select() { - let sql = "EXPLAIN ANALYZE SELECT sqrt(id) FROM foo"; - - match verified_stmt(sql) { - Statement::Explain { analyze, query } => { - assert_eq!(analyze, true); - assert_eq!("SELECT sqrt(id) FROM foo", query.to_string()); - } - _ => panic!("Expected EXPLAIN"), - } + 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 ANALYZE VERBOSE SELECT sqrt(id) FROM foo", + true, + true, + ); } #[test]