diff --git a/src/app.rs b/src/app.rs index 28fab7e..93cff23 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,7 +24,8 @@ pub enum Focus { } pub struct App { record_table: RecordTableComponent, - structure_table: TableComponent, + column_table: TableComponent, + constraint_table: TableComponent, focus: Focus, tab: TabComponent, help: HelpComponent, @@ -42,7 +43,8 @@ impl App { config: config.clone(), connections: ConnectionsComponent::new(config.key_config.clone(), config.conn), record_table: RecordTableComponent::new(config.key_config.clone()), - structure_table: TableComponent::new(config.key_config.clone()), + column_table: TableComponent::new(config.key_config.clone()), + constraint_table: TableComponent::new(config.key_config.clone()), tab: TabComponent::new(config.key_config.clone()), help: HelpComponent::new(config.key_config.clone()), databases: DatabasesComponent::new(config.key_config.clone()), @@ -93,10 +95,15 @@ impl App { self.record_table .draw(f, right_chunks[1], matches!(self.focus, Focus::Table))? } - Tab::Structure => { - self.structure_table + Tab::Columns => { + self.column_table .draw(f, right_chunks[1], matches!(self.focus, Focus::Table))? } + Tab::Constraints => self.constraint_table.draw( + f, + right_chunks[1], + matches!(self.focus, Focus::Table), + )?, } self.error.draw(f, Rect::default(), false)?; self.help.draw(f, Rect::default(), false)?; @@ -162,6 +169,7 @@ impl App { async fn update_table(&mut self) -> anyhow::Result<()> { if let Some((database, table)) = self.databases.tree().selected_table() { self.focus = Focus::Table; + self.record_table.reset(); let (headers, records) = self .pool .as_ref() @@ -171,14 +179,42 @@ impl App { self.record_table .update(records, headers, database.clone(), table.clone()); - let (headers, records) = self + self.column_table.reset(); + let columns = self .pool .as_ref() .unwrap() .get_columns(&database, &table) .await?; - self.structure_table - .update(records, headers, database.clone(), table.clone()); + if !columns.is_empty() { + self.column_table.update( + columns + .iter() + .map(|c| c.columns()) + .collect::>>(), + columns.get(0).unwrap().fields(), + database.clone(), + table.clone(), + ); + } + self.constraint_table.reset(); + let constraints = self + .pool + .as_ref() + .unwrap() + .get_constraints(&database, &table) + .await?; + if !constraints.is_empty() { + self.constraint_table.update( + constraints + .iter() + .map(|c| c.columns()) + .collect::>>(), + constraints.get(0).unwrap().fields(), + database.clone(), + table.clone(), + ); + } self.table_status .update(self.record_table.len() as u64, table); } @@ -303,13 +339,24 @@ impl App { } }; } - Tab::Structure => { - if self.structure_table.event(key)?.is_consumed() { + Tab::Columns => { + if self.column_table.event(key)?.is_consumed() { + return Ok(EventState::Consumed); + }; + + if key == self.config.key_config.copy { + if let Some(text) = self.column_table.selected_cells() { + copy_to_clipboard(text.as_str())? + } + }; + } + Tab::Constraints => { + if self.constraint_table.event(key)?.is_consumed() { return Ok(EventState::Consumed); }; if key == self.config.key_config.copy { - if let Some(text) = self.structure_table.selected_cells() { + if let Some(text) = self.column_table.selected_cells() { copy_to_clipboard(text.as_str())? } }; diff --git a/src/components/command.rs b/src/components/command.rs index c5adb28..b10806a 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -99,9 +99,13 @@ pub fn tab_records(key: &KeyConfig) -> CommandText { CommandText::new(format!("Records [{}]", key.tab_records), CMD_GROUP_TABLE) } -pub fn tab_structure(key: &KeyConfig) -> CommandText { +pub fn tab_columns(key: &KeyConfig) -> CommandText { + CommandText::new(format!("Columns [{}]", key.tab_columns), CMD_GROUP_TABLE) +} + +pub fn tab_constraints(key: &KeyConfig) -> CommandText { CommandText::new( - format!("Structure [{}]", key.tab_structure), + format!("Constraints [{}]", key.tab_constraints), CMD_GROUP_TABLE, ) } @@ -109,8 +113,8 @@ pub fn tab_structure(key: &KeyConfig) -> CommandText { pub fn toggle_tabs(key_config: &KeyConfig) -> CommandText { CommandText::new( format!( - "Tab [{},{}]", - key_config.tab_records, key_config.tab_structure + "Tab [{},{},{}]", + key_config.tab_records, key_config.tab_columns, key_config.tab_constraints ), CMD_GROUP_GENERAL, ) diff --git a/src/components/tab.rs b/src/components/tab.rs index d853bc6..f7b946f 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -16,7 +16,8 @@ use tui::{ #[derive(Debug, Clone, Copy, EnumIter)] pub enum Tab { Records, - Structure, + Columns, + Constraints, } impl std::fmt::Display for Tab { @@ -45,7 +46,8 @@ impl TabComponent { fn names(&self) -> Vec { vec![ command::tab_records(&self.key_config).name, - command::tab_structure(&self.key_config).name, + command::tab_columns(&self.key_config).name, + command::tab_constraints(&self.key_config).name, ] } } @@ -74,8 +76,11 @@ impl Component for TabComponent { if key == self.key_config.tab_records { self.selected_tab = Tab::Records; return Ok(EventState::Consumed); - } else if key == self.key_config.tab_structure { - self.selected_tab = Tab::Structure; + } else if key == self.key_config.tab_columns { + self.selected_tab = Tab::Columns; + return Ok(EventState::Consumed); + } else if key == self.key_config.tab_constraints { + self.selected_tab = Tab::Constraints; return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) diff --git a/src/components/table.rs b/src/components/table.rs index b420b00..1237732 100644 --- a/src/components/table.rs +++ b/src/components/table.rs @@ -48,7 +48,7 @@ impl TableComponent { fn title(&self) -> String { self.table.as_ref().map_or(" - ".to_string(), |table| { - format!("{}/{}", table.0.name, table.1.name) + format!("{}.{}", table.0.name, table.1.name) }) } diff --git a/src/config.rs b/src/config.rs index 86f209e..14098b3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -79,7 +79,8 @@ pub struct KeyConfig { pub extend_selection_by_one_cell_up: Key, pub extend_selection_by_one_cell_down: Key, pub tab_records: Key, - pub tab_structure: Key, + pub tab_columns: Key, + pub tab_constraints: Key, } impl Default for KeyConfig { @@ -108,7 +109,8 @@ impl Default for KeyConfig { extend_selection_by_one_cell_down: Key::Char('J'), extend_selection_by_one_cell_up: Key::Char('K'), tab_records: Key::Char('1'), - tab_structure: Key::Char('2'), + tab_columns: Key::Char('2'), + tab_constraints: Key::Char('3'), } } } diff --git a/src/database/mod.rs b/src/database/mod.rs index c3e2572..93f219f 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -24,6 +24,16 @@ pub trait Pool { &self, database: &Database, table: &Table, - ) -> anyhow::Result<(Vec, Vec>)>; + ) -> anyhow::Result>>; + async fn get_constraints( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result>>; async fn close(&self); } + +pub trait TableRow: std::marker::Send { + fn fields(&self) -> Vec; + fn columns(&self) -> Vec; +} diff --git a/src/database/mysql.rs b/src/database/mysql.rs index 62b3d8d..e143c79 100644 --- a/src/database/mysql.rs +++ b/src/database/mysql.rs @@ -1,4 +1,4 @@ -use super::{Pool, RECORDS_LIMIT_PER_PAGE}; +use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE}; use async_trait::async_trait; use chrono::NaiveDate; use database_tree::{Child, Database, Table}; @@ -18,6 +18,61 @@ impl MySqlPool { } } +pub struct Constraint { + name: String, + column_name: String, +} + +impl TableRow for Constraint { + fn fields(&self) -> Vec { + vec!["name".to_string(), "column_name".to_string()] + } + + fn columns(&self) -> Vec { + vec![self.name.to_string(), self.column_name.to_string()] + } +} + +pub struct Column { + name: Option, + r#type: Option, + null: Option, + default: Option, + comment: Option, +} + +impl TableRow for Column { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "type".to_string(), + "null".to_string(), + "default".to_string(), + "comment".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.r#type + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.null + .as_ref() + .map_or(String::new(), |null| null.to_string()), + self.default + .as_ref() + .map_or(String::new(), |default| default.to_string()), + self.comment + .as_ref() + .map_or(String::new(), |comment| comment.to_string()), + ] + } +} + #[async_trait] impl Pool for MySqlPool { async fn get_databases(&self) -> anyhow::Result> { @@ -92,27 +147,53 @@ impl Pool for MySqlPool { &self, database: &Database, table: &Table, - ) -> anyhow::Result<(Vec, Vec>)> { + ) -> anyhow::Result>> { let query = format!( "SHOW FULL COLUMNS FROM `{}`.`{}`", database.name, table.name ); let mut rows = sqlx::query(query.as_str()).fetch(&self.pool); - let mut headers = vec![]; - let mut records = vec![]; + let mut columns: Vec> = vec![]; while let Some(row) = rows.try_next().await? { - headers = row - .columns() - .iter() - .map(|column| column.name().to_string()) - .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) + columns.push(Box::new(Column { + name: row.try_get("Field")?, + r#type: row.try_get("Type")?, + null: row.try_get("Null")?, + default: row.try_get("Default")?, + comment: row.try_get("Comment")?, + })) } - Ok((headers, records)) + Ok(columns) + } + + async fn get_constraints( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result>> { + let mut rows = sqlx::query( + " + SELECT + COLUMN_NAME, + CONSTRAINT_NAME + FROM + information_schema.KEY_COLUMN_USAGE + WHERE + TABLE_SCHEMA = ? + AND TABLE_NAME = ? + ", + ) + .bind(&database.name) + .bind(&table.name) + .fetch(&self.pool); + let mut constraints: Vec> = vec![]; + while let Some(row) = rows.try_next().await? { + constraints.push(Box::new(Constraint { + name: row.try_get("CONSTRAINT_NAME")?, + column_name: row.try_get("COLUMN_NAME")?, + })) + } + Ok(constraints) } async fn close(&self) { @@ -124,7 +205,7 @@ fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyho let column_name = column.name(); 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())); + return Ok(value.unwrap_or_else(|| "NULL".to_string())); } if let Ok(value) = row.try_get(column_name) { let value: Option<&str> = value; diff --git a/src/database/postgres.rs b/src/database/postgres.rs index 9686b6f..0362ab5 100644 --- a/src/database/postgres.rs +++ b/src/database/postgres.rs @@ -1,4 +1,4 @@ -use super::{Pool, RECORDS_LIMIT_PER_PAGE}; +use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE}; use async_trait::async_trait; use chrono::NaiveDate; use database_tree::{Child, Database, Schema, Table}; @@ -19,6 +19,61 @@ impl PostgresPool { } } +pub struct Constraint { + name: String, + column_name: String, +} + +impl TableRow for Constraint { + fn fields(&self) -> Vec { + vec!["name".to_string(), "column_name".to_string()] + } + + fn columns(&self) -> Vec { + vec![self.name.to_string(), self.column_name.to_string()] + } +} + +pub struct Column { + name: Option, + r#type: Option, + null: Option, + default: Option, + comment: Option, +} + +impl TableRow for Column { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "type".to_string(), + "null".to_string(), + "default".to_string(), + "comment".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.r#type + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.null + .as_ref() + .map_or(String::new(), |null| null.to_string()), + self.default + .as_ref() + .map_or(String::new(), |default| default.to_string()), + self.comment + .as_ref() + .map_or(String::new(), |comment| comment.to_string()), + ] + } +} + #[async_trait] impl Pool for PostgresPool { async fn get_databases(&self) -> anyhow::Result> { @@ -46,11 +101,11 @@ impl Pool for PostgresPool { let mut tables = Vec::new(); while let Some(row) = rows.try_next().await? { tables.push(Table { - name: row.get("table_name"), + name: row.try_get("table_name")?, create_time: None, update_time: None, engine: None, - schema: row.get("table_schema"), + schema: row.try_get("table_schema")?, }) } let mut schemas = vec![]; @@ -147,7 +202,7 @@ impl Pool for PostgresPool { &self, database: &Database, table: &Table, - ) -> anyhow::Result<(Vec, Vec>)> { + ) -> anyhow::Result>> { let table_schema = table .schema .as_ref() @@ -157,21 +212,49 @@ impl Pool for PostgresPool { ) .bind(&database.name).bind(table_schema).bind(&table.name) .fetch(&self.pool); - let mut headers = vec![]; - let mut records = vec![]; + let mut columns: Vec> = vec![]; while let Some(row) = rows.try_next().await? { - headers = row - .columns() - .iter() - .map(|column| column.name().to_string()) - .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) + columns.push(Box::new(Column { + name: row.try_get("column_name")?, + r#type: row.try_get("data_type")?, + null: row.try_get("is_nullable")?, + default: row.try_get("column_default")?, + comment: None, + })) } - Ok((headers, records)) + Ok(columns) + } + + async fn get_constraints( + &self, + _database: &Database, + table: &Table, + ) -> anyhow::Result>> { + let mut rows = sqlx::query( + " + SELECT + tc.constraint_name, tc.table_name, kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + WHERE ccu.table_name = $1 + ", + ) + .bind(&table.name) + .fetch(&self.pool); + let mut constraints: Vec> = vec![]; + while let Some(row) = rows.try_next().await? { + constraints.push(Box::new(Constraint { + name: row.try_get("constraint_name")?, + column_name: row.try_get("column_name")?, + })) + } + Ok(constraints) } async fn close(&self) {