Add constraints tab (#41)

* get constraints

* replace / with .

* add constraint tab

* return TableRow

* remove comment column

* fix clippy warnings
This commit is contained in:
Takayuki Maeda 2021-08-28 12:46:33 +09:00 committed by GitHub
parent 99961aea72
commit 2a0abf65cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 287 additions and 55 deletions

View File

@ -24,7 +24,8 @@ pub enum Focus {
} }
pub struct App { pub struct App {
record_table: RecordTableComponent, record_table: RecordTableComponent,
structure_table: TableComponent, column_table: TableComponent,
constraint_table: TableComponent,
focus: Focus, focus: Focus,
tab: TabComponent, tab: TabComponent,
help: HelpComponent, help: HelpComponent,
@ -42,7 +43,8 @@ impl App {
config: config.clone(), config: config.clone(),
connections: ConnectionsComponent::new(config.key_config.clone(), config.conn), connections: ConnectionsComponent::new(config.key_config.clone(), config.conn),
record_table: RecordTableComponent::new(config.key_config.clone()), 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()), 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()),
@ -93,10 +95,15 @@ impl App {
self.record_table self.record_table
.draw(f, right_chunks[1], matches!(self.focus, Focus::Table))? .draw(f, right_chunks[1], matches!(self.focus, Focus::Table))?
} }
Tab::Structure => { Tab::Columns => {
self.structure_table self.column_table
.draw(f, right_chunks[1], matches!(self.focus, Focus::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.error.draw(f, Rect::default(), false)?;
self.help.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<()> { async fn update_table(&mut self) -> anyhow::Result<()> {
if let Some((database, table)) = self.databases.tree().selected_table() { if let Some((database, table)) = self.databases.tree().selected_table() {
self.focus = Focus::Table; self.focus = Focus::Table;
self.record_table.reset();
let (headers, records) = self let (headers, records) = self
.pool .pool
.as_ref() .as_ref()
@ -171,14 +179,42 @@ impl App {
self.record_table self.record_table
.update(records, headers, database.clone(), table.clone()); .update(records, headers, database.clone(), table.clone());
let (headers, records) = self self.column_table.reset();
let columns = self
.pool .pool
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_columns(&database, &table) .get_columns(&database, &table)
.await?; .await?;
self.structure_table if !columns.is_empty() {
.update(records, headers, database.clone(), table.clone()); self.column_table.update(
columns
.iter()
.map(|c| c.columns())
.collect::<Vec<Vec<String>>>(),
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::<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);
} }
@ -303,13 +339,24 @@ impl App {
} }
}; };
} }
Tab::Structure => { Tab::Columns => {
if self.structure_table.event(key)?.is_consumed() { if self.column_table.event(key)?.is_consumed() {
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
}; };
if key == self.config.key_config.copy { 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())?
}
};
}
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.column_table.selected_cells() {
copy_to_clipboard(text.as_str())? copy_to_clipboard(text.as_str())?
} }
}; };

View File

@ -99,9 +99,13 @@ pub fn tab_records(key: &KeyConfig) -> CommandText {
CommandText::new(format!("Records [{}]", key.tab_records), CMD_GROUP_TABLE) 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( CommandText::new(
format!("Structure [{}]", key.tab_structure), format!("Constraints [{}]", key.tab_constraints),
CMD_GROUP_TABLE, CMD_GROUP_TABLE,
) )
} }
@ -109,8 +113,8 @@ pub fn tab_structure(key: &KeyConfig) -> CommandText {
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_structure key_config.tab_records, key_config.tab_columns, key_config.tab_constraints
), ),
CMD_GROUP_GENERAL, CMD_GROUP_GENERAL,
) )

View File

