Filter records (#24)

* filter records

* refactor components event

* fix scrollbar style

* remove unneeded doc

* fix event order and table filter cursor

* fix and enhance databases filter
pull/27/head
Takayuki Maeda 3 years ago committed by GitHub
parent 4472b63754
commit 98ca815c68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

12
Cargo.lock generated

@ -38,6 +38,17 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "async-trait"
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atoi"
version = "0.4.0"
@ -514,6 +525,7 @@ name = "gobang"
version = "0.1.0-alpha.0"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"copypasta",
"crossterm 0.19.0",

@ -32,6 +32,7 @@ strum_macros = "0.21"
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"
[target.'cfg(any(target_os = "macos", windows))'.dependencies]
copypasta = { version = "0.7.0", default-features = false }

@ -1,14 +1,18 @@
use crate::clipboard::Clipboard;
use crate::components::Component as _;
use crate::components::DrawableComponent as _;
use crate::components::EventState;
use crate::event::Key;
use crate::utils::{MySqlPool, Pool};
use crate::{
components::tab::Tab,
components::{
ConnectionsComponent, DatabasesComponent, ErrorComponent, QueryComponent, TabComponent,
TableComponent, TableStatusComponent,
ConnectionsComponent, DatabasesComponent, ErrorComponent, RecordTableComponent,
TabComponent, TableComponent, TableStatusComponent,
},
user_config::UserConfig,
};
use sqlx::MySqlPool;
use database_tree::Database;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
@ -16,17 +20,15 @@ use tui::{
Frame,
};
pub enum FocusBlock {
pub enum Focus {
DabataseList,
Table,
ConnectionList,
Query,
}
pub struct App {
pub query: QueryComponent,
pub record_table: TableComponent,
pub record_table: RecordTableComponent,
pub structure_table: TableComponent,
pub focus_block: FocusBlock,
pub focus: Focus,
pub tab: TabComponent,
pub user_config: Option<UserConfig>,
pub selected_connection: ListState,
@ -34,17 +36,16 @@ pub struct App {
pub connections: ConnectionsComponent,
pub table_status: TableStatusComponent,
pub clipboard: Clipboard,
pub pool: Option<MySqlPool>,
pub pool: Option<Box<dyn Pool>>,
pub error: ErrorComponent,
}
impl Default for App {
fn default() -> App {
App {
query: QueryComponent::default(),
record_table: TableComponent::default(),
record_table: RecordTableComponent::default(),
structure_table: TableComponent::default(),
focus_block: FocusBlock::DabataseList,
focus: Focus::DabataseList,
tab: TabComponent::default(),
user_config: None,
selected_connection: ListState::default(),
@ -63,13 +64,13 @@ impl App {
App {
user_config: Some(user_config.clone()),
connections: ConnectionsComponent::new(user_config.conn),
focus_block: FocusBlock::ConnectionList,
focus: Focus::ConnectionList,
..App::default()
}
}
pub fn draw<B: Backend>(&mut self, f: &mut Frame<'_, B>) -> anyhow::Result<()> {
if let FocusBlock::ConnectionList = self.focus_block {
if let Focus::ConnectionList = self.focus {
self.connections.draw(
f,
Layout::default()
@ -89,50 +90,233 @@ impl App {
.split(main_chunks[0]);
self.databases
.draw(
f,
left_chunks[0],
matches!(self.focus_block, FocusBlock::DabataseList),
)
.draw(f, left_chunks[0], matches!(self.focus, Focus::DabataseList))
.unwrap();
self.table_status.draw(
f,
left_chunks[1],
matches!(self.focus_block, FocusBlock::DabataseList),
)?;
self.table_status
.draw(f, left_chunks[1], matches!(self.focus, Focus::DabataseList))?;
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(5),
]
.as_ref(),
)
.constraints([Constraint::Length(3), Constraint::Length(5)].as_ref())
.split(main_chunks[1]);
self.tab.draw(f, right_chunks[0], false)?;
self.query.draw(
f,
right_chunks[1],
matches!(self.focus_block, FocusBlock::Query),
)?;
match self.tab.selected_tab {
Tab::Records => self.record_table.draw(
f,
right_chunks[2],
matches!(self.focus_block, FocusBlock::Table),
)?,
Tab::Structure => self.structure_table.draw(
f,
right_chunks[2],
matches!(self.focus_block, FocusBlock::Table),
)?,
Tab::Records => {
self.record_table
.draw(f, right_chunks[1], matches!(self.focus, Focus::Table))?
}
Tab::Structure => {
self.structure_table
.draw(f, right_chunks[1], matches!(self.focus, Focus::Table))?
}
}
self.error.draw(f, Rect::default(), false)?;
Ok(())
}
pub async fn event(&mut self, key: Key) -> anyhow::Result<EventState> {
if let Key::Esc = key {
if self.error.error.is_some() {
self.error.error = None;
return Ok(EventState::Consumed);
}
}
if self.components_event(key).await?.is_consumed() {
return Ok(EventState::Consumed);
};
if self.move_focus(key)?.is_consumed() {
return Ok(EventState::Consumed);
};
Ok(EventState::NotConsumed)
}
pub async fn components_event(&mut self, key: Key) -> anyhow::Result<EventState> {
match self.focus {
Focus::ConnectionList => {
if self.connections.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
}
if let Key::Enter = key {
self.record_table.reset();
if let Some(conn) = self.connections.selected_connection() {
if let Some(pool) = self.pool.as_ref() {
pool.close().await;
}
self.pool = Some(Box::new(
MySqlPool::new(conn.database_url().as_str()).await?,
));
let databases = match &conn.database {
Some(database) => vec![Database::new(
database.clone(),
self.pool
.as_ref()
.unwrap()
.get_tables(database.clone())
.await?,
)],
None => self.pool.as_ref().unwrap().get_databases().await?,
};
self.databases.update(databases.as_slice()).unwrap();
self.focus = Focus::DabataseList
}
return Ok(EventState::Consumed);
}
}
Focus::DabataseList => {
if self.databases.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
}
if matches!(key, Key::Enter) && self.databases.tree_focused() {
if let Some((table, database)) = self.databases.tree().selected_table() {
self.focus = Focus::Table;
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_records(&database, &table.name, 0, None)
.await?;
self.record_table = RecordTableComponent::new(records, headers);
self.record_table.set_table(table.name.to_string());
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_columns(&database, &table.name)
.await?;
self.structure_table = TableComponent::new(records, headers);
self.table_status
.update(self.record_table.len() as u64, table);
}
return Ok(EventState::Consumed);
}
}
Focus::Table => {
match self.tab.selected_tab {
Tab::Records => {
if self.record_table.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
};
if let Key::Char('y') = key {
if let Some(text) = self.record_table.table.selected_cell() {
self.clipboard.store(text)
}
}
if matches!(key, Key::Enter) && self.record_table.filter_focused() {
self.record_table.focus = crate::components::record_table::Focus::Table;
if let Some((table, database)) = self.databases.tree().selected_table()
{
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_records(
&database.clone(),
&table.name,
0,
if self.record_table.filter.input.is_empty() {
None
} else {
Some(self.record_table.filter.input_str())
},
)
.await?;
self.record_table.update(records, headers);
}
}
if self.record_table.table.eod {
return Ok(EventState::Consumed);
}
if let Some(index) = self.record_table.table.state.selected() {
if index.saturating_add(1)
% crate::utils::RECORDS_LIMIT_PER_PAGE as usize
== 0
{
if let Some((table, database)) =
self.databases.tree().selected_table()
{
let (_, records) = self
.pool
.as_ref()
.unwrap()
.get_records(
&database.clone(),
&table.name,
index as u16,
if self.record_table.filter.input.is_empty() {
None
} else {
Some(self.record_table.filter.input_str())
},
)
.await?;
if !records.is_empty() {
self.record_table.table.rows.extend(records);
} else {
self.record_table.table.end()
}
}
}
};
}
Tab::Structure => {
if self.structure_table.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
};
if let Key::Char('y') = key {
if let Some(text) = self.structure_table.selected_cell() {
self.clipboard.store(text)
}
};
}
};
}
}
Ok(EventState::NotConsumed)
}
pub fn move_focus(&mut self, key: Key) -> anyhow::Result<EventState> {
if let Key::Char('c') = key {
self.focus = Focus::ConnectionList;
return Ok(EventState::Consumed);
}
if self.tab.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
}
match self.focus {
Focus::ConnectionList => {
if let Key::Enter = key {
self.focus = Focus::DabataseList;
return Ok(EventState::Consumed);
}
}
Focus::DabataseList => match key {
Key::Right if self.databases.tree_focused() => {
self.focus = Focus::Table;
return Ok(EventState::Consumed);
}
_ => (),
},
Focus::Table => match key {
Key::Left => {
self.focus = Focus::DabataseList;
return Ok(EventState::Consumed);
}
_ => (),
},
}
Ok(EventState::NotConsumed)
}
}

@ -1,10 +1,10 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use crate::user_config::Connection;
use anyhow::Result;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
layout::Rect,
style::{Color, Style},
text::{Span, Spans},
widgets::{Block, Borders, Clear, List, ListItem, ListState},
@ -71,8 +71,8 @@ impl ConnectionsComponent {
impl DrawableComponent for ConnectionsComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
let percent_x = 60;
let percent_y = 50;
let width = 80;
let height = 20;
let conns = &self.connections;
let connections: Vec<ListItem> = conns
.iter()
@ -85,29 +85,13 @@ impl DrawableComponent for ConnectionsComponent {
.block(Block::default().borders(Borders::ALL).title("Connections"))
.highlight_style(Style::default().bg(Color::Blue))
.style(Style::default());
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]
.as_ref(),
)
.split(f.size());
let area = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]
.as_ref(),
)
.split(popup_layout[1])[1];
let area = Rect::new(
(f.size().width.saturating_sub(width)) / 2,
(f.size().height.saturating_sub(height)) / 2,
width.min(f.size().width),
height.min(f.size().height),
);
f.render_widget(Clear, area);
f.render_stateful_widget(tasks, area, &mut self.state);
Ok(())
@ -115,12 +99,18 @@ impl DrawableComponent for ConnectionsComponent {
}
impl Component for ConnectionsComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char('j') => self.next_connection(),
Key::Char('k') => self.previous_connection(),
Key::Char('j') => {
self.next_connection();
return Ok(EventState::Consumed);
}
Key::Char('k') => {
self.previous_connection();
return Ok(EventState::Consumed);
}
_ => (),
}
Ok(())
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,8 @@
use super::{utils::scroll_vertical::VerticalScroll, Component, DrawableComponent};
use super::{
compute_character_width, utils::scroll_vertical::VerticalScroll, Component, DrawableComponent,
EventState,
};
use crate::components::RecordTableComponent;
use crate::event::Key;
use crate::ui::common_nav;
use crate::ui::scrolllist::draw_list_block;
@ -32,8 +36,10 @@ pub struct DatabasesComponent {
pub tree: DatabaseTree,
pub filterd_tree: Option<DatabaseTree>,
pub scroll: VerticalScroll,
pub input: String,
pub input_cursor_x: u16,
pub input: Vec<char>,
pub input_idx: usize,
pub input_cursor_position: u16,
pub record_table: RecordTableComponent,
pub focus_block: FocusBlock,
}
@ -43,17 +49,23 @@ impl DatabasesComponent {
tree: DatabaseTree::default(),
filterd_tree: None,
scroll: VerticalScroll::new(),
input: String::new(),
input_cursor_x: 0,
input: Vec::new(),
input_idx: 0,
input_cursor_position: 0,
record_table: RecordTableComponent::default(),
focus_block: FocusBlock::Tree,
}
}
pub fn update(&mut self, list: &[Database], collapsed: &BTreeSet<&String>) -> Result<()> {
self.tree = DatabaseTree::new(list, collapsed)?;
pub fn input_str(&self) -> String {
self.input.iter().collect()
}
pub fn update(&mut self, list: &[Database]) -> Result<()> {
self.tree = DatabaseTree::new(list, &BTreeSet::new())?;
self.filterd_tree = None;
self.input = String::new();
self.input_cursor_x = 0;
self.input = Vec::new();
self.input_idx = 0;
Ok(())
}
@ -65,18 +77,6 @@ impl DatabasesComponent {
self.filterd_tree.as_ref().unwrap_or(&self.tree)
}
pub fn increment_input_cursor_x(&mut self) {
if self.input_cursor_x > 0 {
self.input_cursor_x -= 1;
}
}
pub fn decrement_input_cursor_x(&mut self) {
if self.input_cursor_x < self.input.width() as u16 {
self.input_cursor_x += 1;
}
}
fn tree_item_to_span(item: DatabaseTreeItem, selected: bool, width: u16) -> Span<'static> {
let name = item.kind().name();
let indent = item.info().indent();
@ -156,9 +156,9 @@ impl DatabasesComponent {
format!(
"{}{:w$}",
if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) {
" / to filter tables".to_string()
"Filter tables".to_string()
} else {
self.input.clone()
self.input_str()
},
w = area.width as usize
),
@ -187,10 +187,7 @@ impl DatabasesComponent {
);
self.scroll.draw(f, area);
if let FocusBlock::Filter = self.focus_block {
f.set_cursor(
area.x + self.input.width() as u16 + 1 - self.input_cursor_x,
area.y + 1,
)
f.set_cursor(area.x + self.input_cursor_position + 1, area.y + 1)
}
}
}
@ -208,54 +205,92 @@ impl DrawableComponent for DatabasesComponent {
}
impl Component for DatabasesComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
let input_str: String = self.input.iter().collect();
if tree_nav(
if let Some(tree) = self.filterd_tree.as_mut() {
tree
} else {
&mut self.tree
},
key,
) {
return Ok(EventState::Consumed);
}
match key {
Key::Char('/') if matches!(self.focus_block, FocusBlock::Tree) => {
self.focus_block = FocusBlock::Filter
self.focus_block = FocusBlock::Filter;
return Ok(EventState::Consumed);
}
Key::Char(c) if matches!(self.focus_block, FocusBlock::Filter) => {
self.input.push(c);
self.filterd_tree = Some(self.tree.filter(self.input.clone()))
self.input.insert(self.input_idx, c);
self.input_idx += 1;
self.input_cursor_position += compute_character_width(c);
self.filterd_tree = Some(self.tree.filter(self.input_str()));
return Ok(EventState::Consumed);
}
Key::Delete | Key::Backspace => {
if !self.input.is_empty() {
if self.input_cursor_x == 0 {
self.input.pop();
} else if self.input.width() - self.input_cursor_x as usize > 0 {
self.input.remove(
self.input
.width()
.saturating_sub(self.input_cursor_x as usize)
.saturating_sub(1),
);
Key::Delete | Key::Backspace if matches!(self.focus_block, FocusBlock::Filter) => {
if input_str.width() > 0 {
if !self.input.is_empty() && self.input_idx > 0 {
let last_c = self.input.remove(self.input_idx - 1);
self.input_idx -= 1;
self.input_cursor_position -= compute_character_width(last_c);
}
self.filterd_tree = if self.input.is_empty() {
None
} else {
Some(self.tree.filter(self.input.clone()))
}
Some(self.tree.filter(self.input_str()))
};
return Ok(EventState::Consumed);
}
}
Key::Left if matches!(self.focus_block, FocusBlock::Filter) => {
if !self.input.is_empty() && self.input_idx > 0 {
self.input_idx -= 1;
self.input_cursor_position = self
.input_cursor_position
.saturating_sub(compute_character_width(self.input[self.input_idx]));
}
return Ok(EventState::Consumed);
}
Key::Ctrl('a') => {
if !self.input.is_empty() && self.input_idx > 0 {
self.input_idx = 0;
self.input_cursor_position = 0
}
return Ok(EventState::Consumed);
}
Key::Right if matches!(self.focus_block, FocusBlock::Filter) => {
if self.input_idx < self.input.len() {
let next_c = self.input[self.input_idx];
self.input_idx += 1;
self.input_cursor_position += compute_character_width(next_c);
}
return Ok(EventState::Consumed);
}
Key::Ctrl('e') => {
if self.input_idx < self.input.len() {
self.input_idx = self.input.len();
self.input_cursor_position = self.input_str().width() as u16;
}
return Ok(EventState::Consumed);
}
Key::Left => self.decrement_input_cursor_x(),
Key::Right => self.increment_input_cursor_x(),
Key::Enter if matches!(self.focus_block, FocusBlock::Filter) => {
self.focus_block = FocusBlock::Tree
self.focus_block = FocusBlock::Tree;
return Ok(EventState::Consumed);
}
key => tree_nav(
if let Some(tree) = self.filterd_tree.as_mut() {
tree
} else {
&mut self.tree
},
key,
),
_ => (),
}
Ok(())
Ok(EventState::NotConsumed)
}
}
fn tree_nav(tree: &mut DatabaseTree, key: Key) {
fn tree_nav(tree: &mut DatabaseTree, key: Key) -> bool {
if let Some(common_nav) = common_nav(key) {
tree.move_selection(common_nav);
tree.move_selection(common_nav)
} else {
false
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use tui::{
@ -49,7 +49,7 @@ impl DrawableComponent for ErrorComponent {
}
impl Component for ErrorComponent {
fn event(&mut self, _key: Key) -> Result<()> {
Ok(())
fn event(&mut self, _key: Key) -> Result<EventState> {
Ok(EventState::NotConsumed)
}
}

@ -2,9 +2,10 @@ pub mod command;
pub mod connections;
pub mod databases;
pub mod error;
pub mod query;
pub mod record_table;
pub mod tab;
pub mod table;
pub mod table_filter;
pub mod table_status;
pub mod table_value;
pub mod utils;
@ -13,14 +14,18 @@ pub use command::{CommandInfo, CommandText};
pub use connections::ConnectionsComponent;
pub use databases::DatabasesComponent;
pub use error::ErrorComponent;
pub use query::QueryComponent;
pub use record_table::RecordTableComponent;
pub use tab::TabComponent;
pub use table::TableComponent;
pub use table_filter::TableFilterComponent;
pub use table_status::TableStatusComponent;
pub use table_value::TableValueComponent;
use anyhow::Result;
use async_trait::async_trait;
use std::convert::TryInto;
use tui::{backend::Backend, layout::Rect, Frame};
use unicode_width::UnicodeWidthChar;
#[derive(Copy, Clone)]
pub enum ScrollType {
@ -38,13 +43,36 @@ pub enum Direction {
Down,
}
#[derive(PartialEq)]
pub enum EventState {
Consumed,
NotConsumed,
}
impl EventState {
pub fn is_consumed(&self) -> bool {
*self == Self::Consumed
}
}
impl From<bool> for EventState {
fn from(consumed: bool) -> Self {
if consumed {
Self::Consumed
} else {
Self::NotConsumed
}
}
}
pub trait DrawableComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, rect: Rect, focused: bool) -> Result<()>;
}
/// base component trait
#[async_trait]
pub trait Component {
fn event(&mut self, key: crate::event::Key) -> Result<()>;
fn event(&mut self, key: crate::event::Key) -> Result<EventState>;
fn focused(&self) -> bool {
false
@ -71,3 +99,7 @@ pub trait Component {
}
}
}
fn compute_character_width(c: char) -> u16 {
UnicodeWidthChar::width(c).unwrap().try_into().unwrap()
}

@ -1,83 +0,0 @@
use super::{Component, DrawableComponent};
use crate::event::Key;
use anyhow::Result;
use tui::{
backend::Backend,
layout::Rect,
style::{Color, Style},
widgets::{Block, Borders, Paragraph},
Frame,
};
use unicode_width::UnicodeWidthStr;
pub struct QueryComponent {
pub input: String,
pub input_cursor_x: u16,
}
impl Default for QueryComponent {
fn default() -> Self {
Self {
input: String::new(),
input_cursor_x: 0,
}
}
}
impl QueryComponent {
pub fn increment_input_cursor_x(&mut self) {
if self.input_cursor_x > 0 {
self.input_cursor_x -= 1;
}
}
pub fn decrement_input_cursor_x(&mut self) {
if self.input_cursor_x < self.input.width() as u16 {
self.input_cursor_x += 1;
}
}
}
impl DrawableComponent for QueryComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let query = Paragraph::new(self.input.as_ref())
.style(if focused {
Style::default()
} else {
Style::default().fg(Color::DarkGray)
})
.block(Block::default().borders(Borders::ALL).title("Query"));
f.render_widget(query, area);
if focused {
f.set_cursor(
area.x + self.input.width() as u16 + 1 - self.input_cursor_x,
area.y + 1,
)
}
Ok(())
}
}
impl Component for QueryComponent {
fn event(&mut self, key: Key) -> Result<()> {
match key {
Key::Char(c) => self.input.push(c),
Key::Delete | Key::Backspace => {
if self.input.width() > 0 {
if self.input_cursor_x == 0 {
self.input.pop();
return Ok(());
}
if self.input.width() - self.input_cursor_x as usize > 0 {
self.input
.remove(self.input.width() - self.input_cursor_x as usize);
}
}
}
Key::Left => self.decrement_input_cursor_x(),
Key::Right => self.increment_input_cursor_x(),
_ => (),
}
Ok(())
}
}

@ -0,0 +1,100 @@
use super::{Component, DrawableComponent, EventState};
use crate::components::{TableComponent, TableFilterComponent};
use crate::event::Key;
use anyhow::Result;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
Frame,
};
pub enum Focus {
Table,
Filter,
}
pub struct RecordTableComponent {
pub filter: TableFilterComponent,
pub table: TableComponent,
pub focus: Focus,
}
impl Default for RecordTableComponent {
fn default() -> Self {
Self {
filter: TableFilterComponent::default(),
table: TableComponent::default(),
focus: Focus::Table,
}
}
}
impl RecordTableComponent {
pub fn new(rows: Vec<Vec<String>>, headers: Vec<String>) -> Self {
Self {
table: TableComponent::new(rows, headers),
..Self::default()
}
}
pub fn update(&mut self, rows: Vec<Vec<String>>, headers: Vec<String>) {
self.table.rows = rows;
self.table.headers = headers;
if !self.table.rows.is_empty() {
self.table.state.select(None);
self.table.state.select(Some(0));
}
}
pub fn reset(&mut self) {
self.table = TableComponent::default();
if !self.table.rows.is_empty() {
self.table.state.select(None);
self.table.state.select(Some(0))
}
self.filter = TableFilterComponent::default();
}
pub fn len(&self) -> usize {
self.table.rows.len()
}
pub fn set_table(&mut self, table: String) {
self.filter.table = Some(table)
}
pub fn filter_focused(&self) -> bool {
matches!(self.focus, Focus::Filter)
}
}
impl DrawableComponent for RecordTableComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(3), Constraint::Length(5)])
.split(area);
self.filter
.draw(f, layout[0], focused && matches!(self.focus, Focus::Filter))?;
self.table
.draw(f, layout[1], focused && matches!(self.focus, Focus::Table))?;
Ok(())
}
}
impl Component for RecordTableComponent {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char('/') => {
self.focus = Focus::Filter;
return Ok(EventState::Consumed);
}
key if matches!(self.focus, Focus::Filter) => return Ok(self.filter.event(key)?),
key if matches!(self.focus, Focus::Table) => return Ok(self.table.event(key)?),
_ => (),
}
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use strum::IntoEnumIterator;
@ -62,12 +62,17 @@ impl DrawableComponent for TabComponent {
}
impl Component for TabComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char('1') => self.selected_tab = Tab::Records,
Key::Char('2') => self.selected_tab = Tab::Structure,
_ => (),
Key::Char('1') => {
self.selected_tab = Tab::Records;
Ok(EventState::Consumed)
}
Key::Char('2') => {
self.selected_tab = Tab::Structure;
Ok(EventState::Consumed)
}
_ => Ok(EventState::NotConsumed),
}
Ok(())
}
}

