diff --git a/Cargo.lock b/Cargo.lock index 3f1da96..c6b9ae9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -532,7 +532,9 @@ dependencies = [ "database-tree", "easy-cast", "futures", + "itertools", "regex", + "rust_decimal", "serde", "serde_json", "sqlx", @@ -615,6 +617,15 @@ dependencies = [ "libc", ] +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.7" @@ -1171,6 +1182,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust_decimal" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5446d1cf2dfe2d6367c8b27f2082bdf011e60e76fa1fcd140047f535156d6e7" +dependencies = [ + "arrayvec", + "num-traits", + "serde", +] + [[package]] name = "rustls" version = "0.18.1" @@ -1389,6 +1411,7 @@ dependencies = [ "percent-encoding", "rand", "rsa", + "rust_decimal", "rustls", "sha-1", "sha2", diff --git a/Cargo.toml b/Cargo.toml index a938073..ba1d5ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ tui = { version = "0.14.0", features = ["crossterm"], default-features = false } crossterm = "0.19" anyhow = "1.0.38" unicode-width = "0.1" -sqlx = { version = "0.4.1", features = ["mysql", "chrono", "runtime-tokio-rustls"] } +sqlx = { version = "0.4.1", features = ["mysql", "chrono", "runtime-tokio-rustls", "decimal"] } chrono = "0.4" tokio = { version = "0.2.22", features = ["full"] } futures = "0.3.5" @@ -33,6 +33,8 @@ database-tree = { path = "./database-tree", version = "0.1" } easy-cast = "0.4" copypasta = { version = "0.7.0", default-features = false } async-trait = "0.1.50" +itertools = "0.10.0" +rust_decimal = "1.15" [target.'cfg(any(target_os = "macos", windows))'.dependencies] copypasta = { version = "0.7.0", default-features = false } diff --git a/src/app.rs b/src/app.rs index 54ccbca..d44aee5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,14 +1,12 @@ use crate::clipboard::Clipboard; -use crate::components::Component as _; -use crate::components::DrawableComponent as _; -use crate::components::EventState; +use crate::components::{CommandInfo, Component as _, DrawableComponent as _, EventState}; use crate::event::Key; use crate::utils::{MySqlPool, Pool}; use crate::{ components::tab::Tab, components::{ - ConnectionsComponent, DatabasesComponent, ErrorComponent, RecordTableComponent, - TabComponent, TableComponent, TableStatusComponent, + ConnectionsComponent, DatabasesComponent, ErrorComponent, HelpComponent, + RecordTableComponent, TabComponent, TableComponent, TableStatusComponent, }, user_config::UserConfig, }; @@ -29,6 +27,7 @@ pub struct App { structure_table: TableComponent, focus: Focus, tab: TabComponent, + help: HelpComponent, databases: DatabasesComponent, connections: ConnectionsComponent, table_status: TableStatusComponent, @@ -45,6 +44,7 @@ impl Default for App { structure_table: TableComponent::default(), focus: Focus::DabataseList, tab: TabComponent::default(), + help: HelpComponent::new(), user_config: None, databases: DatabasesComponent::new(), connections: ConnectionsComponent::default(), @@ -58,7 +58,7 @@ impl Default for App { impl App { pub fn new(user_config: UserConfig) -> App { - App { + Self { user_config: Some(user_config.clone()), connections: ConnectionsComponent::new(user_config.conn), focus: Focus::ConnectionList, @@ -110,10 +110,36 @@ impl App { } } self.error.draw(f, Rect::default(), false)?; + self.help.draw(f, Rect::default(), false)?; Ok(()) } + fn update_commands(&mut self) { + self.help.set_cmds(self.commands()); + } + + fn commands(&self) -> Vec { + let res = vec![ + CommandInfo::new(crate::components::command::move_left("h"), true, true), + CommandInfo::new(crate::components::command::move_down("j"), true, true), + CommandInfo::new(crate::components::command::move_up("k"), true, true), + CommandInfo::new(crate::components::command::move_right("l"), true, true), + CommandInfo::new(crate::components::command::filter("/"), true, true), + CommandInfo::new( + crate::components::command::move_focus_to_right_widget( + Key::Right.to_string().as_str(), + ), + true, + true, + ), + ]; + + res + } + pub async fn event(&mut self, key: Key) -> anyhow::Result { + self.update_commands(); + if let Key::Esc = key { if self.error.error.is_some() { self.error.error = None; @@ -132,6 +158,10 @@ impl App { } pub async fn components_event(&mut self, key: Key) -> anyhow::Result { + if self.help.event(key)?.is_consumed() { + return Ok(EventState::Consumed); + } + match self.focus { Focus::ConnectionList => { if self.connections.event(key)?.is_consumed() { diff --git a/src/components/command.rs b/src/components/command.rs index 87cd9e2..7c94ba3 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -1,3 +1,5 @@ +static CMD_GROUP_GENERAL: &str = "-- General --"; + #[derive(Clone, PartialEq, PartialOrd, Ord, Eq)] pub struct CommandText { pub name: String, @@ -5,6 +7,18 @@ pub struct CommandText { pub group: &'static str, pub hide_help: bool, } + +impl CommandText { + pub const fn new(name: String, desc: &'static str, group: &'static str) -> Self { + Self { + name, + desc, + group, + hide_help: false, + } + } +} + pub struct CommandInfo { pub text: CommandText, pub enabled: bool, @@ -12,3 +26,65 @@ pub struct CommandInfo { pub available: bool, pub order: i8, } + +impl CommandInfo { + pub const fn new(text: CommandText, enabled: bool, available: bool) -> Self { + Self { + text, + enabled, + quick_bar: true, + available, + order: 0, + } + } + + pub const fn order(self, order: i8) -> Self { + let mut res = self; + res.order = order; + res + } +} + +pub fn move_down(key: &str) -> CommandText { + CommandText::new( + format!("Move down [{}]", key), + "move down", + CMD_GROUP_GENERAL, + ) +} + +pub fn move_up(key: &str) -> CommandText { + CommandText::new(format!("Move up [{}]", key), "move up", CMD_GROUP_GENERAL) +} + +pub fn move_right(key: &str) -> CommandText { + CommandText::new( + format!("Move right [{}]", key), + "move right", + CMD_GROUP_GENERAL, + ) +} + +pub fn move_left(key: &str) -> CommandText { + CommandText::new( + format!("Move left [{}]", key), + "move left", + CMD_GROUP_GENERAL, + ) +} + +pub fn filter(key: &str) -> CommandText { + CommandText::new( + format!("Filter [{}]", key), + "enter input for filter", + CMD_GROUP_GENERAL, + ) +} + +pub fn move_focus_to_right_widget(key: &str) -> CommandText { + CommandText::new( + format!("Move focus to right [{}]", key), + "move focus to right", + CMD_GROUP_GENERAL, + ) +} diff --git a/src/components/connections.rs b/src/components/connections.rs index 6395d1a..1d9c33c 100644 --- a/src/components/connections.rs +++ b/src/components/connections.rs @@ -1,4 +1,5 @@ use super::{Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; use crate::event::Key; use crate::user_config::Connection; use anyhow::Result; @@ -99,6 +100,8 @@ impl DrawableComponent for ConnectionsComponent { } impl Component for ConnectionsComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, key: Key) -> Result { match key { Key::Char('j') => { diff --git a/src/components/databases.rs b/src/components/databases.rs index 1363800..f12c570 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -2,6 +2,7 @@ use super::{ compute_character_width, utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState, }; +use crate::components::command::CommandInfo; use crate::event::Key; use crate::ui::common_nav; use crate::ui::scrolllist::draw_list_block; @@ -13,9 +14,8 @@ use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, - symbols::line::HORIZONTAL, text::Span, - widgets::{Block, Borders}, + widgets::{Block, Borders, Paragraph}, Frame, }; use unicode_width::UnicodeWidthStr; @@ -113,7 +113,45 @@ impl DatabasesComponent { } fn draw_tree(&self, f: &mut Frame, area: Rect, focused: bool) { - let tree_height = usize::from(area.height.saturating_sub(4)); + f.render_widget( + Block::default() + .title("Databases") + .borders(Borders::ALL) + .style(if focused { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }), + area, + ); + + let chunks = Layout::default() + .vertical_margin(1) + .horizontal_margin(1) + .direction(Direction::Vertical) + .constraints([Constraint::Length(2), Constraint::Min(1)].as_ref()) + .split(area); + + let filter = Paragraph::new(Span::styled( + format!( + "{}{:w$}", + if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) { + "Filter tables".to_string() + } else { + self.input_str() + }, + w = area.width as usize + ), + if let FocusBlock::Filter = self.focus_block { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }, + )) + .block(Block::default().borders(Borders::BOTTOM)); + f.render_widget(filter, chunks[0]); + + let tree_height = chunks[1].height as usize; let tree = if let Some(tree) = self.filterd_tree.as_ref() { tree } else { @@ -124,64 +162,16 @@ impl DatabasesComponent { self.scroll.reset(); }, |selection| { - self.scroll.update( - selection.index, - selection.count.saturating_sub(2), - tree_height, - ); + self.scroll + .update(selection.index, selection.count, tree_height); }, ); - let mut items = tree + let items = tree .iterate(self.scroll.get_top(), tree_height) - .map(|(item, selected)| Self::tree_item_to_span(item.clone(), selected, area.width)) - .collect::>(); + .map(|(item, selected)| Self::tree_item_to_span(item.clone(), selected, area.width)); - items.insert( - 0, - Span::styled( - (0..area.width as usize) - .map(|_| HORIZONTAL) - .collect::>() - .join(""), - Style::default(), - ), - ); - items.insert( - 0, - Span::styled( - format!( - "{}{:w$}", - if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) { - "Filter tables".to_string() - } else { - self.input_str() - }, - w = area.width as usize - ), - if let FocusBlock::Filter = self.focus_block { - Style::default() - } else { - Style::default().fg(Color::DarkGray) - }, - ), - ); - - let title = "Databases"; - draw_list_block( - f, - area, - Block::default() - .title(Span::styled(title, Style::default())) - .style(if focused { - Style::default() - } else { - Style::default().fg(Color::DarkGray) - }) - .borders(Borders::ALL) - .border_style(Style::default()), - items.into_iter(), - ); + draw_list_block(f, chunks[1], Block::default().borders(Borders::NONE), items); self.scroll.draw(f, area); if let FocusBlock::Filter = self.focus_block { f.set_cursor(area.x + self.input_cursor_position + 1, area.y + 1) @@ -202,6 +192,8 @@ impl DrawableComponent for DatabasesComponent { } impl Component for DatabasesComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, key: Key) -> Result { let input_str: String = self.input.iter().collect(); if tree_nav( diff --git a/src/components/error.rs b/src/components/error.rs index 0eb0cb0..acac088 100644 --- a/src/components/error.rs +++ b/src/components/error.rs @@ -1,4 +1,5 @@ use super::{Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; use crate::event::Key; use anyhow::Result; use tui::{ @@ -49,6 +50,8 @@ impl DrawableComponent for ErrorComponent { } impl Component for ErrorComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, _key: Key) -> Result { Ok(EventState::NotConsumed) } diff --git a/src/components/help.rs b/src/components/help.rs new file mode 100644 index 0000000..f1bc03f --- /dev/null +++ b/src/components/help.rs @@ -0,0 +1,196 @@ +use super::{Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; +use crate::event::Key; +use anyhow::Result; +use itertools::Itertools; +use std::convert::From; +use tui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, BorderType, Borders, Clear, Paragraph}, + Frame, +}; + +pub struct HelpComponent { + cmds: Vec, + visible: bool, + selection: u16, +} + +impl DrawableComponent for HelpComponent { + fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { + if self.visible { + const SIZE: (u16, u16) = (65, 24); + let scroll_threshold = SIZE.1 / 3; + let scroll = self.selection.saturating_sub(scroll_threshold); + + let area = Rect::new( + (f.size().width.saturating_sub(SIZE.0)) / 2, + (f.size().height.saturating_sub(SIZE.1)) / 2, + SIZE.0.min(f.size().width), + SIZE.1.min(f.size().height), + ); + + f.render_widget(Clear, area); + f.render_widget( + Block::default() + .title("Help") + .borders(Borders::ALL) + .border_type(BorderType::Thick), + area, + ); + + let chunks = Layout::default() + .vertical_margin(1) + .horizontal_margin(1) + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(1)].as_ref()) + .split(area); + + f.render_widget( + Paragraph::new(self.get_text(chunks[0].width as usize)).scroll((scroll, 0)), + chunks[0], + ); + + f.render_widget( + Paragraph::new(Spans::from(vec![Span::styled( + format!("gobang {}", "0.1.0"), + Style::default(), + )])) + .alignment(Alignment::Right), + chunks[1], + ); + } + + Ok(()) + } +} + +impl Component for HelpComponent { + fn commands(&self, out: &mut Vec) {} + + fn event(&mut self, key: Key) -> Result { + if self.visible { + match key { + Key::Esc => { + self.hide(); + return Ok(EventState::Consumed); + } + Key::Char('j') => { + self.move_selection(true); + return Ok(EventState::Consumed); + } + Key::Char('k') => { + self.move_selection(false); + return Ok(EventState::Consumed); + } + _ => (), + } + return Ok(EventState::NotConsumed); + } else if let Key::Char('?') = key { + self.show()?; + return Ok(EventState::Consumed); + } + Ok(EventState::NotConsumed) + } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } +} + +impl HelpComponent { + pub const fn new() -> Self { + Self { + cmds: vec![], + visible: false, + selection: 0, + } + } + + pub fn set_cmds(&mut self, cmds: Vec) { + self.cmds = cmds + .into_iter() + .filter(|e| !e.text.hide_help) + .collect::>(); + } + + fn move_selection(&mut self, inc: bool) { + let mut new_selection = self.selection; + + new_selection = if inc { + new_selection.saturating_add(1) + } else { + new_selection.saturating_sub(1) + }; + new_selection = new_selection.max(0); + + self.selection = new_selection.min(self.cmds.len().saturating_sub(1) as u16); + } + + fn get_text(&self, width: usize) -> Vec { + let mut txt: Vec = Vec::new(); + + let mut processed = 0; + + for (key, group) in &self.cmds.iter().group_by(|e| e.text.group) { + txt.push(Spans::from(Span::styled( + key.to_string(), + Style::default().add_modifier(Modifier::REVERSED), + ))); + + for command_info in group { + let is_selected = self.selection == processed; + processed += 1; + + txt.push(Spans::from(Span::styled( + format!("{}{:w$}", command_info.text.name, w = width), + if is_selected { + Style::default().bg(Color::Blue) + } else { + Style::default() + }, + ))); + } + } + + txt + } +} + +#[cfg(test)] +mod test { + use super::{Color, CommandInfo, HelpComponent, Modifier, Span, Spans, Style}; + + #[test] + fn test_get_text() { + let width = 3; + let mut component = HelpComponent::new(); + component.set_cmds(vec![ + CommandInfo::new(crate::components::command::move_left("h"), true, true), + CommandInfo::new(crate::components::command::move_right("l"), true, true), + ]); + assert_eq!( + component.get_text(width), + vec![ + Spans::from(Span::styled( + "-- General --", + Style::default().add_modifier(Modifier::REVERSED) + )), + Spans::from(Span::styled( + "Move left [h] 3", + Style::default().bg(Color::Blue) + )), + Spans::from(Span::styled("Move right [l] 3", Style::default())) + ] + ); + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 80f0012..67d32c7 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -2,6 +2,7 @@ pub mod command; pub mod connections; pub mod databases; pub mod error; +pub mod help; pub mod record_table; pub mod tab; pub mod table; @@ -14,6 +15,7 @@ pub use command::{CommandInfo, CommandText}; pub use connections::ConnectionsComponent; pub use databases::DatabasesComponent; pub use error::ErrorComponent; +pub use help::HelpComponent; pub use record_table::RecordTableComponent; pub use tab::TabComponent; pub use table::TableComponent; @@ -27,22 +29,6 @@ use std::convert::TryInto; use tui::{backend::Backend, layout::Rect, Frame}; use unicode_width::UnicodeWidthChar; -#[derive(Copy, Clone)] -pub enum ScrollType { - Up, - Down, - Home, - End, - PageUp, - PageDown, -} - -#[derive(Copy, Clone)] -pub enum Direction { - Up, - Down, -} - #[derive(PartialEq)] pub enum EventState { Consumed, @@ -72,6 +58,8 @@ pub trait DrawableComponent { /// base component trait #[async_trait] pub trait Component { + fn commands(&self, out: &mut Vec); + fn event(&mut self, key: crate::event::Key) -> Result; fn focused(&self) -> bool { diff --git a/src/components/record_table.rs b/src/components/record_table.rs index d4c2121..8b2a087 100644 --- a/src/components/record_table.rs +++ b/src/components/record_table.rs @@ -1,4 +1,5 @@ use super::{Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; use crate::components::{TableComponent, TableFilterComponent}; use crate::event::Key; use anyhow::Result; @@ -85,6 +86,8 @@ impl DrawableComponent for RecordTableComponent { } impl Component for RecordTableComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, key: Key) -> Result { match key { Key::Char('/') => { diff --git a/src/components/tab.rs b/src/components/tab.rs index 8f43b22..206f5be 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -1,4 +1,5 @@ use super::{Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; use crate::event::Key; use anyhow::Result; use strum::IntoEnumIterator; @@ -62,6 +63,8 @@ impl DrawableComponent for TabComponent { } impl Component for TabComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, key: Key) -> Result { match key { Key::Char('1') => { diff --git a/src/components/table.rs b/src/components/table.rs index 209237e..6bc4406 100644 --- a/src/components/table.rs +++ b/src/components/table.rs @@ -2,6 +2,7 @@ use super::{ utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState, TableValueComponent, }; +use crate::components::command::CommandInfo; use crate::event::Key; use anyhow::Result; use std::convert::From; @@ -447,6 +448,8 @@ impl DrawableComponent for TableComponent { } impl Component for TableComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, key: Key) -> Result { match key { Key::Char('h') => { diff --git a/src/components/table_filter.rs b/src/components/table_filter.rs index 9578fc8..bcf93dd 100644 --- a/src/components/table_filter.rs +++ b/src/components/table_filter.rs @@ -1,4 +1,5 @@ use super::{compute_character_width, Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; use crate::event::Key; use anyhow::Result; use tui::{ @@ -78,6 +79,8 @@ impl DrawableComponent for TableFilterComponent { } impl Component for TableFilterComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, key: Key) -> Result { let input_str: String = self.input.iter().collect(); match key { diff --git a/src/components/table_status.rs b/src/components/table_status.rs index b8ad6e4..582e368 100644 --- a/src/components/table_status.rs +++ b/src/components/table_status.rs @@ -1,4 +1,5 @@ use super::{Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; use crate::event::Key; use anyhow::Result; use database_tree::Table; @@ -85,6 +86,8 @@ impl DrawableComponent for TableStatusComponent { } impl Component for TableStatusComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, _key: Key) -> Result { Ok(EventState::NotConsumed) } diff --git a/src/components/table_value.rs b/src/components/table_value.rs index 5a9acc2..eed0726 100644 --- a/src/components/table_value.rs +++ b/src/components/table_value.rs @@ -1,4 +1,5 @@ use super::{Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; use crate::event::Key; use anyhow::Result; use tui::{ @@ -45,6 +46,8 @@ impl DrawableComponent for TableValueComponent { } impl Component for TableValueComponent { + fn commands(&self, out: &mut Vec) {} + fn event(&mut self, _key: Key) -> Result { todo!("scroll"); } diff --git a/src/components/utils/scroll_vertical.rs b/src/components/utils/scroll_vertical.rs index 9f8b03f..d54cf5a 100644 --- a/src/components/utils/scroll_vertical.rs +++ b/src/components/utils/scroll_vertical.rs @@ -1,4 +1,4 @@ -use crate::{components::ScrollType, ui::scrollbar::draw_scrollbar}; +use crate::ui::scrollbar::draw_scrollbar; use std::cell::Cell; use tui::{backend::Backend, layout::Rect, Frame}; @@ -23,29 +23,6 @@ impl VerticalScroll { self.top.set(0); } - pub fn _move_top(&self, move_type: ScrollType) -> bool { - let old = self.top.get(); - let max = self.max_top.get(); - - let new_scroll_top = match move_type { - ScrollType::Down => old.saturating_add(1), - ScrollType::Up => old.saturating_sub(1), - ScrollType::Home => 0, - ScrollType::End => max, - _ => old, - }; - - let new_scroll_top = new_scroll_top.clamp(0, max); - - if new_scroll_top == old { - return false; - } - - self.top.set(new_scroll_top); - - true - } - pub fn update(&self, selection: usize, selection_max: usize, visual_height: usize) -> usize { let new_top = calc_scroll_top(self.get_top(), visual_height, selection, selection_max); self.top.set(new_top); @@ -60,10 +37,6 @@ impl VerticalScroll { new_top } - pub fn _update_no_selection(&self, line_count: usize, visual_height: usize) -> usize { - self.update(self.get_top(), line_count, visual_height) - } - pub fn draw(&self, f: &mut Frame, r: Rect) { draw_scrollbar(f, r, self.max_top.get(), self.top.get()); } diff --git a/src/utils.rs b/src/utils.rs index 56c3c9d..6431069 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -99,12 +99,11 @@ impl Pool for MySqlPool { .iter() .map(|column| column.name().to_string()) .collect(); - records.push( - row.columns() - .iter() - .map(|col| convert_column_value_to_string(&row, col)) - .collect::>(), - ) + let mut new_row = vec![]; + for column in row.columns() { + new_row.push(convert_column_value_to_string(&row, column)?) + } + records.push(new_row) } Ok((headers, records)) } @@ -124,12 +123,11 @@ impl Pool for MySqlPool { .iter() .map(|column| column.name().to_string()) .collect(); - records.push( - row.columns() - .iter() - .map(|col| convert_column_value_to_string(&row, col)) - .collect::>(), - ) + let mut new_row = vec![]; + for column in row.columns() { + new_row.push(convert_column_value_to_string(&row, column)?) + } + records.push(new_row) } Ok((headers, records)) } @@ -147,47 +145,58 @@ pub async fn get_tables(database: String, pool: &MPool) -> anyhow::Result String { +pub fn convert_column_value_to_string( + row: &MySqlRow, + column: &MySqlColumn, +) -> anyhow::Result { 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() + "INT" | "SMALLINT" | "BIGINT" => { + if let Ok(value) = row.try_get(column_name) { + let value: Option = value; + return Ok(value.map_or("NULL".to_string(), |v| v.to_string())); } - Err(_) => "".to_string(), - }, - "INT UNSIGNED" => match row.try_get(column_name) { - Ok(value) => { - let value: u64 = value; - value.to_string() + } + "DECIMAL" => { + if let Ok(value) = row.try_get(column_name) { + let value: Option = value; + return Ok(value.map_or("NULL".to_string(), |v| v.to_string())); } - Err(_) => "".to_string(), - }, - "VARCHAR" | "CHAR" | "ENUM" | "TEXT" => { - 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() + "INT UNSIGNED" => { + if let Ok(value) = row.try_get(column_name) { + let value: Option = value; + return Ok(value.map_or("NULL".to_string(), |v| v.to_string())); } - Err(_) => "".to_string(), - }, - "TIMESTAMP" => match row.try_get(column_name) { - Ok(value) => { - let value: chrono::DateTime = value; - value.to_string() + } + "VARCHAR" | "CHAR" | "ENUM" | "TEXT" | "LONGTEXT" => { + return Ok(row + .try_get(column_name) + .unwrap_or_else(|_| "NULL".to_string())) + } + "DATE" => { + if let Ok(value) = row.try_get(column_name) { + let value: Option = value; + return Ok(value.map_or("NULL".to_string(), |v| v.to_string())); } - Err(_) => "".to_string(), - }, - "BOOLEAN" => match row.try_get(column_name) { - Ok(value) => { - let value: bool = value; - value.to_string() + } + "TIMESTAMP" => { + if let Ok(value) = row.try_get(column_name) { + let value: Option> = value; + return Ok(value.map_or("NULL".to_string(), |v| v.to_string())); } - Err(_) => "".to_string(), - }, - column_type => unimplemented!("not implemented column type: {}", column_type), + } + "BOOLEAN" => { + if let Ok(value) = row.try_get(column_name) { + let value: Option = value; + return Ok(value.map_or("NULL".to_string(), |v| v.to_string())); + } + } + _ => (), } + Err(anyhow::anyhow!( + "column type not implemented: `{}` {}", + column_name, + column.type_info().clone().name() + )) }