Support PostgreSQL (#35)

* move utils into database module

* draw help in connections view

* support postgres

* add table_schema field

* implement Pool for Postgres

* fix database tree for postgres

* add tests for selection_top, bottom

* fix clippy warnings

* remove unused function

* get schema

* fix clippy warnings

* show user defined types

* fix cell with when selecting far right cell

* split tests into tree functions

* convert TEXT[] to Vec<String>

* add prefix

* remove column pattern matching
pull/36/head
Takayuki Maeda 3 years ago committed by GitHub
parent 8f26ffed09
commit 5da6b1b796
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

59
Cargo.lock generated

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.4.7" version = "0.4.7"
@ -319,9 +321,19 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "database-tree" name = "database-tree"
version = "0.1.2" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -555,13 +567,19 @@ dependencies = [
"ahash 0.4.7", "ahash 0.4.7",
] ]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.6.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8"
dependencies = [ dependencies = [
"hashbrown", "hashbrown 0.9.1",
] ]
[[package]] [[package]]
@ -588,6 +606,16 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 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]] [[package]]
name = "idna" name = "idna"
version = "0.2.3" version = "0.2.3"
@ -599,6 +627,16 @@ dependencies = [
"unicode-normalization", "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]] [[package]]
name = "instant" name = "instant"
version = "0.1.9" version = "0.1.9"
@ -730,6 +768,17 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 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]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.0" version = "2.4.0"
@ -1273,6 +1322,7 @@ version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [ dependencies = [
"indexmap",
"itoa", "itoa",
"ryu", "ryu",
"serde", "serde",
@ -1401,9 +1451,11 @@ dependencies = [
"generic-array", "generic-array",
"hashlink", "hashlink",
"hex", "hex",
"hmac",
"itoa", "itoa",
"libc", "libc",
"log", "log",
"md-5",
"memchr", "memchr",
"num-bigint 0.3.2", "num-bigint 0.3.2",
"once_cell", "once_cell",
@ -1413,6 +1465,8 @@ dependencies = [
"rsa", "rsa",
"rust_decimal", "rust_decimal",
"rustls", "rustls",
"serde",
"serde_json",
"sha-1", "sha-1",
"sha2", "sha2",
"smallvec", "smallvec",
@ -1440,6 +1494,7 @@ dependencies = [
"lazy_static", "lazy_static",
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_json",
"sha2", "sha2",
"sqlx-core", "sqlx-core",
"sqlx-rt", "sqlx-rt",

@ -19,7 +19,7 @@ tui = { version = "0.14.0", features = ["crossterm"], default-features = false }
crossterm = "0.19" crossterm = "0.19"
anyhow = "1.0.38" anyhow = "1.0.38"
unicode-width = "0.1" 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" chrono = "0.4"
tokio = { version = "0.2.22", features = ["full"] } tokio = { version = "0.2.22", features = ["full"] }
futures = "0.3.5" futures = "0.3.5"

@ -1,7 +1,7 @@
[package] [package]
name = "database-tree" name = "database-tree"
version = "0.1.2" version = "0.1.0"
authors = ["Stephan Dilly <dilly.stephan@gmail.com>"] authors = ["Takayuki Maeda <takoyaki0316@gmail.com>"]
edition = "2018" edition = "2018"
license = "MIT" license = "MIT"
homepage = "https://github.com/TaKO8Ki/gobang" homepage = "https://github.com/TaKO8Ki/gobang"

@ -82,6 +82,7 @@ impl DatabaseTree {
DatabaseTreeItemKind::Table { table, database } => { DatabaseTreeItemKind::Table { table, database } => {
Some((database.clone(), table.clone())) Some((database.clone(), table.clone()))
} }
DatabaseTreeItemKind::Schema { .. } => None,
} }
}) })
} }
@ -215,7 +216,8 @@ impl DatabaseTree {
.unwrap(); .unwrap();
if !up 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 && self.selected_item().unwrap() == item
{ {
break; break;
@ -264,6 +266,11 @@ impl DatabaseTree {
return Some(current_index); 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) self.select_parent(current_index)
} }
@ -278,6 +285,14 @@ impl DatabaseTree {
return self.selection_updown(current_selection, false); 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 None
} }
@ -292,8 +307,7 @@ impl DatabaseTree {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{Database, DatabaseTree, MoveSelection, Table}; use crate::{Database, DatabaseTree, MoveSelection, Schema, Table};
// use pretty_assertions::assert_eq;
use std::collections::BTreeSet; use std::collections::BTreeSet;
impl Table { impl Table {
@ -303,6 +317,17 @@ mod test {
create_time: None, create_time: None,
update_time: None, update_time: None,
engine: 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() { fn test_selection() {
let items = vec![Database::new( let items = vec![Database::new(
"a".to_string(), "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 // a
// b // b
// c
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap();
@ -323,6 +368,10 @@ mod test {
assert_eq!(tree.selection, Some(0)); assert_eq!(tree.selection, Some(0));
assert!(tree.move_selection(MoveSelection::Down)); assert!(tree.move_selection(MoveSelection::Down));
assert_eq!(tree.selection, Some(1)); 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] #[test]
@ -330,9 +379,12 @@ mod test {
let items = vec![ let items = vec![
Database::new( Database::new(
"a".to_string(), "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 // a
@ -342,8 +394,38 @@ mod test {
// e // e
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); 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); tree.selection = Some(1);
assert!(tree.move_selection(MoveSelection::Down)); assert!(tree.move_selection(MoveSelection::Down));
@ -354,7 +436,10 @@ mod test {
fn test_selection_left_collapse() { fn test_selection_left_collapse() {
let items = vec![Database::new( let items = vec![Database::new(
"a".to_string(), "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 // a
@ -370,13 +455,52 @@ mod test {
assert!(tree.items.tree_items[0].kind().is_database_collapsed()); assert!(tree.items.tree_items[0].kind().is_database_collapsed());
assert!(!tree.items.tree_items[1].info().is_visible()); assert!(!tree.items.tree_items[1].info().is_visible());
assert!(!tree.items.tree_items[2].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] #[test]
fn test_selection_left_parent() { fn test_selection_left_parent() {
let items = vec![Database::new( let items = vec![Database::new(
"a".to_string(), "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 // a
@ -389,13 +513,43 @@ mod test {
assert!(tree.move_selection(MoveSelection::Left)); assert!(tree.move_selection(MoveSelection::Left));
assert_eq!(tree.selection, Some(0)); 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] #[test]
fn test_selection_right_expand() { fn test_selection_right_expand() {
let items = vec![Database::new( let items = vec![Database::new(
"a".to_string(), "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 // a
@ -403,7 +557,29 @@ mod test {
// c // c
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); 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); tree.selection = Some(0);
assert!(tree.move_selection(MoveSelection::Right)); assert!(tree.move_selection(MoveSelection::Right));
@ -412,6 +588,10 @@ mod test {
assert!(tree.move_selection(MoveSelection::Right)); assert!(tree.move_selection(MoveSelection::Right));
assert_eq!(tree.selection, Some(1)); 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] #[test]
@ -419,9 +599,12 @@ mod test {
let items = vec![ let items = vec![
Database::new( Database::new(
"a".to_string(), "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 // a
@ -437,9 +620,148 @@ mod test {
tree.selection = Some(0); tree.selection = Some(0);
assert!(tree.move_selection(MoveSelection::Left)); assert!(tree.move_selection(MoveSelection::Left));
assert!(tree.move_selection(MoveSelection::Down)); assert!(tree.move_selection(MoveSelection::Down));
assert_eq!(tree.selection, Some(3));
let s = tree.visual_selection().unwrap(); let s = tree.visual_selection().unwrap();
assert_eq!(s.count, 3); assert_eq!(s.count, 3);
assert_eq!(s.index, 1); 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));
} }
} }

@ -1,6 +1,6 @@
use crate::Database;
use crate::{error::Result, treeitems_iter::TreeItemsIterator}; use crate::{error::Result, treeitems_iter::TreeItemsIterator};
use crate::{item::DatabaseTreeItemKind, DatabaseTreeItem}; use crate::{item::DatabaseTreeItemKind, DatabaseTreeItem};
use crate::{Child, Database};
use std::{ use std::{
collections::{BTreeSet, HashMap}, collections::{BTreeSet, HashMap},
usize, usize,
@ -26,8 +26,8 @@ impl DatabaseTreeItems {
.iter() .iter()
.filter(|item| item.is_database() || item.is_match(&filter_text)) .filter(|item| item.is_database() || item.is_match(&filter_text))
.map(|item| { .map(|item| {
let mut item = item.clone();
if item.is_database() { if item.is_database() {
let mut item = item.clone();
item.set_collapsed(false); item.set_collapsed(false);
item item
} else { } else {
@ -51,8 +51,16 @@ impl DatabaseTreeItems {
{ {
Self::push_databases(e, &mut items, &mut items_added, collapsed)?; Self::push_databases(e, &mut items, &mut items_added, collapsed)?;
} }
for table in &e.tables { for child in &e.children {
items.push(DatabaseTreeItem::new_table(e, table)); 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) { pub fn expand(&mut self, index: usize, recursive: bool) {
if self.tree_items[index].kind().is_database() { if self.tree_items[index].kind().is_database() {
self.tree_items[index].expand_database(); self.tree_items[index].expand_database();
let tree_item = self.tree_items[index].clone();
let name = self.tree_items[index].kind().name(); let name = self.tree_items[index].kind().name();
let kind = tree_item.kind();
if recursive { if recursive {
for i in index + 1..self.tree_items.len() { 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<String>, start_idx: usize, set_defaults: bool) { fn update_visibility(&mut self, prefix: &DatabaseTreeItemKind, start_idx: usize) {
let mut inner_collapsed: Option<String> = None; let mut inner_collapsed: Option<DatabaseTreeItemKind> = None;
for i in start_idx..self.tree_items.len() { for i in start_idx..self.tree_items.len() {
if let Some(ref collapsed_item) = inner_collapsed { if let Some(ref collapsed_item) = inner_collapsed {
if let Some(db) = self.tree_items[i].kind().database_name().clone() { match collapsed_item {
if db == *collapsed_item { DatabaseTreeItemKind::Database { name, .. } => {
if set_defaults { if let DatabaseTreeItemKind::Schema { database, .. } =
self.tree_items[i].info_mut().set_visible(false); 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; inner_collapsed = None;
} }
let item_kind = self.tree_items[i].kind().clone(); let item_kind = self.tree_items[i].kind().clone();
if matches!(item_kind, DatabaseTreeItemKind::Database{ collapsed, .. } if collapsed) { if matches!(item_kind, DatabaseTreeItemKind::Database{ collapsed, .. } if collapsed)
inner_collapsed = item_kind.database_name().clone(); || matches!(item_kind, DatabaseTreeItemKind::Schema{ collapsed, .. } if collapsed)
{
inner_collapsed = Some(item_kind.clone());
} }
if let Some(db) = item_kind.database_name() { match prefix {
if prefix.as_ref().map_or(true, |prefix| *prefix == *db) { DatabaseTreeItemKind::Database { name, .. } => {
self.tree_items[i].info_mut().set_visible(true); 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 { DatabaseTreeItemKind::Schema { schema, .. } => {
// if we do not set defaults we can early out if let DatabaseTreeItemKind::Table { table, .. } = item_kind {
if set_defaults { if matches!(table.schema, Some(table_schema) if schema.name == table_schema)
self.tree_items[i].info_mut().set_visible(false); {
} else { self.tree_items[i].info_mut().set_visible(true);
return; }
}
} }
_ => (),
} }
} }
} }

@ -1,4 +1,4 @@
use crate::{Database, Table}; use crate::{Database, Schema, Table};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TreeItemInfo { pub struct TreeItemInfo {
@ -31,8 +31,19 @@ impl TreeItemInfo {
/// `DatabaseTreeItem` can be of two kinds /// `DatabaseTreeItem` can be of two kinds
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum DatabaseTreeItemKind { pub enum DatabaseTreeItemKind {
Database { name: String, collapsed: bool }, Database {
Table { database: Database, table: Table }, name: String,
collapsed: bool,
},
Table {
database: Database,
table: Table,
},
Schema {
database: Database,
schema: Schema,
collapsed: bool,
},
} }
impl DatabaseTreeItemKind { impl DatabaseTreeItemKind {
@ -44,10 +55,23 @@ impl DatabaseTreeItemKind {
matches!(self, Self::Table { .. }) matches!(self, Self::Table { .. })
} }
pub const fn is_schema(&self) -> bool {
matches!(self, Self::Schema { .. })
}
pub const fn is_database_collapsed(&self) -> bool { pub const fn is_database_collapsed(&self) -> bool {
match self { match self {
Self::Database { collapsed, .. } => *collapsed, Self::Database { collapsed, .. } => *collapsed,
Self::Table { .. } => false, 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 { match self {
Self::Database { name, .. } => name.to_string(), Self::Database { name, .. } => name.to_string(),
Self::Table { table, .. } => table.name.clone(), Self::Table { table, .. } => table.name.clone(),
Self::Schema { schema, .. } => schema.name.clone(),
} }
} }
@ -62,6 +87,15 @@ impl DatabaseTreeItemKind {
match self { match self {
Self::Database { .. } => None, Self::Database { .. } => None,
Self::Table { database, .. } => Some(database.name.clone()), Self::Table { database, .. } => Some(database.name.clone()),
Self::Schema { database, .. } => Some(database.name.clone()),
}
}
pub fn schema_name(&self) -> Option<String> {
match self {
Self::Database { .. } => None,
Self::Table { table, .. } => table.schema.clone(),
Self::Schema { .. } => None,
} }
} }
} }
@ -76,7 +110,7 @@ pub struct DatabaseTreeItem {
impl DatabaseTreeItem { impl DatabaseTreeItem {
pub fn new_table(database: &Database, table: &Table) -> Self { pub fn new_table(database: &Database, table: &Table) -> Self {
Self { Self {
info: TreeItemInfo::new(1, false), info: TreeItemInfo::new(if table.schema.is_some() { 2 } else { 1 }, false),
kind: DatabaseTreeItemKind::Table { kind: DatabaseTreeItemKind::Table {
database: database.clone(), database: database.clone(),
table: table.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 { pub fn new_database(database: &Database, _collapsed: bool) -> Self {
Self { Self {
info: TreeItemInfo::new(0, true), 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) { pub fn show(&mut self) {
self.info.visible = true; self.info.visible = true;
} }
@ -145,6 +216,7 @@ impl DatabaseTreeItem {
match self.kind.clone() { match self.kind.clone() {
DatabaseTreeItemKind::Database { name, .. } => name.contains(filter_text), DatabaseTreeItemKind::Database { name, .. } => name.contains(filter_text),
DatabaseTreeItemKind::Table { table, .. } => table.name.contains(filter_text), DatabaseTreeItemKind::Table { table, .. } => table.name.contains(filter_text),
DatabaseTreeItemKind::Schema { schema, .. } => schema.name.contains(filter_text),
} }
} }

@ -14,18 +14,42 @@ pub use crate::{
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub struct Database { pub struct Database {
pub name: String, pub name: String,
pub tables: Vec<Table>, pub children: Vec<Child>,
}
#[derive(Clone, PartialEq, Debug)]
pub enum Child {
Table(Table),
Schema(Schema),
}
impl From<Table> for Child {
fn from(t: Table) -> Self {
Child::Table(t)
}
}
impl From<Schema> for Child {
fn from(s: Schema) -> Self {
Child::Schema(s)
}
} }
impl Database { impl Database {
pub fn new(database: String, tables: Vec<Table>) -> Self { pub fn new(database: String, children: Vec<Child>) -> Self {
Self { Self {
name: database, name: database,
tables, children,
} }
} }
} }
#[derive(Clone, PartialEq, Debug)]
pub struct Schema {
pub name: String,
pub tables: Vec<Table>,
}
#[derive(sqlx::FromRow, Debug, Clone, PartialEq)] #[derive(sqlx::FromRow, Debug, Clone, PartialEq)]
pub struct Table { pub struct Table {
#[sqlx(rename = "Name")] #[sqlx(rename = "Name")]
@ -36,4 +60,6 @@ pub struct Table {
pub update_time: Option<chrono::DateTime<chrono::Utc>>, pub update_time: Option<chrono::DateTime<chrono::Utc>>,
#[sqlx(rename = "Engine")] #[sqlx(rename = "Engine")]
pub engine: Option<String>, pub engine: Option<String>,
#[sqlx(default)]
pub schema: Option<String>,
} }

@ -1,17 +1,33 @@
[[conn]] [[conn]]
type = "mysql"
name = "sample" name = "sample"
user = "root" user = "root"
host = "localhost" host = "localhost"
port = 3306 port = 3306
[[conn]] [[conn]]
type = "mysql"
user = "root" user = "root"
host = "localhost" host = "localhost"
port = 3306 port = 3306
database = "world" database = "world"
[[conn]] [[conn]]
type = "mysql"
user = "root" user = "root"
host = "localhost" host = "localhost"
port = 3306 port = 3306
database = "employees" database = "employees"
[[conn]]
type = "postgres"
user = "postgres"
host = "localhost"
port = 5432
[[conn]]
type = "postgres"
user = "postgres"
host = "localhost"
port = 5432
database = "dvdrental"

@ -1,7 +1,7 @@
use crate::clipboard::Clipboard; use crate::clipboard::Clipboard;
use crate::components::{CommandInfo, Component as _, DrawableComponent as _, EventState}; 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::event::Key;
use crate::utils::{MySqlPool, Pool};
use crate::{ use crate::{
components::tab::Tab, components::tab::Tab,
components::{ components::{
@ -64,6 +64,8 @@ impl App {
.split(f.size())[0], .split(f.size())[0],
false, false,
)?; )?;
self.error.draw(f, Rect::default(), false)?;
self.help.draw(f, Rect::default(), false)?;
return Ok(()); return Ok(());
} }
@ -131,9 +133,15 @@ impl App {
if let Some(pool) = self.pool.as_ref() { if let Some(pool) = self.pool.as_ref() {
pool.close().await; pool.close().await;
} }
self.pool = Some(Box::new( self.pool = if conn.is_mysql() {
MySqlPool::new(conn.database_url().as_str()).await?, 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 { let databases = match &conn.database {
Some(database) => vec![Database::new( Some(database) => vec![Database::new(
database.clone(), database.clone(),
@ -148,6 +156,7 @@ impl App {
self.databases.update(databases.as_slice()).unwrap(); self.databases.update(databases.as_slice()).unwrap();
self.focus = Focus::DabataseList; self.focus = Focus::DabataseList;
self.record_table.reset(); self.record_table.reset();
self.tab.reset();
} }
Ok(()) Ok(())
} }
@ -159,7 +168,7 @@ impl App {
.pool .pool
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_records(&database.name, &table.name, 0, None) .get_records(&database, &table, 0, None)
.await?; .await?;
self.record_table self.record_table
.update(records, headers, database.clone(), table.clone()); .update(records, headers, database.clone(), table.clone());
@ -168,7 +177,7 @@ impl App {
.pool .pool
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_columns(&database.name, &table.name) .get_columns(&database, &table)
.await?; .await?;
self.structure_table self.structure_table
.update(records, headers, database.clone(), table.clone()); .update(records, headers, database.clone(), table.clone());
@ -185,8 +194,8 @@ impl App {
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_records( .get_records(
&database.name, &database,
&table.name, &table,
0, 0,
if self.record_table.filter.input.is_empty() { if self.record_table.filter.input.is_empty() {
None None
@ -268,10 +277,7 @@ impl App {
} }
if let Some(index) = self.record_table.table.selected_row.selected() { if let Some(index) = self.record_table.table.selected_row.selected() {
if index.saturating_add(1) if index.saturating_add(1) % RECORDS_LIMIT_PER_PAGE as usize == 0 {
% crate::utils::RECORDS_LIMIT_PER_PAGE as usize
== 0
{
if let Some((database, table)) = if let Some((database, table)) =
self.databases.tree().selected_table() self.databases.tree().selected_table()
{ {
@ -280,8 +286,8 @@ impl App {
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_records( .get_records(
&database.name.clone(), &database,
&table.name, &table,
index as u16, index as u16,
if self.record_table.filter.input.is_empty() { if self.record_table.filter.input.is_empty() {
None None

@ -20,10 +20,14 @@ pub struct ConnectionsComponent {
impl ConnectionsComponent { impl ConnectionsComponent {
pub fn new(key_config: KeyConfig, connections: Vec<Connection>) -> Self { pub fn new(key_config: KeyConfig, connections: Vec<Connection>) -> Self {
let mut state = ListState::default();
if !connections.is_empty() {
state.select(Some(0));
}
Self { Self {
connections, connections,
key_config, key_config,
state: ListState::default(), state,
} }
} }
@ -31,7 +35,7 @@ impl ConnectionsComponent {
let i = match self.state.selected() { let i = match self.state.selected() {
Some(i) => { Some(i) => {
if i >= self.connections.len() - 1 { if i >= self.connections.len() - 1 {
0 self.connections.len() - 1
} else { } else {
i + 1 i + 1
} }
@ -45,7 +49,7 @@ impl ConnectionsComponent {
let i = match self.state.selected() { let i = match self.state.selected() {
Some(i) => { Some(i) => {
if i == 0 { if i == 0 {
self.connections.len() - 1 0
} else { } else {
i - 1 i - 1
} }

@ -94,8 +94,8 @@ impl DatabasesComponent {
format!("{:w$}", " ", w = (indent as usize) * 2) format!("{:w$}", " ", w = (indent as usize) * 2)
}; };
let arrow = if item.kind().is_database() { let arrow = if item.kind().is_database() || item.kind().is_schema() {
if item.kind().is_database_collapsed() { if item.kind().is_database_collapsed() || item.kind().is_schema_collapsed() {
FOLDER_ICON_COLLAPSED FOLDER_ICON_COLLAPSED
} else { } else {
FOLDER_ICON_EXPANDED FOLDER_ICON_EXPANDED
@ -346,7 +346,7 @@ mod test {
DatabaseTreeItem::new_database( DatabaseTreeItem::new_database(
&Database { &Database {
name: "foo".to_string(), name: "foo".to_string(),
tables: Vec::new(), children: Vec::new(),
}, },
false, false,
), ),
@ -366,7 +366,7 @@ mod test {
DatabaseTreeItem::new_database( DatabaseTreeItem::new_database(
&Database { &Database {
name: "foo".to_string(), name: "foo".to_string(),
tables: Vec::new(), children: Vec::new(),
}, },
false, false,
), ),
@ -389,13 +389,14 @@ mod test {
DatabaseTreeItem::new_table( DatabaseTreeItem::new_table(
&Database { &Database {
name: "foo".to_string(), name: "foo".to_string(),
tables: Vec::new(), children: Vec::new(),
}, },
&Table { &Table {
name: "bar".to_string(), name: "bar".to_string(),
create_time: None, create_time: None,
update_time: None, update_time: None,
engine: None, engine: None,
schema: None
}, },
), ),
false, false,
@ -414,13 +415,14 @@ mod test {
DatabaseTreeItem::new_table( DatabaseTreeItem::new_table(
&Database { &Database {
name: "foo".to_string(), name: "foo".to_string(),
tables: Vec::new(), children: Vec::new(),
}, },
&Table { &Table {
name: "bar".to_string(), name: "bar".to_string(),
create_time: None, create_time: None,
update_time: None, update_time: None,
engine: None, engine: None,
schema: None
}, },
), ),
true, true,
@ -442,13 +444,14 @@ mod test {
DatabaseTreeItem::new_table( DatabaseTreeItem::new_table(
&Database { &Database {
name: "foo".to_string(), name: "foo".to_string(),
tables: Vec::new(), children: Vec::new(),
}, },
&Table { &Table {
name: "barbaz".to_string(), name: "barbaz".to_string(),
create_time: None, create_time: None,
update_time: None, update_time: None,
engine: None, engine: None,
schema: None
}, },
), ),
false, false,
@ -467,13 +470,14 @@ mod test {
DatabaseTreeItem::new_table( DatabaseTreeItem::new_table(
&Database { &Database {
name: "foo".to_string(), name: "foo".to_string(),
tables: Vec::new(), children: Vec::new(),
}, },
&Table { &Table {
name: "barbaz".to_string(), name: "barbaz".to_string(),
create_time: None, create_time: None,
update_time: None, update_time: None,
engine: None, engine: None,
schema: None
}, },
), ),
true, true,

@ -38,6 +38,10 @@ impl TabComponent {
} }
} }
pub fn reset(&mut self) {
self.selected_tab = Tab::Records;
}
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,

@ -363,7 +363,9 @@ impl TableComponent {
} }
column_index += 1 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(); widths.pop();
} }
let far_right_column_index = column_index; let far_right_column_index = column_index;
@ -371,7 +373,9 @@ impl TableComponent {
.iter() .iter()
.map(|(_, width)| Constraint::Length(*width as u16)) .map(|(_, width)| Constraint::Length(*width as u16))
.collect::<Vec<Constraint>>(); .collect::<Vec<Constraint>>();
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.push(Constraint::Min(10));
} }
constraints.insert(0, Constraint::Length(number_column_width)); constraints.insert(0, Constraint::Length(number_column_width));
@ -739,7 +743,7 @@ mod test {
} }
#[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()); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
@ -762,6 +766,19 @@ mod test {
Constraint::Min(10), 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) = let (selected_column_index, headers, rows, constraints) =
component.calculate_cell_widths(20); component.calculate_cell_widths(20);
@ -780,10 +797,13 @@ mod test {
Constraint::Length(1), Constraint::Length(1),
Constraint::Length(5), Constraint::Length(5),
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()); let mut component = TableComponent::new(KeyConfig::default());
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect(); component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![ component.rows = vec![
@ -814,7 +834,7 @@ mod test {
Constraint::Length(1), Constraint::Length(1),
Constraint::Length(10), Constraint::Length(10),
Constraint::Length(5), Constraint::Length(5),
Constraint::Min(10), Constraint::Length(5),
] ]
); );
} }

@ -1,5 +1,6 @@
use crate::Key; use crate::Key;
use serde::Deserialize; use serde::Deserialize;
use std::fmt;
use std::fs::File; use std::fs::File;
use std::io::{BufReader, Read}; use std::io::{BufReader, Read};
@ -10,10 +11,28 @@ pub struct Config {
pub key_config: KeyConfig, 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 { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
conn: vec![Connection { conn: vec![Connection {
r#type: DatabaseType::MySql,
name: None, name: None,
user: "root".to_string(), user: "root".to_string(),
host: "localhost".to_string(), host: "localhost".to_string(),
@ -27,6 +46,7 @@ impl Default for Config {
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct Connection { pub struct Connection {
r#type: DatabaseType,
name: Option<String>, name: Option<String>,
user: String, user: String,
host: String, host: String,
@ -113,19 +133,42 @@ impl Config {
impl Connection { impl Connection {
pub fn database_url(&self) -> String { pub fn database_url(&self) -> String {
match &self.database { match &self.database {
Some(database) => format!( Some(database) => match self.r#type {
"mysql://{user}:@{host}:{port}/{database}", DatabaseType::MySql => format!(
user = self.user, "mysql://{user}:@{host}:{port}/{database}",
host = self.host, user = self.user,
port = self.port, host = self.host,
database = database port = self.port,
), database = database
None => format!( ),
"mysql://{user}:@{host}:{port}", DatabaseType::Postgres => {
user = self.user, format!(
host = self.host, "postgres://{user}@{host}:{port}/{database}",
port = self.port, 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)
}
} }

@ -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<Vec<Database>>;
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Child>>;
async fn get_records(
&self,
database: &Database,
table: &Table,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn get_columns(
&self,
database: &Database,
table: &Table,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn close(&self);
}

@ -1,30 +1,10 @@
use super::{Pool, RECORDS_LIMIT_PER_PAGE};
use async_trait::async_trait; use async_trait::async_trait;
use chrono::NaiveDate; use chrono::NaiveDate;
use database_tree::{Database, Table}; use database_tree::{Child, Database, Table};
use futures::TryStreamExt; use futures::TryStreamExt;
use sqlx::mysql::{MySqlColumn, MySqlPool as MPool, MySqlRow}; use sqlx::mysql::{MySqlColumn, MySqlPool as MPool, MySqlRow};
use sqlx::{Column as _, Row, TypeInfo}; use sqlx::{Column as _, Row as _, TypeInfo as _};
pub const RECORDS_LIMIT_PER_PAGE: u8 = 200;
#[async_trait]
pub trait Pool {
async fn get_databases(&self) -> anyhow::Result<Vec<Database>>;
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Table>>;
async fn get_records(
&self,
database: &str,
table: &str,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn get_columns(
&self,
database: &str,
table: &str,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn close(&self);
}
pub struct MySqlPool { pub struct MySqlPool {
pool: MPool, pool: MPool,
@ -51,32 +31,32 @@ impl Pool for MySqlPool {
for db in databases { for db in databases {
list.push(Database::new( list.push(Database::new(
db.clone(), db.clone(),
get_tables(db.clone(), &self.pool).await?, self.get_tables(db.clone()).await?,
)) ))
} }
Ok(list) Ok(list)
} }
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Table>> { async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Child>> {
let tables = let tables =
sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str()) sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await?; .await?;
Ok(tables) Ok(tables.into_iter().map(|table| table.into()).collect())
} }
async fn get_records( async fn get_records(
&self, &self,
database: &str, database: &Database,
table: &str, table: &Table,
page: u16, page: u16,
filter: Option<String>, filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> { ) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter { let query = if let Some(filter) = filter {
format!( format!(
"SELECT * FROM `{database}`.`{table}` WHERE {filter} LIMIT {page}, {limit}", "SELECT * FROM `{database}`.`{table}` WHERE {filter} LIMIT {page}, {limit}",
database = database, database = database.name,
table = table, table = table.name,
filter = filter, filter = filter,
page = page, page = page,
limit = RECORDS_LIMIT_PER_PAGE limit = RECORDS_LIMIT_PER_PAGE
@ -84,8 +64,8 @@ impl Pool for MySqlPool {
} else { } else {
format!( format!(
"SELECT * FROM `{}`.`{}` limit {page}, {limit}", "SELECT * FROM `{}`.`{}` limit {page}, {limit}",
database, database.name,
table, table.name,
page = page, page = page,
limit = RECORDS_LIMIT_PER_PAGE limit = RECORDS_LIMIT_PER_PAGE
) )
@ -110,10 +90,13 @@ impl Pool for MySqlPool {
async fn get_columns( async fn get_columns(
&self, &self,
database: &str, database: &Database,
table: &str, table: &Table,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> { ) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
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 rows = sqlx::query(query.as_str()).fetch(&self.pool);
let mut headers = vec![]; let mut headers = vec![];
let mut records = vec![]; let mut records = vec![];
@ -137,18 +120,7 @@ impl Pool for MySqlPool {
} }
} }
pub async fn get_tables(database: String, pool: &MPool) -> anyhow::Result<Vec<Table>> { fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyhow::Result<String> {
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<String> {
let column_name = column.name(); let column_name = column.name();
match column.type_info().clone().name() { match column.type_info().clone().name() {
"INT" | "SMALLINT" | "BIGINT" => { "INT" | "SMALLINT" | "BIGINT" => {

@ -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<Self> {
Ok(Self {
pool: PgPool::connect(database_url).await?,
})
}
}
#[async_trait]
impl Pool for PostgresPool {
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SELECT datname FROM pg_database")
.fetch_all(&self.pool)
.await?
.iter()
.map(|table| table.get(0))
.collect::<Vec<String>>();
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<Vec<Child>> {
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<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
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<String>, Vec<Vec<String>>)> {
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<String>,
) -> anyhow::Result<Vec<serde_json::Value>> {
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<String> {
let column_name = column.name();
if let Ok(value) = row.try_get(column_name) {
let value: Option<i16> = 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<i32> = 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<i64> = 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<rust_decimal::Decimal> = 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::<String>()
)
}));
}
if let Ok(value) = row.try_get(column_name) {
let value: Option<NaiveDate> = 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<chrono::DateTime<chrono::Utc>> = 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<chrono::NaiveDateTime> = value;
return Ok(value.map_or("NULL".to_string(), |v| v.to_string()));
}
if let Ok(value) = row.try_get::<Option<bool>, _>(column_name) {
let value: Option<bool> = 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<Vec<String>> = 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()
))
}

@ -2,9 +2,9 @@ mod app;
mod clipboard; mod clipboard;
mod components; mod components;
mod config; mod config;
mod database;
mod event; mod event;
mod ui; mod ui;
mod utils;
#[macro_use] #[macro_use]
mod log; mod log;

Loading…
Cancel
Save