@ -16,7 +16,8 @@ use tui::{
#[derive(Debug, Clone, Copy, EnumIter)] #[derive(Debug, Clone, Copy, EnumIter)]
pub enum Tab { pub enum Tab {
Records, Records,
Structure, Columns,
Constraints,
} }
impl std::fmt::Display for Tab { impl std::fmt::Display for Tab {
@ -45,7 +46,8 @@ impl TabComponent {
fn names(&self) -> Vec<String> { fn names(&self) -> Vec<String> {
vec![ vec![
command::tab_records(&self.key_config).name, 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 { if key == self.key_config.tab_records {
self.selected_tab = Tab::Records; self.selected_tab = Tab::Records;
return Ok(EventState::Consumed); return Ok(EventState::Consumed);
} else if key == self.key_config.tab_structure { } else if key == self.key_config.tab_columns {
self.selected_tab = Tab::Structure; 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); return Ok(EventState::Consumed);
} }
Ok(EventState::NotConsumed) Ok(EventState::NotConsumed)

View File

@ -48,7 +48,7 @@ impl TableComponent {
fn title(&self) -> String { fn title(&self) -> String {
self.table.as_ref().map_or(" - ".to_string(), |table| { self.table.as_ref().map_or(" - ".to_string(), |table| {
format!("{}/{}", table.0.name, table.1.name) format!("{}.{}", table.0.name, table.1.name)
}) })
} }

View File

@ -79,7 +79,8 @@ pub struct KeyConfig {
pub extend_selection_by_one_cell_up: Key, pub extend_selection_by_one_cell_up: Key,
pub extend_selection_by_one_cell_down: Key, pub extend_selection_by_one_cell_down: Key,
pub tab_records: Key, pub tab_records: Key,
pub tab_structure: Key, pub tab_columns: Key,
pub tab_constraints: Key,
} }
impl Default for KeyConfig { 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_down: Key::Char('J'),
extend_selection_by_one_cell_up: Key::Char('K'), extend_selection_by_one_cell_up: Key::Char('K'),
tab_records: Key::Char('1'), tab_records: Key::Char('1'),
tab_structure: Key::Char('2'), tab_columns: Key::Char('2'),
tab_constraints: Key::Char('3'),
} }
} }
} }

View File

@ -24,6 +24,16 @@ pub trait Pool {
&self, &self,
database: &Database, database: &Database,
table: &Table, table: &Table,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>; ) -> anyhow::Result<Vec<Box<dyn TableRow>>>;
async fn get_constraints(
&self,
database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>>;
async fn close(&self); async fn close(&self);
} }
pub trait TableRow: std::marker::Send {
fn fields(&self) -> Vec<String>;
fn columns(&self) -> Vec<String>;
}

View File

@ -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 async_trait::async_trait;
use chrono::NaiveDate; use chrono::NaiveDate;
use database_tree::{Child, Database, Table}; 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<String> {
vec!["name".to_string(), "column_name".to_string()]
}
fn columns(&self) -> Vec<String> {
vec![self.name.to_string(), self.column_name.to_string()]
}
}
pub struct Column {
name: Option<String>,
r#type: Option<String>,
null: Option<String>,
default: Option<String>,
comment: Option<String>,
}
impl TableRow for Column {
fn fields(&self) -> Vec<String> {
vec![
"name".to_string(),
"type".to_string(),
"null".to_string(),
"default".to_string(),
"comment".to_string(),
]
}
fn columns(&self) -> Vec<String> {
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] #[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>> {
@ -92,27 +147,53 @@ impl Pool for MySqlPool {
&self, &self,
database: &Database, database: &Database,
table: &Table, table: &Table,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> { ) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
let query = format!( let query = format!(
"SHOW FULL COLUMNS FROM `{}`.`{}`", "SHOW FULL COLUMNS FROM `{}`.`{}`",
database.name, table.name database.name, table.name
); );
let mut rows = sqlx::query(query.as_str()).fetch(&self.pool); let mut rows = sqlx::query(query.as_str()).fetch(&self.pool);
let mut headers = vec![]; let mut columns: Vec<Box<dyn TableRow>> = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? { while let Some(row) = rows.try_next().await? {
headers = row columns.push(Box::new(Column {
.columns() name: row.try_get("Field")?,
.iter() r#type: row.try_get("Type")?,
.map(|column| column.name().to_string()) null: row.try_get("Null")?,
.collect(); default: row.try_get("Default")?,
let mut new_row = vec![]; comment: row.try_get("Comment")?,
for column in row.columns() { }))
new_row.push(convert_column_value_to_string(&row, column)?)
}
records.push(new_row)
} }
Ok((headers, records)) Ok(columns)
}
async fn get_constraints(
&self,
database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
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<Box<dyn TableRow>> = 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) { async fn close(&self) {
@ -124,7 +205,7 @@ fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyho
let column_name = column.name(); let column_name = column.name();
if let Ok(value) = row.try_get(column_name) { if let Ok(value) = row.try_get(column_name) {
let value: Option<String> = value; let value: Option<String> = 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) { if let Ok(value) = row.try_get(column_name) {
let value: Option<&str> = value; let value: Option<&str> = value;

View File

@ -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 async_trait::async_trait;
use chrono::NaiveDate; use chrono::NaiveDate;
use database_tree::{Child, Database, Schema, Table}; 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<String> {
vec!["name".to_string(), "column_name".to_string()]
}
fn columns(&self) -> Vec<String> {
vec![self.name.to_string(), self.column_name.to_string()]
}
}
pub struct Column {
name: Option<String>,
r#type: Option<String>,
null: Option<String>,
default: Option<String>,
comment: Option<String>,
}
impl TableRow for Column {
fn fields(&self) -> Vec<String> {
vec![
"name".to_string(),
"type".to_string(),
"null".to_string(),
"default".to_string(),
"comment".to_string(),
]
}
fn columns(&self) -> Vec<String> {
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] #[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>> {
@ -46,11 +101,11 @@ impl Pool for PostgresPool {
let mut tables = Vec::new(); let mut tables = Vec::new();
while let Some(row) = rows.try_next().await? { while let Some(row) = rows.try_next().await? {
tables.push(Table { tables.push(Table {
name: row.get("table_name"), name: row.try_get("table_name")?,
create_time: None, create_time: None,
update_time: None, update_time: None,
engine: None, engine: None,
schema: row.get("table_schema"), schema: row.try_get("table_schema")?,
}) })
} }
let mut schemas = vec![]; let mut schemas = vec![];
@ -147,7 +202,7 @@ impl Pool for PostgresPool {
&self, &self,
database: &Database, database: &Database,
table: &Table, table: &Table,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> { ) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
let table_schema = table let table_schema = table
.schema .schema
.as_ref() .as_ref()
@ -157,21 +212,49 @@ impl Pool for PostgresPool {
) )
.bind(&database.name).bind(table_schema).bind(&table.name) .bind(&database.name).bind(table_schema).bind(&table.name)
.fetch(&self.pool); .fetch(&self.pool);
let mut headers = vec![]; let mut columns: Vec<Box<dyn TableRow>> = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? { while let Some(row) = rows.try_next().await? {
headers = row columns.push(Box::new(Column {
.columns() name: row.try_get("column_name")?,
.iter() r#type: row.try_get("data_type")?,
.map(|column| column.name().to_string()) null: row.try_get("is_nullable")?,
.collect(); default: row.try_get("column_default")?,
let mut new_row = vec![]; comment: None,
for column in row.columns() { }))
new_row.push(convert_column_value_to_string(&row, column)?)
}
records.push(new_row)
} }
Ok((headers, records)) Ok(columns)
}
async fn get_constraints(
&self,
_database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
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<Box<dyn TableRow>> = 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) { async fn close(&self) {