diff --git a/rust/datafusion/src/catalog/information_schema.rs b/rust/datafusion/src/catalog/information_schema.rs index 65f051c2afc..5a7b9d5b644 100644 --- a/rust/datafusion/src/catalog/information_schema.rs +++ b/rust/datafusion/src/catalog/information_schema.rs @@ -115,7 +115,7 @@ impl InformationSchemaProvider { builder.add_system_table(&catalog_name, INFORMATION_SCHEMA, COLUMNS); } - let mem_table = builder.build(); + let mem_table: MemTable = builder.into(); Arc::new(mem_table) } @@ -148,7 +148,7 @@ impl InformationSchemaProvider { } } - let mem_table = builder.build(); + let mem_table: MemTable = builder.into(); Arc::new(mem_table) } @@ -231,8 +231,10 @@ impl InformationSchemaTablesBuilder { self.table_names.append_value(table_name.as_ref()).unwrap(); self.table_types.append_value("VIEW").unwrap(); } +} - fn build(self) -> MemTable { +impl From for MemTable { + fn from(value: InformationSchemaTablesBuilder) -> MemTable { let schema = Schema::new(vec![ Field::new("table_catalog", DataType::Utf8, false), Field::new("table_schema", DataType::Utf8, false), @@ -240,12 +242,12 @@ impl InformationSchemaTablesBuilder { Field::new("table_type", DataType::Utf8, false), ]); - let Self { + let InformationSchemaTablesBuilder { mut catalog_names, mut schema_names, mut table_names, mut table_types, - } = self; + } = value; let schema = Arc::new(schema); let batch = RecordBatch::try_new( @@ -422,8 +424,10 @@ impl InformationSchemaColumnsBuilder { self.datetime_precisions.append_option(None).unwrap(); self.interval_types.append_null().unwrap(); } +} - fn build(self) -> MemTable { +impl From for MemTable { + fn from(value: InformationSchemaColumnsBuilder) -> MemTable { let schema = Schema::new(vec![ Field::new("table_catalog", DataType::Utf8, false), Field::new("table_schema", DataType::Utf8, false), @@ -442,7 +446,7 @@ impl InformationSchemaColumnsBuilder { Field::new("interval_type", DataType::Utf8, false), ]); - let Self { + let InformationSchemaColumnsBuilder { mut catalog_names, mut schema_names, mut table_names, @@ -458,7 +462,7 @@ impl InformationSchemaColumnsBuilder { mut numeric_scales, mut datetime_precisions, mut interval_types, - } = self; + } = value; let schema = Arc::new(schema); let batch = RecordBatch::try_new( diff --git a/rust/datafusion/src/execution/context.rs b/rust/datafusion/src/execution/context.rs index 20d0067350c..1e6745c1c2e 100644 --- a/rust/datafusion/src/execution/context.rs +++ b/rust/datafusion/src/execution/context.rs @@ -2253,6 +2253,150 @@ mod tests { assert_batches_sorted_eq!(expected, &result); } + #[tokio::test] + async fn information_schema_show_columns_no_information_schema() { + let mut ctx = ExecutionContext::with_config(ExecutionConfig::new()); + + ctx.register_table("t", table_with_sequence(1, 1).unwrap()) + .unwrap(); + + let err = plan_and_collect(&mut ctx, "SHOW COLUMNS FROM t") + .await + .unwrap_err(); + + assert_eq!(err.to_string(), "Error during planning: SHOW COLUMNS is not supported unless information_schema is enabled"); + } + + #[tokio::test] + async fn information_schema_show_columns_like_where() { + let mut ctx = ExecutionContext::with_config(ExecutionConfig::new()); + + ctx.register_table("t", table_with_sequence(1, 1).unwrap()) + .unwrap(); + + let expected = + "Error during planning: SHOW COLUMNS with WHERE or LIKE is not supported"; + + let err = plan_and_collect(&mut ctx, "SHOW COLUMNS FROM t LIKE 'f'") + .await + .unwrap_err(); + assert_eq!(err.to_string(), expected); + + let err = + plan_and_collect(&mut ctx, "SHOW COLUMNS FROM t WHERE column_name = 'bar'") + .await + .unwrap_err(); + assert_eq!(err.to_string(), expected); + } + + #[tokio::test] + async fn information_schema_show_columns() { + let mut ctx = ExecutionContext::with_config( + ExecutionConfig::new().with_information_schema(true), + ); + + ctx.register_table("t", table_with_sequence(1, 1).unwrap()) + .unwrap(); + + let result = plan_and_collect(&mut ctx, "SHOW COLUMNS FROM t") + .await + .unwrap(); + + let expected = vec![ + "+---------------+--------------+------------+-------------+-----------+-------------+", + "| table_catalog | table_schema | table_name | column_name | data_type | is_nullable |", + "+---------------+--------------+------------+-------------+-----------+-------------+", + "| datafusion | public | t | i | Int32 | YES |", + "+---------------+--------------+------------+-------------+-----------+-------------+", + ]; + assert_batches_sorted_eq!(expected, &result); + + let result = plan_and_collect(&mut ctx, "SHOW columns from t") + .await + .unwrap(); + assert_batches_sorted_eq!(expected, &result); + + // This isn't ideal but it is consistent behavior for `SELECT * from T` + let err = plan_and_collect(&mut ctx, "SHOW columns from T") + .await + .unwrap_err(); + assert_eq!( + err.to_string(), + "Error during planning: Unknown relation for SHOW COLUMNS: T" + ); + } + + // test errors with WHERE and LIKE + #[tokio::test] + async fn information_schema_show_columns_full_extended() { + let mut ctx = ExecutionContext::with_config( + ExecutionConfig::new().with_information_schema(true), + ); + + ctx.register_table("t", table_with_sequence(1, 1).unwrap()) + .unwrap(); + + let result = plan_and_collect(&mut ctx, "SHOW FULL COLUMNS FROM t") + .await + .unwrap(); + let expected = vec![ + + "+---------------+--------------+------------+-------------+------------------+----------------+-------------+-----------+--------------------------+------------------------+-------------------+-------------------------+---------------+--------------------+---------------+", + "| table_catalog | table_schema | table_name | column_name | ordinal_position | column_default | is_nullable | data_type | character_maximum_length | character_octet_length | numeric_precision | numeric_precision_radix | numeric_scale | datetime_precision | interval_type |", + "+---------------+--------------+------------+-------------+------------------+----------------+-------------+-----------+--------------------------+------------------------+-------------------+-------------------------+---------------+--------------------+---------------+", + "| datafusion | public | t | i | 0 | | YES | Int32 | | | 32 | 2 | | | |", + "+---------------+--------------+------------+-------------+------------------+----------------+-------------+-----------+--------------------------+------------------------+-------------------+-------------------------+---------------+--------------------+---------------+", + + ]; + assert_batches_sorted_eq!(expected, &result); + + let result = plan_and_collect(&mut ctx, "SHOW EXTENDED COLUMNS FROM t") + .await + .unwrap(); + assert_batches_sorted_eq!(expected, &result); + } + + #[tokio::test] + async fn information_schema_show_table_table_names() { + let mut ctx = ExecutionContext::with_config( + ExecutionConfig::new().with_information_schema(true), + ); + + ctx.register_table("t", table_with_sequence(1, 1).unwrap()) + .unwrap(); + + let result = plan_and_collect(&mut ctx, "SHOW COLUMNS FROM public.t") + .await + .unwrap(); + + let expected = vec![ + "+---------------+--------------+------------+-------------+-----------+-------------+", + "| table_catalog | table_schema | table_name | column_name | data_type | is_nullable |", + "+---------------+--------------+------------+-------------+-----------+-------------+", + "| datafusion | public | t | i | Int32 | YES |", + "+---------------+--------------+------------+-------------+-----------+-------------+", + ]; + assert_batches_sorted_eq!(expected, &result); + + let result = plan_and_collect(&mut ctx, "SHOW columns from datafusion.public.t") + .await + .unwrap(); + assert_batches_sorted_eq!(expected, &result); + + let err = plan_and_collect(&mut ctx, "SHOW columns from t2") + .await + .unwrap_err(); + assert_eq!( + err.to_string(), + "Error during planning: Unknown relation for SHOW COLUMNS: t2" + ); + + let err = plan_and_collect(&mut ctx, "SHOW columns from datafusion.public.t2") + .await + .unwrap_err(); + assert_eq!(err.to_string(), "Error during planning: Unknown relation for SHOW COLUMNS: datafusion.public.t2"); + } + #[tokio::test] async fn show_unsupported() { let mut ctx = ExecutionContext::with_config(ExecutionConfig::new()); diff --git a/rust/datafusion/src/sql/planner.rs b/rust/datafusion/src/sql/planner.rs index 5d638a3e449..8fc51d4bfad 100644 --- a/rust/datafusion/src/sql/planner.rs +++ b/rust/datafusion/src/sql/planner.rs @@ -46,7 +46,8 @@ use crate::prelude::JoinType; use sqlparser::ast::{ BinaryOperator, DataType as SQLDataType, DateTimeField, Expr as SQLExpr, FunctionArg, Ident, Join, JoinConstraint, JoinOperator, ObjectName, Query, Select, SelectItem, - SetExpr, SetOperator, TableFactor, TableWithJoins, UnaryOperator, Value, + SetExpr, SetOperator, ShowStatementFilter, TableFactor, TableWithJoins, + UnaryOperator, Value, }; use sqlparser::ast::{ColumnDef as SQLColumnDef, ColumnOption}; use sqlparser::ast::{OrderByExpr, Statement}; @@ -100,6 +101,12 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { } => self.explain_statement_to_plan(*verbose, &statement), Statement::Query(query) => self.query_to_plan(&query), Statement::ShowVariable { variable } => self.show_variable_to_plan(&variable), + Statement::ShowColumns { + extended, + full, + table_name, + filter, + } => self.show_columns_to_plan(*extended, *full, table_name, filter.as_ref()), _ => Err(DataFusionError::NotImplemented( "Only SELECT statements are implemented".to_string(), )), @@ -1280,15 +1287,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { // Special case SHOW TABLES let variable = ObjectName(variable.to_vec()).to_string(); if variable.as_str().eq_ignore_ascii_case("tables") { - let tables_reference = TableReference::Partial { - schema: "information_schema", - table: "tables", - }; - if self - .schema_provider - .get_table_provider(tables_reference) - .is_some() - { + if self.has_table("information_schema", "tables") { let rewrite = DFParser::parse_sql("SELECT * FROM information_schema.tables;")?; self.statement_to_plan(&rewrite[0]) @@ -1305,6 +1304,74 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { ))) } } + + fn show_columns_to_plan( + &self, + extended: bool, + full: bool, + table_name: &ObjectName, + filter: Option<&ShowStatementFilter>, + ) -> Result { + if filter.is_some() { + return Err(DataFusionError::Plan( + "SHOW COLUMNS with WHERE or LIKE is not supported".to_string(), + )); + } + + if !self.has_table("information_schema", "columns") { + return Err(DataFusionError::Plan( + "SHOW COLUMNS is not supported unless information_schema is enabled" + .to_string(), + )); + } + + if self + .schema_provider + .get_table_provider(table_name.try_into()?) + .is_none() + { + return Err(DataFusionError::Plan(format!( + "Unknown relation for SHOW COLUMNS: {}", + table_name + ))); + } + + // Figure out the where clause + let columns = vec!["table_name", "table_schema", "table_catalog"].into_iter(); + let where_clause = table_name + .0 + .iter() + .rev() + .zip(columns) + .map(|(ident, column_name)| { + format!(r#"{} = '{}'"#, column_name, ident.to_string()) + }) + .collect::>() + .join(" AND "); + + // treat both FULL and EXTENDED as the same + let select_list = if full || extended { + "*" + } else { + "table_catalog, table_schema, table_name, column_name, data_type, is_nullable" + }; + + let query = format!( + "SELECT {} FROM information_schema.columns WHERE {}", + select_list, where_clause + ); + + let rewrite = DFParser::parse_sql(&query)?; + self.statement_to_plan(&rewrite[0]) + } + + /// Return true if there is a table provider available for "schema.table" + fn has_table(&self, schema: &str, table: &str) -> bool { + let tables_reference = TableReference::Partial { schema, table }; + self.schema_provider + .get_table_provider(tables_reference) + .is_some() + } } /// Remove join expressions from a filter expression