Skip to content

Make code organization match the functional structure #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/app/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,9 @@ use std::sync::Arc;
#[cfg(feature = "flightsql")]
use tonic::transport::Channel;

use crate::{
app::{state::tabs::history::HistoryQuery, AppEvent},
ui::SelectedTab,
};

use super::App;
use crate::app::ui::SelectedTab;
use crate::app::{state::tabs::history::HistoryQuery, AppEvent};

pub fn crossterm_event_handler(event: event::Event) -> Option<AppEvent> {
match event {
Expand Down
135 changes: 5 additions & 130 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,13 @@
// under the License.

pub mod app_execution;
pub mod config;
pub mod handlers;
pub mod state;
pub mod ui;

use crate::cli::DftCli;
use crate::{cli, ui};
use color_eyre::eyre::eyre;
use color_eyre::Result;
use crossterm::event as ct;
use datafusion::arrow::util::pretty::pretty_format_batches;
use datafusion::execution::SendableRecordBatchStream;
use datafusion::sql::parser::DFParser;
use datafusion::sql::sqlparser::dialect::GenericDialect;
use futures::FutureExt;
use log::{debug, error, info, trace};
use ratatui::backend::CrosstermBackend;
Expand All @@ -37,9 +31,6 @@ use ratatui::crossterm::{
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use strum::IntoEnumIterator;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
Expand Down Expand Up @@ -77,7 +68,7 @@ pub enum AppEvent {
}

pub struct App<'app> {
pub cli: DftCli,
//cli: DftArgs,
pub state: state::AppState<'app>,
pub execution: Arc<ExecutionContext>,
pub app_event_tx: UnboundedSender<AppEvent>,
Expand All @@ -88,15 +79,14 @@ pub struct App<'app> {
}

impl<'app> App<'app> {
pub fn new(state: state::AppState<'app>, cli: DftCli) -> Self {
pub fn new(state: state::AppState<'app>) -> Self {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cli arguments don't appear to be used after construction so I removed them from the structures

let (app_event_tx, app_event_rx) = mpsc::unbounded_channel();
let app_cancellation_token = CancellationToken::new();
let task = tokio::spawn(async {});
let streams_task = tokio::spawn(async {});
let execution = Arc::new(ExecutionContext::new(state.config.execution.clone()));

Self {
cli,
state,
task,
streams_task,
Expand Down Expand Up @@ -302,9 +292,9 @@ impl Widget for &App<'_> {
}
}

pub async fn run_app(cli: cli::DftCli, state: state::AppState<'_>) -> Result<()> {
pub async fn run_app(state: state::AppState<'_>) -> Result<()> {
info!("Running app with state: {:?}", state);
let mut app = App::new(state, cli.clone());
let mut app = App::new(state);

app.execute_ddl();

Expand All @@ -329,118 +319,3 @@ pub async fn run_app(cli: cli::DftCli, state: state::AppState<'_>) -> Result<()>
}
app.exit()
}

/// Encapsulates the command line interface
pub struct CliApp {
Copy link
Contributor Author

@alamb alamb Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved into cli

/// Execution context for running queries
execution: ExecutionContext,
}

impl CliApp {
pub fn new(state: state::AppState<'static>) -> Self {
let execution = ExecutionContext::new(state.config.execution.clone());

Self { execution }
}

pub async fn execute_files_or_commands(
&self,
files: Vec<PathBuf>,
commands: Vec<String>,
) -> Result<()> {
match (files.is_empty(), commands.is_empty()) {
(true, true) => Err(eyre!("No files or commands provided to execute")),
(false, true) => self.execute_files(files).await,
(true, false) => self.execute_commands(commands).await,
(false, false) => Err(eyre!(
"Cannot execute both files and commands at the same time"
)),
}
}

async fn execute_files(&self, files: Vec<PathBuf>) -> Result<()> {
info!("Executing files: {:?}", files);
for file in files {
self.exec_from_file(&file).await?
}

Ok(())
}
async fn execute_commands(&self, commands: Vec<String>) -> Result<()> {
info!("Executing commands: {:?}", commands);
for command in commands {
self.exec_from_string(&command).await?
}

Ok(())
}

async fn exec_from_string(&self, sql: &str) -> Result<()> {
let dialect = GenericDialect {};
let statements = DFParser::parse_sql_with_dialect(sql, &dialect)?;
for statement in statements {
let stream = self.execution.execute_statement(statement).await?;
self.print_stream(stream).await;
}
Ok(())
}

/// run and execute SQL statements and commands from a file, against a context
/// with the given print options
pub async fn exec_from_file(&self, file: &Path) -> Result<()> {
let file = File::open(file)?;
let reader = BufReader::new(file);

let mut query = String::new();

for line in reader.lines() {
let line = line?;
if line.starts_with("#!") {
continue;
}
if line.starts_with("--") {
continue;
}

let line = line.trim_end();
query.push_str(line);
// if we found the end of a query, run it
if line.ends_with(';') {
// TODO: if the query errors, should we keep trying to execute
// the other queries in the file? That is what datafusion-cli does...
self.execute_and_print_sql(&query).await?;
query.clear();
} else {
query.push('\n');
}
}

// run the last line(s) in file if the last statement doesn't contain ‘;’
// ignore if it only consists of '\n'
if query.contains(|c| c != '\n') {
self.execute_and_print_sql(&query).await?;
}

Ok(())
}

/// executes a sql statement and prints the result to stdout
pub async fn execute_and_print_sql(&self, sql: &str) -> Result<()> {
let stream = self.execution.execute_sql(sql).await?;
self.print_stream(stream).await;
Ok(())
}

/// Prints the stream to stdout
async fn print_stream(&self, mut stream: SendableRecordBatchStream) {
while let Some(maybe_batch) = stream.next().await {
match maybe_batch {
Ok(batch) => match pretty_format_batches(&[batch]) {
Ok(d) => println!("{}", d),
Err(e) => println!("Error formatting batch: {e}"),
},
Err(e) => println!("Error executing SQL: {e}"),
}
}
}
}
8 changes: 3 additions & 5 deletions src/app/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,17 @@

pub mod tabs;

use crate::app::cli;
use crate::app::config::get_data_dir;
use crate::app::state::tabs::sql::SQLTabState;
use crate::app::ui::SelectedTab;
use crate::config::get_data_dir;
use log::{debug, error, info};
use std::path::PathBuf;

use self::tabs::{history::HistoryTabState, logs::LogsTabState};

use super::config::AppConfig;
#[cfg(feature = "flightsql")]
use crate::app::state::tabs::flightsql::FlightSQLTabState;
use crate::config::AppConfig;

#[derive(Debug)]
pub struct Tabs {
Expand Down Expand Up @@ -56,10 +55,9 @@ pub struct AppState<'app> {
pub tabs: Tabs,
}

pub fn initialize<'app>(args: cli::DftCli) -> AppState<'app> {
pub fn initialize<'app>(config_path: PathBuf) -> AppState<'app> {
debug!("Initializing state");
let data_dir = get_data_dir();
let config_path = args.get_config();
debug!("Config path: {:?}", config_path);
let config = if config_path.exists() {
debug!("Config exists");
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 2 additions & 1 deletion src/ui/tabs/flightsql.rs → src/app/ui/tabs/flightsql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use ratatui::{
widgets::{Block, Borders, Paragraph, Row, StatefulWidget, Table, Widget},
};

use crate::{app::App, ui::convert::record_batches_to_table};
use crate::app::ui::convert::record_batches_to_table;
use crate::app::App;

pub fn render_sql_editor(area: Rect, buf: &mut Buffer, app: &App) {
let border_color = if app.state.flightsql_tab.editor_editable() {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 2 additions & 1 deletion src/ui/tabs/sql.rs → src/app/ui/tabs/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use ratatui::{
widgets::{Block, Borders, Paragraph, Row, StatefulWidget, Table, Widget},
};

use crate::{app::App, ui::convert::record_batches_to_table};
use crate::app::ui::convert::record_batches_to_table;
use crate::app::App;

pub fn render_sql_editor(area: Rect, buf: &mut Buffer, app: &App) {
let border_color = if app.state.sql_tab.editor_editable() {
Expand Down
91 changes: 91 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 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 line argument parsing: [`DftArgs`]

use crate::config::get_data_dir;
use clap::Parser;
use std::path::{Path, PathBuf};

const LONG_ABOUT: &str = "
dft - DataFusion TUI

CLI and terminal UI data analysis tool using Apache DataFusion as query
execution engine.

dft provides a rich terminal UI as well as a broad array of pre-integrated
data sources and formats for querying and analyzing data.

Environment Variables
RUST_LOG { trace | debug | info | error }: Standard rust logging level. Default is info.
";

#[derive(Clone, Debug, Parser, Default)]
#[command(author, version, about, long_about = LONG_ABOUT)]
pub struct DftArgs {
#[clap(
short,
long,
num_args = 0..,
help = "Execute commands from file(s), then exit",
value_parser(parse_valid_file)
)]
pub files: Vec<PathBuf>,

#[clap(
short = 'c',
long,
num_args = 0..,
help = "Execute the given SQL string(s), then exit.",
value_parser(parse_command)
)]
pub commands: Vec<String>,

#[clap(long, help = "Path to the configuration file")]
pub config: Option<String>,
}

impl DftArgs {
pub fn config_path(&self) -> PathBuf {
if let Some(config) = self.config.as_ref() {
Path::new(config).to_path_buf()
} else {
let mut config = get_data_dir();
config.push("config.toml");
config
}
}
}

fn parse_valid_file(file: &str) -> std::result::Result<PathBuf, String> {
let path = PathBuf::from(file);
if !path.exists() {
Err(format!("File does not exist: '{file}'"))
} else if !path.is_file() {
Err(format!("Exists but is not a file: '{file}'"))
} else {
Ok(path)
}
}

fn parse_command(command: &str) -> std::result::Result<String, String> {
if !command.is_empty() {
Ok(command.to_string())
} else {
Err("-c flag expects only non empty commands".to_string())
}
}
Loading
Loading