Merge pull request #51 from TaKO8Ki/support-sqlite

Support SQLite
pull/54/head
Takayuki Maeda 3 years ago committed by GitHub
commit 3a8529f9be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

24
Cargo.lock generated

@ -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"

@ -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"

@ -17,3 +17,7 @@ user = "postgres"
host = "localhost"
port = 5432
database = "dvdrental"
[[conn]]
type = "sqlite"
path = "/Users/tako8ki/Downloads/chinook.db"

@ -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::<Vec<Vec<String>>>(),
constraints.get(0).unwrap().fields(),
foreign_keys.get(0).unwrap().fields(),
database.clone(),
table.clone(),
);

@ -89,7 +89,7 @@ impl DrawableComponent for ConnectionsComponent {
let connections: Vec<ListItem> = 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();

@ -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<String>,
user: String,
host: String,
port: u64,
user: Option<String>,
host: Option<String>,
port: Option<u64>,
path: Option<std::path::PathBuf>,
pub database: Option<String>,
}
@ -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<String> {
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<std::path::PathBuf> {

@ -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};

@ -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};
@ -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,
@ -353,6 +353,10 @@ fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> anyho
let value: Option<i8> = 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<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()));
@ -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<rust_decimal::Decimal> = value;
let value: Option<f64> = 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<u64> = 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<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: Option<NaiveTime> = 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<NaiveDateTime> = 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::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<serde_json::Value> = 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<bool> = value;
return Ok(value.map_or("NULL".to_string(), |v| v.to_string()));

@ -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<chrono::NaiveDateTime> = value;
let value: Option<chrono::DateTime<chrono::Local>> = 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<NaiveDateTime> = 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<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: Option<NaiveTime> = 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<serde_json::Value> = value;
return Ok(value.map_or("NULL".to_string(), |v| v.to_string()));
}
if let Ok(value) = row.try_get::<Option<bool>, _>(column_name) {

@ -0,0 +1,393 @@
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<Self> {
Ok(Self {
pool: SPool::connect(database_url).await?,
})
}
}
pub struct Constraint {
name: String,
column_name: String,
origin: String,
}
impl TableRow for Constraint {
fn fields(&self) -> Vec<String> {
vec![
"name".to_string(),
"column_name".to_string(),
"origin".to_string(),
]
}
fn columns(&self) -> Vec<String> {
vec![
self.name.to_string(),
self.column_name.to_string(),
self.origin.to_string(),
]
}
}
pub struct Column {
name: Option<String>,
r#type: Option<String>,
null: Option<String>,
default: Option<String>,
comment: Option<String>,
}
impl TableRow for Column {
fn fields(&self) -> Vec<String> {
vec![
"name".to_string(),
"type".to_string(),
"null".to_string(),
"default".to_string(),
"comment".to_string(),
]
}
fn columns(&self) -> Vec<String> {
vec![
self.name
.as_ref()
.map_or(String::new(), |name| name.to_string()),
self.r#type
.as_ref()
.map_or(String::new(), |r#type| r#type.to_string()),
self.null
.as_ref()
.map_or(String::new(), |null| null.to_string()),
self.default
.as_ref()
.map_or(String::new(), |default| default.to_string()),
self.comment
.as_ref()
.map_or(String::new(), |comment| comment.to_string()),
]
}
}
pub struct ForeignKey {
column_name: Option<String>,
ref_table: Option<String>,
ref_column: Option<String>,
}
impl TableRow for ForeignKey {
fn fields(&self) -> Vec<String> {
vec![
"column_name".to_string(),
"ref_table".to_string(),
"ref_column".to_string(),
]
}
fn columns(&self) -> Vec<String> {
vec![
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<String>,
column_name: Option<String>,
r#type: Option<String>,
}
impl TableRow for Index {
fn fields(&self) -> Vec<String> {
vec![
"name".to_string(),
"column_name".to_string(),
"type".to_string(),
]
}
fn columns(&self) -> Vec<String> {
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<Vec<Database>> {
let databases = sqlx::query("SELECT name FROM pragma_database_list")
.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 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,
table: &Table,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter {
format!(
"SELECT * FROM `{table}` WHERE {filter} LIMIT {page}, {limit}",
table = table.name,
filter = filter,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else {
format!(
"SELECT * FROM `{}` LIMIT {page}, {limit}",
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<Vec<Box<dyn TableRow>>> {
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<Box<dyn TableRow>> = vec![];
while let Some(row) = rows.try_next().await? {
let null: Option<i16> = row.try_get("notnull")?;
columns.push(Box::new(Column {
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)
}
async fn get_constraints(
&self,
_database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
let mut rows = sqlx::query(
"
SELECT
p.origin,
s.name AS index_name,
i.name AS column_name
FROM
sqlite_master s
JOIN pragma_index_list(s.tbl_name) p ON s.name = p.name,
pragma_index_info(s.name) i
WHERE
s.type = 'index'
AND tbl_name = ?
AND NOT p.origin = 'c'
",
)
.bind(&table.name)
.fetch(&self.pool);
let mut constraints: Vec<Box<dyn TableRow>> = vec![];
while let Some(row) = rows.try_next().await? {
constraints.push(Box::new(Constraint {
name: row.try_get("index_name")?,
column_name: row.try_get("column_name")?,
origin: row.try_get("origin")?,
}))
}
Ok(constraints)
}
async fn get_foreign_keys(
&self,
_database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
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<Box<dyn TableRow>> = vec![];
while let Some(row) = rows.try_next().await? {
foreign_keys.push(Box::new(ForeignKey {
column_name: row.try_get("from")?,
ref_table: row.try_get("table")?,
ref_column: row.try_get("to")?,
}))
}
Ok(foreign_keys)
}
async fn get_indexes(
&self,
_database: &Database,
table: &Table,
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
let mut rows = sqlx::query(
"
SELECT
m.name AS index_name,
p.*
FROM
sqlite_master m,
pragma_index_info(m.name) p
WHERE
m.type = 'index'
AND m.tbl_name = ?
",
)
.bind(&table.name)
.fetch(&self.pool);
let mut foreign_keys: Vec<Box<dyn TableRow>> = 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("name")?,
r#type: Some(String::new()),
}))
}
Ok(foreign_keys)
}
async fn close(&self) {
self.pool.close().await;
}
}
fn convert_column_value_to_string(
row: &SqliteRow,
column: &SqliteColumn,
) -> anyhow::Result<String> {
let column_name = column.name();
if let Ok(value) = row.try_get(column_name) {
let value: Option<String> = 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<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<f32> = 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<f64> = 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::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::DateTime<chrono::Local>> = 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<NaiveDateTime> = 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<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()
))
}

@ -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| ());

Loading…
Cancel
Save