Skip to content

Commit

Permalink
feat: mysql support
Browse files Browse the repository at this point in the history
  • Loading branch information
kareemmahlees committed Dec 22, 2023
1 parent 46df3d8 commit 024a566
Show file tree
Hide file tree
Showing 18 changed files with 329 additions and 55 deletions.
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "3"
services:
tablex_mysql:
image: mysql
container_name: tablex_mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: tablex
MYSQL_DATABASE: tablex
ports:
- 3306:3306
4 changes: 2 additions & 2 deletions src-tauri/src/connection.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
drivers::{postgres, sqlite},
drivers::{mysql, postgres, sqlite},
utils::{read_from_connections_file, write_into_connections_file, Drivers},
DbInstance,
};
Expand Down Expand Up @@ -48,7 +48,7 @@ pub async fn establish_connection(
Drivers::PostgreSQL => {
postgres::connection::establish_connection(&db, conn_string, driver).await
}
Drivers::MySQL => unimplemented!(),
Drivers::MySQL => mysql::connection::establish_connection(&db, conn_string, driver).await,
}
}

Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/drivers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod mysql;
pub mod postgres;
pub mod sqlite;
20 changes: 20 additions & 0 deletions src-tauri/src/drivers/mysql/connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::{utils::Drivers, DbInstance};
use sqlx::mysql::MySqlPoolOptions;
use std::time::Duration;
use tauri::State;

pub async fn establish_connection(
db: &State<'_, DbInstance>,
conn_string: String,
driver: Drivers,
) -> Result<(), String> {
let pool = MySqlPoolOptions::new()
.acquire_timeout(Duration::new(5, 0))
.test_before_acquire(true)
.connect(&conn_string)
.await
.map_err(|_| "Couldn't establish connection to db".to_string())?;
*db.mysql_pool.lock().await = Some(pool);
*db.driver.lock().await = Some(driver);
Ok(())
}
97 changes: 97 additions & 0 deletions src-tauri/src/drivers/mysql/decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use serde_json::Value as JsonValue;
use sqlx::{mysql::MySqlValueRef, TypeInfo, Value, ValueRef};
use time::{Date, OffsetDateTime, PrimitiveDateTime, Time};

// this code was taken from here
// https://github.com/tauri-apps/tauri-plugin-sql/blob/v1/src/decode/mysql.rs
pub fn to_json(v: MySqlValueRef) -> Result<JsonValue, String> {
if v.is_null() {
return Ok(JsonValue::Null);
}

let res = match v.type_info().name() {
"CHAR" | "VARCHAR" | "TINYTEXT" | "TEXT" | "MEDIUMTEXT" | "LONGTEXT" | "ENUM" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode() {
JsonValue::String(v)
} else {
JsonValue::Null
}
}
"FLOAT" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<f32>() {
JsonValue::from(v)
} else {
JsonValue::Null
}
}
"DOUBLE" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<f64>() {
JsonValue::from(v)
} else {
JsonValue::Null
}
}
"TINYINT" | "SMALLINT" | "INT" | "MEDIUMINT" | "BIGINT" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<i64>() {
JsonValue::Number(v.into())
} else {
JsonValue::Null
}
}
"TINYINT UNSIGNED" | "SMALLINT UNSIGNED" | "INT UNSIGNED" | "MEDIUMINT UNSIGNED"
| "BIGINT UNSIGNED" | "YEAR" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<u64>() {
JsonValue::Number(v.into())
} else {
JsonValue::Null
}
}
"BOOLEAN" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode() {
JsonValue::Bool(v)
} else {
JsonValue::Null
}
}
"DATE" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<Date>() {
JsonValue::String(v.to_string())
} else {
JsonValue::Null
}
}
"TIME" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<Time>() {
JsonValue::String(v.to_string())
} else {
JsonValue::Null
}
}
"DATETIME" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<PrimitiveDateTime>() {
JsonValue::String(v.to_string())
} else {
JsonValue::Null
}
}
"TIMESTAMP" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<OffsetDateTime>() {
JsonValue::String(v.to_string())
} else {
JsonValue::Null
}
}
"JSON" => ValueRef::to_owned(&v).try_decode().unwrap_or_default(),
"TINIYBLOB" | "MEDIUMBLOB" | "BLOB" | "LONGBLOB" => {
if let Ok(v) = ValueRef::to_owned(&v).try_decode::<Vec<u8>>() {
JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect())
} else {
JsonValue::Null
}
}
"NULL" => JsonValue::Null,
_ => return Err("Unsupported data type".to_string()),
};

