Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/tauri/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
sqlx = { version = "0.8", default-features = false, features = [
"runtime-tokio",
"postgres",
"mysql",
"json",
"chrono",
"rust_decimal",
Expand Down
122 changes: 121 additions & 1 deletion apps/tauri/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde::Deserialize;
use tauri::ipc::Channel;
use tauri::State;

/// Bundled IPC args for Postgres save+connect (keeps clippy under the limit).
/// Bundled IPC args for Postgres/MySQL save+connect (keeps clippy under the limit).
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConnectPostgresRequest {
Expand All @@ -23,6 +23,9 @@ pub struct ConnectPostgresRequest {
pub id: Option<String>,
}

/// Same shape as Postgres — separate type so Tauri command names stay clear.
pub type ConnectMysqlRequest = ConnectPostgresRequest;

#[tauri::command]
pub fn db_connect_sqlite(
path: String,
Expand Down Expand Up @@ -52,6 +55,26 @@ pub fn db_connect_postgres(
})
}

#[tauri::command]
pub fn db_connect_mysql(
host: String,
port: u16,
database: String,
user: String,
password: Option<String>,
id: Option<String>,
registry: State<'_, ConnectionRegistry>,
) -> Result<ConnectionId, ConnectionError> {
registry.connect(ConnectionConfig::Mysql {
host,
port,
database,
user,
password,
id,
})
}

#[tauri::command]
pub fn profiles_list(
store: State<'_, ProfileStore>,
Expand Down Expand Up @@ -241,6 +264,103 @@ pub fn profiles_connect_postgres(
Ok(profile)
}

/// Save MySQL profile metadata (no password) and open a live session.
#[tauri::command]
pub fn profiles_connect_mysql(
request: ConnectMysqlRequest,
store: State<'_, ProfileStore>,
secrets: State<'_, SharedSecretStore>,
registry: State<'_, ConnectionRegistry>,
) -> Result<ConnectionProfile, ConnectionError> {
let ConnectMysqlRequest {
name,
host,
port,
database,
user,
password,
folder_id,
id,
} = request;

let display_name = {
let t = name.trim();
if t.is_empty() {
format!("{user}@{host}/{database}")
} else {
t.to_owned()
}
};

let mut profile = match id.filter(|s| !s.trim().is_empty()) {
Some(existing_id) => store
.get(&existing_id)
.map_err(|e| ConnectionError::io(e.to_string()))?
.map(|mut p| {
p.name = display_name.clone();
p.host = Some(host.clone());
p.port = Some(i64::from(port));
p.database_name = Some(database.clone());
p.username = Some(user.clone());
p.folder_id = folder_id.clone();
p.driver = "mysql".into();
p.path = None;
p
})
.unwrap_or_else(|| {
let mut p = ConnectionProfile::new_mysql(
display_name.clone(),
host.clone(),
port,
database.clone(),
user.clone(),
);
p.id = existing_id;
p.folder_id = folder_id.clone();
p
}),
None => {
let mut p = ConnectionProfile::new_mysql(
display_name,
host.clone(),
port,
database.clone(),
user.clone(),
);
p.folder_id = folder_id;
p
}
};
profile.touch();
store
.upsert(&profile)
.map_err(|e| ConnectionError::io(e.to_string()))?;

let password = match password {
Some(p) => {
secrets
.set_password(&profile.id, &p)
.map_err(|e| ConnectionError::io(format!("keyring write: {e}")))?;
Some(p)
}
None => secrets
.get_password(&profile.id)
.map_err(|e| ConnectionError::io(format!("keyring read: {e}")))?,
};

if !registry.is_connected(&ConnectionId::from(profile.id.as_str())) {
registry.connect(ConnectionConfig::Mysql {
host,
port,
database,
user,
password,
id: Some(profile.id.clone()),
})?;
}
Ok(profile)
}

#[tauri::command]
pub fn db_disconnect(
id: ConnectionId,
Expand Down
67 changes: 51 additions & 16 deletions apps/tauri/src-tauri/src/connection/manager.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::error::{ConnectionError, ConnectionResult};
use super::id::{ConnectionId, DriverKind};
use super::mysql::MysqlSession;
use super::postgres::PostgresSession;
use super::query::QueryEvent;
use super::sqlite::SqliteSession;
Expand Down Expand Up @@ -31,7 +32,6 @@ pub enum ConnectionConfig {
#[serde(default)]
id: Option<String>,
},
/// Stub until the MySQL adapter epic wires connect.
Mysql {
host: String,
port: u16,
Expand Down Expand Up @@ -91,7 +91,7 @@ impl ConnectionRegistry {
Self::with_builtin_drivers()
}

/// Registry with SQLite + Postgres openers (MySQL still UnsupportedDriver).
/// Registry with SQLite + Postgres + MySQL openers.
pub fn with_builtin_drivers() -> Self {
Self::with_opener(|config| match config {
ConnectionConfig::Sqlite { path, id } => {
Expand Down Expand Up @@ -121,9 +121,24 @@ impl ConnectionRegistry {
})?;
Ok(Arc::new(session) as SessionArc)
}
other => Err(ConnectionError::UnsupportedDriver {
driver: other.driver_kind(),
}),
ConnectionConfig::Mysql {
host,
port,
database,
user,
password,
id,
} => {
let session = super::mysql::open_mysql(super::mysql::MysqlConnectParams {
host,
port,
database,
user,
password,
id: id.filter(|s| !s.trim().is_empty()).map(ConnectionId::from),
})?;
Ok(Arc::new(session) as SessionArc)
}
})
}

Expand Down Expand Up @@ -195,7 +210,22 @@ impl ConnectionRegistry {
f(pg)
}

/// Driver-aware query stream (SQLite + Postgres).
pub fn with_mysql<T>(
&self,
id: &ConnectionId,
f: impl FnOnce(&MysqlSession) -> ConnectionResult<T>,
) -> ConnectionResult<T> {
let session = self.session_arc(id)?;
let mysql = session
.as_any()
.downcast_ref::<MysqlSession>()
.ok_or(ConnectionError::UnsupportedDriver {
driver: session.driver(),
})?;
f(mysql)
}

/// Driver-aware query stream (SQLite + Postgres + MySQL).
pub fn execute_query(
&self,
id: &ConnectionId,
Expand All @@ -212,7 +242,7 @@ impl ConnectionRegistry {
DriverKind::Postgres => {
self.with_postgres(id, |s| s.execute_streaming(sql, limit, emit))
}
other => Err(ConnectionError::UnsupportedDriver { driver: other }),
DriverKind::Mysql => self.with_mysql(id, |s| s.execute_streaming(sql, limit, emit)),
}
}

Expand Down Expand Up @@ -256,7 +286,17 @@ impl ConnectionRegistry {
super::postgres_object_tree::PostgresObjectTree::new(pg.clone_handle()),
))
}
other => Err(ConnectionError::UnsupportedDriver { driver: other }),
DriverKind::Mysql => {
let mysql = session
.as_any()
.downcast_ref::<MysqlSession>()
.ok_or(ConnectionError::UnsupportedDriver {
driver: DriverKind::Mysql,
})?;
Ok(Arc::new(super::mysql_object_tree::MysqlObjectTree::new(
mysql.clone_handle(),
)))
}
}
}

Expand Down Expand Up @@ -411,24 +451,19 @@ mod tests {
}

#[test]
fn registry_mysql_is_unsupported_for_now() {
fn registry_mysql_rejects_empty_host() {
let reg = ConnectionRegistry::with_builtin_drivers();
let err = reg
.connect(ConnectionConfig::Mysql {
host: "localhost".into(),
host: " ".into(),
port: 3306,
database: "db".into(),
user: "u".into(),
password: None,
id: None,
})
.unwrap_err();
assert!(matches!(
err,
ConnectionError::UnsupportedDriver {
driver: DriverKind::Mysql
}
));
assert!(matches!(err, ConnectionError::InvalidConfig { .. }));
}

#[test]
Expand Down
3 changes: 3 additions & 0 deletions apps/tauri/src-tauri/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ mod error;
mod id;
mod introspect;
mod manager;
mod mysql;
mod mysql_object_tree;
pub(crate) mod mysql_query;
mod object_tree;
mod postgres;
mod postgres_erd;
Expand Down
Loading
Loading