Skip to main content

ballista_cli/
command.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Command within CLI
19
20use std::str::FromStr;
21use std::sync::Arc;
22use std::sync::atomic::AtomicBool;
23#[cfg(all(not(feature = "web"), feature = "tui"))]
24use std::sync::atomic::Ordering;
25use std::time::Instant;
26
27use datafusion::arrow::array::{ArrayRef, StringArray};
28use datafusion::arrow::datatypes::{DataType, Field, Schema};
29use datafusion::arrow::record_batch::RecordBatch;
30use datafusion::common::Result;
31use datafusion::error::DataFusionError;
32use datafusion::prelude::SessionContext;
33
34use crate::functions::{Function, display_all_functions};
35use crate::print_format::PrintFormat;
36use crate::print_options::PrintOptions;
37
38/// Command
39#[derive(Debug)]
40pub enum Command {
41    Quit,
42    Help,
43    ListTables,
44    DescribeTable(String),
45    ListFunctions,
46    SearchFunctions(String),
47    QuietMode(Option<bool>),
48    OutputFormat(Option<String>),
49    #[cfg(all(not(feature = "web"), feature = "tui"))]
50    OpenTui,
51}
52
53pub enum OutputFormat {
54    ChangeFormat(String),
55}
56
57impl Command {
58    pub async fn execute(
59        &self,
60        ctx: &SessionContext,
61        print_options: &mut PrintOptions,
62        #[allow(unused_variables)] tui_mode: Arc<AtomicBool>,
63    ) -> Result<()> {
64        let now = Instant::now();
65        let max_rows = match print_options.maxrows {
66            datafusion_cli::print_options::MaxRows::Unlimited => usize::MAX,
67            datafusion_cli::print_options::MaxRows::Limited(max_rows) => max_rows,
68        };
69        match self {
70            Self::Help =>
71            // TODO need to provide valid schema
72            {
73                print_options.print_batches(
74                    Arc::new(Schema::empty()),
75                    &[all_commands_info()],
76                    now,
77                    max_rows,
78                    &Default::default(),
79                )
80            }
81            Self::ListTables => {
82                let df = ctx.sql("SHOW TABLES").await?;
83                let schema = Arc::new(df.schema().as_arrow().clone());
84                let batches = df.collect().await?;
85                print_options.print_batches(
86                    schema,
87                    &batches,
88                    now,
89                    max_rows,
90                    &Default::default(),
91                )
92            }
93            Self::DescribeTable(name) => {
94                let df = ctx.sql(&format!("SHOW COLUMNS FROM {name}")).await?;
95                let schema = Arc::new(df.schema().as_arrow().clone());
96                let batches = df.collect().await?;
97                print_options.print_batches(
98                    schema,
99                    &batches,
100                    now,
101                    max_rows,
102                    &Default::default(),
103                )
104            }
105            Self::QuietMode(quiet) => {
106                if let Some(quiet) = quiet {
107                    print_options.quiet = *quiet;
108                    println!(
109                        "Quiet mode set to {}",
110                        if print_options.quiet { "true" } else { "false" }
111                    );
112                } else {
113                    println!(
114                        "Quiet mode is {}",
115                        if print_options.quiet { "true" } else { "false" }
116                    );
117                }
118                Ok(())
119            }
120            Self::Quit => Err(DataFusionError::Internal(
121                "Unexpected quit, this should be handled outside".to_string(),
122            )),
123            Self::ListFunctions => display_all_functions(),
124            Self::SearchFunctions(function) => {
125                if let Ok(func) = function.parse::<Function>() {
126                    let details = func.function_details()?;
127                    println!("{details}");
128                    Ok(())
129                } else {
130                    let msg = format!("{function} is not a supported function");
131                    Err(DataFusionError::NotImplemented(msg))
132                }
133            }
134            Self::OutputFormat(_) => Err(DataFusionError::Internal(
135                "Unexpected change output format, this should be handled outside"
136                    .to_string(),
137            )),
138            #[cfg(all(not(feature = "web"), feature = "tui"))]
139            Self::OpenTui => {
140                /// RAII guard to reset tui mode back to false when exiting the TUI mode.
141                struct TuiModeGuard(Arc<AtomicBool>);
142                impl Drop for TuiModeGuard {
143                    fn drop(&mut self) {
144                        self.0.store(false, Ordering::Release);
145                    }
146                }
147                let _tui_mode_guard = TuiModeGuard(tui_mode.clone());
148
149                match crate::tui::main(tui_mode.clone()).await {
150                    Ok(()) => Ok(()),
151                    Err(e) => Err(DataFusionError::Internal(format!(
152                        "Error opening TUI: {e}",
153                    ))),
154                }
155            }
156        }
157    }
158
159    fn get_name_and_description(&self) -> (&'static str, &'static str) {
160        match *self {
161            Self::Quit => ("\\q", "quit ballista-cli"),
162            Self::ListTables => ("\\d", "list tables"),
163            Self::DescribeTable(_) => ("\\d name", "describe table"),
164            Self::Help => ("\\?", "help"),
165            Self::ListFunctions => ("\\h", "function list"),
166            Self::SearchFunctions(_) => ("\\h function", "search function"),
167            Self::QuietMode(_) => ("\\quiet (true|false)?", "print or set quiet mode"),
168            Self::OutputFormat(_) => {
169                ("\\pset [NAME [VALUE]]", "set table output option\n(format)")
170            }
171            #[cfg(all(not(feature = "web"), feature = "tui"))]
172            Self::OpenTui => ("\\tui", "open tui"),
173        }
174    }
175}
176
177const ALL_COMMANDS: &[Command] = &[
178    Command::ListTables,
179    Command::DescribeTable(String::new()),
180    Command::Quit,
181    Command::Help,
182    Command::ListFunctions,
183    Command::SearchFunctions(String::new()),
184    Command::QuietMode(None),
185    Command::OutputFormat(None),
186    #[cfg(all(not(feature = "web"), feature = "tui"))]
187    Command::OpenTui,
188];
189
190fn all_commands_info() -> RecordBatch {
191    let schema = Arc::new(Schema::new(vec![
192        Field::new("Command", DataType::Utf8, false),
193        Field::new("Description", DataType::Utf8, false),
194    ]));
195    let (names, description): (Vec<&str>, Vec<&str>) = ALL_COMMANDS
196        .iter()
197        .map(|c| c.get_name_and_description())
198        .unzip();
199    RecordBatch::try_new(
200        schema,
201        [names, description]
202            .into_iter()
203            .map(|i| Arc::new(StringArray::from(i)) as ArrayRef)
204            .collect::<Vec<_>>(),
205    )
206    .expect("This should not fail")
207}
208
209impl FromStr for Command {
210    type Err = ();
211
212    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
213        let (c, arg) = if let Some((a, b)) = s.split_once(' ') {
214            (a, Some(b))
215        } else {
216            (s, None)
217        };
218        Ok(match (c, arg) {
219            ("q", None) => Self::Quit,
220            ("d", None) => Self::ListTables,
221            ("d", Some(name)) => Self::DescribeTable(name.into()),
222            ("?", None) => Self::Help,
223            ("h", None) => Self::ListFunctions,
224            ("h", Some(function)) => Self::SearchFunctions(function.into()),
225            ("quiet", Some("true" | "t" | "yes" | "y" | "on")) => {
226                Self::QuietMode(Some(true))
227            }
228            ("quiet", Some("false" | "f" | "no" | "n" | "off")) => {
229                Self::QuietMode(Some(false))
230            }
231            ("quiet", None) => Self::QuietMode(None),
232            ("pset", Some(subcommand)) => {
233                Self::OutputFormat(Some(subcommand.to_string()))
234            }
235            ("pset", None) => Self::OutputFormat(None),
236            #[cfg(all(not(feature = "web"), feature = "tui"))]
237            ("tui", None) => Self::OpenTui,
238            _ => return Err(()),
239        })
240    }
241}
242
243impl FromStr for OutputFormat {
244    type Err = ();
245
246    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
247        let (c, arg) = if let Some((a, b)) = s.split_once(' ') {
248            (a, Some(b))
249        } else {
250            (s, None)
251        };
252        Ok(match (c, arg) {
253            ("format", Some(format)) => Self::ChangeFormat(format.to_string()),
254            _ => return Err(()),
255        })
256    }
257}
258
259impl OutputFormat {
260    pub async fn execute(&self, print_options: &mut PrintOptions) -> Result<()> {
261        match self {
262            Self::ChangeFormat(format) => {
263                if let Ok(format) = format.parse::<PrintFormat>() {
264                    print_options.format = format;
265                    println!("Output format is {:?}.", print_options.format);
266                    Ok(())
267                } else {
268                    Err(DataFusionError::Execution(format!(
269                        "{:?} is not a valid format type [possible values: {:?}]",
270                        format,
271                        "TO BE FIXED", //PrintFormat::value_variants()
272                    )))
273                }
274            }
275        }
276    }
277}