@ -1,5 +1,6 @@
use super::{
utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, TableValueComponent,
utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState,
TableValueComponent,
};
use crate::event::Key;
use anyhow::Result;
@ -11,9 +12,6 @@ use tui::{
widgets::{Block, Borders, Cell, Row, Table, TableState},
Frame,
};
use unicode_width::UnicodeWidthStr;
pub const RECORDS_LIMIT_PER_PAGE: u8 = 200;
pub struct TableComponent {
pub state: TableState,
@ -23,6 +21,7 @@ pub struct TableComponent {
pub column_page: usize,
pub scroll: VerticalScroll,
pub select_entire_row: bool,
pub eod: bool,
}
impl Default for TableComponent {
@ -32,25 +31,33 @@ impl Default for TableComponent {
headers: vec![],
rows: vec![],
column_page: 0,
column_index: 0,
column_index: 1,
scroll: VerticalScroll::new(),
select_entire_row: false,
eod: false,
}
}
}
impl TableComponent {
pub fn reset(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
self.headers = headers;
self.rows = rows;
self.column_page = 0;
self.column_index = 1;
self.state.select(None);
if !self.rows.is_empty() {
self.state.select(Some(0));
pub fn new(rows: Vec<Vec<String>>, headers: Vec<String>) -> Self {
let mut state = TableState::default();
if !rows.is_empty() {
state.select(None);
state.select(Some(0))
}
Self {
rows,
headers,
state,
..Self::default()
}
}
pub fn end(&mut self) {
self.eod = true;
}
pub fn next(&mut self, lines: usize) {
let i = match self.state.selected() {
Some(i) => {
@ -239,20 +246,47 @@ impl DrawableComponent for TableComponent {
}
impl Component for TableComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char('h') => self.previous_column(),
Key::Char('j') => self.next(1),
Key::Ctrl('d') => self.next(10),
Key::Char('k') => self.previous(1),
Key::Ctrl('u') => self.previous(10),
Key::Char('g') => self.scroll_top(),
Key::Char('r') => self.select_entire_row = true,
Key::Shift('G') | Key::Shift('g') => self.scroll_bottom(),
Key::Char('l') => self.next_column(),
Key::Char('h') => {
self.previous_column();
return Ok(EventState::Consumed);
}
Key::Char('j') => {
self.next(1);
return Ok(EventState::NotConsumed);
}
Key::Ctrl('d') => {
self.next(10);
return Ok(EventState::NotConsumed);
}
Key::Char('k') => {
self.previous(1);
return Ok(EventState::Consumed);
}
Key::Ctrl('u') => {
self.previous(10);
return Ok(EventState::Consumed);
}
Key::Char('g') => {
self.scroll_top();
return Ok(EventState::Consumed);
}
Key::Char('r') => {
self.select_entire_row = true;
return Ok(EventState::Consumed);
}
Key::Char('G') => {
self.scroll_bottom();
return Ok(EventState::Consumed);
}
Key::Char('l') => {
self.next_column();
return Ok(EventState::Consumed);
}
_ => (),
}
Ok(())
Ok(EventState::NotConsumed)
}
}

@ -0,0 +1,136 @@
use super::{compute_character_width, Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use tui::{
backend::Backend,
layout::Rect,
style::{Color, Style},
text::{Span, Spans},
widgets::{Block, Borders, Paragraph},
Frame,
};
use unicode_width::UnicodeWidthStr;
pub struct TableFilterComponent {
pub table: Option<String>,
pub input: Vec<char>,
pub input_idx: usize,
pub input_cursor_position: u16,
}
impl Default for TableFilterComponent {
fn default() -> Self {
Self {
table: None,
input: Vec::new(),
input_idx: 0,
input_cursor_position: 0,
}
}
}
impl TableFilterComponent {
pub fn input_str(&self) -> String {
self.input.iter().collect()
}
}
impl DrawableComponent for TableFilterComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let query = Paragraph::new(Spans::from(vec![
Span::styled(
self.table
.as_ref()
.map_or("-".to_string(), |table| table.to_string()),
Style::default().fg(Color::Blue),
),
Span::from(format!(
" {}",
if focused || !self.input.is_empty() {
self.input.iter().collect::<String>()
} else {
"Enter a SQL expression in WHERE clause".to_string()
}
)),
]))
.style(if focused {
Style::default()
} else {
Style::default().fg(Color::DarkGray)
})
.block(Block::default().borders(Borders::ALL));
f.render_widget(query, area);
if focused {
f.set_cursor(
(area.x
+ (1 + self
.table
.as_ref()
.map_or(String::new(), |table| table.to_string())
.width()
+ 1) as u16)
.saturating_add(self.input_cursor_position),
area.y + 1,
)
}
Ok(())
}
}
impl Component for TableFilterComponent {
fn event(&mut self, key: Key) -> Result<EventState> {
let input_str: String = self.input.iter().collect();
match key {
Key::Char(c) => {
self.input.insert(self.input_idx, c);
self.input_idx += 1;
self.input_cursor_position += compute_character_width(c);
return Ok(EventState::Consumed);
}
Key::Delete | Key::Backspace => {
if input_str.width() > 0 {
if !self.input.is_empty() && self.input_idx > 0 {
let last_c = self.input.remove(self.input_idx - 1);
self.input_idx -= 1;
self.input_cursor_position -= compute_character_width(last_c);
}
}
return Ok(EventState::Consumed);
}
Key::Left => {
if !self.input.is_empty() && self.input_idx > 0 {
self.input_idx -= 1;
self.input_cursor_position = self
.input_cursor_position
.saturating_sub(compute_character_width(self.input[self.input_idx]));
}
return Ok(EventState::Consumed);
}
Key::Ctrl('a') => {
if !self.input.is_empty() && self.input_idx > 0 {
self.input_idx = 0;
self.input_cursor_position = 0
}
return Ok(EventState::Consumed);
}
Key::Right => {
if self.input_idx < self.input.len() {
let next_c = self.input[self.input_idx];
self.input_idx += 1;
self.input_cursor_position += compute_character_width(next_c);
}
return Ok(EventState::Consumed);
}
Key::Ctrl('e') => {
if self.input_idx < self.input.len() {
self.input_idx = self.input.len();
self.input_cursor_position = self.input_str().width() as u16;
}
return Ok(EventState::Consumed);
}
_ => (),
}
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use database_tree::Table;
@ -85,7 +85,7 @@ impl DrawableComponent for TableStatusComponent {
}
impl Component for TableStatusComponent {
fn event(&mut self, _key: Key) -> Result<()> {
Ok(())
fn event(&mut self, _key: Key) -> Result<EventState> {
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use tui::{
@ -45,7 +45,7 @@ impl DrawableComponent for TableValueComponent {
}
impl Component for TableValueComponent {
fn event(&mut self, _key: Key) -> Result<()> {
fn event(&mut self, _key: Key) -> Result<EventState> {
todo!("scroll");
}
}

@ -1,9 +1,7 @@
use crate::{components::ScrollType, ui::scrollbar::draw_scrollbar};
use std::cell::Cell;
use tui::{backend::Backend, layout::Rect, Frame};
use crate::{components::ScrollType, ui::scrollbar::draw_scrollbar};
pub struct VerticalScroll {
top: Cell<usize>,
max_top: Cell<usize>,

@ -63,7 +63,6 @@ pub enum Key {
F12,
Char(char),
Ctrl(char),
Shift(char),
Alt(char),
Unkown,
}
@ -104,7 +103,6 @@ impl fmt::Display for Key {
Key::Char(' ') => write!(f, "<Space>"),
Key::Alt(c) => write!(f, "<Alt+{}>", c),
Key::Ctrl(c) => write!(f, "<Ctrl+{}>", c),
Key::Shift(c) => write!(f, "<Shift+{}>", c),
Key::Char(c) => write!(f, "{}", c),
Key::Left | Key::Right | Key::Up | Key::Down => write!(f, "<{:?} Arrow Key>", self),
Key::Enter
@ -195,10 +193,6 @@ impl From<event::KeyEvent> for Key {
code: event::KeyCode::Char(c),
modifiers: event::KeyModifiers::CONTROL,
} => Key::Ctrl(c),
event::KeyEvent {
code: event::KeyCode::Char(c),
modifiers: event::KeyModifiers::SHIFT,
} => Key::Shift(c),
event::KeyEvent {
code: event::KeyCode::Char(c),

@ -1,47 +0,0 @@
use crate::app::{App, FocusBlock};
use crate::components::Component as _;
use crate::event::Key;
use crate::utils::{get_databases, get_tables};
use database_tree::Database;
use sqlx::mysql::MySqlPool;
use std::collections::BTreeSet;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Enter => {
app.record_table.reset(vec![], vec![]);
app.record_table.state.select(Some(0));
if let Some(conn) = app.connections.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_block = FocusBlock::DabataseList;
}
if let Some(conn) = app.connections.selected_connection() {
match &conn.database {
Some(database) => app
.databases
.update(
&[Database::new(
database.clone(),
get_tables(database.clone(), app.pool.as_ref().unwrap()).await?,
)],
&BTreeSet::new(),
)
.unwrap(),
None => app
.databases
.update(
get_databases(app.pool.as_ref().unwrap()).await?.as_slice(),
&BTreeSet::new(),
)
.unwrap(),
}
};
}
key => app.connections.event(key)?,
}
Ok(())
}

@ -1,48 +0,0 @@
use crate::app::{App, FocusBlock};
use crate::components::table::RECORDS_LIMIT_PER_PAGE;
use crate::components::Component as _;
use crate::event::Key;
use crate::utils::{get_columns, get_records};
use database_tree::Database;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Char('c') if app.databases.tree_focused() => {
app.focus_block = FocusBlock::ConnectionList
}
Key::Right if app.databases.tree_focused() => app.focus_block = FocusBlock::Table,
Key::Enter if app.databases.tree_focused() => {
if let Some((table, database)) = app.databases.tree().selected_table() {
app.focus_block = FocusBlock::Table;
let (headers, records) = get_records(
&Database {
name: database.clone(),
tables: vec![],
},
&table,
0,
RECORDS_LIMIT_PER_PAGE,
app.pool.as_ref().unwrap(),
)
.await?;
app.record_table.reset(headers, records);
let (headers, records) = get_columns(
&Database {
name: database,
tables: vec![],
},
&table,
app.pool.as_ref().unwrap(),
)
.await?;
app.structure_table.reset(headers, records);
app.table_status
.update(app.record_table.rows.len() as u64, table);
}
}
key => app.databases.event(key)?,
}
Ok(())
}

@ -1,32 +0,0 @@
pub mod connection_list;
pub mod database_list;
pub mod query;
pub mod record_table;
pub mod structure_table;
use crate::app::{App, FocusBlock};
use crate::components::tab::Tab;
use crate::components::Component as _;
use crate::event::Key;
pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Ctrl('e') => app.focus_block = FocusBlock::Query,
Key::Esc if app.error.error.is_some() => {
app.error.error = None;
return Ok(());
}
key => app.tab.event(key)?,
}
match app.focus_block {
FocusBlock::ConnectionList => connection_list::handler(key, app).await?,
FocusBlock::DabataseList => database_list::handler(key, app).await?,
FocusBlock::Table => match app.tab.selected_tab {
Tab::Records => record_table::handler(key, app).await?,
Tab::Structure => structure_table::handler(key, app).await?,
},
FocusBlock::Query => query::handler(key, app).await?,
}
Ok(())
}

@ -1,46 +0,0 @@
use crate::app::{App, FocusBlock};
use crate::components::Component as _;
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<()> {
match key {
Key::Enter => {
let re = Regex::new(r"select .+ from ([^ ]+).*").unwrap();
match re.captures(app.query.input.as_str()) {
Some(caps) => {
let mut rows =
sqlx::query(app.query.input.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::<Vec<String>>();
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::<Vec<String>>(),
)
}
app.record_table.reset(headers, records);
}
None => {
sqlx::query(app.query.input.as_str())
.execute(app.pool.as_ref().unwrap())
.await?;
}
}
}
Key::Esc => app.focus_block = FocusBlock::Table,
key => app.query.event(key)?,
}
Ok(())
}

@ -1,42 +0,0 @@
use crate::app::{App, FocusBlock};
use crate::components::table::RECORDS_LIMIT_PER_PAGE;
use crate::components::Component as _;
use crate::event::Key;
use crate::utils::get_records;
use database_tree::Database;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Left => app.focus_block = FocusBlock::DabataseList,
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Char('y') => {
if let Some(text) = app.record_table.selected_cell() {
app.clipboard.store(text)
}
}
key => {
app.record_table.event(key)?;
if let Some(index) = app.record_table.state.selected() {
if index == app.record_table.rows.len().saturating_sub(1) {
if let Some((table, database)) = app.databases.tree().selected_table() {
let (_, records) = get_records(
&Database {
name: database.clone(),
tables: vec![],
},
&table,
index as u16,
RECORDS_LIMIT_PER_PAGE,
app.pool.as_ref().unwrap(),
)
.await?;
if !records.is_empty() {
app.record_table.rows.extend(records);
}
}
}
}
}
}
Ok(())
}

@ -1,17 +0,0 @@
use crate::app::{App, FocusBlock};
use crate::components::Component as _;
use crate::event::Key;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Left => app.focus_block = FocusBlock::DabataseList,
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Char('y') => {
if let Some(text) = app.structure_table.selected_cell() {
app.clipboard.store(text)
}
}
key => app.structure_table.event(key)?,
}
Ok(())
}

