mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 00:09:52 +00:00
feat: initial release
Multi-database management tool built with Tauri + Vue 3. Supports MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server.
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::db;
|
||||
use crate::db::ssh_tunnel::TunnelManager;
|
||||
use crate::models::connection::{ConnectionConfig, DatabaseType};
|
||||
|
||||
pub enum PoolKind {
|
||||
Mysql(sqlx::mysql::MySqlPool),
|
||||
Postgres(sqlx::postgres::PgPool),
|
||||
Sqlite(sqlx::sqlite::SqlitePool),
|
||||
Redis(tokio::sync::Mutex<redis::aio::MultiplexedConnection>),
|
||||
DuckDb(std::sync::Arc<std::sync::Mutex<duckdb::Connection>>),
|
||||
MongoDb(mongodb::Client),
|
||||
ClickHouse(db::clickhouse_driver::ChClient),
|
||||
SqlServer(std::sync::Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>),
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub connections: Mutex<HashMap<String, PoolKind>>,
|
||||
pub configs: Mutex<HashMap<String, ConnectionConfig>>,
|
||||
pub tunnels: TunnelManager,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
connections: Mutex::new(HashMap::new()),
|
||||
configs: Mutex::new(HashMap::new()),
|
||||
tunnels: TunnelManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_or_create_pool(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
database: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let is_embedded = {
|
||||
let configs = self.configs.lock().await;
|
||||
configs.get(connection_id)
|
||||
.map(|c| c.db_type == DatabaseType::Sqlite || c.db_type == DatabaseType::DuckDb)
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if is_embedded {
|
||||
return Ok(connection_id.to_string());
|
||||
}
|
||||
|
||||
let pool_key = match database {
|
||||
Some(db) => format!("{connection_id}:{db}"),
|
||||
None => connection_id.to_string(),
|
||||
};
|
||||
|
||||
let conns = self.connections.lock().await;
|
||||
if conns.contains_key(&pool_key) {
|
||||
return Ok(pool_key);
|
||||
}
|
||||
drop(conns);
|
||||
|
||||
let configs = self.configs.lock().await;
|
||||
let config = configs
|
||||
.get(connection_id)
|
||||
.ok_or("Connection config not found")?
|
||||
.clone();
|
||||
drop(configs);
|
||||
|
||||
let mut db_config = config.clone();
|
||||
if let Some(db) = database {
|
||||
db_config.database = Some(db.to_string());
|
||||
}
|
||||
|
||||
let url = db_config.connection_url();
|
||||
let pool = match db_config.db_type {
|
||||
DatabaseType::Mysql => PoolKind::Mysql(db::mysql::connect(&url).await?),
|
||||
DatabaseType::Postgres => PoolKind::Postgres(db::postgres::connect(&url).await?),
|
||||
DatabaseType::Sqlite => PoolKind::Sqlite(db::sqlite::connect(&url).await?),
|
||||
DatabaseType::Redis => {
|
||||
let con = db::redis_driver::connect(&url).await?;
|
||||
PoolKind::Redis(tokio::sync::Mutex::new(con))
|
||||
}
|
||||
DatabaseType::DuckDb => {
|
||||
let con = duckdb::Connection::open(&db_config.host).map_err(|e| e.to_string())?;
|
||||
PoolKind::DuckDb(std::sync::Arc::new(std::sync::Mutex::new(con)))
|
||||
}
|
||||
DatabaseType::MongoDb => {
|
||||
let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?;
|
||||
PoolKind::MongoDb(client)
|
||||
}
|
||||
DatabaseType::ClickHouse => {
|
||||
let client = db::clickhouse_driver::ChClient::new(&url);
|
||||
db::clickhouse_driver::test_connection(&client).await?;
|
||||
PoolKind::ClickHouse(client)
|
||||
}
|
||||
DatabaseType::SqlServer => {
|
||||
let client = db::sqlserver::connect(
|
||||
&db_config.host, db_config.port,
|
||||
&db_config.username, &db_config.password,
|
||||
db_config.database.as_deref(),
|
||||
).await?;
|
||||
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
};
|
||||
|
||||
self.connections.lock().await.insert(pool_key.clone(), pool);
|
||||
Ok(pool_key)
|
||||
}
|
||||
|
||||
pub async fn reconnect_pool(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
database: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let pool_key = match database {
|
||||
Some(db) => format!("{connection_id}:{db}"),
|
||||
None => connection_id.to_string(),
|
||||
};
|
||||
self.connections.lock().await.remove(&pool_key);
|
||||
self.get_or_create_pool(connection_id, database).await
|
||||
}
|
||||
}
|
||||
|
||||
fn connections_file(app: &AppHandle) -> Result<std::path::PathBuf, String> {
|
||||
let dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
Ok(dir.join("connections.json"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_connections(
|
||||
app: AppHandle,
|
||||
configs: Vec<ConnectionConfig>,
|
||||
) -> Result<(), String> {
|
||||
let path = connections_file(&app)?;
|
||||
let json = serde_json::to_string_pretty(&configs).map_err(|e| e.to_string())?;
|
||||
std::fs::write(path, json).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_connections(app: AppHandle) -> Result<Vec<ConnectionConfig>, String> {
|
||||
let path = connections_file(&app)?;
|
||||
if !path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||
let configs: Vec<ConnectionConfig> =
|
||||
serde_json::from_str(&json).map_err(|e| e.to_string())?;
|
||||
Ok(configs)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_connection(config: ConnectionConfig) -> Result<String, String> {
|
||||
let url = config.connection_url();
|
||||
match config.db_type {
|
||||
DatabaseType::Mysql => {
|
||||
let pool = db::mysql::connect(&url).await?;
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::Postgres => {
|
||||
let pool = db::postgres::connect(&url).await?;
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::Sqlite => {
|
||||
let pool = db::sqlite::connect(&url).await?;
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::Redis => {
|
||||
let _con = db::redis_driver::connect(&url).await?;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::DuckDb => {
|
||||
let _con = duckdb::Connection::open(&config.host).map_err(|e| e.to_string())?;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::MongoDb => {
|
||||
let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?;
|
||||
client.list_database_names().await.map_err(|e| e.to_string())?;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::ClickHouse => {
|
||||
let client = db::clickhouse_driver::ChClient::new(&url);
|
||||
db::clickhouse_driver::test_connection(&client).await?;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::SqlServer => {
|
||||
let _client = db::sqlserver::connect(
|
||||
&config.host, config.port,
|
||||
&config.username, &config.password,
|
||||
config.database.as_deref(),
|
||||
).await?;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connect_db(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
config: ConnectionConfig,
|
||||
) -> Result<String, String> {
|
||||
let id = config.id.clone();
|
||||
|
||||
let url = if config.ssh_enabled && !config.ssh_host.is_empty() {
|
||||
let local_port = state.tunnels.start_tunnel(
|
||||
&id, &config.ssh_host, config.ssh_port,
|
||||
&config.ssh_user, &config.ssh_key_path,
|
||||
&config.host, config.port,
|
||||
).await?;
|
||||
config.connection_url_with_host("127.0.0.1", local_port)
|
||||
} else {
|
||||
config.connection_url()
|
||||
};
|
||||
|
||||
let pool = match config.db_type {
|
||||
DatabaseType::Mysql => PoolKind::Mysql(db::mysql::connect(&url).await?),
|
||||
DatabaseType::Postgres => PoolKind::Postgres(db::postgres::connect(&url).await?),
|
||||
DatabaseType::Sqlite => PoolKind::Sqlite(db::sqlite::connect(&url).await?),
|
||||
DatabaseType::Redis => {
|
||||
let con = db::redis_driver::connect(&url).await?;
|
||||
PoolKind::Redis(tokio::sync::Mutex::new(con))
|
||||
}
|
||||
DatabaseType::DuckDb => {
|
||||
let con = duckdb::Connection::open(&config.host).map_err(|e| e.to_string())?;
|
||||
PoolKind::DuckDb(std::sync::Arc::new(std::sync::Mutex::new(con)))
|
||||
}
|
||||
DatabaseType::MongoDb => {
|
||||
let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?;
|
||||
PoolKind::MongoDb(client)
|
||||
}
|
||||
DatabaseType::ClickHouse => {
|
||||
let client = db::clickhouse_driver::ChClient::new(&url);
|
||||
db::clickhouse_driver::test_connection(&client).await?;
|
||||
PoolKind::ClickHouse(client)
|
||||
}
|
||||
DatabaseType::SqlServer => {
|
||||
let client = db::sqlserver::connect(
|
||||
&config.host, config.port,
|
||||
&config.username, &config.password,
|
||||
config.database.as_deref(),
|
||||
).await?;
|
||||
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client))) }
|
||||
};
|
||||
|
||||
state.connections.lock().await.insert(id.clone(), pool);
|
||||
state.configs.lock().await.insert(id.clone(), config);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disconnect_db(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
) -> Result<(), String> {
|
||||
let mut conns = state.connections.lock().await;
|
||||
let keys_to_remove: Vec<String> = conns
|
||||
.keys()
|
||||
.filter(|k| *k == &connection_id || k.starts_with(&format!("{connection_id}:")))
|
||||
.cloned()
|
||||
.collect();
|
||||
for key in keys_to_remove {
|
||||
if let Some(pool) = conns.remove(&key) {
|
||||
match pool {
|
||||
PoolKind::Mysql(p) => p.close().await,
|
||||
PoolKind::Postgres(p) => p.close().await,
|
||||
PoolKind::Sqlite(p) => p.close().await,
|
||||
PoolKind::Redis(_) => {},
|
||||
PoolKind::DuckDb(_) => {},
|
||||
PoolKind::MongoDb(_) => {},
|
||||
PoolKind::ClickHouse(_) => {},
|
||||
PoolKind::SqlServer(_) => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(conns);
|
||||
state.configs.lock().await.remove(&connection_id);
|
||||
state.tunnels.stop_tunnel(&connection_id).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HistoryEntry {
|
||||
pub id: String,
|
||||
pub connection_name: String,
|
||||
pub database: String,
|
||||
pub sql: String,
|
||||
pub executed_at: String,
|
||||
pub execution_time_ms: u128,
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
fn history_file(app: &AppHandle) -> Result<std::path::PathBuf, String> {
|
||||
let dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
Ok(dir.join("query_history.json"))
|
||||
}
|
||||
|
||||
fn read_all(app: &AppHandle) -> Result<Vec<HistoryEntry>, String> {
|
||||
let path = history_file(app)?;
|
||||
if !path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let json = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(&json).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn write_all(app: &AppHandle, entries: &[HistoryEntry]) -> Result<(), String> {
|
||||
let path = history_file(app)?;
|
||||
let json = serde_json::to_string(entries).map_err(|e| e.to_string())?;
|
||||
std::fs::write(path, json).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
const MAX_HISTORY: usize = 1000;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_history(app: AppHandle, entry: HistoryEntry) -> Result<(), String> {
|
||||
let mut entries = read_all(&app)?;
|
||||
entries.insert(0, entry);
|
||||
entries.truncate(MAX_HISTORY);
|
||||
write_all(&app, &entries)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_history(
|
||||
app: AppHandle,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Vec<HistoryEntry>, String> {
|
||||
let entries = read_all(&app)?;
|
||||
Ok(entries.into_iter().skip(offset).take(limit).collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_history(app: AppHandle) -> Result<(), String> {
|
||||
write_all(&app, &[])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_history_entry(app: AppHandle, id: String) -> Result<(), String> {
|
||||
let entries: Vec<HistoryEntry> = read_all(&app)?
|
||||
.into_iter()
|
||||
.filter(|e| e.id != id)
|
||||
.collect();
|
||||
write_all(&app, &entries)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod connection;
|
||||
pub mod history;
|
||||
pub mod mongo_cmd;
|
||||
pub mod query;
|
||||
pub mod redis_cmd;
|
||||
pub mod schema;
|
||||
@@ -0,0 +1,100 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use crate::commands::connection::{AppState, PoolKind};
|
||||
use crate::db::mongo_driver::{self, MongoDocumentResult};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_list_databases(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => mongo_driver::list_databases(client).await,
|
||||
_ => Err("Not a MongoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_list_collections(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => mongo_driver::list_collections(client, &database).await,
|
||||
_ => Err("Not a MongoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_find_documents(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
collection: String,
|
||||
skip: u64,
|
||||
limit: i64,
|
||||
) -> Result<MongoDocumentResult, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => {
|
||||
mongo_driver::find_documents(client, &database, &collection, skip, limit).await
|
||||
}
|
||||
_ => Err("Not a MongoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_insert_document(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
collection: String,
|
||||
doc_json: String,
|
||||
) -> Result<String, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => {
|
||||
mongo_driver::insert_document(client, &database, &collection, &doc_json).await
|
||||
}
|
||||
_ => Err("Not a MongoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_update_document(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
collection: String,
|
||||
id: String,
|
||||
doc_json: String,
|
||||
) -> Result<u64, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => {
|
||||
mongo_driver::update_document(client, &database, &collection, &id, &doc_json).await
|
||||
}
|
||||
_ => Err("Not a MongoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_delete_document(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
collection: String,
|
||||
id: String,
|
||||
) -> Result<u64, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => {
|
||||
mongo_driver::delete_document(client, &database, &collection, &id).await
|
||||
}
|
||||
_ => Err("Not a MongoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::State;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::commands::connection::{AppState, PoolKind};
|
||||
use crate::db;
|
||||
|
||||
const QUERY_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const MAX_ROWS: usize = 10000;
|
||||
|
||||
fn duckdb_execute(con: &duckdb::Connection, sql: &str) -> Result<db::QueryResult, String> {
|
||||
let start = std::time::Instant::now();
|
||||
let trimmed = sql.trim().to_uppercase();
|
||||
|
||||
if trimmed.starts_with("SELECT") || trimmed.starts_with("SHOW") || trimmed.starts_with("DESCRIBE")
|
||||
|| trimmed.starts_with("EXPLAIN") || trimmed.starts_with("WITH") || trimmed.starts_with("PRAGMA")
|
||||
{
|
||||
let mut stmt = con.prepare(sql).map_err(|e| e.to_string())?;
|
||||
let mut rows = stmt.query([]).map_err(|e| e.to_string())?;
|
||||
let stmt_ref = rows.as_ref().ok_or("DuckDB statement unavailable")?;
|
||||
let col_count = stmt_ref.column_count();
|
||||
let columns: Vec<String> = (0..col_count)
|
||||
.map(|i| stmt_ref.column_name(i).map(|s| s.to_string()).unwrap_or_else(|_| "?".to_string()))
|
||||
.collect();
|
||||
|
||||
let mut result_rows = Vec::new();
|
||||
while let Some(row) = rows.next().map_err(|e| e.to_string())? {
|
||||
if result_rows.len() >= MAX_ROWS { break; }
|
||||
let vals: Vec<serde_json::Value> = (0..col_count).map(|i| {
|
||||
row.get::<_, String>(i)
|
||||
.map(serde_json::Value::String)
|
||||
.or_else(|_| row.get::<_, i64>(i).map(|v| serde_json::Value::Number(v.into())))
|
||||
.or_else(|_| row.get::<_, f64>(i).map(|v| {
|
||||
serde_json::Number::from_f64(v)
|
||||
.map(serde_json::Value::Number)
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}))
|
||||
.or_else(|_| row.get::<_, bool>(i).map(serde_json::Value::Bool))
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}).collect();
|
||||
result_rows.push(vals);
|
||||
}
|
||||
|
||||
let truncated = result_rows.len() >= MAX_ROWS;
|
||||
Ok(db::QueryResult { columns, rows: result_rows, affected_rows: 0, execution_time_ms: start.elapsed().as_millis(), truncated })
|
||||
} else {
|
||||
let affected = con.execute(sql, []).map_err(|e| e.to_string())?;
|
||||
Ok(db::QueryResult { columns: vec![], rows: vec![], affected_rows: affected as u64, execution_time_ms: start.elapsed().as_millis(), truncated: false })
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_result(mut result: db::QueryResult) -> db::QueryResult {
|
||||
if result.rows.len() > MAX_ROWS {
|
||||
result.rows.truncate(MAX_ROWS);
|
||||
result.truncated = true;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn is_connection_error(err: &str) -> bool {
|
||||
let lower = err.to_lowercase();
|
||||
lower.contains("connection")
|
||||
|| lower.contains("broken pipe")
|
||||
|| lower.contains("reset by peer")
|
||||
|| lower.contains("timed out")
|
||||
|| lower.contains("closed")
|
||||
|| lower.contains("eof")
|
||||
}
|
||||
|
||||
async fn do_execute(
|
||||
state: &AppState,
|
||||
pool_key: &str,
|
||||
sql: &str,
|
||||
) -> Result<db::QueryResult, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(pool_key).ok_or("Connection not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::DuckDb(con) => {
|
||||
let con = con.clone();
|
||||
let sql = sql.to_string();
|
||||
drop(connections);
|
||||
let task = tokio::task::spawn_blocking(move || {
|
||||
let con = con.lock().map_err(|e| e.to_string())?;
|
||||
duckdb_execute(&con, &sql)
|
||||
});
|
||||
timeout(QUERY_TIMEOUT, task)
|
||||
.await
|
||||
.map_err(|_| format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()))?
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
PoolKind::Mysql(p) => {
|
||||
let p = p.clone();
|
||||
drop(connections);
|
||||
timeout(QUERY_TIMEOUT, db::mysql::execute_query(&p, sql))
|
||||
.await
|
||||
.map_err(|_| format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()))?
|
||||
.map(truncate_result)
|
||||
}
|
||||
PoolKind::Postgres(p) => {
|
||||
let p = p.clone();
|
||||
drop(connections);
|
||||
timeout(QUERY_TIMEOUT, db::postgres::execute_query(&p, sql))
|
||||
.await
|
||||
.map_err(|_| format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()))?
|
||||
.map(truncate_result)
|
||||
}
|
||||
PoolKind::Sqlite(p) => {
|
||||
let p = p.clone();
|
||||
drop(connections);
|
||||
timeout(QUERY_TIMEOUT, db::sqlite::execute_query(&p, sql))
|
||||
.await
|
||||
.map_err(|_| format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()))?
|
||||
.map(truncate_result)
|
||||
}
|
||||
PoolKind::ClickHouse(client) => {
|
||||
let client = client.clone();
|
||||
let database = pool_key.split(':').nth(1).unwrap_or("default").to_string();
|
||||
drop(connections);
|
||||
timeout(QUERY_TIMEOUT, db::clickhouse_driver::execute_query(&client, &database, sql))
|
||||
.await
|
||||
.map_err(|_| format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()))?
|
||||
.map(truncate_result)
|
||||
}
|
||||
PoolKind::SqlServer(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
timeout(QUERY_TIMEOUT, db::sqlserver::execute_query(&mut client, sql))
|
||||
.await
|
||||
.map_err(|_| format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()))?
|
||||
.map(truncate_result)
|
||||
}
|
||||
PoolKind::Redis(_) => Err("Use Redis-specific commands".to_string()),
|
||||
PoolKind::MongoDb(_) => Err("Use MongoDB-specific commands".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn execute_query(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
sql: String,
|
||||
) -> Result<db::QueryResult, String> {
|
||||
let pool_key = if database.is_empty() {
|
||||
connection_id.clone()
|
||||
} else {
|
||||
state.get_or_create_pool(&connection_id, Some(&database)).await?
|
||||
};
|
||||
|
||||
let result = do_execute(&state, &pool_key, &sql).await;
|
||||
|
||||
match &result {
|
||||
Err(e) if is_connection_error(e) => {
|
||||
let db_opt = if database.is_empty() { None } else { Some(database.as_str()) };
|
||||
let new_key = state.reconnect_pool(&connection_id, db_opt).await?;
|
||||
do_execute(&state, &new_key, &sql).await
|
||||
}
|
||||
_ => result,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use crate::commands::connection::{AppState, PoolKind};
|
||||
use crate::db::redis_driver::{self, RedisKeyInfo, RedisValue};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_list_databases(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<u32>, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&connection_id).ok_or("Connection not found")?;
|
||||
match pool {
|
||||
PoolKind::Redis(con) => {
|
||||
let mut con = con.lock().await;
|
||||
redis_driver::list_databases(&mut con).await
|
||||
}
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_scan_keys(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
db: u32,
|
||||
pattern: String,
|
||||
count: usize,
|
||||
) -> Result<Vec<RedisKeyInfo>, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&connection_id).ok_or("Connection not found")?;
|
||||
match pool {
|
||||
PoolKind::Redis(con) => {
|
||||
let mut con = con.lock().await;
|
||||
redis_driver::select_db(&mut con, db).await?;
|
||||
redis_driver::scan_keys(&mut con, &pattern, count).await
|
||||
}
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_get_value(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
key: String,
|
||||
) -> Result<RedisValue, String> {
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&connection_id).ok_or("Connection not found")?;
|
||||
match pool {
|
||||
PoolKind::Redis(con) => {
|
||||
let mut con = con.lock().await;
|
||||
redis_driver::get_value(&mut con, &key).await
|
||||
}
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_set_string(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
key: String,
|
||||
value: String,
|
||||
ttl: Option<i64>,
|
||||
) -> Result<(), String> {
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&connection_id).ok_or("Connection not found")?;
|
||||
match pool {
|
||||
PoolKind::Redis(con) => {
|
||||
let mut con = con.lock().await;
|
||||
redis_driver::set_string(&mut con, &key, &value, ttl).await
|
||||
}
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_delete_key(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
key: String,
|
||||
) -> Result<(), String> {
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&connection_id).ok_or("Connection not found")?;
|
||||
match pool {
|
||||
PoolKind::Redis(con) => {
|
||||
let mut con = con.lock().await;
|
||||
redis_driver::delete_key(&mut con, &key).await
|
||||
}
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_hash_set(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String, key: String, field: String, value: String,
|
||||
) -> Result<(), String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::Redis(con) => redis_driver::hash_set(&mut *con.lock().await, &key, &field, &value).await,
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_hash_del(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String, key: String, field: String,
|
||||
) -> Result<(), String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::Redis(con) => redis_driver::hash_del(&mut *con.lock().await, &key, &field).await,
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_list_push(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String, key: String, value: String,
|
||||
) -> Result<(), String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::Redis(con) => redis_driver::list_push(&mut *con.lock().await, &key, &value).await,
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_list_remove(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String, key: String, index: i64,
|
||||
) -> Result<(), String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::Redis(con) => redis_driver::list_remove(&mut *con.lock().await, &key, index).await,
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_set_add(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String, key: String, member: String,
|
||||
) -> Result<(), String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::Redis(con) => redis_driver::set_add(&mut *con.lock().await, &key, &member).await,
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn redis_set_remove(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String, key: String, member: String,
|
||||
) -> Result<(), String> {
|
||||
let connections = state.connections.lock().await;
|
||||
match connections.get(&connection_id).ok_or("Not found")? {
|
||||
PoolKind::Redis(con) => redis_driver::set_remove(&mut *con.lock().await, &key, &member).await,
|
||||
_ => Err("Not a Redis connection".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use crate::commands::connection::{AppState, PoolKind};
|
||||
use crate::db;
|
||||
|
||||
fn duckdb_query_tables(con: &duckdb::Connection) -> Result<Vec<db::TableInfo>, String> {
|
||||
let mut stmt = con.prepare(
|
||||
"SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name"
|
||||
).map_err(|e| e.to_string())?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(db::TableInfo {
|
||||
name: row.get::<_, String>(0)?,
|
||||
table_type: row.get::<_, String>(1)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||
}
|
||||
|
||||
fn duckdb_query_columns(con: &duckdb::Connection, table: &str) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
let mut pk_stmt = con.prepare(
|
||||
"SELECT kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
AND tc.table_name = kcu.table_name
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||
AND tc.table_schema = 'main'
|
||||
AND tc.table_name = ?
|
||||
ORDER BY kcu.ordinal_position"
|
||||
).map_err(|e| e.to_string())?;
|
||||
let pk_rows = pk_stmt.query_map([table], |row| row.get::<_, String>(0))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let primary_keys: std::collections::HashSet<String> = pk_rows.filter_map(|r| r.ok()).collect();
|
||||
|
||||
let mut stmt = con.prepare(
|
||||
"SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'main' AND table_name = ?
|
||||
ORDER BY ordinal_position"
|
||||
).map_err(|e| e.to_string())?;
|
||||
let rows = stmt.query_map([table], |row| {
|
||||
let name = row.get::<_, String>(0)?;
|
||||
Ok(db::ColumnInfo {
|
||||
is_primary_key: primary_keys.contains(&name),
|
||||
name,
|
||||
data_type: row.get::<_, String>(1)?,
|
||||
is_nullable: row.get::<_, String>(2).unwrap_or_default() == "YES",
|
||||
column_default: row.get::<_, Option<String>>(3)?,
|
||||
extra: None,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||
}
|
||||
|
||||
fn extract_duckdb(connections: &std::collections::HashMap<String, PoolKind>, key: &str) -> Option<std::sync::Arc<std::sync::Mutex<duckdb::Connection>>> {
|
||||
match connections.get(key)? {
|
||||
PoolKind::DuckDb(con) => Some(con.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_sqlserver(connections: &std::collections::HashMap<String, PoolKind>, key: &str) -> Option<std::sync::Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>> {
|
||||
match connections.get(key)? {
|
||||
PoolKind::SqlServer(client) => Some(client.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_clickhouse(connections: &std::collections::HashMap<String, PoolKind>, key: &str) -> Option<db::clickhouse_driver::ChClient> {
|
||||
match connections.get(key)? {
|
||||
PoolKind::ClickHouse(client) => Some(client.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_databases(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
) -> Result<Vec<db::DatabaseInfo>, String> {
|
||||
{
|
||||
let connections = state.connections.lock().await;
|
||||
if let Some(client) = extract_clickhouse(&connections, &connection_id) {
|
||||
drop(connections);
|
||||
return db::clickhouse_driver::list_databases(&client).await;
|
||||
}
|
||||
if let Some(client) = extract_sqlserver(&connections, &connection_id) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_databases(&mut client).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&connection_id).ok_or("Connection not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p) => db::mysql::list_databases(p).await,
|
||||
PoolKind::Postgres(p) => db::postgres::list_databases(p).await,
|
||||
PoolKind::Sqlite(p) => db::sqlite::list_databases(p).await,
|
||||
PoolKind::DuckDb(_) => Ok(vec![db::DatabaseInfo { name: "main".to_string() }]),
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_schemas(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?;
|
||||
|
||||
{
|
||||
let connections = state.connections.lock().await;
|
||||
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_schemas(&mut client).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Postgres(p) => db::postgres::list_schemas(p).await,
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_tables(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
schema: String,
|
||||
) -> Result<Vec<db::TableInfo>, String> {
|
||||
let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?;
|
||||
|
||||
{
|
||||
let connections = state.connections.lock().await;
|
||||
if let Some(con) = extract_duckdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let con = con.lock().map_err(|e| e.to_string())?;
|
||||
return duckdb_query_tables(&con);
|
||||
}
|
||||
if let Some(client) = extract_clickhouse(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
return db::clickhouse_driver::list_tables(&client, &database).await;
|
||||
}
|
||||
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_tables(&mut client, &schema).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p) => db::mysql::list_tables(p, &schema).await,
|
||||
PoolKind::Postgres(p) => db::postgres::list_tables(p, &schema).await,
|
||||
PoolKind::Sqlite(p) => db::sqlite::list_tables(p, &schema).await,
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_columns(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
schema: String,
|
||||
table: String,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?;
|
||||
|
||||
{
|
||||
let connections = state.connections.lock().await;
|
||||
if let Some(con) = extract_duckdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let con = con.lock().map_err(|e| e.to_string())?;
|
||||
return duckdb_query_columns(&con, &table);
|
||||
}
|
||||
if let Some(client) = extract_clickhouse(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
return db::clickhouse_driver::get_columns(&client, &database, &table).await;
|
||||
}
|
||||
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::get_columns(&mut client, &schema, &table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p) => db::mysql::get_columns(p, &schema, &table).await,
|
||||
PoolKind::Postgres(p) => db::postgres::get_columns(p, &schema, &table).await,
|
||||
PoolKind::Sqlite(p) => db::sqlite::get_columns(p, &schema, &table).await,
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_indexes(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
schema: String,
|
||||
table: String,
|
||||
) -> Result<Vec<db::IndexInfo>, String> {
|
||||
let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?;
|
||||
|
||||
{
|
||||
let connections = state.connections.lock().await;
|
||||
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_indexes(&mut client, &schema, &table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p) => db::mysql::list_indexes(p, &schema, &table).await,
|
||||
PoolKind::Postgres(p) => db::postgres::list_indexes(p, &schema, &table).await,
|
||||
PoolKind::Sqlite(p) => db::sqlite::list_indexes(p, &schema, &table).await,
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_foreign_keys(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
schema: String,
|
||||
table: String,
|
||||
) -> Result<Vec<db::ForeignKeyInfo>, String> {
|
||||
let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?;
|
||||
|
||||
{
|
||||
let connections = state.connections.lock().await;
|
||||
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_foreign_keys(&mut client, &schema, &table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p) => db::mysql::list_foreign_keys(p, &schema, &table).await,
|
||||
PoolKind::Postgres(p) => db::postgres::list_foreign_keys(p, &schema, &table).await,
|
||||
PoolKind::Sqlite(p) => db::sqlite::list_foreign_keys(p, &schema, &table).await,
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_triggers(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
schema: String,
|
||||
table: String,
|
||||
) -> Result<Vec<db::TriggerInfo>, String> {
|
||||
let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?;
|
||||
|
||||
{
|
||||
let connections = state.connections.lock().await;
|
||||
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::sqlserver::list_triggers(&mut client, &schema, &table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p) => db::mysql::list_triggers(p, &schema, &table).await,
|
||||
PoolKind::Postgres(p) => db::postgres::list_triggers(p, &schema, &table).await,
|
||||
PoolKind::Sqlite(p) => db::sqlite::list_triggers(p, &schema, &table).await,
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
use reqwest::Client as HttpClient;
|
||||
use serde::Deserialize;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::{ColumnInfo, DatabaseInfo, QueryResult, TableInfo};
|
||||
|
||||
pub struct ChClient {
|
||||
http: HttpClient,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl ChClient {
|
||||
pub fn new(url: &str) -> Self {
|
||||
Self {
|
||||
http: HttpClient::new(),
|
||||
base_url: url.trim_end_matches('/').to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ChClient {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
http: self.http.clone(),
|
||||
base_url: self.base_url.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChJsonResult {
|
||||
meta: Vec<ChColumn>,
|
||||
data: Vec<Vec<serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
#[allow(dead_code)]
|
||||
rows: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChColumn {
|
||||
name: String,
|
||||
#[serde(rename = "type")]
|
||||
_type: String,
|
||||
}
|
||||
|
||||
async fn ch_query(client: &ChClient, sql: &str, database: Option<&str>) -> Result<ChJsonResult, String> {
|
||||
let mut url = format!("{}/?default_format=JSONCompact", client.base_url);
|
||||
if let Some(db) = database {
|
||||
url.push_str(&format!("&database={}", db));
|
||||
}
|
||||
let resp = client.http.post(&url)
|
||||
.body(sql.to_string())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("ClickHouse request failed: {e}"))?;
|
||||
if !resp.status().is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("ClickHouse error: {body}"));
|
||||
}
|
||||
resp.json::<ChJsonResult>().await.map_err(|e| format!("ClickHouse parse error: {e}"))
|
||||
}
|
||||
|
||||
pub async fn test_connection(client: &ChClient) -> Result<(), String> {
|
||||
let url = format!("{}/ping", client.base_url);
|
||||
client.http.get(&url).send().await
|
||||
.map_err(|e| format!("ClickHouse connection failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_databases(client: &ChClient) -> Result<Vec<DatabaseInfo>, String> {
|
||||
let result = ch_query(client, "SELECT name FROM system.databases ORDER BY name", None).await?;
|
||||
Ok(result.data.iter().map(|row| {
|
||||
DatabaseInfo { name: row[0].as_str().unwrap_or("").to_string() }
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn list_tables(client: &ChClient, database: &str) -> Result<Vec<TableInfo>, String> {
|
||||
let sql = format!(
|
||||
"SELECT name, engine FROM system.tables WHERE database = '{}' ORDER BY name",
|
||||
database.replace('\'', "\\'")
|
||||
);
|
||||
let result = ch_query(client, &sql, Some(database)).await?;
|
||||
Ok(result.data.iter().map(|row| {
|
||||
let engine = row.get(1).and_then(|v| v.as_str()).unwrap_or("");
|
||||
let table_type = if engine.contains("View") { "VIEW" } else { "BASE TABLE" };
|
||||
TableInfo {
|
||||
name: row[0].as_str().unwrap_or("").to_string(),
|
||||
table_type: table_type.to_string(),
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn get_columns(client: &ChClient, database: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
|
||||
let sql = format!(
|
||||
"SELECT name, type, default_kind, default_expression, is_in_primary_key \
|
||||
FROM system.columns WHERE database = '{}' AND table = '{}' ORDER BY position",
|
||||
database.replace('\'', "\\'"),
|
||||
table.replace('\'', "\\'")
|
||||
);
|
||||
let result = ch_query(client, &sql, Some(database)).await?;
|
||||
Ok(result.data.iter().map(|row| {
|
||||
let data_type = row.get(1).and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let is_nullable = data_type.starts_with("Nullable");
|
||||
let is_pk = row.get(4).and_then(|v| v.as_u64()).unwrap_or(0) == 1;
|
||||
let default_kind = row.get(2).and_then(|v| v.as_str()).unwrap_or("");
|
||||
let default_expr = row.get(3).and_then(|v| v.as_str()).unwrap_or("");
|
||||
let column_default = if default_kind.is_empty() { None } else { Some(default_expr.to_string()) };
|
||||
ColumnInfo {
|
||||
name: row[0].as_str().unwrap_or("").to_string(),
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default,
|
||||
is_primary_key: is_pk,
|
||||
extra: None,
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn execute_query(client: &ChClient, database: &str, sql: &str) -> Result<QueryResult, String> {
|
||||
let start = Instant::now();
|
||||
let trimmed = sql.trim().to_uppercase();
|
||||
|
||||
if trimmed.starts_with("SELECT")
|
||||
|| trimmed.starts_with("SHOW")
|
||||
|| trimmed.starts_with("DESCRIBE")
|
||||
|| trimmed.starts_with("EXPLAIN")
|
||||
|| trimmed.starts_with("WITH")
|
||||
{
|
||||
let result = ch_query(client, sql, Some(database)).await?;
|
||||
let columns: Vec<String> = result.meta.iter().map(|c| c.name.clone()).collect();
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
rows: result.data,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
} else {
|
||||
let url = format!("{}/?default_format=JSONCompact&database={}", client.base_url, database);
|
||||
let resp = client.http.post(&url)
|
||||
.body(sql.to_string())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("ClickHouse request failed: {e}"))?;
|
||||
if !resp.status().is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("ClickHouse error: {body}"));
|
||||
}
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
pub mod clickhouse_driver;
|
||||
pub mod mongo_driver;
|
||||
pub mod mysql;
|
||||
pub mod postgres;
|
||||
pub mod redis_driver;
|
||||
pub mod sqlite;
|
||||
pub mod sqlserver;
|
||||
pub mod ssh_tunnel;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DatabaseInfo {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TableInfo {
|
||||
pub name: String,
|
||||
pub table_type: String, // "TABLE" or "VIEW"
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ColumnInfo {
|
||||
pub name: String,
|
||||
pub data_type: String,
|
||||
pub is_nullable: bool,
|
||||
pub column_default: Option<String>,
|
||||
pub is_primary_key: bool,
|
||||
pub extra: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QueryResult {
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<Vec<serde_json::Value>>,
|
||||
pub affected_rows: u64,
|
||||
pub execution_time_ms: u128,
|
||||
#[serde(default)]
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IndexInfo {
|
||||
pub name: String,
|
||||
pub columns: Vec<String>,
|
||||
pub is_unique: bool,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ForeignKeyInfo {
|
||||
pub name: String,
|
||||
pub column: String,
|
||||
pub ref_table: String,
|
||||
pub ref_column: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TriggerInfo {
|
||||
pub name: String,
|
||||
pub event: String,
|
||||
pub timing: String,
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use mongodb::{bson::{doc, Document, Bson}, Client};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MongoDocumentResult {
|
||||
pub documents: Vec<serde_json::Value>,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
pub async fn list_databases(client: &Client) -> Result<Vec<String>, String> {
|
||||
client
|
||||
.list_database_names()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn list_collections(client: &Client, database: &str) -> Result<Vec<String>, String> {
|
||||
client
|
||||
.database(database)
|
||||
.list_collection_names()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn find_documents(
|
||||
client: &Client,
|
||||
database: &str,
|
||||
collection: &str,
|
||||
skip: u64,
|
||||
limit: i64,
|
||||
) -> Result<MongoDocumentResult, String> {
|
||||
let col = client.database(database).collection::<Document>(collection);
|
||||
|
||||
let total = col.count_documents(doc! {}).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut cursor = col
|
||||
.find(doc! {})
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut documents = Vec::new();
|
||||
while cursor.advance().await.map_err(|e| e.to_string())? {
|
||||
let doc = cursor.deserialize_current().map_err(|e| e.to_string())?;
|
||||
let json = bson_to_json(&Bson::Document(doc));
|
||||
documents.push(json);
|
||||
}
|
||||
|
||||
Ok(MongoDocumentResult { documents, total })
|
||||
}
|
||||
|
||||
pub async fn insert_document(
|
||||
client: &Client,
|
||||
database: &str,
|
||||
collection: &str,
|
||||
doc_json: &str,
|
||||
) -> Result<String, String> {
|
||||
let doc: Document = serde_json::from_str(doc_json)
|
||||
.map_err(|e| format!("Invalid JSON: {e}"))?;
|
||||
let col = client.database(database).collection::<Document>(collection);
|
||||
let result = col.insert_one(doc).await.map_err(|e| e.to_string())?;
|
||||
Ok(format!("{}", result.inserted_id))
|
||||
}
|
||||
|
||||
pub async fn update_document(
|
||||
client: &Client,
|
||||
database: &str,
|
||||
collection: &str,
|
||||
id: &str,
|
||||
doc_json: &str,
|
||||
) -> Result<u64, String> {
|
||||
let oid = mongodb::bson::oid::ObjectId::parse_str(id)
|
||||
.map_err(|e| format!("Invalid ObjectId: {e}"))?;
|
||||
let new_doc: Document = serde_json::from_str(doc_json)
|
||||
.map_err(|e| format!("Invalid JSON: {e}"))?;
|
||||
let col = client.database(database).collection::<Document>(collection);
|
||||
let result = col
|
||||
.replace_one(doc! { "_id": oid }, new_doc)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(result.modified_count)
|
||||
}
|
||||
|
||||
pub async fn delete_document(
|
||||
client: &Client,
|
||||
database: &str,
|
||||
collection: &str,
|
||||
id: &str,
|
||||
) -> Result<u64, String> {
|
||||
let oid = mongodb::bson::oid::ObjectId::parse_str(id)
|
||||
.map_err(|e| format!("Invalid ObjectId: {e}"))?;
|
||||
let col = client.database(database).collection::<Document>(collection);
|
||||
let result = col
|
||||
.delete_one(doc! { "_id": oid })
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(result.deleted_count)
|
||||
}
|
||||
|
||||
fn bson_to_json(bson: &Bson) -> serde_json::Value {
|
||||
match bson {
|
||||
Bson::Double(v) => serde_json::json!(v),
|
||||
Bson::String(v) => serde_json::Value::String(v.clone()),
|
||||
Bson::Boolean(v) => serde_json::Value::Bool(*v),
|
||||
Bson::Null => serde_json::Value::Null,
|
||||
Bson::Int32(v) => serde_json::json!(v),
|
||||
Bson::Int64(v) => serde_json::json!(v),
|
||||
Bson::ObjectId(oid) => serde_json::Value::String(oid.to_hex()),
|
||||
Bson::DateTime(dt) => serde_json::Value::String(dt.to_string()),
|
||||
Bson::Array(arr) => serde_json::Value::Array(arr.iter().map(bson_to_json).collect()),
|
||||
Bson::Document(doc) => {
|
||||
let mut map = serde_json::Map::new();
|
||||
for (k, v) in doc {
|
||||
map.insert(k.clone(), bson_to_json(v));
|
||||
}
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
_ => serde_json::Value::String(format!("{bson}")),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
use sqlx::mysql::{MySqlPool, MySqlPoolOptions, MySqlRow};
|
||||
use sqlx::{Column, Executor, Row};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo};
|
||||
|
||||
pub async fn connect(url: &str) -> Result<MySqlPool, String> {
|
||||
MySqlPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.acquire_timeout(Duration::from_secs(10))
|
||||
.idle_timeout(Duration::from_secs(300))
|
||||
.connect(url)
|
||||
.await
|
||||
.map_err(|e| format!("MySQL connection failed: {e}"))
|
||||
}
|
||||
|
||||
pub async fn list_databases(pool: &MySqlPool) -> Result<Vec<DatabaseInfo>, String> {
|
||||
let rows: Vec<MySqlRow> = sqlx::query("SHOW DATABASES")
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| DatabaseInfo {
|
||||
name: row.get::<String, _>(0),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_tables(pool: &MySqlPool, database: &str) -> Result<Vec<TableInfo>, String> {
|
||||
let rows: Vec<MySqlRow> = sqlx::query(
|
||||
"SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
|
||||
)
|
||||
.bind(database)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| TableInfo {
|
||||
name: row.get::<String, _>("TABLE_NAME"),
|
||||
table_type: row.get::<String, _>("TABLE_TYPE"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_columns(
|
||||
pool: &MySqlPool,
|
||||
database: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<ColumnInfo>, String> {
|
||||
let rows: Vec<MySqlRow> = sqlx::query(
|
||||
"SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.EXTRA, \
|
||||
CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK \
|
||||
FROM information_schema.COLUMNS c \
|
||||
LEFT JOIN information_schema.KEY_COLUMN_USAGE kcu \
|
||||
ON c.TABLE_SCHEMA = kcu.TABLE_SCHEMA \
|
||||
AND c.TABLE_NAME = kcu.TABLE_NAME \
|
||||
AND c.COLUMN_NAME = kcu.COLUMN_NAME \
|
||||
AND kcu.CONSTRAINT_NAME = 'PRIMARY' \
|
||||
WHERE c.TABLE_SCHEMA = ? AND c.TABLE_NAME = ? \
|
||||
ORDER BY c.ORDINAL_POSITION",
|
||||
)
|
||||
.bind(database)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| ColumnInfo {
|
||||
name: row.get::<String, _>("COLUMN_NAME"),
|
||||
data_type: row.get::<String, _>("DATA_TYPE"),
|
||||
is_nullable: row.get::<String, _>("IS_NULLABLE") == "YES",
|
||||
column_default: row.get::<Option<String>, _>("COLUMN_DEFAULT"),
|
||||
is_primary_key: row.get::<i32, _>("IS_PK") == 1,
|
||||
extra: row.get::<Option<String>, _>("EXTRA"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn execute_query(pool: &MySqlPool, sql: &str) -> Result<QueryResult, String> {
|
||||
let start = Instant::now();
|
||||
let trimmed = sql.trim().to_uppercase();
|
||||
|
||||
if trimmed.starts_with("SELECT") || trimmed.starts_with("SHOW") || trimmed.starts_with("DESCRIBE") || trimmed.starts_with("EXPLAIN") {
|
||||
let desc = pool.describe(sql).await.map_err(|e| e.to_string())?;
|
||||
let columns: Vec<String> = desc.columns().iter().map(|c| c.name().to_string()).collect();
|
||||
|
||||
let rows: Vec<MySqlRow> = sqlx::query(sql)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let result_rows: Vec<Vec<serde_json::Value>> = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
(0..row.len())
|
||||
.map(|i| {
|
||||
row.try_get::<String, _>(i)
|
||||
.map(serde_json::Value::String)
|
||||
.or_else(|_| row.try_get::<i64, _>(i).map(|v| serde_json::Value::Number(v.into())))
|
||||
.or_else(|_| row.try_get::<f64, _>(i).map(|v| {
|
||||
serde_json::Number::from_f64(v)
|
||||
.map(serde_json::Value::Number)
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}))
|
||||
.or_else(|_| row.try_get::<bool, _>(i).map(serde_json::Value::Bool))
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
} else {
|
||||
let result = sqlx::query(sql)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: result.rows_affected(),
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_indexes(pool: &MySqlPool, database: &str, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let rows: Vec<MySqlRow> = sqlx::query(
|
||||
"SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns, \
|
||||
NOT NON_UNIQUE AS is_unique, INDEX_NAME = 'PRIMARY' AS is_primary \
|
||||
FROM information_schema.STATISTICS \
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? \
|
||||
GROUP BY INDEX_NAME, NON_UNIQUE \
|
||||
ORDER BY INDEX_NAME",
|
||||
)
|
||||
.bind(database)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let cols_str: String = row.get("columns");
|
||||
IndexInfo {
|
||||
name: row.get::<String, _>("INDEX_NAME"),
|
||||
columns: cols_str.split(',').map(|s| s.to_string()).collect(),
|
||||
is_unique: row.get::<bool, _>("is_unique"),
|
||||
is_primary: row.get::<bool, _>("is_primary"),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_foreign_keys(pool: &MySqlPool, database: &str, table: &str) -> Result<Vec<ForeignKeyInfo>, String> {
|
||||
let rows: Vec<MySqlRow> = sqlx::query(
|
||||
"SELECT kcu.CONSTRAINT_NAME, kcu.COLUMN_NAME, \
|
||||
kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME \
|
||||
FROM information_schema.KEY_COLUMN_USAGE kcu \
|
||||
WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? \
|
||||
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL \
|
||||
ORDER BY kcu.CONSTRAINT_NAME",
|
||||
)
|
||||
.bind(database)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| ForeignKeyInfo {
|
||||
name: row.get::<String, _>("CONSTRAINT_NAME"),
|
||||
column: row.get::<String, _>("COLUMN_NAME"),
|
||||
ref_table: row.get::<String, _>("REFERENCED_TABLE_NAME"),
|
||||
ref_column: row.get::<String, _>("REFERENCED_COLUMN_NAME"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_triggers(pool: &MySqlPool, database: &str, table: &str) -> Result<Vec<TriggerInfo>, String> {
|
||||
let rows: Vec<MySqlRow> = sqlx::query(
|
||||
"SELECT TRIGGER_NAME, EVENT_MANIPULATION, ACTION_TIMING \
|
||||
FROM information_schema.TRIGGERS \
|
||||
WHERE TRIGGER_SCHEMA = ? AND EVENT_OBJECT_TABLE = ? \
|
||||
ORDER BY TRIGGER_NAME",
|
||||
)
|
||||
.bind(database)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| TriggerInfo {
|
||||
name: row.get::<String, _>("TRIGGER_NAME"),
|
||||
event: row.get::<String, _>("EVENT_MANIPULATION"),
|
||||
timing: row.get::<String, _>("ACTION_TIMING"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
use sqlx::postgres::{PgPool, PgPoolOptions, PgRow};
|
||||
use sqlx::{Column, Executor, Row};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo};
|
||||
|
||||
pub async fn connect(url: &str) -> Result<PgPool, String> {
|
||||
PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.acquire_timeout(Duration::from_secs(10))
|
||||
.idle_timeout(Duration::from_secs(300))
|
||||
.connect(url)
|
||||
.await
|
||||
.map_err(|e| format!("PostgreSQL connection failed: {e}"))
|
||||
}
|
||||
|
||||
pub async fn list_databases(pool: &PgPool) -> Result<Vec<DatabaseInfo>, String> {
|
||||
let rows: Vec<PgRow> =
|
||||
sqlx::query("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname")
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| DatabaseInfo {
|
||||
name: row.get::<String, _>("datname"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_tables(pool: &PgPool, schema: &str) -> Result<Vec<TableInfo>, String> {
|
||||
let rows: Vec<PgRow> = sqlx::query(
|
||||
"SELECT table_name, table_type FROM information_schema.tables \
|
||||
WHERE table_schema = $1 ORDER BY table_name",
|
||||
)
|
||||
.bind(schema)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| TableInfo {
|
||||
name: row.get::<String, _>("table_name"),
|
||||
table_type: row.get::<String, _>("table_type"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_schemas(pool: &PgPool) -> Result<Vec<String>, String> {
|
||||
let rows: Vec<PgRow> = sqlx::query(
|
||||
"SELECT schema_name FROM information_schema.schemata \
|
||||
WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast') \
|
||||
ORDER BY schema_name",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| row.get::<String, _>("schema_name"))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_columns(
|
||||
pool: &PgPool,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<ColumnInfo>, String> {
|
||||
let rows: Vec<PgRow> = sqlx::query(
|
||||
"SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, \
|
||||
CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN true ELSE false END AS is_pk \
|
||||
FROM information_schema.columns c \
|
||||
LEFT JOIN information_schema.key_column_usage kcu \
|
||||
ON c.table_schema = kcu.table_schema \
|
||||
AND c.table_name = kcu.table_name \
|
||||
AND c.column_name = kcu.column_name \
|
||||
LEFT JOIN information_schema.table_constraints tc \
|
||||
ON kcu.constraint_name = tc.constraint_name \
|
||||
AND kcu.table_schema = tc.table_schema \
|
||||
AND tc.constraint_type = 'PRIMARY KEY' \
|
||||
WHERE c.table_schema = $1 AND c.table_name = $2 \
|
||||
ORDER BY c.ordinal_position",
|
||||
)
|
||||
.bind(schema)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| ColumnInfo {
|
||||
name: row.get::<String, _>("column_name"),
|
||||
data_type: row.get::<String, _>("data_type"),
|
||||
is_nullable: row.get::<String, _>("is_nullable") == "YES",
|
||||
column_default: row.get::<Option<String>, _>("column_default"),
|
||||
is_primary_key: row.get::<bool, _>("is_pk"),
|
||||
extra: None,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn execute_query(pool: &PgPool, sql: &str) -> Result<QueryResult, String> {
|
||||
let start = Instant::now();
|
||||
let trimmed = sql.trim().to_uppercase();
|
||||
|
||||
if trimmed.starts_with("SELECT")
|
||||
|| trimmed.starts_with("SHOW")
|
||||
|| trimmed.starts_with("EXPLAIN")
|
||||
|| trimmed.starts_with("WITH")
|
||||
|| trimmed.starts_with("TABLE")
|
||||
{
|
||||
let desc = pool.describe(sql).await.map_err(|e| e.to_string())?;
|
||||
let columns: Vec<String> = desc.columns().iter().map(|c| c.name().to_string()).collect();
|
||||
|
||||
let rows: Vec<PgRow> = sqlx::query(sql)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let result_rows: Vec<Vec<serde_json::Value>> = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
(0..row.len())
|
||||
.map(|i| {
|
||||
row.try_get::<String, _>(i)
|
||||
.map(serde_json::Value::String)
|
||||
.or_else(|_| {
|
||||
row.try_get::<i64, _>(i)
|
||||
.map(|v| serde_json::Value::Number(v.into()))
|
||||
})
|
||||
.or_else(|_| {
|
||||
row.try_get::<i32, _>(i)
|
||||
.map(|v| serde_json::Value::Number(v.into()))
|
||||
})
|
||||
.or_else(|_| {
|
||||
row.try_get::<f64, _>(i).map(|v| {
|
||||
serde_json::Number::from_f64(v)
|
||||
.map(serde_json::Value::Number)
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
})
|
||||
})
|
||||
.or_else(|_| row.try_get::<bool, _>(i).map(serde_json::Value::Bool))
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
} else {
|
||||
let result = sqlx::query(sql)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: result.rows_affected(),
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_indexes(pool: &PgPool, schema: &str, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let rows: Vec<PgRow> = sqlx::query(
|
||||
"SELECT i.relname AS index_name, \
|
||||
array_agg(a.attname ORDER BY k.n) AS columns, \
|
||||
ix.indisunique AS is_unique, \
|
||||
ix.indisprimary AS is_primary \
|
||||
FROM pg_index ix \
|
||||
JOIN pg_class t ON t.oid = ix.indrelid \
|
||||
JOIN pg_class i ON i.oid = ix.indexrelid \
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace \
|
||||
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true \
|
||||
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum \
|
||||
WHERE n.nspname = $1 AND t.relname = $2 \
|
||||
GROUP BY i.relname, ix.indisunique, ix.indisprimary \
|
||||
ORDER BY i.relname",
|
||||
)
|
||||
.bind(schema)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| IndexInfo {
|
||||
name: row.get::<String, _>("index_name"),
|
||||
columns: row.get::<Vec<String>, _>("columns"),
|
||||
is_unique: row.get::<bool, _>("is_unique"),
|
||||
is_primary: row.get::<bool, _>("is_primary"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_foreign_keys(pool: &PgPool, schema: &str, table: &str) -> Result<Vec<ForeignKeyInfo>, String> {
|
||||
let rows: Vec<PgRow> = sqlx::query(
|
||||
"SELECT kcu.constraint_name, kcu.column_name, \
|
||||
ccu.table_name AS ref_table, ccu.column_name AS ref_column \
|
||||
FROM information_schema.key_column_usage kcu \
|
||||
JOIN information_schema.referential_constraints rc \
|
||||
ON kcu.constraint_name = rc.constraint_name \
|
||||
AND kcu.constraint_schema = rc.constraint_schema \
|
||||
JOIN information_schema.constraint_column_usage ccu \
|
||||
ON rc.unique_constraint_name = ccu.constraint_name \
|
||||
AND rc.unique_constraint_schema = ccu.constraint_schema \
|
||||
WHERE kcu.table_schema = $1 AND kcu.table_name = $2 \
|
||||
ORDER BY kcu.constraint_name",
|
||||
)
|
||||
.bind(schema)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| ForeignKeyInfo {
|
||||
name: row.get::<String, _>("constraint_name"),
|
||||
column: row.get::<String, _>("column_name"),
|
||||
ref_table: row.get::<String, _>("ref_table"),
|
||||
ref_column: row.get::<String, _>("ref_column"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_triggers(pool: &PgPool, schema: &str, table: &str) -> Result<Vec<TriggerInfo>, String> {
|
||||
let rows: Vec<PgRow> = sqlx::query(
|
||||
"SELECT trigger_name, event_manipulation, action_timing \
|
||||
FROM information_schema.triggers \
|
||||
WHERE trigger_schema = $1 AND event_object_table = $2 \
|
||||
ORDER BY trigger_name",
|
||||
)
|
||||
.bind(schema)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| TriggerInfo {
|
||||
name: row.get::<String, _>("trigger_name"),
|
||||
event: row.get::<String, _>("event_manipulation"),
|
||||
timing: row.get::<String, _>("action_timing"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
use redis::AsyncCommands;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RedisKeyInfo {
|
||||
pub key: String,
|
||||
pub key_type: String,
|
||||
pub ttl: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RedisValue {
|
||||
pub key: String,
|
||||
pub key_type: String,
|
||||
pub ttl: i64,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
pub async fn connect(url: &str) -> Result<redis::aio::MultiplexedConnection, String> {
|
||||
let client = redis::Client::open(url).map_err(|e| format!("Redis connection failed: {e}"))?;
|
||||
client
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|e| format!("Redis connection failed: {e}"))
|
||||
}
|
||||
|
||||
pub async fn list_databases(con: &mut redis::aio::MultiplexedConnection) -> Result<Vec<u32>, String> {
|
||||
let info: String = redis::cmd("INFO")
|
||||
.arg("keyspace")
|
||||
.query_async(con)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut dbs: Vec<u32> = Vec::new();
|
||||
for line in info.lines() {
|
||||
if line.starts_with("db") {
|
||||
if let Some(num) = line.strip_prefix("db").and_then(|s| s.split(':').next()) {
|
||||
if let Ok(n) = num.parse::<u32>() {
|
||||
dbs.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if dbs.is_empty() {
|
||||
dbs.push(0);
|
||||
}
|
||||
Ok(dbs)
|
||||
}
|
||||
|
||||
pub async fn select_db(con: &mut redis::aio::MultiplexedConnection, db: u32) -> Result<(), String> {
|
||||
redis::cmd("SELECT")
|
||||
.arg(db)
|
||||
.query_async(con)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn scan_keys(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
pattern: &str,
|
||||
count: usize,
|
||||
) -> Result<Vec<RedisKeyInfo>, String> {
|
||||
let mut cursor: u64 = 0;
|
||||
let mut all_keys: Vec<String> = Vec::new();
|
||||
loop {
|
||||
let (new_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
|
||||
.arg(cursor)
|
||||
.arg("MATCH")
|
||||
.arg(pattern)
|
||||
.arg("COUNT")
|
||||
.arg(100)
|
||||
.query_async(con)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
all_keys.extend(keys);
|
||||
cursor = new_cursor;
|
||||
if cursor == 0 || all_keys.len() >= count {
|
||||
break;
|
||||
}
|
||||
}
|
||||
all_keys.truncate(count);
|
||||
|
||||
let mut result = Vec::new();
|
||||
for key in &all_keys {
|
||||
let key_type: String = redis::cmd("TYPE")
|
||||
.arg(key.as_str())
|
||||
.query_async(con)
|
||||
.await
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
|
||||
let ttl: i64 = con.ttl(key.as_str()).await.unwrap_or(-1);
|
||||
|
||||
result.push(RedisKeyInfo {
|
||||
key: key.clone(),
|
||||
key_type,
|
||||
ttl,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_value(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
) -> Result<RedisValue, String> {
|
||||
let key_type: String = redis::cmd("TYPE")
|
||||
.arg(key)
|
||||
.query_async(con)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let ttl: i64 = con.ttl(key).await.unwrap_or(-1);
|
||||
|
||||
let value = match key_type.as_str() {
|
||||
"string" => {
|
||||
let v: String = con.get(key).await.map_err(|e| e.to_string())?;
|
||||
serde_json::Value::String(v)
|
||||
}
|
||||
"list" => {
|
||||
let v: Vec<String> = con.lrange(key, 0, -1).await.map_err(|e| e.to_string())?;
|
||||
serde_json::json!(v)
|
||||
}
|
||||
"set" => {
|
||||
let v: Vec<String> = con.smembers(key).await.map_err(|e| e.to_string())?;
|
||||
serde_json::json!(v)
|
||||
}
|
||||
"zset" => {
|
||||
let v: Vec<(String, f64)> = con
|
||||
.zrange_withscores(key, 0, -1)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::json!(v.iter().map(|(m, s)| serde_json::json!({"member": m, "score": s})).collect::<Vec<_>>())
|
||||
}
|
||||
"hash" => {
|
||||
let v: Vec<(String, String)> = con.hgetall(key).await.map_err(|e| e.to_string())?;
|
||||
let map: serde_json::Map<String, serde_json::Value> = v
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, serde_json::Value::String(v)))
|
||||
.collect();
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
_ => serde_json::Value::Null,
|
||||
};
|
||||
|
||||
Ok(RedisValue {
|
||||
key: key.to_string(),
|
||||
key_type,
|
||||
ttl,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_string(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
value: &str,
|
||||
ttl: Option<i64>,
|
||||
) -> Result<(), String> {
|
||||
con.set::<_, _, ()>(key, value)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if let Some(t) = ttl {
|
||||
if t > 0 {
|
||||
con.expire::<_, ()>(key, t)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_key(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
) -> Result<(), String> {
|
||||
con.del::<_, ()>(key).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn hash_set(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
field: &str,
|
||||
value: &str,
|
||||
) -> Result<(), String> {
|
||||
con.hset::<_, _, _, ()>(key, field, value)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn hash_del(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
field: &str,
|
||||
) -> Result<(), String> {
|
||||
con.hdel::<_, _, ()>(key, field)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn list_push(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
value: &str,
|
||||
) -> Result<(), String> {
|
||||
con.rpush::<_, _, ()>(key, value)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn list_remove(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
index: i64,
|
||||
) -> Result<(), String> {
|
||||
let placeholder = "__DELETED_PLACEHOLDER__";
|
||||
redis::cmd("LSET")
|
||||
.arg(key)
|
||||
.arg(index)
|
||||
.arg(placeholder)
|
||||
.query_async::<()>(con)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
con.lrem::<_, _, ()>(key, 1, placeholder)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn set_add(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
member: &str,
|
||||
) -> Result<(), String> {
|
||||
con.sadd::<_, _, ()>(key, member)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn set_remove(
|
||||
con: &mut redis::aio::MultiplexedConnection,
|
||||
key: &str,
|
||||
member: &str,
|
||||
) -> Result<(), String> {
|
||||
con.srem::<_, _, ()>(key, member)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions, SqliteRow};
|
||||
use sqlx::{Column, Executor, Row};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo};
|
||||
|
||||
pub async fn connect(url: &str) -> Result<SqlitePool, String> {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.acquire_timeout(Duration::from_secs(10))
|
||||
.idle_timeout(Duration::from_secs(300))
|
||||
.connect(url)
|
||||
.await
|
||||
.map_err(|e| format!("SQLite connection failed: {e}"))
|
||||
}
|
||||
|
||||
pub async fn list_databases(_pool: &SqlitePool) -> Result<Vec<DatabaseInfo>, String> {
|
||||
Ok(vec![DatabaseInfo { name: "main".to_string() }])
|
||||
}
|
||||
|
||||
pub async fn list_tables(pool: &SqlitePool, _schema: &str) -> Result<Vec<TableInfo>, String> {
|
||||
let rows: Vec<SqliteRow> = sqlx::query(
|
||||
"SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let t: String = row.get("type");
|
||||
TableInfo {
|
||||
name: row.get::<String, _>("name"),
|
||||
table_type: if t == "view" { "VIEW".to_string() } else { "BASE TABLE".to_string() },
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_columns(pool: &SqlitePool, _schema: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
|
||||
let rows: Vec<SqliteRow> = sqlx::query(&format!("PRAGMA table_info(\"{}\")", table))
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| ColumnInfo {
|
||||
name: row.get::<String, _>("name"),
|
||||
data_type: row.get::<String, _>("type"),
|
||||
is_nullable: row.get::<i32, _>("notnull") == 0,
|
||||
column_default: row.get::<Option<String>, _>("dflt_value"),
|
||||
is_primary_key: row.get::<i32, _>("pk") > 0,
|
||||
extra: None,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_indexes(pool: &SqlitePool, _schema: &str, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let idx_rows: Vec<SqliteRow> = sqlx::query(&format!("PRAGMA index_list(\"{}\")", table))
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut indexes = Vec::new();
|
||||
for idx_row in &idx_rows {
|
||||
let name: String = idx_row.get("name");
|
||||
let is_unique: bool = idx_row.get::<i32, _>("unique") != 0;
|
||||
|
||||
let col_rows: Vec<SqliteRow> = sqlx::query(&format!("PRAGMA index_info(\"{}\")", name))
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let columns: Vec<String> = col_rows.iter().map(|r| r.get::<String, _>("name")).collect();
|
||||
|
||||
indexes.push(IndexInfo {
|
||||
name,
|
||||
columns,
|
||||
is_unique,
|
||||
is_primary: false,
|
||||
});
|
||||
}
|
||||
Ok(indexes)
|
||||
}
|
||||
|
||||
pub async fn list_foreign_keys(pool: &SqlitePool, _schema: &str, table: &str) -> Result<Vec<ForeignKeyInfo>, String> {
|
||||
let rows: Vec<SqliteRow> = sqlx::query(&format!("PRAGMA foreign_key_list(\"{}\")", table))
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| ForeignKeyInfo {
|
||||
name: format!("fk_{}", row.get::<i32, _>("id")),
|
||||
column: row.get::<String, _>("from"),
|
||||
ref_table: row.get::<String, _>("table"),
|
||||
ref_column: row.get::<String, _>("to"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_triggers(pool: &SqlitePool, _schema: &str, table: &str) -> Result<Vec<TriggerInfo>, String> {
|
||||
let rows: Vec<SqliteRow> = sqlx::query(
|
||||
"SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ? ORDER BY name",
|
||||
)
|
||||
.bind(table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let sql_text: String = row.get::<Option<String>, _>("sql").unwrap_or_default();
|
||||
let upper = sql_text.to_uppercase();
|
||||
let timing = if upper.contains("BEFORE") { "BEFORE" } else if upper.contains("AFTER") { "AFTER" } else { "INSTEAD OF" };
|
||||
let event = if upper.contains("INSERT") { "INSERT" } else if upper.contains("UPDATE") { "UPDATE" } else { "DELETE" };
|
||||
TriggerInfo {
|
||||
name: row.get::<String, _>("name"),
|
||||
event: event.to_string(),
|
||||
timing: timing.to_string(),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn execute_query(pool: &SqlitePool, sql: &str) -> Result<QueryResult, String> {
|
||||
let start = Instant::now();
|
||||
let trimmed = sql.trim().to_uppercase();
|
||||
|
||||
if trimmed.starts_with("SELECT")
|
||||
|| trimmed.starts_with("PRAGMA")
|
||||
|| trimmed.starts_with("EXPLAIN")
|
||||
|| trimmed.starts_with("WITH")
|
||||
{
|
||||
let desc = pool.describe(sql).await.map_err(|e| e.to_string())?;
|
||||
let columns: Vec<String> = desc.columns().iter().map(|c| c.name().to_string()).collect();
|
||||
|
||||
let rows: Vec<SqliteRow> = sqlx::query(sql)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let result_rows: Vec<Vec<serde_json::Value>> = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
(0..row.len())
|
||||
.map(|i| {
|
||||
row.try_get::<String, _>(i)
|
||||
.map(serde_json::Value::String)
|
||||
.or_else(|_| row.try_get::<i64, _>(i).map(|v| serde_json::Value::Number(v.into())))
|
||||
.or_else(|_| row.try_get::<f64, _>(i).map(|v| {
|
||||
serde_json::Number::from_f64(v)
|
||||
.map(serde_json::Value::Number)
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}))
|
||||
.or_else(|_| row.try_get::<bool, _>(i).map(serde_json::Value::Bool))
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
} else {
|
||||
let result = sqlx::query(sql)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: result.rows_affected(),
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
use tiberius::{AuthMethod, Client, Config};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
|
||||
use std::time::Instant;
|
||||
|
||||
use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo};
|
||||
|
||||
pub type SqlServerClient = Client<Compat<TcpStream>>;
|
||||
|
||||
pub async fn connect(host: &str, port: u16, user: &str, pass: &str, database: Option<&str>) -> Result<SqlServerClient, String> {
|
||||
let mut config = Config::new();
|
||||
config.host(host);
|
||||
config.port(port);
|
||||
config.authentication(AuthMethod::sql_server(user, pass));
|
||||
if let Some(db) = database {
|
||||
config.database(db);
|
||||
}
|
||||
config.trust_cert();
|
||||
|
||||
let tcp = TcpStream::connect(config.get_addr())
|
||||
.await
|
||||
.map_err(|e| format!("SQL Server connection failed: {e}"))?;
|
||||
Client::connect(config, tcp.compat_write())
|
||||
.await
|
||||
.map_err(|e| format!("SQL Server connection failed: {e}"))
|
||||
}
|
||||
|
||||
fn row_to_json(row: &tiberius::Row) -> Vec<serde_json::Value> {
|
||||
(0..row.len()).map(|i| {
|
||||
if let Some(v) = row.try_get::<&str, _>(i).ok().flatten() {
|
||||
serde_json::Value::String(v.to_string())
|
||||
} else if let Some(v) = row.try_get::<i32, _>(i).ok().flatten() {
|
||||
serde_json::Value::Number(v.into())
|
||||
} else if let Some(v) = row.try_get::<i64, _>(i).ok().flatten() {
|
||||
serde_json::Value::Number(v.into())
|
||||
} else if let Some(v) = row.try_get::<f64, _>(i).ok().flatten() {
|
||||
serde_json::Number::from_f64(v).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null)
|
||||
} else if let Some(v) = row.try_get::<bool, _>(i).ok().flatten() {
|
||||
serde_json::Value::Bool(v)
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub async fn list_databases(client: &mut SqlServerClient) -> Result<Vec<DatabaseInfo>, String> {
|
||||
let stream = client.query("SELECT name FROM sys.databases ORDER BY name", &[])
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows.iter().map(|row| {
|
||||
DatabaseInfo { name: row.get::<&str, _>(0).unwrap_or("").to_string() }
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn list_schemas(client: &mut SqlServerClient) -> Result<Vec<String>, String> {
|
||||
let stream = client.query(
|
||||
"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA \
|
||||
WHERE SCHEMA_NAME NOT IN ('guest','INFORMATION_SCHEMA','sys') \
|
||||
ORDER BY SCHEMA_NAME",
|
||||
&[],
|
||||
).await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows.iter().map(|row| {
|
||||
row.get::<&str, _>(0).unwrap_or("").to_string()
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn list_tables(client: &mut SqlServerClient, schema: &str) -> Result<Vec<TableInfo>, String> {
|
||||
let sql = format!(
|
||||
"SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '{}' ORDER BY TABLE_NAME",
|
||||
schema.replace('\'', "''")
|
||||
);
|
||||
let stream = client.query(&*sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows.iter().map(|row| {
|
||||
TableInfo {
|
||||
name: row.get::<&str, _>(0).unwrap_or("").to_string(),
|
||||
table_type: row.get::<&str, _>(1).unwrap_or("BASE TABLE").to_string(),
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn get_columns(client: &mut SqlServerClient, schema: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
|
||||
let sql = format!(
|
||||
"SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, \
|
||||
CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK \
|
||||
FROM INFORMATION_SCHEMA.COLUMNS c \
|
||||
LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu \
|
||||
ON c.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND c.TABLE_NAME = kcu.TABLE_NAME AND c.COLUMN_NAME = kcu.COLUMN_NAME \
|
||||
AND kcu.CONSTRAINT_NAME IN (SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_TYPE = 'PRIMARY KEY' AND TABLE_SCHEMA = '{s}' AND TABLE_NAME = '{t}') \
|
||||
WHERE c.TABLE_SCHEMA = '{s}' AND c.TABLE_NAME = '{t}' \
|
||||
ORDER BY c.ORDINAL_POSITION",
|
||||
s = schema.replace('\'', "''"), t = table.replace('\'', "''")
|
||||
);
|
||||
let stream = client.query(&*sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows.iter().map(|row| {
|
||||
ColumnInfo {
|
||||
name: row.get::<&str, _>(0).unwrap_or("").to_string(),
|
||||
data_type: row.get::<&str, _>(1).unwrap_or("").to_string(),
|
||||
is_nullable: row.get::<&str, _>(2).unwrap_or("NO") == "YES",
|
||||
column_default: row.get::<&str, _>(3).map(|s| s.to_string()),
|
||||
is_primary_key: row.get::<i32, _>(4).unwrap_or(0) == 1,
|
||||
extra: None,
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn list_indexes(client: &mut SqlServerClient, schema: &str, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let sql = format!(
|
||||
"SELECT i.name, STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal) AS columns, \
|
||||
i.is_unique, i.is_primary_key \
|
||||
FROM sys.indexes i \
|
||||
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id \
|
||||
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id \
|
||||
WHERE i.object_id = OBJECT_ID('{s}.{t}') AND i.name IS NOT NULL \
|
||||
GROUP BY i.name, i.is_unique, i.is_primary_key \
|
||||
ORDER BY i.name",
|
||||
s = schema.replace('\'', "''"), t = table.replace('\'', "''")
|
||||
);
|
||||
let stream = client.query(&*sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows.iter().map(|row| {
|
||||
let cols_str = row.get::<&str, _>(1).unwrap_or("");
|
||||
IndexInfo {
|
||||
name: row.get::<&str, _>(0).unwrap_or("").to_string(),
|
||||
columns: cols_str.split(',').map(|s| s.to_string()).collect(),
|
||||
is_unique: row.get::<bool, _>(2).unwrap_or(false),
|
||||
is_primary: row.get::<bool, _>(3).unwrap_or(false),
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn list_foreign_keys(client: &mut SqlServerClient, schema: &str, table: &str) -> Result<Vec<ForeignKeyInfo>, String> {
|
||||
let sql = format!(
|
||||
"SELECT fk.name, c.name, rt.name, rc.name \
|
||||
FROM sys.foreign_keys fk \
|
||||
JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id \
|
||||
JOIN sys.columns c ON fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id \
|
||||
JOIN sys.tables rt ON fkc.referenced_object_id = rt.object_id \
|
||||
JOIN sys.columns rc ON fkc.referenced_object_id = rc.object_id AND fkc.referenced_column_id = rc.column_id \
|
||||
WHERE fk.parent_object_id = OBJECT_ID('{s}.{t}') \
|
||||
ORDER BY fk.name",
|
||||
s = schema.replace('\'', "''"), t = table.replace('\'', "''")
|
||||
);
|
||||
let stream = client.query(&*sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows.iter().map(|row| {
|
||||
ForeignKeyInfo {
|
||||
name: row.get::<&str, _>(0).unwrap_or("").to_string(),
|
||||
column: row.get::<&str, _>(1).unwrap_or("").to_string(),
|
||||
ref_table: row.get::<&str, _>(2).unwrap_or("").to_string(),
|
||||
ref_column: row.get::<&str, _>(3).unwrap_or("").to_string(),
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn list_triggers(client: &mut SqlServerClient, schema: &str, table: &str) -> Result<Vec<TriggerInfo>, String> {
|
||||
let sql = format!(
|
||||
"SELECT t.name, te.type_desc, CASE WHEN t.is_instead_of_trigger = 1 THEN 'INSTEAD OF' ELSE 'AFTER' END \
|
||||
FROM sys.triggers t \
|
||||
JOIN sys.trigger_events te ON t.object_id = te.object_id \
|
||||
WHERE t.parent_id = OBJECT_ID('{s}.{t}') \
|
||||
ORDER BY t.name",
|
||||
s = schema.replace('\'', "''"), t = table.replace('\'', "''")
|
||||
);
|
||||
let stream = client.query(&*sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows.iter().map(|row| {
|
||||
TriggerInfo {
|
||||
name: row.get::<&str, _>(0).unwrap_or("").to_string(),
|
||||
event: row.get::<&str, _>(1).unwrap_or("").to_string(),
|
||||
timing: row.get::<&str, _>(2).unwrap_or("AFTER").to_string(),
|
||||
}
|
||||
}).collect())
|
||||
}
|
||||
|
||||
pub async fn execute_query(client: &mut SqlServerClient, sql: &str) -> Result<QueryResult, String> {
|
||||
let start = Instant::now();
|
||||
let trimmed = sql.trim().to_uppercase();
|
||||
|
||||
if trimmed.starts_with("SELECT")
|
||||
|| trimmed.starts_with("EXEC")
|
||||
|| trimmed.starts_with("WITH")
|
||||
|| trimmed.starts_with("TABLE")
|
||||
{
|
||||
let mut stream = client.query(sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
let columns_meta = stream.columns().await.map_err(|e| e.to_string())?
|
||||
.map(|cols| cols.iter().map(|c| c.name().to_string()).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
let result_rows: Vec<Vec<serde_json::Value>> = rows.iter().map(|row| row_to_json(row)).collect();
|
||||
|
||||
Ok(QueryResult {
|
||||
columns: columns_meta,
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
} else {
|
||||
let result = client.execute(sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: result.rows_affected().iter().sum::<u64>(),
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
use std::collections::HashMap;
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub struct TunnelManager {
|
||||
tunnels: Mutex<HashMap<String, (Child, u16)>>,
|
||||
}
|
||||
|
||||
impl TunnelManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tunnels: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_tunnel(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
ssh_host: &str,
|
||||
ssh_port: u16,
|
||||
ssh_user: &str,
|
||||
ssh_key_path: &str,
|
||||
remote_host: &str,
|
||||
remote_port: u16,
|
||||
) -> Result<u16, String> {
|
||||
let local_port = portpicker::pick_unused_port().ok_or("No available port")?;
|
||||
|
||||
let mut args = vec![
|
||||
"-N".to_string(),
|
||||
"-o".to_string(), "StrictHostKeyChecking=no".to_string(),
|
||||
"-o".to_string(), "ServerAliveInterval=60".to_string(),
|
||||
"-L".to_string(), format!("{local_port}:{remote_host}:{remote_port}"),
|
||||
"-p".to_string(), ssh_port.to_string(),
|
||||
];
|
||||
|
||||
if !ssh_key_path.is_empty() {
|
||||
args.push("-i".to_string());
|
||||
args.push(ssh_key_path.to_string());
|
||||
}
|
||||
|
||||
args.push(format!("{ssh_user}@{ssh_host}"));
|
||||
|
||||
let child = Command::new("ssh")
|
||||
.args(&args)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start SSH tunnel: {e}"))?;
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
|
||||
|
||||
self.tunnels
|
||||
.lock()
|
||||
.await
|
||||
.insert(connection_id.to_string(), (child, local_port));
|
||||
|
||||
Ok(local_port)
|
||||
}
|
||||
|
||||
pub async fn stop_tunnel(&self, connection_id: &str) {
|
||||
if let Some((mut child, _)) = self.tunnels.lock().await.remove(connection_id) {
|
||||
let _ = child.kill().await;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
mod commands;
|
||||
mod db;
|
||||
mod models;
|
||||
|
||||
use commands::connection::AppState;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let state = Arc::new(AppState::new());
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.manage(state)
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
window.hide().unwrap();
|
||||
api.prevent_close();
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::connection::test_connection,
|
||||
commands::connection::connect_db,
|
||||
commands::connection::disconnect_db,
|
||||
commands::connection::save_connections,
|
||||
commands::connection::load_connections,
|
||||
commands::schema::list_databases,
|
||||
commands::schema::list_tables,
|
||||
commands::schema::list_schemas,
|
||||
commands::schema::get_columns,
|
||||
commands::schema::list_indexes,
|
||||
commands::schema::list_foreign_keys,
|
||||
commands::schema::list_triggers,
|
||||
commands::query::execute_query,
|
||||
commands::redis_cmd::redis_list_databases,
|
||||
commands::redis_cmd::redis_scan_keys,
|
||||
commands::redis_cmd::redis_get_value,
|
||||
commands::redis_cmd::redis_set_string,
|
||||
commands::redis_cmd::redis_delete_key,
|
||||
commands::redis_cmd::redis_hash_set,
|
||||
commands::redis_cmd::redis_hash_del,
|
||||
commands::redis_cmd::redis_list_push,
|
||||
commands::redis_cmd::redis_list_remove,
|
||||
commands::redis_cmd::redis_set_add,
|
||||
commands::redis_cmd::redis_set_remove,
|
||||
commands::mongo_cmd::mongo_list_databases,
|
||||
commands::mongo_cmd::mongo_list_collections,
|
||||
commands::mongo_cmd::mongo_find_documents,
|
||||
commands::mongo_cmd::mongo_insert_document,
|
||||
commands::mongo_cmd::mongo_update_document,
|
||||
commands::mongo_cmd::mongo_delete_document,
|
||||
commands::history::save_history,
|
||||
commands::history::load_history,
|
||||
commands::history::clear_history,
|
||||
commands::history::delete_history_entry,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
dbx_lib::run();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectionConfig {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub db_type: DatabaseType,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub database: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ssh_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub ssh_host: String,
|
||||
#[serde(default = "default_ssh_port")]
|
||||
pub ssh_port: u16,
|
||||
#[serde(default)]
|
||||
pub ssh_user: String,
|
||||
#[serde(default)]
|
||||
pub ssh_key_path: String,
|
||||
}
|
||||
|
||||
fn default_ssh_port() -> u16 { 22 }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DatabaseType {
|
||||
Mysql,
|
||||
Postgres,
|
||||
Sqlite,
|
||||
Redis,
|
||||
#[serde(rename = "duckdb")]
|
||||
DuckDb,
|
||||
#[serde(rename = "clickhouse")]
|
||||
ClickHouse,
|
||||
#[serde(rename = "sqlserver")]
|
||||
SqlServer,
|
||||
#[serde(rename = "mongodb")]
|
||||
MongoDb,
|
||||
}
|
||||
|
||||
impl ConnectionConfig {
|
||||
pub fn connection_url(&self) -> String {
|
||||
self.connection_url_with_host(&self.host, self.port)
|
||||
}
|
||||
|
||||
pub fn connection_url_with_host(&self, host: &str, port: u16) -> String {
|
||||
let db_part = self.database.as_deref().map(|d| format!("/{d}")).unwrap_or_default();
|
||||
|
||||
match self.db_type {
|
||||
DatabaseType::Sqlite | DatabaseType::DuckDb => {
|
||||
format!("{}?mode=rwc", self.host)
|
||||
}
|
||||
DatabaseType::Redis => {
|
||||
if self.password.is_empty() {
|
||||
format!("redis://{host}:{port}/")
|
||||
} else {
|
||||
format!("redis://:{}@{host}:{port}/", self.password)
|
||||
}
|
||||
}
|
||||
DatabaseType::Mysql => format!(
|
||||
"mysql://{}:{}@{host}:{port}{db_part}",
|
||||
self.username, self.password
|
||||
),
|
||||
DatabaseType::Postgres => format!(
|
||||
"postgres://{}:{}@{host}:{port}{db_part}",
|
||||
self.username, self.password
|
||||
),
|
||||
DatabaseType::ClickHouse => format!(
|
||||
"http://{host}:{port}{db_part}"
|
||||
),
|
||||
DatabaseType::SqlServer => format!(
|
||||
"server=tcp:{host},{port};user={};password={};database={}",
|
||||
self.username, self.password, self.database.as_deref().unwrap_or("master")
|
||||
),
|
||||
DatabaseType::MongoDb => {
|
||||
if self.username.is_empty() {
|
||||
format!("mongodb://{host}:{port}{db_part}")
|
||||
} else {
|
||||
format!("mongodb://{}:{}@{host}:{port}{db_part}", self.username, self.password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod connection;
|
||||
Reference in New Issue
Block a user