From bba5883c5f42506554fdc6e1b0c655a88a6ff49b Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Sun, 29 Aug 2021 12:18:31 +0900 Subject: [PATCH 1/5] create sqlite module --- Cargo.lock | 24 +++ Cargo.toml | 2 +- src/database/mod.rs | 2 + src/database/mysql.rs | 24 ++- src/database/postgres.rs | 20 +- src/database/sqlite.rs | 396 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 10 +- 7 files changed, 464 insertions(+), 14 deletions(-) create mode 100644 src/database/sqlite.rs diff --git a/Cargo.lock b/Cargo.lock index 4c440ae..2fa1191 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -788,6 +788,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +[[package]] +name = "libsqlite3-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.4" @@ -1144,6 +1155,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -1547,6 +1564,7 @@ dependencies = [ "hmac", "itoa", "libc", + "libsqlite3-sys", "log", "md-5", "memchr", @@ -1900,6 +1918,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index ae9ec24..10f7ef7 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", "postgres", "chrono", "runtime-tokio-rustls", "decimal", "json"] } +sqlx = { version = "0.4.1", features = ["mysql", "postgres", "sqlite", "chrono", "runtime-tokio-rustls", "decimal", "json"] } chrono = "0.4" tokio = { version = "0.2.22", features = ["full"] } futures = "0.3.5" diff --git a/src/database/mod.rs b/src/database/mod.rs index 5bdd866..18014a1 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,8 +1,10 @@ pub mod mysql; pub mod postgres; +pub mod sqlite; pub use mysql::MySqlPool; pub use postgres::PostgresPool; +pub use sqlite::SqlitePool; use async_trait::async_trait; use database_tree::{Child, Database, Table}; diff --git a/src/database/mysql.rs b/src/database/mysql.rs index c9f91f3..29556b8 100644 --- a/src/database/mysql.rs +++ b/src/database/mysql.rs @@ -1,6 +1,6 @@ use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE}; use async_trait::async_trait; -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use database_tree::{Child, Database, Table}; use futures::TryStreamExt; use sqlx::mysql::{MySqlColumn, MySqlPool as MPool, MySqlRow}; @@ -353,6 +353,10 @@ fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyho 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())); @@ -366,7 +370,7 @@ fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyho 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; + 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) { @@ -385,14 +389,30 @@ fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyho 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 = 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())); diff --git a/src/database/postgres.rs b/src/database/postgres.rs index dbf4767..537b55a 100644 --- a/src/database/postgres.rs +++ b/src/database/postgres.rs @@ -1,6 +1,6 @@ use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE}; use async_trait::async_trait; -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use database_tree::{Child, Database, Schema, Table}; use futures::TryStreamExt; use itertools::Itertools; @@ -494,7 +494,23 @@ fn convert_column_value_to_string(row: &PgRow, column: &PgColumn) -> anyhow::Res 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; + 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 = value; return Ok(value.map_or("NULL".to_string(), |v| v.to_string())); } if let Ok(value) = row.try_get::, _>(column_name) { diff --git a/src/database/sqlite.rs b/src/database/sqlite.rs new file mode 100644 index 0000000..a98ea93 --- /dev/null +++ b/src/database/sqlite.rs @@ -0,0 +1,396 @@ +use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE}; +use async_trait::async_trait; +use chrono::NaiveDateTime; +use database_tree::{Child, Database, Table}; +use futures::TryStreamExt; +use sqlx::sqlite::{SqliteColumn, SqlitePool as SPool, SqliteRow}; +use sqlx::{Column as _, Row as _, TypeInfo as _}; + +pub struct SqlitePool { + pool: SPool, +} + +impl SqlitePool { + pub async fn new(database_url: &str) -> anyhow::Result { + Ok(Self { + pool: SPool::connect(database_url).await?, + }) + } +} + +pub struct Constraint { + name: String, + column_name: String, +} + +impl TableRow for Constraint { + fn fields(&self) -> Vec { + vec!["name".to_string(), "column_name".to_string()] + } + + fn columns(&self) -> Vec { + vec![self.name.to_string(), self.column_name.to_string()] + } +} + +pub struct Column { + name: Option, + r#type: Option, + null: Option, + default: Option, + comment: Option, +} + +impl TableRow for Column { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "type".to_string(), + "null".to_string(), + "default".to_string(), + "comment".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.r#type + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.null + .as_ref() + .map_or(String::new(), |null| null.to_string()), + self.default + .as_ref() + .map_or(String::new(), |default| default.to_string()), + self.comment + .as_ref() + .map_or(String::new(), |comment| comment.to_string()), + ] + } +} + +pub struct ForeignKey { + name: Option, + column_name: Option, + ref_table: Option, + ref_column: Option, +} + +impl TableRow for ForeignKey { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "column_name".to_string(), + "ref_table".to_string(), + "ref_column".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.column_name + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.ref_table + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.ref_column + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + ] + } +} + +pub struct Index { + name: Option, + column_name: Option, + r#type: Option, +} + +impl TableRow for Index { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "column_name".to_string(), + "type".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.column_name + .as_ref() + .map_or(String::new(), |column_name| column_name.to_string()), + self.r#type + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + ] + } +} + +#[async_trait] +impl Pool for SqlitePool { + async fn get_databases(&self) -> anyhow::Result> { + let databases = sqlx::query("SHOW DATABASES") + .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 tables = + sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str()) + .fetch_all(&self.pool) + .await?; + Ok(tables.into_iter().map(|table| table.into()).collect()) + } + + async fn get_records( + &self, + 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.name, + table = table.name, + filter = filter, + page = page, + limit = RECORDS_LIMIT_PER_PAGE + ) + } else { + format!( + "SELECT * FROM `{}`.`{}` limit {page}, {limit}", + database.name, + table.name, + 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: &Database, + table: &Table, + ) -> anyhow::Result>> { + let query = format!( + "SHOW FULL COLUMNS FROM `{}`.`{}`", + database.name, table.name + ); + let mut rows = sqlx::query(query.as_str()).fetch(&self.pool); + let mut columns: Vec> = vec![]; + while let Some(row) = rows.try_next().await? { + columns.push(Box::new(Column { + name: row.try_get("Field")?, + r#type: row.try_get("Type")?, + null: row.try_get("Null")?, + default: row.try_get("Default")?, + comment: row.try_get("Comment")?, + })) + } + Ok(columns) + } + + async fn get_constraints( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result>> { + let mut rows = sqlx::query( + " + SELECT + COLUMN_NAME, + CONSTRAINT_NAME + FROM + information_schema.KEY_COLUMN_USAGE + WHERE + REFERENCED_TABLE_SCHEMA IS NULL + AND REFERENCED_TABLE_NAME IS NULL + AND TABLE_SCHEMA = ? + AND TABLE_NAME = ? + ", + ) + .bind(&database.name) + .bind(&table.name) + .fetch(&self.pool); + let mut constraints: Vec> = vec![]; + while let Some(row) = rows.try_next().await? { + constraints.push(Box::new(Constraint { + name: row.try_get("CONSTRAINT_NAME")?, + column_name: row.try_get("COLUMN_NAME")?, + })) + } + Ok(constraints) + } + + async fn get_foreign_keys( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result>> { + let mut rows = sqlx::query( + " + SELECT + TABLE_NAME, + COLUMN_NAME, + CONSTRAINT_NAME, + REFERENCED_TABLE_SCHEMA, + REFERENCED_TABLE_NAME, + REFERENCED_COLUMN_NAME + FROM + INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE + REFERENCED_TABLE_SCHEMA IS NOT NULL + AND REFERENCED_TABLE_NAME IS NOT NULL + AND TABLE_SCHEMA = ? + AND TABLE_NAME = ? + ", + ) + .bind(&database.name) + .bind(&table.name) + .fetch(&self.pool); + let mut foreign_keys: Vec> = vec![]; + while let Some(row) = rows.try_next().await? { + foreign_keys.push(Box::new(ForeignKey { + name: row.try_get("CONSTRAINT_NAME")?, + column_name: row.try_get("COLUMN_NAME")?, + ref_table: row.try_get("REFERENCED_TABLE_NAME")?, + ref_column: row.try_get("REFERENCED_COLUMN_NAME")?, + })) + } + Ok(foreign_keys) + } + + async fn get_indexes( + &self, + database: &Database, + table: &Table, + ) -> anyhow::Result>> { + let mut rows = sqlx::query( + " + SELECT + DISTINCT TABLE_NAME, + INDEX_NAME, + INDEX_TYPE, + COLUMN_NAME + FROM + INFORMATION_SCHEMA.STATISTICS + WHERE + TABLE_SCHEMA = ? + AND TABLE_NAME = ? + ", + ) + .bind(&database.name) + .bind(&table.name) + .fetch(&self.pool); + let mut foreign_keys: Vec> = vec![]; + while let Some(row) = rows.try_next().await? { + foreign_keys.push(Box::new(Index { + name: row.try_get("INDEX_NAME")?, + column_name: row.try_get("COLUMN_NAME")?, + r#type: row.try_get("INDEX_TYPE")?, + })) + } + Ok(foreign_keys) + } + + async fn close(&self) { + self.pool.close().await; + } +} + +fn convert_column_value_to_string( + row: &SqliteRow, + column: &SqliteColumn, +) -> anyhow::Result { + let column_name = column.name(); + if let Ok(value) = row.try_get(column_name) { + let value: Option = value; + return Ok(value.unwrap_or_else(|| "NULL".to_string())); + } + if let Ok(value) = row.try_get(column_name) { + let value: Option<&str> = 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 = 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 = 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())); + } + 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 5cd8ef2..241f73d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; -use std::{io, panic}; +use std::io; use tui::{backend::CrosstermBackend, Terminal}; #[tokio::main] @@ -65,14 +65,6 @@ fn setup_terminal() -> Result<()> { Ok(()) } -fn set_panic_handlers() -> Result<()> { - panic::set_hook(Box::new(|e| { - eprintln!("panic: {:?}", e); - shutdown_terminal(); - })); - Ok(()) -} - fn shutdown_terminal() { let leave_screen = io::stdout().execute(LeaveAlternateScreen).map(|_f| ()); From 4dbf3fd922d272d19b3c1257afc063054235e7d1 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Fri, 3 Sep 2021 16:30:40 +0900 Subject: [PATCH 2/5] upper case --- src/database/mysql.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/mysql.rs b/src/database/mysql.rs index 29556b8..63a5aef 100644 --- a/src/database/mysql.rs +++ b/src/database/mysql.rs @@ -183,7 +183,7 @@ impl Pool for MySqlPool { ) } else { format!( - "SELECT * FROM `{}`.`{}` limit {page}, {limit}", + "SELECT * FROM `{}`.`{}` LIMIT {page}, {limit}", database.name, table.name, page = page, From 06039f1397bed4484cbd42bce4b2874b0a9a1277 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Fri, 3 Sep 2021 16:31:02 +0900 Subject: [PATCH 3/5] add a connection for SQLite --- sample.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sample.toml b/sample.toml index b9e071f..add4507 100644 --- a/sample.toml +++ b/sample.toml @@ -17,3 +17,7 @@ user = "postgres" host = "localhost" port = 5432 database = "dvdrental" + +[[conn]] +type = "sqlite" +path = "$HOME/Downloads/chinook.db" From ba3dcaad69f60d5edc24ff08c50208992d02ce2c Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Fri, 3 Sep 2021 16:31:23 +0900 Subject: [PATCH 4/5] add queries for SQLit --- src/app.rs | 12 ++- src/components/connections.rs | 2 +- src/config.rs | 123 ++++++++++++++++++++--------- src/database/sqlite.rs | 144 +++++++++++++++++----------------- 4 files changed, 165 insertions(+), 116 deletions(-) diff --git a/src/app.rs b/src/app.rs index ea00d1b..fd37b3a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use crate::clipboard::copy_to_clipboard; use crate::components::{CommandInfo, Component as _, DrawableComponent as _, EventState}; -use crate::database::{MySqlPool, Pool, PostgresPool, RECORDS_LIMIT_PER_PAGE}; +use crate::database::{MySqlPool, Pool, PostgresPool, SqlitePool, RECORDS_LIMIT_PER_PAGE}; use crate::event::Key; use crate::{ components::tab::Tab, @@ -153,11 +153,15 @@ impl App { } self.pool = if conn.is_mysql() { Some(Box::new( - MySqlPool::new(conn.database_url().as_str()).await?, + MySqlPool::new(conn.database_url()?.as_str()).await?, + )) + } else if conn.is_postgres() { + Some(Box::new( + PostgresPool::new(conn.database_url()?.as_str()).await?, )) } else { Some(Box::new( - PostgresPool::new(conn.database_url().as_str()).await?, + SqlitePool::new(conn.database_url()?.as_str()).await?, )) }; let databases = match &conn.database { @@ -241,7 +245,7 @@ impl App { .iter() .map(|c| c.columns()) .collect::>>(), - constraints.get(0).unwrap().fields(), + foreign_keys.get(0).unwrap().fields(), database.clone(), table.clone(), ); diff --git a/src/components/connections.rs b/src/components/connections.rs index 383084f..6f223ba 100644 --- a/src/components/connections.rs +++ b/src/components/connections.rs @@ -89,7 +89,7 @@ impl DrawableComponent for ConnectionsComponent { let connections: Vec = conns .iter() .map(|i| { - ListItem::new(vec![Spans::from(Span::raw(i.database_url()))]) + ListItem::new(vec![Spans::from(Span::raw(i.database_url().unwrap()))]) .style(Style::default()) }) .collect(); diff --git a/src/config.rs b/src/config.rs index e0c9b6b..7352997 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,6 +25,8 @@ enum DatabaseType { MySql, #[serde(rename = "postgres")] Postgres, + #[serde(rename = "sqlite")] + Sqlite, } impl fmt::Display for DatabaseType { @@ -32,6 +34,7 @@ impl fmt::Display for DatabaseType { match self { Self::MySql => write!(f, "mysql"), Self::Postgres => write!(f, "postgres"), + Self::Sqlite => write!(f, "sqlite"), } } } @@ -41,10 +44,10 @@ impl Default for Config { Self { conn: vec![Connection { r#type: DatabaseType::MySql, - name: None, - user: "root".to_string(), - host: "localhost".to_string(), - port: 3306, + user: Some("root".to_string()), + host: Some("localhost".to_string()), + port: Some(3306), + path: None, database: None, }], key_config: KeyConfig::default(), @@ -55,10 +58,10 @@ impl Default for Config { #[derive(Debug, Deserialize, Clone)] pub struct Connection { r#type: DatabaseType, - name: Option, - user: String, - host: String, - port: u64, + user: Option, + host: Option, + port: Option, + path: Option, pub database: Option, } @@ -150,46 +153,88 @@ impl Config { } impl Connection { - pub fn database_url(&self) -> String { - match &self.database { - 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!( + pub fn database_url(&self) -> anyhow::Result { + match self.r#type { + DatabaseType::MySql => { + let user = self + .user + .as_ref() + .ok_or(anyhow::anyhow!("user is not set"))?; + let host = self + .host + .as_ref() + .ok_or(anyhow::anyhow!("host is not set"))?; + let port = self + .port + .as_ref() + .ok_or(anyhow::anyhow!("port is not set"))?; + + match self.database.as_ref() { + Some(database) => Ok(format!( + "mysql://{user}:@{host}:{port}/{database}", + user = user, + host = host, + port = port, + database = database + )), + None => Ok(format!( + "mysql://{user}:@{host}:{port}", + user = user, + host = host, + port = port, + )), + } + } + DatabaseType::Postgres => { + let user = self + .user + .as_ref() + .ok_or(anyhow::anyhow!("user is not set"))?; + let host = self + .host + .as_ref() + .ok_or(anyhow::anyhow!("host is not set"))?; + let port = self + .port + .as_ref() + .ok_or(anyhow::anyhow!("port is not set"))?; + + match self.database.as_ref() { + Some(database) => Ok(format!( "postgres://{user}@{host}:{port}/{database}", - user = self.user, - host = self.host, - port = self.port, + user = user, + host = host, + port = port, database = database - ) + )), + None => Ok(format!( + "postgres://{user}@{host}:{port}", + user = user, + host = host, + port = port, + )), } - }, - 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, - ), - }, + } + DatabaseType::Sqlite => { + let path = self + .path + .as_ref() + .map_or(Err(anyhow::anyhow!("path is not set")), |path| { + Ok(path.to_str().unwrap()) + })?; + + Ok(format!("sqlite://{path}", path = path)) + } } } pub fn is_mysql(&self) -> bool { matches!(self.r#type, DatabaseType::MySql) } + + pub fn is_postgres(&self) -> bool { + matches!(self.r#type, DatabaseType::Postgres) + } } pub fn get_app_config_path() -> anyhow::Result { diff --git a/src/database/sqlite.rs b/src/database/sqlite.rs index a98ea93..ceef7bf 100644 --- a/src/database/sqlite.rs +++ b/src/database/sqlite.rs @@ -141,7 +141,7 @@ impl TableRow for Index { #[async_trait] impl Pool for SqlitePool { async fn get_databases(&self) -> anyhow::Result> { - let databases = sqlx::query("SHOW DATABASES") + let databases = sqlx::query("SELECT name FROM pragma_database_list") .fetch_all(&self.pool) .await? .iter() @@ -157,25 +157,32 @@ impl Pool for SqlitePool { Ok(list) } - 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?; + async fn get_tables(&self, _database: String) -> anyhow::Result> { + let mut rows = + sqlx::query("SELECT name FROM sqlite_master WHERE type = 'table'").fetch(&self.pool); + let mut tables = Vec::new(); + while let Some(row) = rows.try_next().await? { + tables.push(Table { + name: row.try_get("name")?, + create_time: None, + update_time: None, + engine: None, + schema: None, + }) + } Ok(tables.into_iter().map(|table| table.into()).collect()) } async fn get_records( &self, - database: &Database, + _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.name, + "SELECT * FROM `{table}` WHERE {filter} LIMIT {page}, {limit}", table = table.name, filter = filter, page = page, @@ -183,8 +190,7 @@ impl Pool for SqlitePool { ) } else { format!( - "SELECT * FROM `{}`.`{}` limit {page}, {limit}", - database.name, + "SELECT * FROM `{}` LIMIT {page}, {limit}", table.name, page = page, limit = RECORDS_LIMIT_PER_PAGE @@ -210,22 +216,24 @@ impl Pool for SqlitePool { async fn get_columns( &self, - database: &Database, + _database: &Database, table: &Table, ) -> anyhow::Result>> { - let query = format!( - "SHOW FULL COLUMNS FROM `{}`.`{}`", - database.name, table.name - ); + let query = format!("SELECT * FROM pragma_table_info('{}');", table.name); let mut rows = sqlx::query(query.as_str()).fetch(&self.pool); let mut columns: Vec> = vec![]; while let Some(row) = rows.try_next().await? { + let null: Option = row.try_get("notnull")?; columns.push(Box::new(Column { - name: row.try_get("Field")?, - r#type: row.try_get("Type")?, - null: row.try_get("Null")?, - default: row.try_get("Default")?, - comment: row.try_get("Comment")?, + name: row.try_get("name")?, + r#type: row.try_get("type")?, + null: if matches!(null, Some(null) if null == 1) { + Some("✔︎".to_string()) + } else { + Some("".to_string()) + }, + default: row.try_get("dflt_value")?, + comment: None, })) } Ok(columns) @@ -233,31 +241,29 @@ impl Pool for SqlitePool { async fn get_constraints( &self, - database: &Database, + _database: &Database, table: &Table, ) -> anyhow::Result>> { let mut rows = sqlx::query( " - SELECT - COLUMN_NAME, - CONSTRAINT_NAME - FROM - information_schema.KEY_COLUMN_USAGE - WHERE - REFERENCED_TABLE_SCHEMA IS NULL - AND REFERENCED_TABLE_NAME IS NULL - AND TABLE_SCHEMA = ? - AND TABLE_NAME = ? - ", + SELECT + m.name AS index_name, + p.* + FROM + sqlite_master m, + pragma_index_info(m.name) p + WHERE + m.type = 'index' + AND tbl_name = ? + ", ) - .bind(&database.name) .bind(&table.name) .fetch(&self.pool); let mut constraints: Vec> = vec![]; while let Some(row) = rows.try_next().await? { constraints.push(Box::new(Constraint { - name: row.try_get("CONSTRAINT_NAME")?, - column_name: row.try_get("COLUMN_NAME")?, + name: row.try_get("index_name")?, + column_name: row.try_get("name")?, })) } Ok(constraints) @@ -265,37 +271,33 @@ impl Pool for SqlitePool { async fn get_foreign_keys( &self, - database: &Database, + _database: &Database, table: &Table, ) -> anyhow::Result>> { let mut rows = sqlx::query( " - SELECT - TABLE_NAME, - COLUMN_NAME, - CONSTRAINT_NAME, - REFERENCED_TABLE_SCHEMA, - REFERENCED_TABLE_NAME, - REFERENCED_COLUMN_NAME - FROM - INFORMATION_SCHEMA.KEY_COLUMN_USAGE - WHERE - REFERENCED_TABLE_SCHEMA IS NOT NULL - AND REFERENCED_TABLE_NAME IS NOT NULL - AND TABLE_SCHEMA = ? - AND TABLE_NAME = ? + SELECT + m.name AS index_name, + f.`from`, + f.`to`, + f.`table` + FROM + sqlite_master m, + pragma_index_info(m.name) p + INNER JOIN pragma_foreign_key_list(m.tbl_name) f ON f.`from` = p.name + WHERE + tbl_name = ? ", ) - .bind(&database.name) .bind(&table.name) .fetch(&self.pool); let mut foreign_keys: Vec> = vec![]; while let Some(row) = rows.try_next().await? { foreign_keys.push(Box::new(ForeignKey { - name: row.try_get("CONSTRAINT_NAME")?, - column_name: row.try_get("COLUMN_NAME")?, - ref_table: row.try_get("REFERENCED_TABLE_NAME")?, - ref_column: row.try_get("REFERENCED_COLUMN_NAME")?, + name: row.try_get("index_name")?, + column_name: row.try_get("from")?, + ref_table: row.try_get("table")?, + ref_column: row.try_get("to")?, })) } Ok(foreign_keys) @@ -303,32 +305,30 @@ impl Pool for SqlitePool { async fn get_indexes( &self, - database: &Database, + _database: &Database, table: &Table, ) -> anyhow::Result>> { let mut rows = sqlx::query( " - SELECT - DISTINCT TABLE_NAME, - INDEX_NAME, - INDEX_TYPE, - COLUMN_NAME - FROM - INFORMATION_SCHEMA.STATISTICS - WHERE - TABLE_SCHEMA = ? - AND TABLE_NAME = ? - ", + SELECT + m.name AS index_name, + p.* + FROM + sqlite_master m, + pragma_index_info(m.name) p + WHERE + m.type = 'index' + AND tbl_name = ? + ", ) - .bind(&database.name) .bind(&table.name) .fetch(&self.pool); let mut foreign_keys: Vec> = vec![]; while let Some(row) = rows.try_next().await? { foreign_keys.push(Box::new(Index { - name: row.try_get("INDEX_NAME")?, - column_name: row.try_get("COLUMN_NAME")?, - r#type: row.try_get("INDEX_TYPE")?, + name: row.try_get("index_name")?, + column_name: row.try_get("name")?, + r#type: Some(String::new()), })) } Ok(foreign_keys) From ce312c435ac6810a2d8fe34aee7f36fa2dae5e9e Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Fri, 3 Sep 2021 23:31:03 +0900 Subject: [PATCH 5/5] get index_name --- sample.toml | 2 +- src/database/sqlite.rs | 61 ++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/sample.toml b/sample.toml index add4507..e33e232 100644 --- a/sample.toml +++ b/sample.toml @@ -20,4 +20,4 @@ database = "dvdrental" [[conn]] type = "sqlite" -path = "$HOME/Downloads/chinook.db" +path = "/Users/tako8ki/Downloads/chinook.db" diff --git a/src/database/sqlite.rs b/src/database/sqlite.rs index ceef7bf..26d1771 100644 --- a/src/database/sqlite.rs +++ b/src/database/sqlite.rs @@ -21,15 +21,24 @@ impl SqlitePool { pub struct Constraint { name: String, column_name: String, + origin: String, } impl TableRow for Constraint { fn fields(&self) -> Vec { - vec!["name".to_string(), "column_name".to_string()] + vec![ + "name".to_string(), + "column_name".to_string(), + "origin".to_string(), + ] } fn columns(&self) -> Vec { - vec![self.name.to_string(), self.column_name.to_string()] + vec![ + self.name.to_string(), + self.column_name.to_string(), + self.origin.to_string(), + ] } } @@ -74,7 +83,6 @@ impl TableRow for Column { } pub struct ForeignKey { - name: Option, column_name: Option, ref_table: Option, ref_column: Option, @@ -83,7 +91,6 @@ pub struct ForeignKey { impl TableRow for ForeignKey { fn fields(&self) -> Vec { vec![ - "name".to_string(), "column_name".to_string(), "ref_table".to_string(), "ref_column".to_string(), @@ -92,9 +99,6 @@ impl TableRow for ForeignKey { fn columns(&self) -> Vec { vec![ - self.name - .as_ref() - .map_or(String::new(), |name| name.to_string()), self.column_name .as_ref() .map_or(String::new(), |r#type| r#type.to_string()), @@ -247,14 +251,17 @@ impl Pool for SqlitePool { let mut rows = sqlx::query( " SELECT - m.name AS index_name, - p.* + p.origin, + s.name AS index_name, + i.name AS column_name FROM - sqlite_master m, - pragma_index_info(m.name) p + sqlite_master s + JOIN pragma_index_list(s.tbl_name) p ON s.name = p.name, + pragma_index_info(s.name) i WHERE - m.type = 'index' + s.type = 'index' AND tbl_name = ? + AND NOT p.origin = 'c' ", ) .bind(&table.name) @@ -263,7 +270,8 @@ impl Pool for SqlitePool { while let Some(row) = rows.try_next().await? { constraints.push(Box::new(Constraint { name: row.try_get("index_name")?, - column_name: row.try_get("name")?, + column_name: row.try_get("column_name")?, + origin: row.try_get("origin")?, })) } Ok(constraints) @@ -274,27 +282,16 @@ impl Pool for SqlitePool { _database: &Database, table: &Table, ) -> anyhow::Result>> { - let mut rows = sqlx::query( - " - SELECT - m.name AS index_name, - f.`from`, - f.`to`, - f.`table` - FROM - sqlite_master m, - pragma_index_info(m.name) p - INNER JOIN pragma_foreign_key_list(m.tbl_name) f ON f.`from` = p.name - WHERE - tbl_name = ? - ", - ) - .bind(&table.name) - .fetch(&self.pool); + let query = format!( + "SELECT p.`from`, p.`to`, p.`table` FROM pragma_foreign_key_list('{}') p", + &table.name + ); + let mut rows = sqlx::query(query.as_str()) + .bind(&table.name) + .fetch(&self.pool); let mut foreign_keys: Vec> = vec![]; while let Some(row) = rows.try_next().await? { foreign_keys.push(Box::new(ForeignKey { - name: row.try_get("index_name")?, column_name: row.try_get("from")?, ref_table: row.try_get("table")?, ref_column: row.try_get("to")?, @@ -318,7 +315,7 @@ impl Pool for SqlitePool { pragma_index_info(m.name) p WHERE m.type = 'index' - AND tbl_name = ? + AND m.tbl_name = ? ", ) .bind(&table.name)