diff --git a/Cargo.lock b/Cargo.lock index c6b9ae9..4f71d39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "ahash" version = "0.4.7" @@ -319,9 +321,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "database-tree" -version = "0.1.2" +version = "0.1.0" dependencies = [ "anyhow", "chrono", @@ -555,13 +567,19 @@ dependencies = [ "ahash 0.4.7", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + [[package]] name = "hashlink" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" dependencies = [ - "hashbrown", + "hashbrown 0.9.1", ] [[package]] @@ -588,6 +606,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest", +] + [[package]] name = "idna" version = "0.2.3" @@ -599,6 +627,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg 1.0.1", + "hashbrown 0.11.2", +] + [[package]] name = "instant" version = "0.1.9" @@ -730,6 +768,17 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + [[package]] name = "memchr" version = "2.4.0" @@ -1273,6 +1322,7 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -1401,9 +1451,11 @@ dependencies = [ "generic-array", "hashlink", "hex", + "hmac", "itoa", "libc", "log", + "md-5", "memchr", "num-bigint 0.3.2", "once_cell", @@ -1413,6 +1465,8 @@ dependencies = [ "rsa", "rust_decimal", "rustls", + "serde", + "serde_json", "sha-1", "sha2", "smallvec", @@ -1440,6 +1494,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", + "serde_json", "sha2", "sqlx-core", "sqlx-rt", diff --git a/Cargo.toml b/Cargo.toml index ba1d5ea..693b45f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ tui = { version = "0.14.0", features = ["crossterm"], default-features = false } crossterm = "0.19" anyhow = "1.0.38" unicode-width = "0.1" -sqlx = { version = "0.4.1", features = ["mysql", "chrono", "runtime-tokio-rustls", "decimal"] } +sqlx = { version = "0.4.1", features = ["mysql", "postgres", "chrono", "runtime-tokio-rustls", "decimal", "json"] } chrono = "0.4" tokio = { version = "0.2.22", features = ["full"] } futures = "0.3.5" diff --git a/database-tree/Cargo.toml b/database-tree/Cargo.toml index 0e0805f..284a002 100644 --- a/database-tree/Cargo.toml +++ b/database-tree/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "database-tree" -version = "0.1.2" -authors = ["Stephan Dilly "] +version = "0.1.0" +authors = ["Takayuki Maeda "] edition = "2018" license = "MIT" homepage = "https://github.com/TaKO8Ki/gobang" diff --git a/database-tree/src/databasetree.rs b/database-tree/src/databasetree.rs index b8842d3..032ab36 100644 --- a/database-tree/src/databasetree.rs +++ b/database-tree/src/databasetree.rs @@ -82,6 +82,7 @@ impl DatabaseTree { DatabaseTreeItemKind::Table { table, database } => { Some((database.clone(), table.clone())) } + DatabaseTreeItemKind::Schema { .. } => None, } }) } @@ -215,7 +216,8 @@ impl DatabaseTree { .unwrap(); if !up - && self.selected_item().unwrap().kind().is_database() + && (self.selected_item().unwrap().kind().is_database() + || self.selected_item().unwrap().kind().is_schema()) && self.selected_item().unwrap() == item { break; @@ -264,6 +266,11 @@ impl DatabaseTree { return Some(current_index); } + if item.kind().is_schema() && !item.kind().is_schema_collapsed() { + self.items.collapse(current_index, false); + return Some(current_index); + } + self.select_parent(current_index) } @@ -278,6 +285,14 @@ impl DatabaseTree { return self.selection_updown(current_selection, false); } + if item.kind().is_schema() { + if item.kind().is_schema_collapsed() { + self.items.expand(current_selection, false); + return Some(current_selection); + } + return self.selection_updown(current_selection, false); + } + None } @@ -292,8 +307,7 @@ impl DatabaseTree { #[cfg(test)] mod test { - use crate::{Database, DatabaseTree, MoveSelection, Table}; - // use pretty_assertions::assert_eq; + use crate::{Database, DatabaseTree, MoveSelection, Schema, Table}; use std::collections::BTreeSet; impl Table { @@ -303,6 +317,17 @@ mod test { create_time: None, update_time: None, engine: None, + schema: None, + } + } + + fn new_with_schema(name: String, schema: String) -> Self { + Table { + name, + create_time: None, + update_time: None, + engine: None, + schema: Some(schema), } } } @@ -311,11 +336,31 @@ mod test { fn test_selection() { let items = vec![Database::new( "a".to_string(), - vec![Table::new("b".to_string())], + vec![Table::new("b".to_string()).into()], + )]; + + // a + // b + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + + assert!(tree.move_selection(MoveSelection::Right)); + assert_eq!(tree.selection, Some(0)); + assert!(tree.move_selection(MoveSelection::Down)); + assert_eq!(tree.selection, Some(1)); + + let items = vec![Database::new( + "a".to_string(), + vec![Schema { + name: "b".to_string(), + tables: vec![Table::new("c".to_string()).into()], + } + .into()], )]; // a // b + // c let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); @@ -323,6 +368,10 @@ mod test { assert_eq!(tree.selection, Some(0)); assert!(tree.move_selection(MoveSelection::Down)); assert_eq!(tree.selection, Some(1)); + assert!(tree.move_selection(MoveSelection::Right)); + assert_eq!(tree.selection, Some(1)); + assert!(tree.move_selection(MoveSelection::Down)); + assert_eq!(tree.selection, Some(2)); } #[test] @@ -330,9 +379,12 @@ mod test { let items = vec![ Database::new( "a".to_string(), - vec![Table::new("b".to_string()), Table::new("c".to_string())], + vec![ + Table::new("b".to_string()).into(), + Table::new("c".to_string()).into(), + ], ), - Database::new("d".to_string(), vec![Table::new("e".to_string())]), + Database::new("d".to_string(), vec![Table::new("e".to_string()).into()]), ]; // a @@ -342,8 +394,38 @@ mod test { // e let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(1); + + assert!(tree.move_selection(MoveSelection::Down)); + assert_eq!(tree.selection, Some(3)); + + let items = vec![ + Database::new( + "a".to_string(), + vec![Schema { + name: "b".to_string(), + tables: vec![Table::new("c".to_string()).into()], + } + .into()], + ), + Database::new( + "d".to_string(), + vec![Schema { + name: "e".to_string(), + tables: vec![Table::new("f".to_string()).into()], + } + .into()], + ), + ]; + + // a + // b + // c + // d + // e + // f - tree.items.collapse(0, false); + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); tree.selection = Some(1); assert!(tree.move_selection(MoveSelection::Down)); @@ -354,7 +436,10 @@ mod test { fn test_selection_left_collapse() { let items = vec![Database::new( "a".to_string(), - vec![Table::new("b".to_string()), Table::new("c".to_string())], + vec![ + Table::new("b".to_string()).into(), + Table::new("c".to_string()).into(), + ], )]; // a @@ -370,13 +455,52 @@ mod test { assert!(tree.items.tree_items[0].kind().is_database_collapsed()); assert!(!tree.items.tree_items[1].info().is_visible()); assert!(!tree.items.tree_items[2].info().is_visible()); + + let items = vec![ + Database::new( + "a".to_string(), + vec![Schema { + name: "b".to_string(), + tables: vec![Table::new("c".to_string()).into()], + } + .into()], + ), + Database::new( + "d".to_string(), + vec![Schema { + name: "e".to_string(), + tables: vec![Table::new("f".to_string()).into()], + } + .into()], + ), + ]; + + // a + // b + // c + // d + // e + // f + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(0); + tree.items.expand(0, false); + + assert!(tree.move_selection(MoveSelection::Left)); + assert_eq!(tree.selection, Some(0)); + assert!(tree.items.tree_items[0].kind().is_database_collapsed()); + assert!(!tree.items.tree_items[1].info().is_visible()); + assert!(!tree.items.tree_items[2].info().is_visible()); } #[test] fn test_selection_left_parent() { let items = vec![Database::new( "a".to_string(), - vec![Table::new("b".to_string()), Table::new("c".to_string())], + vec![ + Table::new("b".to_string()).into(), + Table::new("c".to_string()).into(), + ], )]; // a @@ -389,13 +513,43 @@ mod test { assert!(tree.move_selection(MoveSelection::Left)); assert_eq!(tree.selection, Some(0)); + + let items = vec![Database::new( + "a".to_string(), + vec![Schema { + name: "b".to_string(), + tables: vec![Table::new_with_schema("c".to_string(), "a".to_string()).into()], + } + .into()], + )]; + + // a + // b + // c + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + + tree.selection = Some(2); + tree.items.expand(0, false); + tree.items.expand(1, false); + assert!(tree.move_selection(MoveSelection::Left)); + assert_eq!(tree.selection, Some(1)); + + assert!(tree.move_selection(MoveSelection::Left)); + assert_eq!(tree.selection, Some(1)); + + assert!(tree.move_selection(MoveSelection::Left)); + assert_eq!(tree.selection, Some(0)); } #[test] fn test_selection_right_expand() { let items = vec![Database::new( "a".to_string(), - vec![Table::new("b".to_string()), Table::new("c".to_string())], + vec![ + Table::new("b".to_string()).into(), + Table::new("c".to_string()).into(), + ], )]; // a @@ -403,7 +557,29 @@ mod test { // c let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(0); + + assert!(tree.move_selection(MoveSelection::Right)); + assert_eq!(tree.selection, Some(0)); + assert!(!tree.items.tree_items[0].kind().is_database_collapsed()); + + assert!(tree.move_selection(MoveSelection::Right)); + assert_eq!(tree.selection, Some(1)); + + let items = vec![Database::new( + "a".to_string(), + vec![Schema { + name: "b".to_string(), + tables: vec![Table::new_with_schema("c".to_string(), "a".to_string()).into()], + } + .into()], + )]; + + // a + // b + // c + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); tree.selection = Some(0); assert!(tree.move_selection(MoveSelection::Right)); @@ -412,6 +588,10 @@ mod test { assert!(tree.move_selection(MoveSelection::Right)); assert_eq!(tree.selection, Some(1)); + + assert!(tree.move_selection(MoveSelection::Right)); + assert_eq!(tree.selection, Some(1)); + assert!(!tree.items.tree_items[0].kind().is_schema_collapsed()); } #[test] @@ -419,9 +599,12 @@ mod test { let items = vec![ Database::new( "a".to_string(), - vec![Table::new("b".to_string()), Table::new("c".to_string())], + vec![ + Table::new("b".to_string()).into(), + Table::new("c".to_string()).into(), + ], ), - Database::new("d".to_string(), vec![Table::new("e".to_string())]), + Database::new("d".to_string(), vec![Table::new("e".to_string()).into()]), ]; // a @@ -437,9 +620,148 @@ mod test { tree.selection = Some(0); assert!(tree.move_selection(MoveSelection::Left)); assert!(tree.move_selection(MoveSelection::Down)); + assert_eq!(tree.selection, Some(3)); let s = tree.visual_selection().unwrap(); assert_eq!(s.count, 3); assert_eq!(s.index, 1); + + let items = vec![ + Database::new( + "a".to_string(), + vec![Schema { + name: "b".to_string(), + tables: vec![Table::new_with_schema("c".to_string(), "a".to_string()).into()], + } + .into()], + ), + Database::new( + "d".to_string(), + vec![Schema { + name: "e".to_string(), + tables: vec![Table::new_with_schema("f".to_string(), "d".to_string()).into()], + } + .into()], + ), + ]; + + // a + // b + // c + // d + // e + // f + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.items.expand(0, false); + tree.items.expand(1, false); + tree.items.expand(3, false); + tree.selection = Some(0); + + assert!(tree.move_selection(MoveSelection::Left)); + assert!(tree.move_selection(MoveSelection::Down)); + assert_eq!(tree.selection, Some(3)); + let s = tree.visual_selection().unwrap(); + + assert_eq!(s.count, 4); + assert_eq!(s.index, 1); + } + + #[test] + fn test_selection_top() { + let items = vec![Database::new( + "a".to_string(), + vec![ + Table::new("b".to_string()).into(), + Table::new("c".to_string()).into(), + Table::new("d".to_string()).into(), + ], + )]; + + // a + // b + // c + // d + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(3); + tree.items.expand(0, false); + + assert!(tree.move_selection(MoveSelection::Top)); + assert_eq!(tree.selection, Some(0)); + + let items = vec![Database::new( + "a".to_string(), + vec![Schema { + name: "b".to_string(), + tables: vec![ + Table::new("c".to_string()).into(), + Table::new("d".to_string()).into(), + ], + } + .into()], + )]; + + // a + // b + // c + // d + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(3); + tree.items.expand(0, false); + tree.items.expand(1, false); + + assert!(tree.move_selection(MoveSelection::Top)); + assert_eq!(tree.selection, Some(0)); + } + + #[test] + fn test_selection_bottom() { + let items = vec![Database::new( + "a".to_string(), + vec![ + Table::new("b".to_string()).into(), + Table::new("c".to_string()).into(), + Table::new("d".to_string()).into(), + ], + )]; + + // a + // b + // c + // d + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(0); + tree.items.expand(0, false); + + assert!(tree.move_selection(MoveSelection::End)); + assert_eq!(tree.selection, Some(3)); + + let items = vec![Database::new( + "a".to_string(), + vec![Schema { + name: "b".to_string(), + tables: vec![ + Table::new("c".to_string()).into(), + Table::new("d".to_string()).into(), + ], + } + .into()], + )]; + + // a + // b + // c + // d + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(0); + tree.items.expand(0, false); + tree.items.expand(1, false); + + assert!(tree.move_selection(MoveSelection::End)); + assert_eq!(tree.selection, Some(3)); } } diff --git a/database-tree/src/databasetreeitems.rs b/database-tree/src/databasetreeitems.rs index feeed28..406163e 100644 --- a/database-tree/src/databasetreeitems.rs +++ b/database-tree/src/databasetreeitems.rs @@ -1,6 +1,6 @@ -use crate::Database; use crate::{error::Result, treeitems_iter::TreeItemsIterator}; use crate::{item::DatabaseTreeItemKind, DatabaseTreeItem}; +use crate::{Child, Database}; use std::{ collections::{BTreeSet, HashMap}, usize, @@ -26,8 +26,8 @@ impl DatabaseTreeItems { .iter() .filter(|item| item.is_database() || item.is_match(&filter_text)) .map(|item| { + let mut item = item.clone(); if item.is_database() { - let mut item = item.clone(); item.set_collapsed(false); item } else { @@ -51,8 +51,16 @@ impl DatabaseTreeItems { { Self::push_databases(e, &mut items, &mut items_added, collapsed)?; } - for table in &e.tables { - items.push(DatabaseTreeItem::new_table(e, table)); + for child in &e.children { + match child { + Child::Table(table) => items.push(DatabaseTreeItem::new_table(e, table)), + Child::Schema(schema) => { + items.push(DatabaseTreeItem::new_schema(e, schema, true)); + for table in &schema.tables { + items.push(DatabaseTreeItem::new_table(e, table)) + } + } + } } } @@ -115,13 +123,37 @@ impl DatabaseTreeItems { } } } + + if self.tree_items[index].kind().is_schema() { + self.tree_items[index].collapse_schema(); + + let name = self.tree_items[index].kind().name(); + + for i in index + 1..self.tree_items.len() { + let item = &mut self.tree_items[i]; + + if recursive && item.kind().is_schema() { + item.collapse_schema(); + } + + if let Some(schema) = item.kind().schema_name() { + if schema == name { + item.hide(); + } + } else { + return; + } + } + } } pub fn expand(&mut self, index: usize, recursive: bool) { if self.tree_items[index].kind().is_database() { self.tree_items[index].expand_database(); + let tree_item = self.tree_items[index].clone(); let name = self.tree_items[index].kind().name(); + let kind = tree_item.kind(); if recursive { for i in index + 1..self.tree_items.len() { @@ -139,43 +171,104 @@ impl DatabaseTreeItems { } } - self.update_visibility(&Some(name), index + 1, false); + self.update_visibility(kind, index + 1); + } + + if self.tree_items[index].kind().is_schema() { + self.tree_items[index].expand_schema(); + + let tree_item = self.tree_items[index].clone(); + let name = self.tree_items[index].kind().name(); + let kind = tree_item.kind(); + + if recursive { + for i in index + 1..self.tree_items.len() { + let item = &mut self.tree_items[i]; + + if let Some(schema) = item.kind().schema_name() { + if *schema != name { + break; + } + } + + if item.kind().is_schema() && item.kind().is_schema_collapsed() { + item.expand_schema(); + } + } + } + + self.update_visibility(kind, index + 1); } } - fn update_visibility(&mut self, prefix: &Option, start_idx: usize, set_defaults: bool) { - let mut inner_collapsed: Option = None; + fn update_visibility(&mut self, prefix: &DatabaseTreeItemKind, start_idx: usize) { + let mut inner_collapsed: Option = None; for i in start_idx..self.tree_items.len() { if let Some(ref collapsed_item) = inner_collapsed { - if let Some(db) = self.tree_items[i].kind().database_name().clone() { - if db == *collapsed_item { - if set_defaults { - self.tree_items[i].info_mut().set_visible(false); + match collapsed_item { + DatabaseTreeItemKind::Database { name, .. } => { + if let DatabaseTreeItemKind::Schema { database, .. } = + self.tree_items[i].kind().clone() + { + if database.name == *name { + continue; + } + } + if let DatabaseTreeItemKind::Table { database, .. } = + self.tree_items[i].kind().clone() + { + if database.name == *name { + continue; + } } - continue; } + DatabaseTreeItemKind::Schema { schema, .. } => { + if let DatabaseTreeItemKind::Table { table, .. } = + self.tree_items[i].kind().clone() + { + if matches!(table.schema, Some(table_schema) if schema.name == table_schema) + { + continue; + } + } + } + _ => (), } inner_collapsed = None; } let item_kind = self.tree_items[i].kind().clone(); - if matches!(item_kind, DatabaseTreeItemKind::Database{ collapsed, .. } if collapsed) { - inner_collapsed = item_kind.database_name().clone(); + if matches!(item_kind, DatabaseTreeItemKind::Database{ collapsed, .. } if collapsed) + || matches!(item_kind, DatabaseTreeItemKind::Schema{ collapsed, .. } if collapsed) + { + inner_collapsed = Some(item_kind.clone()); } - if let Some(db) = item_kind.database_name() { - if prefix.as_ref().map_or(true, |prefix| *prefix == *db) { - self.tree_items[i].info_mut().set_visible(true); + match prefix { + DatabaseTreeItemKind::Database { name, .. } => { + if let DatabaseTreeItemKind::Schema { database, .. } = item_kind.clone() { + if *name == database.name { + self.tree_items[i].info_mut().set_visible(true); + } + } + + if let DatabaseTreeItemKind::Table { database, .. } = item_kind { + if *name == database.name { + self.tree_items[i].info_mut().set_visible(true); + } + } } - } else { - // if we do not set defaults we can early out - if set_defaults { - self.tree_items[i].info_mut().set_visible(false); - } else { - return; + DatabaseTreeItemKind::Schema { schema, .. } => { + if let DatabaseTreeItemKind::Table { table, .. } = item_kind { + if matches!(table.schema, Some(table_schema) if schema.name == table_schema) + { + self.tree_items[i].info_mut().set_visible(true); + } + } } + _ => (), } } } diff --git a/database-tree/src/item.rs b/database-tree/src/item.rs index aa8aa37..b1f600d 100644 --- a/database-tree/src/item.rs +++ b/database-tree/src/item.rs @@ -1,4 +1,4 @@ -use crate::{Database, Table}; +use crate::{Database, Schema, Table}; #[derive(Debug, Clone)] pub struct TreeItemInfo { @@ -31,8 +31,19 @@ impl TreeItemInfo { /// `DatabaseTreeItem` can be of two kinds #[derive(PartialEq, Debug, Clone)] pub enum DatabaseTreeItemKind { - Database { name: String, collapsed: bool }, - Table { database: Database, table: Table }, + Database { + name: String, + collapsed: bool, + }, + Table { + database: Database, + table: Table, + }, + Schema { + database: Database, + schema: Schema, + collapsed: bool, + }, } impl DatabaseTreeItemKind { @@ -44,10 +55,23 @@ impl DatabaseTreeItemKind { matches!(self, Self::Table { .. }) } + pub const fn is_schema(&self) -> bool { + matches!(self, Self::Schema { .. }) + } + pub const fn is_database_collapsed(&self) -> bool { match self { Self::Database { collapsed, .. } => *collapsed, Self::Table { .. } => false, + Self::Schema { .. } => false, + } + } + + pub const fn is_schema_collapsed(&self) -> bool { + match self { + Self::Database { .. } => false, + Self::Table { .. } => false, + Self::Schema { collapsed, .. } => *collapsed, } } @@ -55,6 +79,7 @@ impl DatabaseTreeItemKind { match self { Self::Database { name, .. } => name.to_string(), Self::Table { table, .. } => table.name.clone(), + Self::Schema { schema, .. } => schema.name.clone(), } } @@ -62,6 +87,15 @@ impl DatabaseTreeItemKind { match self { Self::Database { .. } => None, Self::Table { database, .. } => Some(database.name.clone()), + Self::Schema { database, .. } => Some(database.name.clone()), + } + } + + pub fn schema_name(&self) -> Option { + match self { + Self::Database { .. } => None, + Self::Table { table, .. } => table.schema.clone(), + Self::Schema { .. } => None, } } } @@ -76,7 +110,7 @@ pub struct DatabaseTreeItem { impl DatabaseTreeItem { pub fn new_table(database: &Database, table: &Table) -> Self { Self { - info: TreeItemInfo::new(1, false), + info: TreeItemInfo::new(if table.schema.is_some() { 2 } else { 1 }, false), kind: DatabaseTreeItemKind::Table { database: database.clone(), table: table.clone(), @@ -84,6 +118,17 @@ impl DatabaseTreeItem { } } + pub fn new_schema(database: &Database, schema: &Schema, _collapsed: bool) -> Self { + Self { + info: TreeItemInfo::new(1, false), + kind: DatabaseTreeItemKind::Schema { + database: database.clone(), + schema: schema.clone(), + collapsed: true, + }, + } + } + pub fn new_database(database: &Database, _collapsed: bool) -> Self { Self { info: TreeItemInfo::new(0, true), @@ -133,6 +178,32 @@ impl DatabaseTreeItem { } } + pub fn collapse_schema(&mut self) { + if let DatabaseTreeItemKind::Schema { + schema, database, .. + } = &self.kind + { + self.kind = DatabaseTreeItemKind::Schema { + database: database.clone(), + schema: schema.clone(), + collapsed: true, + } + } + } + + pub fn expand_schema(&mut self) { + if let DatabaseTreeItemKind::Schema { + schema, database, .. + } = &self.kind + { + self.kind = DatabaseTreeItemKind::Schema { + database: database.clone(), + schema: schema.clone(), + collapsed: false, + }; + } + } + pub fn show(&mut self) { self.info.visible = true; } @@ -145,6 +216,7 @@ impl DatabaseTreeItem { match self.kind.clone() { DatabaseTreeItemKind::Database { name, .. } => name.contains(filter_text), DatabaseTreeItemKind::Table { table, .. } => table.name.contains(filter_text), + DatabaseTreeItemKind::Schema { schema, .. } => schema.name.contains(filter_text), } } diff --git a/database-tree/src/lib.rs b/database-tree/src/lib.rs index 34f107d..3b9d0b4 100644 --- a/database-tree/src/lib.rs +++ b/database-tree/src/lib.rs @@ -14,18 +14,42 @@ pub use crate::{ #[derive(Clone, PartialEq, Debug)] pub struct Database { pub name: String, - pub tables: Vec, + pub children: Vec, +} + +#[derive(Clone, PartialEq, Debug)] +pub enum Child { + Table(Table), + Schema(Schema), +} + +impl From
for Child { + fn from(t: Table) -> Self { + Child::Table(t) + } +} + +impl From for Child { + fn from(s: Schema) -> Self { + Child::Schema(s) + } } impl Database { - pub fn new(database: String, tables: Vec
) -> Self { + pub fn new(database: String, children: Vec) -> Self { Self { name: database, - tables, + children, } } } +#[derive(Clone, PartialEq, Debug)] +pub struct Schema { + pub name: String, + pub tables: Vec
, +} + #[derive(sqlx::FromRow, Debug, Clone, PartialEq)] pub struct Table { #[sqlx(rename = "Name")] @@ -36,4 +60,6 @@ pub struct Table { pub update_time: Option>, #[sqlx(rename = "Engine")] pub engine: Option, + #[sqlx(default)] + pub schema: Option, } diff --git a/sample.toml b/sample.toml index 28c5487..038e106 100644 --- a/sample.toml +++ b/sample.toml @@ -1,17 +1,33 @@ [[conn]] +type = "mysql" name = "sample" user = "root" host = "localhost" port = 3306 [[conn]] +type = "mysql" user = "root" host = "localhost" port = 3306 database = "world" [[conn]] +type = "mysql" user = "root" host = "localhost" port = 3306 database = "employees" + +[[conn]] +type = "postgres" +user = "postgres" +host = "localhost" +port = 5432 + +[[conn]] +type = "postgres" +user = "postgres" +host = "localhost" +port = 5432 +database = "dvdrental" diff --git a/src/app.rs b/src/app.rs index 78d4bad..5e94820 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,7 @@ use crate::clipboard::Clipboard; use crate::components::{CommandInfo, Component as _, DrawableComponent as _, EventState}; +use crate::database::{MySqlPool, Pool, PostgresPool, RECORDS_LIMIT_PER_PAGE}; use crate::event::Key; -use crate::utils::{MySqlPool, Pool}; use crate::{ components::tab::Tab, components::{ @@ -64,6 +64,8 @@ impl App { .split(f.size())[0], false, )?; + self.error.draw(f, Rect::default(), false)?; + self.help.draw(f, Rect::default(), false)?; return Ok(()); } @@ -131,9 +133,15 @@ impl App { if let Some(pool) = self.pool.as_ref() { pool.close().await; } - self.pool = Some(Box::new( - MySqlPool::new(conn.database_url().as_str()).await?, - )); + self.pool = if conn.is_mysql() { + Some(Box::new( + MySqlPool::new(conn.database_url().as_str()).await?, + )) + } else { + Some(Box::new( + PostgresPool::new(conn.database_url().as_str()).await?, + )) + }; let databases = match &conn.database { Some(database) => vec![Database::new( database.clone(), @@ -148,6 +156,7 @@ impl App { self.databases.update(databases.as_slice()).unwrap(); self.focus = Focus::DabataseList; self.record_table.reset(); + self.tab.reset(); } Ok(()) } @@ -159,7 +168,7 @@ impl App { .pool .as_ref() .unwrap() - .get_records(&database.name, &table.name, 0, None) + .get_records(&database, &table, 0, None) .await?; self.record_table .update(records, headers, database.clone(), table.clone()); @@ -168,7 +177,7 @@ impl App { .pool .as_ref() .unwrap() - .get_columns(&database.name, &table.name) + .get_columns(&database, &table) .await?; self.structure_table .update(records, headers, database.clone(), table.clone()); @@ -185,8 +194,8 @@ impl App { .as_ref() .unwrap() .get_records( - &database.name, - &table.name, + &database, + &table, 0, if self.record_table.filter.input.is_empty() { None @@ -268,10 +277,7 @@ impl App { } if let Some(index) = self.record_table.table.selected_row.selected() { - if index.saturating_add(1) - % crate::utils::RECORDS_LIMIT_PER_PAGE as usize - == 0 - { + if index.saturating_add(1) % RECORDS_LIMIT_PER_PAGE as usize == 0 { if let Some((database, table)) = self.databases.tree().selected_table() { @@ -280,8 +286,8 @@ impl App { .as_ref() .unwrap() .get_records( - &database.name.clone(), - &table.name, + &database, + &table, index as u16, if self.record_table.filter.input.is_empty() { None diff --git a/src/components/connections.rs b/src/components/connections.rs index 4d87bb5..f1d9ac3 100644 --- a/src/components/connections.rs +++ b/src/components/connections.rs @@ -20,10 +20,14 @@ pub struct ConnectionsComponent { impl ConnectionsComponent { pub fn new(key_config: KeyConfig, connections: Vec) -> Self { + let mut state = ListState::default(); + if !connections.is_empty() { + state.select(Some(0)); + } Self { connections, key_config, - state: ListState::default(), + state, } } @@ -31,7 +35,7 @@ impl ConnectionsComponent { let i = match self.state.selected() { Some(i) => { if i >= self.connections.len() - 1 { - 0 + self.connections.len() - 1 } else { i + 1 } @@ -45,7 +49,7 @@ impl ConnectionsComponent { let i = match self.state.selected() { Some(i) => { if i == 0 { - self.connections.len() - 1 + 0 } else { i - 1 } diff --git a/src/components/databases.rs b/src/components/databases.rs index 9e46e03..a11cf3e 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -94,8 +94,8 @@ impl DatabasesComponent { format!("{:w$}", " ", w = (indent as usize) * 2) }; - let arrow = if item.kind().is_database() { - if item.kind().is_database_collapsed() { + let arrow = if item.kind().is_database() || item.kind().is_schema() { + if item.kind().is_database_collapsed() || item.kind().is_schema_collapsed() { FOLDER_ICON_COLLAPSED } else { FOLDER_ICON_EXPANDED @@ -346,7 +346,7 @@ mod test { DatabaseTreeItem::new_database( &Database { name: "foo".to_string(), - tables: Vec::new(), + children: Vec::new(), }, false, ), @@ -366,7 +366,7 @@ mod test { DatabaseTreeItem::new_database( &Database { name: "foo".to_string(), - tables: Vec::new(), + children: Vec::new(), }, false, ), @@ -389,13 +389,14 @@ mod test { DatabaseTreeItem::new_table( &Database { name: "foo".to_string(), - tables: Vec::new(), + children: Vec::new(), }, &Table { name: "bar".to_string(), create_time: None, update_time: None, engine: None, + schema: None }, ), false, @@ -414,13 +415,14 @@ mod test { DatabaseTreeItem::new_table( &Database { name: "foo".to_string(), - tables: Vec::new(), + children: Vec::new(), }, &Table { name: "bar".to_string(), create_time: None, update_time: None, engine: None, + schema: None }, ), true, @@ -442,13 +444,14 @@ mod test { DatabaseTreeItem::new_table( &Database { name: "foo".to_string(), - tables: Vec::new(), + children: Vec::new(), }, &Table { name: "barbaz".to_string(), create_time: None, update_time: None, engine: None, + schema: None }, ), false, @@ -467,13 +470,14 @@ mod test { DatabaseTreeItem::new_table( &Database { name: "foo".to_string(), - tables: Vec::new(), + children: Vec::new(), }, &Table { name: "barbaz".to_string(), create_time: None, update_time: None, engine: None, + schema: None }, ), true, diff --git a/src/components/tab.rs b/src/components/tab.rs index 4e7d030..d853bc6 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -38,6 +38,10 @@ impl TabComponent { } } + pub fn reset(&mut self) { + self.selected_tab = Tab::Records; + } + fn names(&self) -> Vec { vec![ command::tab_records(&self.key_config).name, diff --git a/src/components/table.rs b/src/components/table.rs index c4ba7e7..f1ebd2e 100644 --- a/src/components/table.rs +++ b/src/components/table.rs @@ -363,7 +363,9 @@ impl TableComponent { } column_index += 1 } - if self.selected_column_index() != self.headers.len().saturating_sub(1) { + if self.selected_column_index() != self.headers.len().saturating_sub(1) + && column_index.saturating_sub(1) != self.headers.len().saturating_sub(1) + { widths.pop(); } let far_right_column_index = column_index; @@ -371,7 +373,9 @@ impl TableComponent { .iter() .map(|(_, width)| Constraint::Length(*width as u16)) .collect::>(); - if self.selected_column_index() != self.headers.len().saturating_sub(1) { + if self.selected_column_index() != self.headers.len().saturating_sub(1) + && column_index.saturating_sub(1) != self.headers.len().saturating_sub(1) + { constraints.push(Constraint::Min(10)); } constraints.insert(0, Constraint::Length(number_column_width)); @@ -739,7 +743,7 @@ mod test { } #[test] - fn test_calculate_cell_widths() { + fn test_calculate_cell_widths_when_sum_of_cell_widths_is_greater_than_table_width() { let mut component = TableComponent::new(KeyConfig::default()); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.rows = vec![ @@ -762,6 +766,19 @@ mod test { Constraint::Min(10), ] ); + } + + #[test] + fn test_calculate_cell_widths_when_sum_of_cell_widths_is_less_than_table_width() { + let mut component = TableComponent::new(KeyConfig::default()); + component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); + component.rows = vec![ + vec!["aaaaa", "bbbbb", "ccccc"] + .iter() + .map(|h| h.to_string()) + .collect(), + vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(), + ]; let (selected_column_index, headers, rows, constraints) = component.calculate_cell_widths(20); @@ -780,10 +797,13 @@ mod test { Constraint::Length(1), Constraint::Length(5), Constraint::Length(5), - Constraint::Min(10), + Constraint::Length(5), ] ); + } + #[test] + fn test_calculate_cell_widths_when_component_has_multiple_rows() { let mut component = TableComponent::new(KeyConfig::default()); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.rows = vec![ @@ -814,7 +834,7 @@ mod test { Constraint::Length(1), Constraint::Length(10), Constraint::Length(5), - Constraint::Min(10), + Constraint::Length(5), ] ); } diff --git a/src/config.rs b/src/config.rs index cb85e04..86f209e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use crate::Key; use serde::Deserialize; +use std::fmt; use std::fs::File; use std::io::{BufReader, Read}; @@ -10,10 +11,28 @@ pub struct Config { pub key_config: KeyConfig, } +#[derive(Debug, Deserialize, Clone)] +enum DatabaseType { + #[serde(rename = "mysql")] + MySql, + #[serde(rename = "postgres")] + Postgres, +} + +impl fmt::Display for DatabaseType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::MySql => write!(f, "mysql"), + Self::Postgres => write!(f, "postgres"), + } + } +} + impl Default for Config { fn default() -> Self { Self { conn: vec![Connection { + r#type: DatabaseType::MySql, name: None, user: "root".to_string(), host: "localhost".to_string(), @@ -27,6 +46,7 @@ impl Default for Config { #[derive(Debug, Deserialize, Clone)] pub struct Connection { + r#type: DatabaseType, name: Option, user: String, host: String, @@ -113,19 +133,42 @@ impl Config { impl Connection { pub fn database_url(&self) -> String { match &self.database { - Some(database) => format!( - "mysql://{user}:@{host}:{port}/{database}", - user = self.user, - host = self.host, - port = self.port, - database = database - ), - None => format!( - "mysql://{user}:@{host}:{port}", - user = self.user, - host = self.host, - port = self.port, - ), + Some(database) => match self.r#type { + DatabaseType::MySql => format!( + "mysql://{user}:@{host}:{port}/{database}", + user = self.user, + host = self.host, + port = self.port, + database = database + ), + DatabaseType::Postgres => { + format!( + "postgres://{user}@{host}:{port}/{database}", + user = self.user, + host = self.host, + port = self.port, + database = database + ) + } + }, + None => match self.r#type { + DatabaseType::MySql => format!( + "mysql://{user}:@{host}:{port}", + user = self.user, + host = self.host, + port = self.port, + ), + DatabaseType::Postgres => format!( + "postgres://{user}@{host}:{port}", + user = self.user, + host = self.host, + port = self.port, + ), + }, } } + + pub fn is_mysql(&self) -> bool { + matches!(self.r#type, DatabaseType::MySql) + } } diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..c3e2572 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,29 @@ +pub mod mysql; +pub mod postgres; + +pub use mysql::MySqlPool; +pub use postgres::PostgresPool; + +use async_trait::async_trait; +use database_tree::{Child, Database, Table}; + +pub const RECORDS_LIMIT_PER_PAGE: u8 = 200; + +#[async_trait] +pub trait Pool { + async fn get_databases(&self) -> anyhow::Result>; + async fn get_tables(&self, database: String) -> anyhow::Result>; + async fn get_records( + &self, + database: &Database, + table: &Table, + page: u16, + filter: Option, + ) -> anyhow::Result<(Vec, Vec>)>; + async fn get_columns( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result<(Vec, Vec>)>; + async fn close(&self); +} diff --git a/src/utils.rs b/src/database/mysql.rs similarity index 77% rename from src/utils.rs rename to src/database/mysql.rs index 6431069..00a1f74 100644 --- a/src/utils.rs +++ b/src/database/mysql.rs @@ -1,30 +1,10 @@ +use super::{Pool, RECORDS_LIMIT_PER_PAGE}; use async_trait::async_trait; use chrono::NaiveDate; -use database_tree::{Database, Table}; +use database_tree::{Child, Database, Table}; use futures::TryStreamExt; use sqlx::mysql::{MySqlColumn, MySqlPool as MPool, MySqlRow}; -use sqlx::{Column as _, Row, TypeInfo}; - -pub const RECORDS_LIMIT_PER_PAGE: u8 = 200; - -#[async_trait] -pub trait Pool { - async fn get_databases(&self) -> anyhow::Result>; - async fn get_tables(&self, database: String) -> anyhow::Result>; - async fn get_records( - &self, - database: &str, - table: &str, - page: u16, - filter: Option, - ) -> anyhow::Result<(Vec, Vec>)>; - async fn get_columns( - &self, - database: &str, - table: &str, - ) -> anyhow::Result<(Vec, Vec>)>; - async fn close(&self); -} +use sqlx::{Column as _, Row as _, TypeInfo as _}; pub struct MySqlPool { pool: MPool, @@ -51,32 +31,32 @@ impl Pool for MySqlPool { for db in databases { list.push(Database::new( db.clone(), - get_tables(db.clone(), &self.pool).await?, + self.get_tables(db.clone()).await?, )) } Ok(list) } - async fn get_tables(&self, database: String) -> anyhow::Result> { + async fn get_tables(&self, database: String) -> anyhow::Result> { let tables = sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str()) .fetch_all(&self.pool) .await?; - Ok(tables) + Ok(tables.into_iter().map(|table| table.into()).collect()) } async fn get_records( &self, - database: &str, - table: &str, + database: &Database, + table: &Table, page: u16, filter: Option, ) -> anyhow::Result<(Vec, Vec>)> { let query = if let Some(filter) = filter { format!( "SELECT * FROM `{database}`.`{table}` WHERE {filter} LIMIT {page}, {limit}", - database = database, - table = table, + database = database.name, + table = table.name, filter = filter, page = page, limit = RECORDS_LIMIT_PER_PAGE @@ -84,8 +64,8 @@ impl Pool for MySqlPool { } else { format!( "SELECT * FROM `{}`.`{}` limit {page}, {limit}", - database, - table, + database.name, + table.name, page = page, limit = RECORDS_LIMIT_PER_PAGE ) @@ -110,10 +90,13 @@ impl Pool for MySqlPool { async fn get_columns( &self, - database: &str, - table: &str, + database: &Database, + table: &Table, ) -> anyhow::Result<(Vec, Vec>)> { - let query = format!("SHOW FULL COLUMNS FROM `{}`.`{}`", database, table); + 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![]; @@ -137,18 +120,7 @@ impl Pool for MySqlPool { } } -pub async fn get_tables(database: String, pool: &MPool) -> anyhow::Result> { - 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, -) -> anyhow::Result { +fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyhow::Result { let column_name = column.name(); match column.type_info().clone().name() { "INT" | "SMALLINT" | "BIGINT" => { diff --git a/src/database/postgres.rs b/src/database/postgres.rs new file mode 100644 index 0000000..7c2365a --- /dev/null +++ b/src/database/postgres.rs @@ -0,0 +1,275 @@ +use super::{Pool, RECORDS_LIMIT_PER_PAGE}; +use async_trait::async_trait; +use chrono::NaiveDate; +use database_tree::{Child, Database, Schema, Table}; +use futures::TryStreamExt; +use itertools::Itertools; +use sqlx::postgres::{PgColumn, PgPool, PgRow}; +use sqlx::{Column as _, Row as _, TypeInfo as _}; + +pub struct PostgresPool { + pool: PgPool, +} + +impl PostgresPool { + pub async fn new(database_url: &str) -> anyhow::Result { + Ok(Self { + pool: PgPool::connect(database_url).await?, + }) + } +} + +#[async_trait] +impl Pool for PostgresPool { + async fn get_databases(&self) -> anyhow::Result> { + let databases = sqlx::query("SELECT datname FROM pg_database") + .fetch_all(&self.pool) + .await? + .iter() + .map(|table| table.get(0)) + .collect::>(); + let mut list = vec![]; + for db in databases { + list.push(Database::new( + db.clone(), + self.get_tables(db.clone()).await?, + )) + } + Ok(list) + } + + async fn get_tables(&self, database: String) -> anyhow::Result> { + let mut rows = + sqlx::query("SELECT * FROM information_schema.tables WHERE table_catalog = $1") + .bind(database) + .fetch(&self.pool); + let mut tables = Vec::new(); + while let Some(row) = rows.try_next().await? { + tables.push(Table { + name: row.get("table_name"), + create_time: None, + update_time: None, + engine: None, + schema: row.get("table_schema"), + }) + } + let mut schemas = vec![]; + for (key, group) in &tables + .iter() + .sorted_by(|a, b| Ord::cmp(&b.schema, &a.schema)) + .group_by(|t| t.schema.as_ref()) + { + if let Some(key) = key { + schemas.push( + Schema { + name: key.to_string(), + tables: group.cloned().collect(), + } + .into(), + ) + } + } + Ok(schemas) + } + + async fn get_records( + &self, + database: &Database, + table: &Table, + page: u16, + filter: Option, + ) -> anyhow::Result<(Vec, Vec>)> { + let query = if let Some(filter) = filter.as_ref() { + format!( + r#"SELECT * FROM "{database}""{table_schema}"."{table}" WHERE {filter} LIMIT {page}, {limit}"#, + database = database.name, + table = table.name, + filter = filter, + table_schema = table.schema.clone().unwrap_or_else(|| "public".to_string()), + page = page, + limit = RECORDS_LIMIT_PER_PAGE + ) + } else { + format!( + r#"SELECT * FROM "{database}"."{table_schema}"."{table}" limit {limit} offset {page}"#, + database = database.name, + table = table.name, + table_schema = table.schema.clone().unwrap_or_else(|| "public".to_string()), + 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![]; + let mut json_records = None; + 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() { + match convert_column_value_to_string(&row, column) { + Ok(v) => new_row.push(v), + Err(_) => { + if json_records.is_none() { + json_records = Some( + self.get_json_records(database, table, page, filter.clone()) + .await?, + ); + } + if let Some(json_records) = &json_records { + match json_records + .get(records.len()) + .unwrap() + .get(column.name()) + .unwrap() + { + serde_json::Value::String(v) => new_row.push(v.to_string()), + serde_json::Value::Null => new_row.push("NULL".to_string()), + serde_json::Value::Array(v) => { + new_row.push(v.iter().map(|v| v.to_string()).join(",")) + } + _ => (), + } + } + } + } + } + records.push(new_row) + } + Ok((headers, records)) + } + + async fn get_columns( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result<(Vec, Vec>)> { + let table_schema = table + .schema + .as_ref() + .map_or("public", |schema| schema.as_str()); + let mut rows = sqlx::query( + "SELECT * FROM information_schema.columns WHERE table_catalog = $1 AND table_schema = $2 AND table_name = $3" + ) + .bind(&database.name).bind(table_schema).bind(&table.name) + .fetch(&self.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(); + let mut new_row = vec![]; + for column in row.columns() { + new_row.push(convert_column_value_to_string(&row, column)?) + } + records.push(new_row) + } + Ok((headers, records)) + } + + async fn close(&self) { + self.pool.close().await; + } +} + +impl PostgresPool { + async fn get_json_records( + &self, + database: &Database, + table: &Table, + page: u16, + filter: Option, + ) -> anyhow::Result> { + let query = if let Some(filter) = filter { + format!( + r#"SELECT to_json({table}.*) FROM "{database}""{table_schema}"."{table}" WHERE {filter} LIMIT {page}, {limit}"#, + database = database.name, + table = table.name, + filter = filter, + table_schema = table.schema.clone().unwrap_or_else(|| "public".to_string()), + page = page, + limit = RECORDS_LIMIT_PER_PAGE + ) + } else { + format!( + r#"SELECT to_json({table}.*) FROM "{database}"."{table_schema}"."{table}" limit {limit} offset {page}"#, + database = database.name, + table = table.name, + table_schema = table.schema.clone().unwrap_or_else(|| "public".to_string()), + page = page, + limit = RECORDS_LIMIT_PER_PAGE + ) + }; + let json: Vec<(serde_json::Value,)> = + sqlx::query_as(query.as_str()).fetch_all(&self.pool).await?; + Ok(json.iter().map(|v| v.clone().0).collect()) + } +} + +fn convert_column_value_to_string(row: &PgRow, column: &PgColumn) -> anyhow::Result { + 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())); + } + 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())); + } + 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())); + } + 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())); + } + if let Ok(value) = row.try_get(column_name) { + let value: Option<&[u8]> = value; + return Ok(value.map_or("NULL".to_string(), |values| { + format!( + "\\x{}", + values + .iter() + .map(|v| format!("{:02x}", v)) + .collect::() + ) + })); + } + 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())); + } + if let Ok(value) = row.try_get(column_name) { + let value: String = value; + return Ok(value); + } + 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())); + } + 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())); + } + 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())); + } + if let Ok(value) = row.try_get(column_name) { + let value: Option> = value; + return Ok(value.map_or("NULL".to_string(), |v| v.join(",").to_string())); + } + Err(anyhow::anyhow!( + "column type not implemented: `{}` {}", + column_name, + column.type_info().clone().name() + )) +} diff --git a/src/main.rs b/src/main.rs index 8fe10a2..643dd9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,9 @@ mod app; mod clipboard; mod components; mod config; +mod database; mod event; mod ui; -mod utils; #[macro_use] mod log;