From 108b78e6d91b0cb595a95b5ee5353c27259d61ea Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 15 Aug 2023 10:24:45 -0400 Subject: [PATCH 1/6] Support EXPLAIN COPY --- datafusion/core/src/execution/context.rs | 52 +++++++---- datafusion/sql/src/parser.rs | 91 +++++++++++++++++-- datafusion/sql/src/statement.rs | 22 ++++- datafusion/sql/tests/sql_integration.rs | 12 +++ datafusion/sqllogictest/test_files/copy.slt | 13 ++- .../sqllogictest/test_files/explain.slt | 8 +- 6 files changed, 158 insertions(+), 40 deletions(-) diff --git a/datafusion/core/src/execution/context.rs b/datafusion/core/src/execution/context.rs index 6593e22e6cc01..e8d7025a9faf4 100644 --- a/datafusion/core/src/execution/context.rs +++ b/datafusion/core/src/execution/context.rs @@ -1698,30 +1698,42 @@ impl SessionState { } let mut visitor = RelationVisitor(&mut relations); - match statement { - DFStatement::Statement(s) => { - let _ = s.as_ref().visit(&mut visitor); - } - DFStatement::CreateExternalTable(table) => { - visitor - .0 - .insert(ObjectName(vec![Ident::from(table.name.as_str())])); - } - DFStatement::DescribeTableStmt(table) => visitor.insert(&table.table_name), - DFStatement::CopyTo(CopyToStatement { - source, - target: _, - options: _, - }) => match source { - CopyToSource::Relation(table_name) => { - visitor.insert(table_name); + fn visit_statement<'a>( + statement: &DFStatement, + visitor: &mut RelationVisitor<'a>, + ) { + match statement { + DFStatement::Statement(s) => { + let _ = s.as_ref().visit(visitor); } - CopyToSource::Query(query) => { - query.visit(&mut visitor); + DFStatement::CreateExternalTable(table) => { + visitor + .0 + .insert(ObjectName(vec![Ident::from(table.name.as_str())])); } - }, + DFStatement::DescribeTableStmt(table) => { + visitor.insert(&table.table_name) + } + DFStatement::CopyTo(CopyToStatement { + source, + target: _, + options: _, + }) => match source { + CopyToSource::Relation(table_name) => { + visitor.insert(table_name); + } + CopyToSource::Query(query) => { + query.visit(visitor); + } + }, + DFStatement::Explain(explain) => { + visit_statement(&explain.statement, visitor) + } + } } + visit_statement(statement, &mut visitor); + // Always include information_schema if available if self.config.information_schema() { for s in INFORMATION_SCHEMA_TABLES { diff --git a/datafusion/sql/src/parser.rs b/datafusion/sql/src/parser.rs index 6bd6ffded2230..c9e98d6f89f90 100644 --- a/datafusion/sql/src/parser.rs +++ b/datafusion/sql/src/parser.rs @@ -44,6 +44,35 @@ fn parse_file_type(s: &str) -> Result { Ok(s.to_uppercase()) } +/// DataFusion specific EXPLAIN (needed so we can EXPLAIN datafusion +/// specific COPY and other statements) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExplainStatement { + pub analyze: bool, + pub verbose: bool, + pub statement: Box, +} + +impl fmt::Display for ExplainStatement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + analyze, + verbose, + statement, + } = self; + + write!(f, "EXPLAIN ")?; + if *analyze { + write!(f, "ANALYZE ")?; + } + if *verbose { + write!(f, "VERBOSE ")?; + } + + write!(f, "{statement}") + } +} + /// DataFusion extension DDL for `COPY` /// /// # Syntax: @@ -204,6 +233,8 @@ pub enum Statement { DescribeTableStmt(DescribeTableStmt), /// Extension: `COPY TO` CopyTo(CopyToStatement), + /// EXPLAIN for extensions + Explain(ExplainStatement), } impl fmt::Display for Statement { @@ -213,11 +244,12 @@ impl fmt::Display for Statement { Statement::CreateExternalTable(stmt) => write!(f, "{stmt}"), Statement::DescribeTableStmt(_) => write!(f, "DESCRIBE TABLE ..."), Statement::CopyTo(stmt) => write!(f, "{stmt}"), + Statement::Explain(stmt) => write!(f, "{stmt}"), } } } -/// DataFusion SQL Parser based on [`sqlparser`] +/// Datafusion1 SQL Parser based on [`sqlparser`] /// /// This parser handles DataFusion specific statements, delegating to /// [`Parser`](sqlparser::parser::Parser) for other SQL statements. @@ -298,24 +330,24 @@ impl<'a> DFParser<'a> { Token::Word(w) => { match w.keyword { Keyword::CREATE => { - // move one token forward - self.parser.next_token(); - // use custom parsing + self.parser.next_token(); // CREATE self.parse_create() } Keyword::COPY => { - // move one token forward - self.parser.next_token(); + self.parser.next_token(); // COPY self.parse_copy() } Keyword::DESCRIBE => { - // move one token forward - self.parser.next_token(); - // use custom parsing + self.parser.next_token(); // DESCRIBE self.parse_describe() } + Keyword::EXPLAIN => { + // (TODO parse all supported statements) + self.parser.next_token(); // EXPLAIN + self.parse_explain() + } _ => { - // use the native parser + // use sqlparser-rs parser Ok(Statement::Statement(Box::from( self.parser.parse_statement()?, ))) @@ -412,6 +444,19 @@ impl<'a> DFParser<'a> { } } + /// Parse a SQL `EXPLAIN` + pub fn parse_explain(&mut self) -> Result { + let analyze = self.parser.parse_keyword(Keyword::ANALYZE); + let verbose = self.parser.parse_keyword(Keyword::VERBOSE); + let statement = self.parse_statement()?; + + Ok(Statement::Explain(ExplainStatement { + statement: Box::new(statement), + analyze, + verbose, + })) + } + /// Parse a SQL `CREATE` statement handling `CREATE EXTERNAL TABLE` pub fn parse_create(&mut self) -> Result { if self.parser.parse_keyword(Keyword::EXTERNAL) { @@ -1283,6 +1328,32 @@ mod tests { Ok(()) } + #[test] + fn explain_copy_to_table_to_table() -> Result<(), ParserError> { + let cases = vec![ + ("EXPLAIN COPY foo TO bar", false, false), + ("EXPLAIN ANALYZE COPY foo TO bar", true, false), + ("EXPLAIN VERBOSE COPY foo TO bar", false, true), + ("EXPLAIN ANALYZE VERBOSE COPY foo TO bar", true, true), + ]; + for (sql, analyze, verbose) in cases { + println!("sql: {sql}, analyze: {analyze}, verbose: {verbose}"); + + let expected_copy = Statement::CopyTo(CopyToStatement { + source: object_name("foo"), + target: "bar".to_string(), + options: HashMap::new(), + }); + let expected = Statement::Explain(ExplainStatement { + analyze, + verbose, + statement: Box::new(expected_copy), + }); + assert_eq!(verified_stmt(sql), expected); + } + Ok(()) + } + #[test] fn copy_to_query_to_table() -> Result<(), ParserError> { let statement = verified_stmt("SELECT 1"); diff --git a/datafusion/sql/src/statement.rs b/datafusion/sql/src/statement.rs index 0f5dbb9ec0b7b..5eece102e454f 100644 --- a/datafusion/sql/src/statement.rs +++ b/datafusion/sql/src/statement.rs @@ -17,7 +17,7 @@ use crate::parser::{ CopyToSource, CopyToStatement, CreateExternalTable, DFParser, DescribeTableStmt, - LexOrdering, Statement as DFStatement, + ExplainStatement, LexOrdering, Statement as DFStatement, }; use crate::planner::{ object_name_to_qualifier, ContextProvider, PlannerContext, SqlToRel, @@ -93,6 +93,11 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { DFStatement::Statement(s) => self.sql_statement_to_plan(*s), DFStatement::DescribeTableStmt(s) => self.describe_table_to_plan(s), DFStatement::CopyTo(s) => self.copy_to_plan(s), + DFStatement::Explain(ExplainStatement { + verbose, + analyze, + statement, + }) => self.explain_to_plan(verbose, analyze, *statement), } } @@ -116,7 +121,9 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { format: _, describe_alias: _, .. - } => self.explain_statement_to_plan(verbose, analyze, *statement), + } => { + self.explain_to_plan(verbose, analyze, DFStatement::Statement(statement)) + } Statement::Query(query) => self.query_to_plan(*query, planner_context), Statement::ShowVariable { variable } => self.show_variable_to_plan(&variable), Statement::SetVariable { @@ -706,13 +713,18 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { /// Generate a plan for EXPLAIN ... that will print out a plan /// - fn explain_statement_to_plan( + /// Note this is the sqlparser explain statement, not the + /// datafusion `EXPLAIN` statement. + fn explain_to_plan( &self, verbose: bool, analyze: bool, - statement: Statement, + statement: DFStatement, ) -> Result { - let plan = self.sql_statement_to_plan(statement)?; + let plan = self.statement_to_plan(statement)?; + if matches!(plan, LogicalPlan::Explain(_)) { + return plan_err!("Nested explain not supported"); + } let plan = Arc::new(plan); let schema = LogicalPlan::explain_schema(); let schema = schema.to_dfschema_ref()?; diff --git a/datafusion/sql/tests/sql_integration.rs b/datafusion/sql/tests/sql_integration.rs index 53ffccbdb4c3e..ba878f8ec9922 100644 --- a/datafusion/sql/tests/sql_integration.rs +++ b/datafusion/sql/tests/sql_integration.rs @@ -336,6 +336,18 @@ CopyTo: format=csv output_url=output.csv per_thread_output=false options: () quick_test(sql, plan); } +#[test] +fn plan_explain_copy_to() { + let sql = "EXPLAIN COPY test_decimal to 'output.csv'"; + let plan = r#" +Explain + CopyTo: format=csv output_url=output.csv per_thread_output=false options: () + TableScan: test_decimal + "# + .trim(); + quick_test(sql, plan); +} + #[test] fn plan_copy_to_query() { let sql = "COPY (select * from test_decimal limit 10) to 'output.csv'"; diff --git a/datafusion/sqllogictest/test_files/copy.slt b/datafusion/sqllogictest/test_files/copy.slt index 364459fa2df1a..369c525a4777f 100644 --- a/datafusion/sqllogictest/test_files/copy.slt +++ b/datafusion/sqllogictest/test_files/copy.slt @@ -25,12 +25,19 @@ COPY source_table TO 'test_files/scratch/table' (format parquet, per_thread_outp ---- 2 -#Explain copy queries not currently working -query error DataFusion error: This feature is not implemented: Unsupported SQL statement: Some\("COPY source_table TO 'test_files/scratch/table'"\) +# Error case +query error DataFusion error: Error during planning: Copy To format not explicitly set and unable to get file extension! EXPLAIN COPY source_table to 'test_files/scratch/table' -query error DataFusion error: SQL error: ParserError\("Expected end of statement, found: source_table"\) +query TT EXPLAIN COPY source_table to 'test_files/scratch/table' (format parquet, per_thread_output true) +---- +logical_plan +CopyTo: format=parquet output_url=test_files/scratch/table per_thread_output=true options: (,per_thread_output true,format parquet) +--TableScan: source_table projection=[col1, col2] +physical_plan +InsertExec: sink=ParquetSink(writer_mode=PutMultipart, file_groups=[]) +--MemoryExec: partitions=4, partition_sizes=[1, 0, 0, 0] # Copy more files to directory via query query IT diff --git a/datafusion/sqllogictest/test_files/explain.slt b/datafusion/sqllogictest/test_files/explain.slt index 7aa7870630dfd..0e2b40f510ad6 100644 --- a/datafusion/sqllogictest/test_files/explain.slt +++ b/datafusion/sqllogictest/test_files/explain.slt @@ -101,13 +101,17 @@ set datafusion.explain.physical_plan_only = false ## explain nested -statement error Explain must be root of the plan +query error DataFusion error: Error during planning: Nested explain not supported EXPLAIN explain select 1 +## explain nested +statement error DataFusion error: Error during planning: Nested explain not supported +EXPLAIN EXPLAIN explain select 1 + statement ok set datafusion.explain.physical_plan_only = true -statement error Explain must be root of the plan +statement error DataFusion error: Error during planning: Nested explain not supported EXPLAIN explain select 1 statement ok From b3f980db5a2106e38d0eacdb6ad8b9dde76a9ebb Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 16 Aug 2023 13:30:14 -0400 Subject: [PATCH 2/6] clippy --- datafusion/core/src/execution/context.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datafusion/core/src/execution/context.rs b/datafusion/core/src/execution/context.rs index e8d7025a9faf4..c090bdcf398fc 100644 --- a/datafusion/core/src/execution/context.rs +++ b/datafusion/core/src/execution/context.rs @@ -1698,9 +1698,9 @@ impl SessionState { } let mut visitor = RelationVisitor(&mut relations); - fn visit_statement<'a>( + fn visit_statement<'_>( statement: &DFStatement, - visitor: &mut RelationVisitor<'a>, + visitor: &mut RelationVisitor<'_>, ) { match statement { DFStatement::Statement(s) => { From 89d75d07d282e997bbd9c8522bcb716650fc3d06 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 16 Aug 2023 13:31:42 -0400 Subject: [PATCH 3/6] clippy --- datafusion/core/src/execution/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafusion/core/src/execution/context.rs b/datafusion/core/src/execution/context.rs index c090bdcf398fc..fb74fb5da477e 100644 --- a/datafusion/core/src/execution/context.rs +++ b/datafusion/core/src/execution/context.rs @@ -1698,7 +1698,7 @@ impl SessionState { } let mut visitor = RelationVisitor(&mut relations); - fn visit_statement<'_>( + fn visit_statement( statement: &DFStatement, visitor: &mut RelationVisitor<'_>, ) { From f61913e2befe9234a86035ec65eb40cc5497b89a Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 16 Aug 2023 13:48:51 -0400 Subject: [PATCH 4/6] Fix argument handling --- datafusion/core/src/execution/context.rs | 5 +- datafusion/expr/src/logical_plan/plan.rs | 6 +-- datafusion/sql/src/parser.rs | 51 ++++++++++----------- datafusion/sqllogictest/test_files/copy.slt | 2 +- 4 files changed, 27 insertions(+), 37 deletions(-) diff --git a/datafusion/core/src/execution/context.rs b/datafusion/core/src/execution/context.rs index fb74fb5da477e..799fedd7eb85f 100644 --- a/datafusion/core/src/execution/context.rs +++ b/datafusion/core/src/execution/context.rs @@ -1698,10 +1698,7 @@ impl SessionState { } let mut visitor = RelationVisitor(&mut relations); - fn visit_statement( - statement: &DFStatement, - visitor: &mut RelationVisitor<'_>, - ) { + fn visit_statement(statement: &DFStatement, visitor: &mut RelationVisitor<'_>) { match statement { DFStatement::Statement(s) => { let _ = s.as_ref().visit(visitor); diff --git a/datafusion/expr/src/logical_plan/plan.rs b/datafusion/expr/src/logical_plan/plan.rs index 85eb59098c8ff..7507977490531 100644 --- a/datafusion/expr/src/logical_plan/plan.rs +++ b/datafusion/expr/src/logical_plan/plan.rs @@ -1100,15 +1100,13 @@ impl LogicalPlan { options, }) => { let mut op_str = String::new(); - op_str.push('('); for (key, val) in options { if !op_str.is_empty() { - op_str.push(','); + op_str.push_str(", "); } op_str.push_str(&format!("{key} {val}")); } - op_str.push(')'); - write!(f, "CopyTo: format={file_format} output_url={output_url} per_thread_output={per_thread_output} options: {op_str}") + write!(f, "CopyTo: format={file_format} output_url={output_url} per_thread_output={per_thread_output} options: ({op_str})") } LogicalPlan::Ddl(ddl) => { write!(f, "{}", ddl.display()) diff --git a/datafusion/sql/src/parser.rs b/datafusion/sql/src/parser.rs index c9e98d6f89f90..202b35b1fdced 100644 --- a/datafusion/sql/src/parser.rs +++ b/datafusion/sql/src/parser.rs @@ -103,7 +103,7 @@ pub struct CopyToStatement { /// The URL to where the data is heading pub target: String, /// Target specific options - pub options: HashMap, + pub options: Vec<(String, Value)>, } impl fmt::Display for CopyToStatement { @@ -117,10 +117,8 @@ impl fmt::Display for CopyToStatement { write!(f, "COPY {source} TO {target}")?; if !options.is_empty() { - let mut opts: Vec<_> = - options.iter().map(|(k, v)| format!("{k} {v}")).collect(); + let opts: Vec<_> = options.iter().map(|(k, v)| format!("{k} {v}")).collect(); // print them in sorted order - opts.sort_unstable(); write!(f, " ({})", opts.join(", "))?; } @@ -392,7 +390,7 @@ impl<'a> DFParser<'a> { let options = if self.parser.peek_token().token == Token::LParen { self.parse_value_options()? } else { - HashMap::new() + vec![] }; Ok(Statement::CopyTo(CopyToStatement { @@ -794,14 +792,14 @@ impl<'a> DFParser<'a> { /// Unlike [`Self::parse_string_options`], this method supports /// keywords as key names as well as multiple value types such as /// Numbers as well as Strings. - fn parse_value_options(&mut self) -> Result, ParserError> { - let mut options = HashMap::new(); + fn parse_value_options(&mut self) -> Result, ParserError> { + let mut options = vec![]; self.parser.expect_token(&Token::LParen)?; loop { let key = self.parse_option_key()?; let value = self.parse_option_value()?; - options.insert(key, value); + options.push((key, value)); let comma = self.parser.consume_token(&Token::Comma); if self.parser.consume_token(&Token::RParen) { // allow a trailing comma, even though it's not in standard @@ -1321,7 +1319,7 @@ mod tests { let expected = Statement::CopyTo(CopyToStatement { source: object_name("foo"), target: "bar".to_string(), - options: HashMap::new(), + options: vec![], }); assert_eq!(verified_stmt(sql), expected); @@ -1342,7 +1340,7 @@ mod tests { let expected_copy = Statement::CopyTo(CopyToStatement { source: object_name("foo"), target: "bar".to_string(), - options: HashMap::new(), + options: vec![], }); let expected = Statement::Explain(ExplainStatement { analyze, @@ -1375,7 +1373,7 @@ mod tests { let expected = Statement::CopyTo(CopyToStatement { source: CopyToSource::Query(query), target: "bar".to_string(), - options: HashMap::new(), + options: vec![], }); assert_eq!(verified_stmt(sql), expected); Ok(()) @@ -1387,10 +1385,10 @@ mod tests { let expected = Statement::CopyTo(CopyToStatement { source: object_name("foo"), target: "bar".to_string(), - options: HashMap::from([( + options: vec![( "row_group_size".to_string(), Value::Number("55".to_string(), false), - )]), + )], }); assert_eq!(verified_stmt(sql), expected); Ok(()) @@ -1398,17 +1396,11 @@ mod tests { #[test] fn copy_to_multi_options() -> Result<(), ParserError> { + // order of options is preserved let sql = "COPY foo TO bar (format parquet, row_group_size 55, compression snappy)"; - // canonical order is alphabetical - let canonical = - "COPY foo TO bar (compression snappy, format parquet, row_group_size 55)"; - let expected_options = HashMap::from([ - ( - "compression".to_string(), - Value::UnQuotedString("snappy".to_string()), - ), + let expected_options = vec![ ( "format".to_string(), Value::UnQuotedString("parquet".to_string()), @@ -1417,14 +1409,17 @@ mod tests { "row_group_size".to_string(), Value::Number("55".to_string(), false), ), - ]); + ( + "compression".to_string(), + Value::UnQuotedString("snappy".to_string()), + ), + ]; - let options = - if let Statement::CopyTo(copy_to) = one_statement_parses_to(sql, canonical) { - copy_to.options - } else { - panic!("Expected copy"); - }; + let options = if let Statement::CopyTo(copy_to) = verified_stmt(sql) { + copy_to.options + } else { + panic!("Expected copy"); + }; assert_eq!(options, expected_options); diff --git a/datafusion/sqllogictest/test_files/copy.slt b/datafusion/sqllogictest/test_files/copy.slt index 369c525a4777f..a44a662ada239 100644 --- a/datafusion/sqllogictest/test_files/copy.slt +++ b/datafusion/sqllogictest/test_files/copy.slt @@ -33,7 +33,7 @@ query TT EXPLAIN COPY source_table to 'test_files/scratch/table' (format parquet, per_thread_output true) ---- logical_plan -CopyTo: format=parquet output_url=test_files/scratch/table per_thread_output=true options: (,per_thread_output true,format parquet) +CopyTo: format=parquet output_url=test_files/scratch/table per_thread_output=true options: (format parquet, per_thread_output true) --TableScan: source_table projection=[col1, col2] physical_plan InsertExec: sink=ParquetSink(writer_mode=PutMultipart, file_groups=[]) From 147b264f74236c1c1aec020c5779271ba1e55929 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 18 Aug 2023 12:46:03 -0400 Subject: [PATCH 5/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Metehan Yıldırım <100111937+metesynnada@users.noreply.github.com> --- datafusion/sql/src/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafusion/sql/src/parser.rs b/datafusion/sql/src/parser.rs index 202b35b1fdced..51836f918d1f6 100644 --- a/datafusion/sql/src/parser.rs +++ b/datafusion/sql/src/parser.rs @@ -247,7 +247,7 @@ impl fmt::Display for Statement { } } -/// Datafusion1 SQL Parser based on [`sqlparser`] +/// Datafusion SQL Parser based on [`sqlparser`] /// /// This parser handles DataFusion specific statements, delegating to /// [`Parser`](sqlparser::parser::Parser) for other SQL statements. From 5c46949eab13aa751eedc370cc7b92f345d609ab Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 18 Aug 2023 12:52:14 -0400 Subject: [PATCH 6/6] Improve nested explain error --- datafusion/sql/src/statement.rs | 2 +- datafusion/sqllogictest/test_files/explain.slt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/datafusion/sql/src/statement.rs b/datafusion/sql/src/statement.rs index 57700f9e5fa5e..f21b80937ea39 100644 --- a/datafusion/sql/src/statement.rs +++ b/datafusion/sql/src/statement.rs @@ -723,7 +723,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { ) -> Result { let plan = self.statement_to_plan(statement)?; if matches!(plan, LogicalPlan::Explain(_)) { - return plan_err!("Nested explain not supported"); + return plan_err!("Nested EXPLAINs are not supported"); } let plan = Arc::new(plan); let schema = LogicalPlan::explain_schema(); diff --git a/datafusion/sqllogictest/test_files/explain.slt b/datafusion/sqllogictest/test_files/explain.slt index 0e2b40f510ad6..ad9b2be40e9e3 100644 --- a/datafusion/sqllogictest/test_files/explain.slt +++ b/datafusion/sqllogictest/test_files/explain.slt @@ -101,17 +101,17 @@ set datafusion.explain.physical_plan_only = false ## explain nested -query error DataFusion error: Error during planning: Nested explain not supported +query error DataFusion error: Error during planning: Nested EXPLAINs are not supported EXPLAIN explain select 1 ## explain nested -statement error DataFusion error: Error during planning: Nested explain not supported +statement error DataFusion error: Error during planning: Nested EXPLAINs are not supported EXPLAIN EXPLAIN explain select 1 statement ok set datafusion.explain.physical_plan_only = true -statement error DataFusion error: Error during planning: Nested explain not supported +statement error DataFusion error: Error during planning: Nested EXPLAINs are not supported EXPLAIN explain select 1 statement ok