Ok(res)
}
4 changes: 4 additions & 0 deletions src-tauri/src/drivers/mysql/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod connection;
pub mod decode;
pub mod row;
pub mod table;
89 changes: 89 additions & 0 deletions src-tauri/src/drivers/mysql/row.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use crate::{drivers::mysql::decode, DbInstance};
use serde_json::value::Value as JsonValue;
use sqlx::{Column, Row};
use std::collections::HashMap;
use tauri::State;

pub async fn get_rows(
db: &State<'_, DbInstance>,
table_name: String,
) -> Result<Vec<HashMap<String, JsonValue>>, String> {
let long_lived = db.mysql_pool.lock().await;
let conn = long_lived.as_ref().unwrap();

let rows = sqlx::query(format!("SELECT * FROM {};", table_name).as_str())
.fetch_all(conn)
.await
.unwrap();
let mut values = Vec::new();
for row in rows {
let mut value = HashMap::default();
for (i, column) in row.columns().iter().enumerate() {
let v = row.try_get_raw(i).unwrap();

let v = decode::to_json(v)?;

value.insert(column.name().to_string(), v);
}

values.push(value);
}
Ok(values)
}

pub async fn delete_rows(
db: &State<'_, DbInstance>,
pk_col_name: String,
table_name: String,
params: String,
) -> Result<u64, String> {
let long_lived = db.mysql_pool.lock().await;
let conn = long_lived.as_ref().unwrap();

let query_str = format!("DELETE FROM {table_name} WHERE {pk_col_name} in ({params});");
let result = sqlx::query(&query_str)
.execute(conn)
.await
.map_err(|_| "Failed to delete rows".to_string())?;
Ok(result.rows_affected())
}

pub async fn create_row(
db: &State<'_, DbInstance>,
table_name: String,
columns: String,
values: String,
) -> Result<u64, String> {
let long_lived = db.mysql_pool.lock().await;
let conn = long_lived.as_ref().unwrap();

let res =
sqlx::query(format!("INSERT INTO {table_name} ({columns}) VALUES({values})").as_str())
.execute(conn)
.await
.map_err(|err| err.to_string())?;
Ok(res.rows_affected())
}

pub async fn update_row(
db: &State<'_, DbInstance>,
table_name: String,
set_condition: String,
pk_col_name: String,
pk_col_value: JsonValue,
) -> Result<u64, String> {
let long_lived = db.mysql_pool.lock().await;
let conn = long_lived.as_ref().unwrap();

let res = sqlx::query(
format!(
"UPDATE {table_name} SET {set_condition} WHERE {pk_col_name}={}",
pk_col_value
)
.as_str(),
)
.execute(conn)
.await
.map_err(|_| "Failed to update row".to_string())?;
Ok(res.rows_affected())
}
75 changes: 75 additions & 0 deletions src-tauri/src/drivers/mysql/table.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use crate::{drivers::mysql::decode, utils, DbInstance};
use serde_json::{
value::Value::{Bool as JsonBool, String as JsonString},
Value as JsonValue,
};
use sqlx::Row;
use std::collections::HashMap;
use tauri::State;

pub async fn get_tables(db: &State<'_, DbInstance>) -> Result<Option<Vec<String>>, String> {
let long_lived = db.mysql_pool.lock().await;
let conn = long_lived.as_ref().unwrap();

let rows = sqlx::query("show tables;")
.fetch_all(conn)
.await
.map_err(|err| err.to_string())?;

if rows.is_empty() {
return Ok(None);
}

let mut result: Vec<String> = Default::default();
for (_, row) in rows.iter().enumerate() {
result.push(row.try_get::<String, usize>(0).unwrap())
}
Ok(Some(result))
}

