get foreign keys (#44)

This commit is contained in:
Takayuki Maeda 2021-08-28 14:01:11 +09:00 committed by GitHub
parent 2a0abf65cb
commit 1d7d6b48dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 223 additions and 9 deletions

View File

@ -26,6 +26,7 @@ pub struct App {
record_table: RecordTableComponent, record_table: RecordTableComponent,
column_table: TableComponent, column_table: TableComponent,
constraint_table: TableComponent, constraint_table: TableComponent,
foreign_key_table: TableComponent,
focus: Focus, focus: Focus,
tab: TabComponent, tab: TabComponent,
help: HelpComponent, help: HelpComponent,
@ -45,6 +46,7 @@ impl App {
record_table: RecordTableComponent::new(config.key_config.clone()), record_table: RecordTableComponent::new(config.key_config.clone()),
column_table: TableComponent::new(config.key_config.clone()), column_table: TableComponent::new(config.key_config.clone()),
constraint_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()), tab: TabComponent::new(config.key_config.clone()),
help: HelpComponent::new(config.key_config.clone()), help: HelpComponent::new(config.key_config.clone()),
databases: DatabasesComponent::new(config.key_config.clone()), databases: DatabasesComponent::new(config.key_config.clone()),
@ -104,6 +106,11 @@ impl App {
right_chunks[1], right_chunks[1],
matches!(self.focus, Focus::Table), 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.error.draw(f, Rect::default(), false)?;
self.help.draw(f, Rect::default(), false)?; self.help.draw(f, Rect::default(), false)?;
@ -215,6 +222,24 @@ impl App {
table.clone(), 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::<Vec<Vec<String>>>(),
constraints.get(0).unwrap().fields(),
database.clone(),
table.clone(),
);
}
self.table_status self.table_status
.update(self.record_table.len() as u64, table); .update(self.record_table.len() as u64, table);
} }
@ -356,7 +381,18 @@ impl App {
}; };
if key == self.config.key_config.copy { 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())? copy_to_clipboard(text.as_str())?
} }
}; };

View File

@ -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 { pub fn toggle_tabs(key_config: &KeyConfig) -> CommandText {
CommandText::new( CommandText::new(
format!( format!(
"Tab [{},{},{}]", "Tab [{},{},{},{}]",
key_config.tab_records, key_config.tab_columns, key_config.tab_constraints key_config.tab_records,
key_config.tab_columns,
key_config.tab_constraints,
key_config.tab_foreign_keys
), ),
CMD_GROUP_GENERAL, CMD_GROUP_GENERAL,
) )

View File

@ -18,6 +18,7 @@ pub enum Tab {
Records, Records,
Columns, Columns,
Constraints, Constraints,
ForeignKeys,
} }
impl std::fmt::Display for Tab { impl std::fmt::Display for Tab {
@ -48,6 +49,7 @@ impl TabComponent {
command::tab_records(&self.key_config).name, command::tab_records(&self.key_config).name,
command::tab_columns(&self.key_config).name, command::tab_columns(&self.key_config).name,
command::tab_constraints(&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 { } else if key == self.key_config.tab_constraints {
self.selected_tab = Tab::Constraints; self.selected_tab = Tab::Constraints;
return Ok(EventState::Consumed); 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) Ok(EventState::NotConsumed)
} }

View File

@ -81,6 +81,7 @@ pub struct KeyConfig {
pub tab_records: Key, pub tab_records: Key,
pub tab_columns: Key, pub tab_columns: Key,
pub tab_constraints: Key, pub tab_constraints: Key,
pub tab_foreign_keys: Key,
} }
impl Default for KeyConfig { impl Default for KeyConfig {
@ -111,6 +112,7 @@ impl Default for KeyConfig {
tab_records: Key::Char('1'), tab_records: Key::Char('1'),
tab_columns: Key::Char('2'), tab_columns: Key::Char('2'),
tab_constraints: Key::Char('3'), tab_constraints: Key::Char('3'),
tab_foreign_keys: Key::Char('4'),
} }
} }
} }

View File

@ -30,6 +30,11 @@ pub trait Pool {
database: &Database, database: &Database,
table: &Table, table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>>; ) -> anyhow::Result<Vec<Box<dyn TableRow>>>;
async fn get_foreign_keys(
&self,
database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>>;
async fn close(&self); async fn close(&self);
} }

View File

@ -73,6 +73,41 @@ impl TableRow for Column {
} }
} }
pub struct ForeignKey {
name: Option<String>,
column_name: Option<String>,
ref_table: Option<String>,
ref_column: Option<String>,
}
impl TableRow for ForeignKey {
fn fields(&self) -> Vec<String> {
vec![
"name".to_string(),
"column_name".to_string(),
"ref_table".to_string(),
"ref_column".to_string(),
]
}
fn columns(&self) -> Vec<String> {
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] #[async_trait]
impl Pool for MySqlPool { impl Pool for MySqlPool {
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> { async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
@ -179,6 +214,8 @@ impl Pool for MySqlPool {
FROM FROM
information_schema.KEY_COLUMN_USAGE information_schema.KEY_COLUMN_USAGE
WHERE WHERE
REFERENCED_TABLE_SCHEMA IS NULL
AND REFERENCED_TABLE_NAME IS NULL
TABLE_SCHEMA = ? TABLE_SCHEMA = ?
AND TABLE_NAME = ? AND TABLE_NAME = ?
", ",
@ -196,6 +233,44 @@ impl Pool for MySqlPool {
Ok(constraints) Ok(constraints)
} }
async fn get_foreign_keys(
&self,
database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
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<Box<dyn TableRow>> = 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) { async fn close(&self) {
self.pool.close().await; self.pool.close().await;
} }

View File

@ -74,6 +74,41 @@ impl TableRow for Column {
} }
} }
pub struct ForeignKey {
name: Option<String>,
column_name: Option<String>,
ref_table: Option<String>,
ref_column: Option<String>,
}
impl TableRow for ForeignKey {
fn fields(&self) -> Vec<String> {
vec![
"name".to_string(),
"column_name".to_string(),
"ref_table".to_string(),
"ref_column".to_string(),
]
}
fn columns(&self) -> Vec<String> {
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] #[async_trait]
impl Pool for PostgresPool { impl Pool for PostgresPool {
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> { async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
@ -233,16 +268,22 @@ impl Pool for PostgresPool {
let mut rows = sqlx::query( let mut rows = sqlx::query(
" "
SELECT 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.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name ccu.column_name AS foreign_column_name
FROM FROM
information_schema.table_constraints AS tc information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
WHERE ccu.table_name = $1 WHERE
NOT tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = $1
", ",
) )
.bind(&table.name) .bind(&table.name)
@ -257,6 +298,46 @@ impl Pool for PostgresPool {
Ok(constraints) Ok(constraints)
} }
async fn get_foreign_keys(
&self,
_database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
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<Box<dyn TableRow>> = 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) { async fn close(&self) {
self.pool.close().await; self.pool.close().await;
} }