| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| //! Command within CLI | |
| use crate::cli_context::CliSessionContext; | |
| use crate::exec::{exec_and_print, exec_from_lines}; | |
| use crate::functions::{Function, display_all_functions}; | |
| use crate::print_format::PrintFormat; | |
| use crate::print_options::PrintOptions; | |
| use clap::ValueEnum; | |
| use datafusion::arrow::array::{ArrayRef, StringArray}; | |
| use datafusion::arrow::datatypes::{DataType, Field, Schema}; | |
| use datafusion::arrow::record_batch::RecordBatch; | |
| use datafusion::common::instant::Instant; | |
| use datafusion::common::{exec_datafusion_err, exec_err}; | |
| use datafusion::error::Result; | |
| use std::fs::File; | |
| use std::io::BufReader; | |
| use std::str::FromStr; | |
| use std::sync::Arc; | |
| /// Command | |
| #[derive(Debug)] | |
| pub enum Command { | |
| Quit, | |
| Help, | |
| ListTables, | |
| DescribeTableStmt(String), | |
| ListFunctions, | |
| Include(Option<String>), | |
| SearchFunctions(String), | |
| QuietMode(Option<bool>), | |
| OutputFormat(Option<String>), | |
| ObjectStoreProfileMode(Option<String>), | |
| } | |
| pub enum OutputFormat { | |
| ChangeFormat(String), | |
| } | |
| impl Command { | |
| pub async fn execute( | |
| &self, | |
| ctx: &dyn CliSessionContext, | |
| print_options: &mut PrintOptions, | |
| ) -> Result<()> { | |
| match self { | |
| Self::Help => { | |
| let now = Instant::now(); | |
| let command_batch = all_commands_info(); | |
| let schema = command_batch.schema(); | |
| let num_rows = command_batch.num_rows(); | |
| let task_ctx = ctx.task_ctx(); | |
| let config = &task_ctx.session_config().options().format; | |
| print_options.print_batches( | |
| schema, | |
| &[command_batch], | |
| now, | |
| num_rows, | |
| config, | |
| ) | |
| } | |
| Self::ListTables => { | |
| exec_and_print(ctx, print_options, "SHOW TABLES".into()).await | |
| } | |
| Self::DescribeTableStmt(name) => { | |
| exec_and_print(ctx, print_options, format!("SHOW COLUMNS FROM {name}")) | |
| .await | |
| } | |
| Self::Include(filename) => { | |
| if let Some(filename) = filename { | |
| let file = File::open(filename).map_err(|e| { | |
| exec_datafusion_err!("Error opening {filename:?} {e}") | |
| })?; | |
| exec_from_lines(ctx, &mut BufReader::new(file), print_options) | |
| .await?; | |
| Ok(()) | |
| } else { | |
| exec_err!("Required filename argument is missing") | |
| } | |
| } | |
| Self::QuietMode(quiet) => { | |
| if let Some(quiet) = quiet { | |
| print_options.quiet = *quiet; | |
| println!( | |
| "Quiet mode set to {}", | |
| if print_options.quiet { "true" } else { "false" } | |
| ); | |
| } else { | |
| println!( | |
| "Quiet mode is {}", | |
| if print_options.quiet { "true" } else { "false" } | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| Self::Quit => exec_err!("Unexpected quit, this should be handled outside"), | |
| Self::ListFunctions => display_all_functions(), | |
| Self::SearchFunctions(function) => { | |
| if let Ok(func) = function.parse::<Function>() { | |
| let details = func.function_details()?; | |
| println!("{details}"); | |
| Ok(()) | |
| } else { | |
| exec_err!("{function} is not a supported function") | |
| } | |
| } | |
| Self::OutputFormat(_) => exec_err!( | |
| "Unexpected change output format, this should be handled outside" | |
| ), | |
| Self::ObjectStoreProfileMode(mode) => { | |
| if let Some(mode) = mode { | |
| let profile_mode = mode | |
| .parse() | |
| .map_err(|_| | |
| exec_datafusion_err!("Failed to parse input: {mode}. Valid options are disabled, summary, trace") | |
| )?; | |
| print_options | |
| .instrumented_registry | |
| .set_instrument_mode(profile_mode); | |
| println!( | |
| "ObjectStore Profile mode set to {}", | |
| print_options.instrumented_registry.instrument_mode() | |
| ); | |
| } else { | |
| println!( | |
| "ObjectStore Profile mode is {}", | |
| print_options.instrumented_registry.instrument_mode() | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| } | |
| } | |
| fn get_name_and_description(&self) -> (&'static str, &'static str) { | |
| match self { | |
| Self::Quit => ("\\q", "quit datafusion-cli"), | |
| Self::ListTables => ("\\d", "list tables"), | |
| Self::DescribeTableStmt(_) => ("\\d name", "describe table"), | |
| Self::Help => ("\\?", "help"), | |
| Self::Include(_) => { | |
| ("\\i filename", "reads input from the specified filename") | |
| } | |
| Self::ListFunctions => ("\\h", "function list"), | |
| Self::SearchFunctions(_) => ("\\h function", "search function"), | |
| Self::QuietMode(_) => ("\\quiet (true|false)?", "print or set quiet mode"), | |
| Self::OutputFormat(_) => { | |
| ("\\pset [NAME [VALUE]]", "set table output option\n(format)") | |
| } | |
| Self::ObjectStoreProfileMode(_) => ( | |
| "\\object_store_profiling (disabled|summary|trace)", | |
| "print or set object store profile mode", | |
| ), | |
| } | |
| } | |
| } | |
| const ALL_COMMANDS: [Command; 10] = [ | |
| Command::ListTables, | |
| Command::DescribeTableStmt(String::new()), | |
| Command::Quit, | |
| Command::Help, | |
| Command::Include(Some(String::new())), | |
| Command::ListFunctions, | |
| Command::SearchFunctions(String::new()), | |
| Command::QuietMode(None), | |
| Command::OutputFormat(None), | |
| Command::ObjectStoreProfileMode(None), | |
| ]; | |
| fn all_commands_info() -> RecordBatch { | |
| let schema = Arc::new(Schema::new(vec![ | |
| Field::new("Command", DataType::Utf8, false), | |
| Field::new("Description", DataType::Utf8, false), | |
| ])); | |
| let (names, description): (Vec<&str>, Vec<&str>) = ALL_COMMANDS | |
| .into_iter() | |
| .map(|c| c.get_name_and_description()) | |
| .unzip(); | |
| RecordBatch::try_new( | |
| schema, | |
| [names, description] | |
| .into_iter() | |
| .map(|i| Arc::new(StringArray::from(i)) as ArrayRef) | |
| .collect::<Vec<_>>(), | |
| ) | |
| .expect("This should not fail") | |
| } | |
| impl FromStr for Command { | |
| type Err = (); | |
| fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| let (c, arg) = if let Some((a, b)) = s.split_once(' ') { | |
| (a, Some(b)) | |
| } else { | |
| (s, None) | |
| }; | |
| Ok(match (c, arg) { | |
| ("q", None) => Self::Quit, | |
| ("d", None) => Self::ListTables, | |
| ("d", Some(name)) => Self::DescribeTableStmt(name.into()), | |
| ("?", None) => Self::Help, | |
| ("h", None) => Self::ListFunctions, | |
| ("h", Some(function)) => Self::SearchFunctions(function.into()), | |
| ("i", None) => Self::Include(None), | |
| ("i", Some(filename)) => Self::Include(Some(filename.to_owned())), | |
| ("quiet", Some("true" | "t" | "yes" | "y" | "on")) => { | |
| Self::QuietMode(Some(true)) | |
| } | |
| ("quiet", Some("false" | "f" | "no" | "n" | "off")) => { | |
| Self::QuietMode(Some(false)) | |
| } | |
| ("quiet", None) => Self::QuietMode(None), | |
| ("pset", Some(subcommand)) => { | |
| Self::OutputFormat(Some(subcommand.to_string())) | |
| } | |
| ("pset", None) => Self::OutputFormat(None), | |
| ("object_store_profiling", Some(mode)) => { | |
| Self::ObjectStoreProfileMode(Some(mode.to_string())) | |
| } | |
| ("object_store_profiling", None) => Self::ObjectStoreProfileMode(None), | |
| _ => return Err(()), | |
| }) | |
| } | |
| } | |
| impl FromStr for OutputFormat { | |
| type Err = (); | |
| fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| let (c, arg) = if let Some((a, b)) = s.split_once(' ') { | |
| (a, Some(b)) | |
| } else { | |
| (s, None) | |
| }; | |
| Ok(match (c, arg) { | |
| ("format", Some(format)) => Self::ChangeFormat(format.to_string()), | |
| _ => return Err(()), | |
| }) | |
| } | |
| } | |
| impl OutputFormat { | |
| pub fn execute(&self, print_options: &mut PrintOptions) -> Result<()> { | |
| match self { | |
| Self::ChangeFormat(format) => { | |
| if let Ok(format) = format.parse::<PrintFormat>() { | |
| print_options.format = format; | |
| println!("Output format is {:?}.", print_options.format); | |
| Ok(()) | |
| } else { | |
| exec_err!( | |
| "{:?} is not a valid format type [possible values: {:?}]", | |
| format, | |
| PrintFormat::value_variants() | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use datafusion::prelude::SessionContext; | |
| use crate::{ | |
| object_storage::instrumented::{ | |
| InstrumentedObjectStoreMode, InstrumentedObjectStoreRegistry, | |
| }, | |
| print_options::MaxRows, | |
| }; | |
| use super::*; | |
| #[tokio::test] | |
| async fn command_execute_profile_mode() { | |
| let ctx = SessionContext::new(); | |
| let mut print_options = PrintOptions { | |
| format: PrintFormat::Automatic, | |
| quiet: false, | |
| maxrows: MaxRows::Unlimited, | |
| color: true, | |
| instrumented_registry: Arc::new(InstrumentedObjectStoreRegistry::new()), | |
| }; | |
| let mut cmd: Command = "object_store_profiling" | |
| .parse() | |
| .expect("expected parse to succeed"); | |
| assert!(cmd.execute(&ctx, &mut print_options).await.is_ok()); | |
| assert_eq!( | |
| print_options.instrumented_registry.instrument_mode(), | |
| InstrumentedObjectStoreMode::default() | |
| ); | |
| cmd = "object_store_profiling summary" | |
| .parse() | |
| .expect("expected parse to succeed"); | |
| assert!(cmd.execute(&ctx, &mut print_options).await.is_ok()); | |
| assert_eq!( | |
| print_options.instrumented_registry.instrument_mode(), | |
| InstrumentedObjectStoreMode::Summary | |
| ); | |
| cmd = "object_store_profiling trace" | |
| .parse() | |
| .expect("expected parse to succeed"); | |
| assert!(cmd.execute(&ctx, &mut print_options).await.is_ok()); | |
| assert_eq!( | |
| print_options.instrumented_registry.instrument_mode(), | |
| InstrumentedObjectStoreMode::Trace | |
| ); | |
| cmd = "object_store_profiling does_not_exist" | |
| .parse() | |
| .expect("expected parse to succeed"); | |
| assert!(cmd.execute(&ctx, &mut print_options).await.is_err()); | |
| } | |
| } |