pub async fn get_columns_definition(
db: &State<'_, DbInstance>,
table_name: String,
) -> Result<HashMap<String, HashMap<String, JsonValue>>, String> {
let long_lived = db.mysql_pool.lock().await;
let conn = long_lived.as_ref().unwrap();
let rows = sqlx::query(
format!(
"SELECT cols.column_name,
cols.data_type,
if(cols.is_nullable = \"YES\", TRUE, FALSE) AS is_nullable,
cols.column_default,
if(kcu.constraint_name = 'PRIMARY', TRUE, FALSE) AS is_pk
FROM information_schema.columns AS cols
LEFT JOIN information_schema.key_column_usage AS kcu ON cols.column_name = kcu.column_name
WHERE cols.table_name = \"{table_name}\";"
)
.as_str(),
)
.fetch_all(conn)
.await
.map_err(|err| err.to_string())?;

let mut result = HashMap::<String, HashMap<String, JsonValue>>::new();

rows.iter().for_each(|row| {
let column_props = utils::create_column_definition_map(
JsonString(row.get(1)),
JsonBool(row.get::<i16, usize>(2) == 1),
decode::to_json(row.try_get_raw(3).unwrap()).unwrap(),
JsonBool(row.get::<i16, usize>(4) == 1),
);
result.insert(row.get(0), column_props);
});
Ok(result)
}

/*
select cols.column_name,cols.data_type,
if(cols.is_nullable = "YES",true,false) as is_nullable ,
cols.column_default,
if(kcu.constraint_name = 'PRIMARY',true,false) as is_pk
from information_schema.columns as cols
left join information_schema.key_column_usage as kcu on cols.column_name = kcu.column_name
where cols.table_name = "test";
*/
1 change: 0 additions & 1 deletion src-tauri/src/drivers/sqlite/decode.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use serde_json::Value as JsonValue;
// use sqlx::types::chrono::{DateTime, NaiveDate, NaiveTime};
use sqlx::{sqlite::SqliteValueRef, TypeInfo, Value, ValueRef};
use time::{Date, PrimitiveDateTime, Time};

Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use utils::Drivers;
pub struct DbInstance {
sqlite_pool: Mutex<Option<SqlitePool>>,
postgres_pool: Mutex<Option<PgPool>>,
_mysql_pool: Mutex<Option<MySqlPool>>,
mysql_pool: Mutex<Option<MySqlPool>>,
driver: Mutex<Option<Drivers>>,
}

Expand All @@ -31,7 +31,7 @@ fn main() {
.manage(DbInstance {
sqlite_pool: Default::default(),
postgres_pool: Default::default(),
_mysql_pool: Default::default(),
mysql_pool: Default::default(),
driver: Default::default(),
})
.invoke_handler(tauri::generate_handler![
Expand Down
18 changes: 11 additions & 7 deletions src-tauri/src/row.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::drivers::postgres;
use crate::utils::Drivers;
use crate::{drivers::sqlite, DbInstance};
use crate::{
drivers::{mysql, postgres, sqlite},
utils::Drivers,
DbInstance,
};
use serde_json::Map;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
Expand All @@ -18,7 +20,7 @@ pub async fn get_rows(
match driver {
Drivers::SQLite => sqlite::row::get_rows(&db, table_name).await,
Drivers::PostgreSQL => postgres::row::get_rows(&db, table_name).await,
Drivers::MySQL => todo!(),
Drivers::MySQL => mysql::row::get_rows(&db, table_name).await,
}
}

Expand Down Expand Up @@ -48,7 +50,7 @@ pub async fn delete_rows(
Drivers::PostgreSQL => {
postgres::row::delete_rows(&db, pk_col_name, table_name, params).await
}
Drivers::MySQL => todo!(),
Drivers::MySQL => mysql::row::delete_rows(&db, pk_col_name, table_name, params).await,
}
}

Expand Down Expand Up @@ -79,7 +81,7 @@ pub async fn create_row(
match driver {
Drivers::SQLite => sqlite::row::create_row(&db, table_name, columns, values).await,
Drivers::PostgreSQL => postgres::row::create_row(&db, table_name, columns, values).await,
Drivers::MySQL => todo!(),
Drivers::MySQL => mysql::row::create_row(&db, table_name, columns, values).await,
}
}

Expand Down Expand Up @@ -111,6 +113,8 @@ pub async fn update_row(
postgres::row::update_row(&db, table_name, set_condition, pk_col_name, pk_col_value)
.await
}
Drivers::MySQL => todo!(),
Drivers::MySQL => {
mysql::row::update_row(&db, table_name, set_condition, pk_col_name, pk_col_value).await
}
}
}
Loading

0 comments on commit 024a566

Please sign in to comment.