support postgres

pull/35/head
Takayuki Maeda 3 years ago
parent 1ac7e48899
commit 2fe45cacee

35
Cargo.lock generated

@ -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,6 +321,16 @@ 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"
@ -588,6 +600,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"
@ -730,6 +752,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"
@ -1401,9 +1434,11 @@ dependencies = [
"generic-array",
"hashlink",
"hex",
"hmac",
"itoa",
"libc",
"log",
"md-5",
"memchr",
"num-bigint 0.3.2",
"once_cell",

@ -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"] }
chrono = "0.4"
tokio = { version = "0.2.22", features = ["full"] }
futures = "0.3.5"

@ -303,6 +303,7 @@ mod test {
create_time: None,
update_time: None,
engine: None,
table_schema: None,
}
}
}

@ -36,4 +36,6 @@ pub struct Table {
pub update_time: Option<chrono::DateTime<chrono::Utc>>,
#[sqlx(rename = "Engine")]
pub engine: Option<String>,
#[sqlx(default)]
pub table_schema: Option<String>,
}

@ -1,17 +1,27 @@
[[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
database = "dvdrental"

@ -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<String>,
user: String,
host: String,
@ -113,19 +133,46 @@ 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)
}
pub fn is_postgres(&self) -> bool {
matches!(self.r#type, DatabaseType::Postgres)
}
}

@ -1,6 +1,8 @@
pub mod mysql;
pub mod postgres;
pub use mysql::MySqlPool;
pub use postgres::PostgresPool;
use async_trait::async_trait;
use database_tree::{Database, Table};

@ -125,10 +125,7 @@ pub async fn get_tables(database: String, pool: &MPool) -> anyhow::Result<Vec<Ta
Ok(tables)
}
pub fn convert_column_value_to_string(
row: &MySqlRow,
column: &MySqlColumn,
) -> anyhow::Result<String> {
fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyhow::Result<String> {
let column_name = column.name();
match column.type_info().clone().name() {
"INT" | "SMALLINT" | "BIGINT" => {

@ -0,0 +1,207 @@
use super::{Pool, RECORDS_LIMIT_PER_PAGE};
use async_trait::async_trait;
use chrono::NaiveDate;
use database_tree::{Database, Table};
use futures::TryStreamExt;
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(),
get_tables(db.clone(), &self.pool).await?,
))
}
Ok(list)
}
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Table>> {
let mut rows = sqlx::query(
"SELECT * FROM information_schema.tables WHERE table_schema='public' and 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,
table_schema: row.get("table_name"),
})
}
Ok(tables)
}
async fn get_records(
&self,
database: &str,
table: &str,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter {
format!(
r#"SELECT * FROM "{database}""{table_schema}"."{table}" WHERE {filter} LIMIT {page}, {limit}"#,
database = database,
table = table,
filter = filter,
table_schema = "public",
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else {
format!(
r#"SELECT * FROM "{database}"."{table_schema}"."{table}" limit {limit} offset {page}"#,
database = database,
table = table,
table_schema = "public",
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![];
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 get_columns(
&self,
database: &str,
table: &str,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let mut rows = sqlx::query(
"SELECT * FROM information_schema.columns WHERE table_catalog = $1 AND table_schema = 'public' AND table_name = $2"
)
.bind(database).bind(table)
.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;
}
}
pub async fn get_tables(database: String, pool: &PgPool) -> anyhow::Result<Vec<Table>> {
let tables =
sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
.fetch_all(pool)
.await?;
Ok(tables)
}
fn convert_column_value_to_string(row: &PgRow, column: &PgColumn) -> anyhow::Result<String> {
let column_name = column.name();
match column.type_info().clone().name() {
"INT2" => {
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()));
}
}
"INT4" => {
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()));
}
}
"BIGINT" | "BIGSERIAL" | "INT8" => {
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()));
}
}
"NUMERIC" => {
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()));
}
}
"VARCHAR" | "CHAR" | "ENUM" | "TEXT" | "NAME" => {
return Ok(row
.try_get(column_name)
.unwrap_or_else(|_| "NULL".to_string()))
}
"DATE" => {
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()));
}
}
"TIMESTAMPZ" => {
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()));
}
}
"TIMESTAMP" => {
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()));
}
}
"BOOL" => {
if let Ok(value) = row.try_get(column_name) {
let value: Option<bool> = value;
return Ok(value.map_or("NULL".to_string(), |v| v.to_string()));
}
}
_ => (),
}
Err(anyhow::anyhow!(
"column type not implemented: `{}` {}",
column_name,
column.type_info().clone().name()
))
}
Loading…
Cancel
Save