use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
#[cfg(feature = "tui")]
use std::sync::atomic::Ordering;
use std::time::Instant;
use datafusion::arrow::array::{ArrayRef, StringArray};
use datafusion::arrow::datatypes::{DataType, Field, Schema};
use datafusion::arrow::record_batch::RecordBatch;
use datafusion::common::Result;
use datafusion::error::DataFusionError;
use datafusion::prelude::SessionContext;
use crate::functions::{Function, display_all_functions};
use crate::print_format::PrintFormat;
use crate::print_options::PrintOptions;
#[derive(Debug)]
pub enum Command {
Quit,
Help,
ListTables,
DescribeTable(String),
ListFunctions,
SearchFunctions(String),
QuietMode(Option<bool>),
OutputFormat(Option<String>),
#[cfg(feature = "tui")]
OpenTui,
}
pub enum OutputFormat {
ChangeFormat(String),
}
impl Command {
pub async fn execute(
&self,
ctx: &SessionContext,
print_options: &mut PrintOptions,
#[allow(unused_variables)] tui_mode: Arc<AtomicBool>,
) -> Result<()> {
let now = Instant::now();
let max_rows = match print_options.maxrows {
datafusion_cli::print_options::MaxRows::Unlimited => usize::MAX,
datafusion_cli::print_options::MaxRows::Limited(max_rows) => max_rows,
};
match self {
Self::Help =>
{
print_options.print_batches(
Arc::new(Schema::empty()),
&[all_commands_info()],
now,
max_rows,
&Default::default(),
)
}
Self::ListTables => {
let df = ctx.sql("SHOW TABLES").await?;
let schema = Arc::new(df.schema().as_arrow().clone());
let batches = df.collect().await?;
print_options.print_batches(
schema,
&batches,
now,
max_rows,
&Default::default(),
)
}
Self::DescribeTable(name) => {
let df = ctx.sql(&format!("SHOW COLUMNS FROM {name}")).await?;
let schema = Arc::new(df.schema().as_arrow().clone());
let batches = df.collect().await?;
print_options.print_batches(
schema,
&batches,
now,
max_rows,
&Default::default(),
)
}
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 => Err(DataFusionError::Internal(
"Unexpected quit, this should be handled outside".to_string(),
)),
Self::ListFunctions => display_all_functions(),
Self::SearchFunctions(function) => {
if let Ok(func) = function.parse::<Function>() {
let details = func.function_details()?;
println!("{details}");
Ok(())
} else {
let msg = format!("{function} is not a supported function");
Err(DataFusionError::NotImplemented(msg))
}
}
Self::OutputFormat(_) => Err(DataFusionError::Internal(
"Unexpected change output format, this should be handled outside"
.to_string(),
)),
#[cfg(feature = "tui")]
Self::OpenTui => {
struct TuiModeGuard(Arc<AtomicBool>);
impl Drop for TuiModeGuard {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
let _tui_mode_guard = TuiModeGuard(tui_mode.clone());
match crate::tui::tui_main(tui_mode.clone()).await {
Ok(()) => Ok(()),
Err(e) => Err(DataFusionError::Internal(format!(
"Error opening TUI: {e}",
))),
}
}
}
}
fn get_name_and_description(&self) -> (&'static str, &'static str) {
match *self {
Self::Quit => ("\\q", "quit ballista-cli"),
Self::ListTables => ("\\d", "list tables"),
Self::DescribeTable(_) => ("\\d name", "describe table"),
Self::Help => ("\\?", "help"),
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)")
}
#[cfg(feature = "tui")]
Self::OpenTui => ("\\tui", "open tui"),
}
}
}
const ALL_COMMANDS: &[Command] = &[
Command::ListTables,
Command::DescribeTable(String::new()),
Command::Quit,
Command::Help,
Command::ListFunctions,
Command::SearchFunctions(String::new()),
Command::QuietMode(None),
Command::OutputFormat(None),
#[cfg(feature = "tui")]
Command::OpenTui,
];
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
.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) -> std::result::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::DescribeTable(name.into()),
("?", None) => Self::Help,
("h", None) => Self::ListFunctions,
("h", Some(function)) => Self::SearchFunctions(function.into()),
("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),
#[cfg(feature = "tui")]
("tui", None) => Self::OpenTui,
_ => return Err(()),
})
}
}
impl FromStr for OutputFormat {
type Err = ();
fn from_str(s: &str) -> std::result::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 async 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 {
Err(DataFusionError::Execution(format!(
"{:?} is not a valid format type [possible values: {:?}]",
format,
"TO BE FIXED", )))
}
}
}
}
}