@ -2,7 +2,6 @@ mod app;
mod clipboard;
mod components;
mod event;
mod handlers;
mod ui;
mod user_config;
mod utils;
@ -12,7 +11,6 @@ mod log;
use crate::app::App;
use crate::event::{Event, Key};
use crate::handlers::handle_app;
use anyhow::Result;
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
@ -45,15 +43,14 @@ async fn main() -> anyhow::Result<()> {
loop {
terminal.draw(|f| app.draw(f).unwrap())?;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
};
match handle_app(key, &mut app).await {
Ok(_) => (),
Err(err) => app.error.set(err.to_string()),
Event::Input(key) => match app.event(key).await {
Ok(state) => {
if !state.is_consumed() && (key == Key::Char('q') || key == Key::Ctrl('c')) {
break;
}
}
}
Err(err) => app.error.set(err.to_string()),
},
Event::Tick => (),
}
}

@ -4,13 +4,12 @@ use tui::{
backend::Backend,
buffer::Buffer,
layout::{Margin, Rect},
style::Style,
style::{Color, Style},
symbols::{block::FULL, line::DOUBLE_VERTICAL},
widgets::Widget,
Frame,
};
///
struct Scrollbar {
max: u16,
pos: u16,
@ -70,6 +69,6 @@ impl Widget for Scrollbar {
pub fn draw_scrollbar<B: Backend>(f: &mut Frame<B>, r: Rect, max: usize, pos: usize) {
let mut widget = Scrollbar::new(max, pos);
widget.style_pos = Style::default();
widget.style_pos = Style::default().fg(Color::Blue);
f.render_widget(widget, r);
}

@ -1,94 +1,150 @@
use async_trait::async_trait;
use chrono::NaiveDate;
use database_tree::{Database, Table};
use futures::TryStreamExt;
use sqlx::mysql::{MySqlColumn, MySqlPool, MySqlRow};
use sqlx::mysql::{MySqlColumn, MySqlPool as MPool, MySqlRow};
use sqlx::{Column as _, Row, TypeInfo};
pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SHOW DATABASES")
.fetch_all(pool)
.await?
.iter()
.map(|table| table.get(0))
.collect::<Vec<String>>();
let mut list = vec![];
for db in databases {
list.push(Database::new(
db.clone(),
get_tables(db.clone(), pool).await?,
))
}
Ok(list)
pub const RECORDS_LIMIT_PER_PAGE: u8 = 200;
#[async_trait]
pub trait Pool {
async fn get_databases(&self) -> anyhow::Result<Vec<Database>>;
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Table>>;
async fn get_records(
&self,
database: &String,
table: &String,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn get_columns(
&self,
database: &String,
table: &String,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn close(&self);
}
pub async fn get_tables(database: String, pool: &MySqlPool) -> anyhow::Result<Vec<Table>> {
let tables =
sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
.fetch_all(pool)
.await?;
Ok(tables)
pub struct MySqlPool {
pool: MPool,
}
pub async fn get_records(
database: &Database,
table: &Table,
page: u16,
limit: u8,
pool: &MySqlPool,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = format!(
"SELECT * FROM `{}`.`{}` limit {page}, {limit}",
database.name,
table.name,
page = page,
limit = limit
);
let mut rows = sqlx::query(query.as_str()).fetch(pool);
let headers =
sqlx::query(format!("SHOW COLUMNS FROM `{}`.`{}`", database.name, table.name).as_str())
.fetch_all(pool)
impl MySqlPool {
pub async fn new(database_url: &str) -> anyhow::Result<Self> {
Ok(Self {
pool: MPool::connect(database_url).await?,
})
}
}
#[async_trait]
impl Pool for MySqlPool {
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SHOW DATABASES")
.fetch_all(&self.pool)
.await?
.iter()
.map(|table| table.get(0))
.collect::<Vec<String>>();
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
records.push(
row.columns()
let mut list = vec![];
for db in databases {
list.push(Database::new(
db.clone(),
get_tables(db.clone(), &self.pool).await?,
))
}
Ok(list)
}
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Table>> {
let tables =
sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
.fetch_all(&self.pool)
.await?;
Ok(tables)
}
async fn get_records(
&self,
database: &String,
table: &String,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter {
format!(
"SELECT * FROM `{database}`.`{table}` WHERE {filter} LIMIT {page}, {limit}",
database = database,
table = table,
filter = filter,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else {
format!(
"SELECT * FROM `{}`.`{}` limit {page}, {limit}",
database,
table,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
};
let mut rows = sqlx::query(query.as_str()).fetch(&self.pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
.map(|column| column.name().to_string())
.collect();
records.push(
row.columns()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
}
Ok((headers, records))
}
Ok((headers, records))
}
pub async fn get_columns(
database: &Database,
table: &Table,
pool: &MySqlPool,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = format!(
"SHOW FULL COLUMNS FROM `{}`.`{}`",
database.name, table.name
);
let mut rows = sqlx::query(query.as_str()).fetch(pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|column| column.name().to_string())
.collect();
records.push(
row.columns()
async fn get_columns(
&self,
database: &String,
table: &String,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = format!("SHOW FULL COLUMNS FROM `{}`.`{}`", database, table);
let mut rows = sqlx::query(query.as_str()).fetch(&self.pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
.map(|column| column.name().to_string())
.collect();
records.push(
row.columns()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
}
Ok((headers, records))
}
Ok((headers, records))
async fn close(&self) {
self.pool.close().await;
}
}
pub async fn get_tables(database: String, pool: &MPool) -> anyhow::Result<Vec<Table>> {
let tables =
sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
.fetch_all(pool)
.await?;
Ok(tables)
}
pub fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> String {

Loading…
Cancel
Save