diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ef5889..d836008 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} needs: check steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16bcf02..0c81e5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,10 @@ on: jobs: check: name: Check - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Cargo check diff --git a/Cargo.lock b/Cargo.lock index da0b9db..ece13d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,6 +472,7 @@ dependencies = [ "chrono", "crossterm 0.19.0", "futures", + "regex", "serde", "serde_json", "sqlx", diff --git a/Cargo.toml b/Cargo.toml index c31a01d..74f4a4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ futures = "0.3.5" serde_json = "1.0" serde = "1.0" toml = "0.4" +regex = "1" diff --git a/README.md b/README.md index 8f64334..48106eb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A cross-platform terminal database tool written in Rust -[![github workflow status](https://img.shields.io/github/workflow/status/TaKO8Ki/gobang/CI/main)](https://github.com/TaKO8Ki/gobang/actions) +[![github workflow status](https://img.shields.io/github/workflow/status/TaKO8Ki/gobang/CI/main)](https://github.com/TaKO8Ki/gobang/actions) [![crates](https://img.shields.io/crates/v/gobang.svg?logo=rust)](https://crates.io/crates/gobang) ![gobang](./resources/gobang.gif) diff --git a/resources/gobang.gif b/resources/gobang.gif index ac85192..d602581 100644 Binary files a/resources/gobang.gif and b/resources/gobang.gif differ diff --git a/src/app.rs b/src/app.rs index 7017522..e7f2933 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,23 +10,29 @@ pub enum InputMode { Editing, } -pub enum FocusType { - Dabatases(bool), - Tables(bool), - Records(bool), - Connections, +pub enum FocusBlock { + DabataseList(bool), + TableList(bool), + RecordTable(bool), + ConnectionList, } #[derive(Clone)] pub struct Database { - pub selected_table: ListState, pub name: String, pub tables: Vec, } -#[derive(Clone)] +#[derive(sqlx::FromRow, Debug, Clone)] pub struct Table { + #[sqlx(rename = "Name")] pub name: String, + #[sqlx(rename = "Create_time")] + pub create_time: chrono::DateTime, + #[sqlx(rename = "Update_time")] + pub update_time: Option>, + #[sqlx(rename = "Engine")] + pub engine: String, } pub struct RecordTable { @@ -92,51 +98,23 @@ impl RecordTable { impl Database { pub async fn new(name: String, pool: &MySqlPool) -> anyhow::Result { Ok(Self { - selected_table: ListState::default(), name: name.clone(), tables: get_tables(name, pool).await?, }) } - - pub fn next(&mut self) { - let i = match self.selected_table.selected() { - Some(i) => { - if i >= self.tables.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.selected_table.select(Some(i)); - } - - pub fn previous(&mut self) { - let i = match self.selected_table.selected() { - Some(i) => { - if i == 0 { - self.tables.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.selected_table.select(Some(i)); - } } pub struct App { pub input: String, pub input_mode: InputMode, - pub messages: Vec>, - pub selected_database: ListState, + pub query: String, pub databases: Vec, pub record_table: RecordTable, - pub focus_type: FocusType, + pub focus_type: FocusBlock, pub user_config: Option, pub selected_connection: ListState, + pub selected_database: ListState, + pub selected_table: ListState, pub pool: Option, } @@ -145,21 +123,46 @@ impl Default for App { App { input: String::new(), input_mode: InputMode::Normal, - messages: Vec::new(), - selected_database: ListState::default(), + query: String::new(), databases: Vec::new(), record_table: RecordTable::default(), - focus_type: FocusType::Dabatases(false), + focus_type: FocusBlock::DabataseList(false), user_config: None, selected_connection: ListState::default(), + selected_database: ListState::default(), + selected_table: ListState::default(), pool: None, } } } impl App { - pub fn new(title: &str, enhanced_graphics: bool) -> App { - Self::default() + pub fn next_table(&mut self) { + let i = match self.selected_table.selected() { + Some(i) => { + if i >= self.selected_database().unwrap().tables.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.selected_table.select(Some(i)); + } + + pub fn previous_table(&mut self) { + let i = match self.selected_table.selected() { + Some(i) => { + if i == 0 { + self.selected_database().unwrap().tables.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.selected_table.select(Some(i)); } pub fn next_database(&mut self) { @@ -173,6 +176,7 @@ impl App { } None => 0, }; + self.selected_table.select(Some(0)); self.selected_database.select(Some(i)); } @@ -187,6 +191,7 @@ impl App { } None => 0, }; + self.selected_table.select(Some(0)); self.selected_database.select(Some(i)); } @@ -224,18 +229,15 @@ impl App { pub fn selected_database(&self) -> Option<&Database> { match self.selected_database.selected() { - Some(i) => match self.databases.get(i) { - Some(db) => Some(db), - None => None, - }, + Some(i) => self.databases.get(i), None => None, } } pub fn selected_table(&self) -> Option<&Table> { - match self.selected_database() { - Some(db) => match db.selected_table.selected() { - Some(i) => db.tables.get(i), + match self.selected_table.selected() { + Some(i) => match self.selected_database() { + Some(db) => db.tables.get(i), None => None, }, None => None, diff --git a/src/handlers/connection_list.rs b/src/handlers/connection_list.rs new file mode 100644 index 0000000..fc07108 --- /dev/null +++ b/src/handlers/connection_list.rs @@ -0,0 +1,34 @@ +use crate::app::{App, Database, FocusBlock}; +use crate::event::Key; +use crate::utils::get_databases; +use sqlx::mysql::MySqlPool; + +pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { + match key { + Key::Char('j') => app.next_connection(), + Key::Char('k') => app.previous_connection(), + Key::Enter => { + app.selected_database.select(Some(0)); + app.selected_table.select(Some(0)); + if let Some(conn) = app.selected_connection() { + if let Some(pool) = app.pool.as_ref() { + pool.close().await; + } + let pool = MySqlPool::connect(conn.database_url().as_str()).await?; + app.pool = Some(pool); + app.focus_type = FocusBlock::DabataseList(false); + } + app.databases = match app.selected_connection() { + Some(conn) => match &conn.database { + Some(database) => { + vec![Database::new(database.clone(), app.pool.as_ref().unwrap()).await?] + } + None => get_databases(app.pool.as_ref().unwrap()).await?, + }, + None => vec![], + }; + } + _ => (), + } + Ok(()) +} diff --git a/src/handlers/create_connection.rs b/src/handlers/create_connection.rs deleted file mode 100644 index 1bb72ff..0000000 --- a/src/handlers/create_connection.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::app::{App, FocusType}; -use crate::event::Key; -use sqlx::mysql::MySqlPool; - -pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> { - if let Some(conn) = app.selected_connection() { - app.pool.as_ref().unwrap().close().await; - let pool = MySqlPool::connect(conn.database_url().as_str()).await?; - app.pool = Some(pool); - app.focus_type = FocusType::Dabatases(true); - } - Ok(()) -} diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs index f86d1f0..e5016e9 100644 --- a/src/handlers/database_list.rs +++ b/src/handlers/database_list.rs @@ -1,16 +1,22 @@ -use crate::app::{App, Database}; +use crate::app::{App, FocusBlock}; use crate::event::Key; -use crate::utils::get_databases; -pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> { - app.databases = match app.selected_connection() { - Some(conn) => match &conn.database { - Some(database) => { - vec![Database::new(database.clone(), app.pool.as_ref().unwrap()).await?] - } - None => get_databases(app.pool.as_ref().unwrap()).await?, - }, - None => vec![], - }; +pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> { + if focused { + match key { + Key::Char('j') => app.next_database(), + Key::Char('k') => app.previous_database(), + Key::Esc => app.focus_type = FocusBlock::DabataseList(false), + _ => (), + } + } else { + match key { + Key::Char('j') => app.focus_type = FocusBlock::TableList(false), + Key::Char('l') => app.focus_type = FocusBlock::RecordTable(false), + Key::Char('c') => app.focus_type = FocusBlock::ConnectionList, + Key::Enter => app.focus_type = FocusBlock::DabataseList(true), + _ => (), + } + } Ok(()) } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index c70ad9b..101e5a1 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,93 +1,36 @@ -pub mod create_connection; +pub mod connection_list; pub mod database_list; +pub mod query; pub mod record_table; +pub mod table_list; -use crate::app::{App, FocusType, InputMode}; +use crate::app::{App, FocusBlock, InputMode}; use crate::event::Key; pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> { match app.input_mode { - InputMode::Normal => match key { - Key::Char('e') => { - app.input_mode = InputMode::Editing; - } - Key::Char('c') => { - app.focus_type = FocusType::Connections; - } - Key::Char('l') => app.focus_type = FocusType::Records(false), - Key::Char('h') => app.focus_type = FocusType::Tables(false), - Key::Char('j') => { - if let FocusType::Dabatases(_) = app.focus_type { - app.focus_type = FocusType::Tables(false) + InputMode::Normal => { + match app.focus_type { + FocusBlock::ConnectionList => connection_list::handler(key, app).await?, + FocusBlock::DabataseList(focused) => { + database_list::handler(key, app, focused).await? } - } - Key::Char('k') => { - if let FocusType::Tables(_) = app.focus_type { - app.focus_type = FocusType::Dabatases(false) - } - } - Key::Right => match app.focus_type { - FocusType::Records(true) => app.record_table.next_column(), - _ => (), - }, - Key::Left => match app.focus_type { - FocusType::Records(true) => app.record_table.previous_column(), - _ => (), - }, - Key::Up => match app.focus_type { - FocusType::Connections => app.previous_connection(), - FocusType::Records(true) => app.record_table.previous(), - FocusType::Dabatases(true) => app.previous_database(), - FocusType::Tables(true) => match app.selected_database.selected() { - Some(index) => { - app.record_table.column_index = 0; - app.databases[index].previous(); - record_table::handler(key, app).await?; - } - None => (), - }, - _ => (), - }, - Key::Down => match app.focus_type { - FocusType::Connections => app.next_connection(), - FocusType::Records(true) => app.record_table.next(), - FocusType::Dabatases(true) => app.next_database(), - FocusType::Tables(true) => match app.selected_database.selected() { - Some(index) => { - app.record_table.column_index = 0; - app.databases[index].next(); - record_table::handler(key, app).await? - } - None => (), - }, - _ => (), - }, - Key::Enter => match app.focus_type { - FocusType::Connections => { - app.selected_database.select(Some(0)); - create_connection::handler(key, app).await?; - database_list::handler(key, app).await?; + FocusBlock::TableList(focused) => table_list::handler(key, app, focused).await?, + FocusBlock::RecordTable(focused) => { + record_table::handler(key, app, focused).await? } - FocusType::Records(false) => app.focus_type = FocusType::Records(true), - FocusType::Dabatases(false) => app.focus_type = FocusType::Dabatases(true), - FocusType::Tables(false) => app.focus_type = FocusType::Tables(true), - _ => (), - }, - _ => {} - }, - InputMode::Editing => match key { - Key::Enter => { - app.messages.push(vec![app.input.drain(..).collect()]); } - Key::Char(c) => { - app.input.push(c); + if let Key::Char('e') = key { + app.input_mode = InputMode::Editing } + } + InputMode::Editing => match key { + Key::Enter => query::handler(key, app).await?, + Key::Char(c) => app.input.push(c), Key::Backspace => { app.input.pop(); } - Key::Esc => { - app.input_mode = InputMode::Normal; - } + Key::Esc => app.input_mode = InputMode::Normal, _ => {} }, } diff --git a/src/handlers/query.rs b/src/handlers/query.rs new file mode 100644 index 0000000..69e479d --- /dev/null +++ b/src/handlers/query.rs @@ -0,0 +1,32 @@ +use crate::app::App; +use crate::event::Key; +use crate::utils::convert_column_value_to_string; +use futures::TryStreamExt; +use regex::Regex; +use sqlx::Row; + +pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> { + app.query = app.input.drain(..).collect(); + let re = Regex::new(r"select .+ from (.+)").unwrap(); + if let Some(caps) = re.captures(app.query.as_str()) { + let mut rows = sqlx::query(app.query.as_str()).fetch(app.pool.as_ref().unwrap()); + let headers = sqlx::query(format!("desc `{}`", caps.get(1).unwrap().as_str()).as_str()) + .fetch_all(app.pool.as_ref().unwrap()) + .await? + .iter() + .map(|table| table.get(0)) + .collect::>(); + let mut records = vec![]; + while let Some(row) = rows.try_next().await? { + records.push( + row.columns() + .iter() + .map(|col| convert_column_value_to_string(&row, col)) + .collect::>(), + ) + } + app.record_table.headers = headers; + app.record_table.rows = records; + } + Ok(()) +} diff --git a/src/handlers/record_table.rs b/src/handlers/record_table.rs index a2b3532..fae8a7c 100644 --- a/src/handlers/record_table.rs +++ b/src/handlers/record_table.rs @@ -1,14 +1,22 @@ -use crate::app::App; +use crate::app::{App, FocusBlock}; use crate::event::Key; -use crate::utils::get_records; -pub async fn handler(_key: Key, app: &mut App) -> anyhow::Result<()> { - if let Some(database) = app.selected_database() { - if let Some(table) = app.selected_table() { - let (headers, records) = - get_records(database, table, app.pool.as_ref().unwrap()).await?; - app.record_table.headers = headers; - app.record_table.rows = records; +pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> { + if focused { + match key { + Key::Char('h') => app.record_table.previous_column(), + Key::Char('j') => app.record_table.next(), + Key::Char('k') => app.record_table.previous(), + Key::Char('l') => app.record_table.next_column(), + Key::Esc => app.focus_type = FocusBlock::RecordTable(false), + _ => (), + } + } else { + match key { + Key::Char('h') => app.focus_type = FocusBlock::TableList(false), + Key::Char('c') => app.focus_type = FocusBlock::ConnectionList, + Key::Enter => app.focus_type = FocusBlock::RecordTable(true), + _ => (), } } Ok(()) diff --git a/src/handlers/table_list.rs b/src/handlers/table_list.rs new file mode 100644 index 0000000..284b645 --- /dev/null +++ b/src/handlers/table_list.rs @@ -0,0 +1,51 @@ +use crate::app::{App, FocusBlock}; +use crate::event::Key; +use crate::utils::get_records; + +pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> { + if focused { + match key { + Key::Char('j') => { + if app.selected_database.selected().is_some() { + app.record_table.column_index = 0; + app.next_table(); + if let Some(database) = app.selected_database() { + if let Some(table) = app.selected_table() { + let (headers, records) = + get_records(database, table, app.pool.as_ref().unwrap()).await?; + app.record_table.state.select(Some(0)); + app.record_table.headers = headers; + app.record_table.rows = records; + } + } + } + } + Key::Char('k') => { + if app.selected_database.selected().is_some() { + app.record_table.column_index = 0; + app.previous_table(); + if let Some(database) = app.selected_database() { + if let Some(table) = app.selected_table() { + let (headers, records) = + get_records(database, table, app.pool.as_ref().unwrap()).await?; + app.record_table.state.select(Some(0)); + app.record_table.headers = headers; + app.record_table.rows = records; + } + } + } + } + Key::Esc => app.focus_type = FocusBlock::TableList(false), + _ => (), + } + } else { + match key { + Key::Char('k') => app.focus_type = FocusBlock::DabataseList(false), + Key::Char('l') => app.focus_type = FocusBlock::RecordTable(false), + Key::Char('c') => app.focus_type = FocusBlock::ConnectionList, + Key::Enter => app.focus_type = FocusBlock::TableList(true), + _ => (), + } + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 066079f..7a32790 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ mod ui; mod user_config; mod utils; -use crate::app::FocusType; +use crate::app::{App, FocusBlock}; use crate::event::{Event, Key}; use crate::handlers::handle_app; use crossterm::{ @@ -13,7 +13,6 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use sqlx::mysql::MySqlPool; use std::io::stdout; use tui::{backend::CrosstermBackend, Terminal}; @@ -21,7 +20,7 @@ use tui::{backend::CrosstermBackend, Terminal}; async fn main() -> anyhow::Result<()> { enable_raw_mode()?; - let config = user_config::UserConfig::new("sample.toml").unwrap(); + let user_config = user_config::UserConfig::new("sample.toml").ok(); let mut stdout = stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; @@ -30,32 +29,11 @@ async fn main() -> anyhow::Result<()> { let mut terminal = Terminal::new(backend)?; let events = event::Events::new(250); - let mut app = &mut app::App::default(); - app.user_config = Some(config); - let conn = &app.user_config.as_ref().unwrap().conn.get(0).unwrap(); - let pool = MySqlPool::connect( - format!( - "mysql://{user}:@{host}:{port}", - user = conn.user, - host = conn.host, - port = conn.port - ) - .as_str(), - ) - .await?; - app.pool = Some(pool); - - app.databases = utils::get_databases(app.pool.as_ref().unwrap()).await?; - let (headers, records) = utils::get_records( - app.databases.first().unwrap(), - app.databases.first().unwrap().tables.first().unwrap(), - app.pool.as_ref().unwrap(), - ) - .await?; - app.record_table.rows = records; - app.record_table.headers = headers; - app.selected_database.select(Some(0)); - app.focus_type = FocusType::Connections; + let mut app = App { + user_config, + focus_type: FocusBlock::ConnectionList, + ..App::default() + }; terminal.clear()?; @@ -66,7 +44,7 @@ async fn main() -> anyhow::Result<()> { if key == Key::Char('q') { break; }; - handle_app(key, app).await? + handle_app(key, &mut app).await? } Event::Tick => (), } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d24a9ca..be806d3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,5 @@ use crate::app::InputMode; -use crate::app::{App, FocusType}; +use crate::app::{App, FocusBlock}; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout}, @@ -11,7 +11,7 @@ use tui::{ use unicode_width::UnicodeWidthStr; pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<()> { - if let FocusType::Connections = app.focus_type { + if let FocusBlock::ConnectionList = app.focus_type { let percent_x = 60; let percent_y = 50; let conns = &app.user_config.as_ref().unwrap().conn; @@ -26,7 +26,7 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<( .block(Block::default().borders(Borders::ALL).title("Connections")) .highlight_style(Style::default().fg(Color::Green)) .style(match app.focus_type { - FocusType::Connections => Style::default().fg(Color::Green), + FocusBlock::ConnectionList => Style::default().fg(Color::Green), _ => Style::default(), }); let popup_layout = Layout::default() @@ -58,15 +58,20 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<( } let main_chunks = Layout::default() - .direction(Direction::Vertical) .margin(2) - .constraints([Constraint::Percentage(15), Constraint::Percentage(85)]) .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(15), Constraint::Percentage(85)]) .split(f.size()); let left_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref()) + .constraints( + [ + Constraint::Length(9), + Constraint::Min(8), + Constraint::Length(7), + ] + .as_ref(), + ) .split(main_chunks[0]); let databases: Vec = app .databases @@ -80,8 +85,8 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<( .block(Block::default().borders(Borders::ALL).title("Databases")) .highlight_style(Style::default().fg(Color::Green)) .style(match app.focus_type { - FocusType::Dabatases(false) => Style::default().fg(Color::Magenta), - FocusType::Dabatases(true) => Style::default().fg(Color::Green), + FocusBlock::DabataseList(false) => Style::default().fg(Color::Magenta), + FocusBlock::DabataseList(true) => Style::default().fg(Color::Green), _ => Style::default(), }); f.render_stateful_widget(tasks, left_chunks[0], &mut app.selected_database); @@ -99,28 +104,46 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<( .block(Block::default().borders(Borders::ALL).title("Tables")) .highlight_style(Style::default().fg(Color::Green)) .style(match app.focus_type { - FocusType::Tables(false) => Style::default().fg(Color::Magenta), - FocusType::Tables(true) => Style::default().fg(Color::Green), + FocusBlock::TableList(false) => Style::default().fg(Color::Magenta), + FocusBlock::TableList(true) => Style::default().fg(Color::Green), _ => Style::default(), }); - f.render_stateful_widget( - tasks, - left_chunks[1], - &mut app.databases[app.selected_database.selected().unwrap_or(0)].selected_table, - ); + f.render_stateful_widget(tasks, left_chunks[1], &mut app.selected_table); + + let info: Vec = vec![ + format!( + "created: {}", + app.selected_table().unwrap().create_time.to_string() + ), + // format!( + // "updated: {}", + // app.selected_table().unwrap().update_time.to_string() + // ), + format!("rows: {}", app.record_table.rows.len()), + ] + .iter() + .map(|i| { + ListItem::new(vec![Spans::from(Span::raw(i.to_string()))]) + .style(Style::default().fg(Color::White)) + }) + .collect(); + let tasks = List::new(info) + .block(Block::default().borders(Borders::ALL)) + .highlight_style(Style::default().fg(Color::Green)); + f.render_widget(tasks, left_chunks[2]); let right_chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) + .constraints([Constraint::Length(3), Constraint::Length(5)].as_ref()) .split(main_chunks[1]); - let input = Paragraph::new(app.input.as_ref()) + let query = Paragraph::new(app.input.as_ref()) .style(match app.input_mode { InputMode::Normal => Style::default(), InputMode::Editing => Style::default().fg(Color::Yellow), }) .block(Block::default().borders(Borders::ALL).title("Query")); - f.render_widget(input, right_chunks[0]); + f.render_widget(query, right_chunks[0]); match app.input_mode { InputMode::Normal => (), InputMode::Editing => f.set_cursor( @@ -153,8 +176,8 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<( .block(Block::default().borders(Borders::ALL).title("Records")) .highlight_style(Style::default().fg(Color::Green)) .style(match app.focus_type { - FocusType::Records(false) => Style::default().fg(Color::Magenta), - FocusType::Records(true) => Style::default().fg(Color::Green), + FocusBlock::RecordTable(false) => Style::default().fg(Color::Magenta), + FocusBlock::RecordTable(true) => Style::default().fg(Color::Green), _ => Style::default(), }) .widths(&widths); diff --git a/src/utils.rs b/src/utils.rs index 6045bf6..49018dc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,11 @@ use crate::app::{Database, Table}; use chrono::NaiveDate; use futures::TryStreamExt; -use sqlx::mysql::MySqlPool; -use sqlx::{Column, Executor, Row, TypeInfo}; +use sqlx::mysql::{MySqlColumn, MySqlPool, MySqlRow}; +use sqlx::{Column, Row, TypeInfo}; pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result> { - let databases = sqlx::query("show databases") + let databases = sqlx::query("SHOW DATABASES") .fetch_all(pool) .await? .iter() @@ -19,12 +19,10 @@ pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result> { } pub async fn get_tables(database: String, pool: &MySqlPool) -> anyhow::Result> { - let tables = sqlx::query(format!("show tables from `{}`", database).as_str()) - .fetch_all(pool) - .await? - .iter() - .map(|table| Table { name: table.get(0) }) - .collect::>(); + let tables = + sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str()) + .fetch_all(pool) + .await?; Ok(tables) } @@ -33,10 +31,8 @@ pub async fn get_records( table: &Table, pool: &MySqlPool, ) -> anyhow::Result<(Vec, Vec>)> { - pool.execute(format!("use `{}`", database.name).as_str()) - .await?; - let table_name = format!("SELECT * FROM `{}`", table.name); - let mut rows = sqlx::query(table_name.as_str()).fetch(pool); + let query = format!("SELECT * FROM `{}`.`{}`", database.name, table.name); + let mut rows = sqlx::query(query.as_str()).fetch(pool); let headers = sqlx::query(format!("desc `{}`", table.name).as_str()) .fetch_all(pool) .await? @@ -45,57 +41,55 @@ pub async fn get_records( .collect::>(); let mut records = vec![]; while let Some(row) = rows.try_next().await? { - let mut row_vec = vec![]; - for col in row.columns() { - let col_name = col.name(); - match col.type_info().clone().name() { - "INT" | "DECIMAL" | "SMALLINT" => match row.try_get(col_name) { - Ok(value) => { - let value: i64 = value; - row_vec.push(value.to_string()) - } - Err(_) => row_vec.push("".to_string()), - }, - "INT UNSIGNED" => match row.try_get(col_name) { - Ok(value) => { - let value: u64 = value; - row_vec.push(value.to_string()) - } - Err(_) => row_vec.push("".to_string()), - }, - "VARCHAR" | "CHAR" => { - let value: String = row.try_get(col_name).unwrap_or("".to_string()); - row_vec.push(value); - } - "DATE" => match row.try_get(col_name) { - Ok(value) => { - let value: NaiveDate = value; - row_vec.push(value.to_string()) - } - Err(_) => row_vec.push("".to_string()), - }, - "TIMESTAMP" => match row.try_get(col_name) { - Ok(value) => { - let value: chrono::DateTime = value; - row_vec.push(value.to_string()) - } - Err(_) => row_vec.push("".to_string()), - }, - "BOOLEAN" => match row.try_get(col_name) { - Ok(value) => { - let value: bool = value; - row_vec.push(value.to_string()) - } - Err(_) => row_vec.push("".to_string()), - }, - "ENUM" => { - let value: String = row.try_get(col_name).unwrap_or("".to_string()); - row_vec.push(value); - } - _ => (), - } - } - records.push(row_vec) + records.push( + row.columns() + .iter() + .map(|col| convert_column_value_to_string(&row, col)) + .collect::>(), + ) } Ok((headers, records)) } + +pub fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> String { + let column_name = column.name(); + match column.type_info().clone().name() { + "INT" | "DECIMAL" | "SMALLINT" => match row.try_get(column_name) { + Ok(value) => { + let value: i64 = value; + value.to_string() + } + Err(_) => "".to_string(), + }, + "INT UNSIGNED" => match row.try_get(column_name) { + Ok(value) => { + let value: u64 = value; + value.to_string() + } + Err(_) => "".to_string(), + }, + "VARCHAR" | "CHAR" | "ENUM" => row.try_get(column_name).unwrap_or_else(|_| "".to_string()), + "DATE" => match row.try_get(column_name) { + Ok(value) => { + let value: NaiveDate = value; + value.to_string() + } + Err(_) => "".to_string(), + }, + "TIMESTAMP" => match row.try_get(column_name) { + Ok(value) => { + let value: chrono::DateTime = value; + value.to_string() + } + Err(_) => "".to_string(), + }, + "BOOLEAN" => match row.try_get(column_name) { + Ok(value) => { + let value: bool = value; + value.to_string() + } + Err(_) => "".to_string(), + }, + _ => "".to_string(), + } +}