diff --git a/src/app.rs b/src/app.rs index 93cff23..74c8bd2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -26,6 +26,7 @@ pub struct App { record_table: RecordTableComponent, column_table: TableComponent, constraint_table: TableComponent, + foreign_key_table: TableComponent, focus: Focus, tab: TabComponent, help: HelpComponent, @@ -45,6 +46,7 @@ impl App { record_table: RecordTableComponent::new(config.key_config.clone()), column_table: TableComponent::new(config.key_config.clone()), constraint_table: TableComponent::new(config.key_config.clone()), + foreign_key_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()), @@ -104,6 +106,11 @@ impl App { right_chunks[1], matches!(self.focus, Focus::Table), )?, + Tab::ForeignKeys => self.foreign_key_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)?; @@ -215,6 +222,24 @@ impl App { table.clone(), ); } + self.foreign_key_table.reset(); + let foreign_keys = self + .pool + .as_ref() + .unwrap() + .get_foreign_keys(&database, &table) + .await?; + if !constraints.is_empty() { + self.foreign_key_table.update( + foreign_keys + .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); } @@ -356,7 +381,18 @@ impl App { }; if key == self.config.key_config.copy { - if let Some(text) = self.column_table.selected_cells() { + if let Some(text) = self.constraint_table.selected_cells() { + copy_to_clipboard(text.as_str())? + } + }; + } + Tab::ForeignKeys => { + if self.foreign_key_table.event(key)?.is_consumed() { + return Ok(EventState::Consumed); + }; + + if key == self.config.key_config.copy { + if let Some(text) = self.foreign_key_table.selected_cells() { copy_to_clipboard(text.as_str())? } }; diff --git a/src/components/command.rs b/src/components/command.rs index b10806a..bb54d1c 100644 --- a/src/components/command.rs +++ b/src/components/command.rs @@ -110,11 +110,21 @@ pub fn tab_constraints(key: &KeyConfig) -> CommandText { ) } +pub fn tab_foreign_keys(key: &KeyConfig) -> CommandText { + CommandText::new( + format!("Foreign keys [{}]", key.tab_foreign_keys), + CMD_GROUP_TABLE, + ) +} + pub fn toggle_tabs(key_config: &KeyConfig) -> CommandText { CommandText::new( format!( - "Tab [{},{},{}]", - key_config.tab_records, key_config.tab_columns, key_config.tab_constraints + "Tab [{},{},{},{}]", + key_config.tab_records, + key_config.tab_columns, + key_config.tab_constraints, + key_config.tab_foreign_keys ), CMD_GROUP_GENERAL, ) diff --git a/src/components/tab.rs b/src/components/tab.rs index f7b946f..47cda93 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -18,6 +18,7 @@ pub enum Tab { Records, Columns, Constraints, + ForeignKeys, } impl std::fmt::Display for Tab { @@ -48,6 +49,7 @@ impl TabComponent { command::tab_records(&self.key_config).name, command::tab_columns(&self.key_config).name, command::tab_constraints(&self.key_config).name, + command::tab_foreign_keys(&self.key_config).name, ] } } @@ -82,6 +84,9 @@ impl Component for TabComponent { } else if key == self.key_config.tab_constraints { self.selected_tab = Tab::Constraints; return Ok(EventState::Consumed); + } else if key == self.key_config.tab_foreign_keys { + self.selected_tab = Tab::ForeignKeys; + return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) } diff --git a/src/config.rs b/src/config.rs index 14098b3..862969d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -81,6 +81,7 @@ pub struct KeyConfig { pub tab_records: Key, pub tab_columns: Key, pub tab_constraints: Key, + pub tab_foreign_keys: Key, } impl Default for KeyConfig { @@ -111,6 +112,7 @@ impl Default for KeyConfig { tab_records: Key::Char('1'), tab_columns: Key::Char('2'), tab_constraints: Key::Char('3'), + tab_foreign_keys: Key::Char('4'), } } } diff --git a/src/database/mod.rs b/src/database/mod.rs index 93f219f..2b0cc5c 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -30,6 +30,11 @@ pub trait Pool { database: &Database, table: &Table, ) -> anyhow::Result>>; + async fn get_foreign_keys( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result>>; async fn close(&self); } diff --git a/src/database/mysql.rs b/src/database/mysql.rs index e143c79..a21899c 100644 --- a/src/database/mysql.rs +++ b/src/database/mysql.rs @@ -73,6 +73,41 @@ impl TableRow for Column { } } +pub struct ForeignKey { + name: Option, + column_name: Option, + ref_table: Option, + ref_column: Option, +} + +impl TableRow for ForeignKey { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "column_name".to_string(), + "ref_table".to_string(), + "ref_column".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.column_name + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.ref_table + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.ref_column + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + ] + } +} + #[async_trait] impl Pool for MySqlPool { async fn get_databases(&self) -> anyhow::Result> { @@ -179,6 +214,8 @@ impl Pool for MySqlPool { FROM information_schema.KEY_COLUMN_USAGE WHERE + REFERENCED_TABLE_SCHEMA IS NULL + AND REFERENCED_TABLE_NAME IS NULL TABLE_SCHEMA = ? AND TABLE_NAME = ? ", @@ -196,6 +233,44 @@ impl Pool for MySqlPool { Ok(constraints) } + async fn get_foreign_keys( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result>> { + let mut rows = sqlx::query( + " + SELECT + TABLE_NAME, + COLUMN_NAME, + CONSTRAINT_NAME, + REFERENCED_TABLE_SCHEMA, + REFERENCED_TABLE_NAME, + REFERENCED_COLUMN_NAME + FROM + INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE + REFERENCED_TABLE_SCHEMA IS NOT NULL + AND REFERENCED_TABLE_NAME IS NOT NULL + AND TABLE_SCHEMA = ? + AND TABLE_NAME = ? + ", + ) + .bind(&database.name) + .bind(&table.name) + .fetch(&self.pool); + let mut foreign_keys: Vec> = vec![]; + while let Some(row) = rows.try_next().await? { + foreign_keys.push(Box::new(ForeignKey { + name: row.try_get("CONSTRAINT_NAME")?, + column_name: row.try_get("COLUMN_NAME")?, + ref_table: row.try_get("REFERENCED_TABLE_NAME")?, + ref_column: row.try_get("REFERENCED_COLUMN_NAME")?, + })) + } + Ok(foreign_keys) + } + async fn close(&self) { self.pool.close().await; } diff --git a/src/database/postgres.rs b/src/database/postgres.rs index 0362ab5..1a0b281 100644 --- a/src/database/postgres.rs +++ b/src/database/postgres.rs @@ -74,6 +74,41 @@ impl TableRow for Column { } } +pub struct ForeignKey { + name: Option, + column_name: Option, + ref_table: Option, + ref_column: Option, +} + +impl TableRow for ForeignKey { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "column_name".to_string(), + "ref_table".to_string(), + "ref_column".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.column_name + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.ref_table + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.ref_column + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + ] + } +} + #[async_trait] impl Pool for PostgresPool { async fn get_databases(&self) -> anyhow::Result> { @@ -233,16 +268,22 @@ impl Pool for PostgresPool { let mut rows = sqlx::query( " SELECT - tc.constraint_name, tc.table_name, kcu.column_name, + tc.table_schema, + tc.constraint_name, + tc.table_name, + kcu.column_name, + ccu.table_schema AS foreign_table_schema, 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 + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE + NOT tc.constraint_type = 'FOREIGN KEY' + AND tc.table_name = $1 ", ) .bind(&table.name) @@ -257,6 +298,46 @@ impl Pool for PostgresPool { Ok(constraints) } + async fn get_foreign_keys( + &self, + _database: &Database, + table: &Table, + ) -> anyhow::Result>> { + let mut rows = sqlx::query( + " + SELECT + tc.table_schema, + tc.constraint_name, + tc.table_name, + kcu.column_name, + ccu.table_schema AS foreign_table_schema, + 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 + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE + tc.constraint_type = 'FOREIGN KEY' + AND tc.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(ForeignKey { + name: row.try_get("constraint_name")?, + column_name: row.try_get("column_name")?, + ref_table: row.try_get("foreign_table_name")?, + ref_column: row.try_get("foreign_column_name")?, + })) + } + Ok(constraints) + } + async fn close(&self) { self.pool.close().await; }