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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 347 additions & 13 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"cross-env": "^10.1.0",
"mode-watcher": "^1.1.0",
"svelte": "^5.55.5",
"vite": "^8.0.12"
"vite": "^8.0.12",
"vitest": "^4.1.9"
},
"dependencies": {
"@dagrejs/dagre": "^3.0.0",
Expand Down
29 changes: 27 additions & 2 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ pub fn toggle_devtools(window: tauri::WebviewWindow) {
use crate::db::{
connect, connect_clickhouse, connect_d1, connect_duckdb, connect_libsql, connect_mssql, connect_mysql, connect_sqlite, disconnect,
delete_table_row, delete_table_rows, execute_ddl, execute_sql, execute_sql_multi, get_table_rows, insert_table_row,
list_schemas, list_tables, list_indexes, list_enums, list_triggers, list_sequences,
list_schemas, list_tables, list_indexes, list_enums, list_functions, list_triggers, list_sequences, ping_connection,
truncate_table, drop_table, get_table_column_structure, get_incoming_foreign_keys, get_table_ddl as db_get_table_ddl,
test_clickhouse_connection, test_connection, test_d1_connection, test_duckdb_connection, test_libsql_connection, test_mssql_connection, test_mysql_connection, test_sqlite_connection,
update_table_cell, ConnectionConfig, D1Config, DbState, EnumInfo, ExplainResult, IndexInfo, LibSqlConfig,
update_table_cell, ConnectionConfig, D1Config, DbState, EnumInfo, FunctionInfo, ExplainResult, IndexInfo, LibSqlConfig,
SqlResult, SqliteConfig, TableInfo, TableRows, TriggerInfo, SequenceInfo,
ColumnStructureRow, IncomingForeignKey, InsertRowResult, TunnelState,
explain_pg, explain_mysql, explain_sqlite,
Expand Down Expand Up @@ -360,6 +360,19 @@ pub async fn pg_list_sequences(
list_sequences(state, schema).await
}

#[tauri::command]
pub async fn pg_list_functions(
state: State<'_, DbState>,
schema: String,
) -> Result<Vec<FunctionInfo>, String> {
list_functions(state, schema).await
}

#[tauri::command]
pub async fn ping_db_connection(state: State<'_, DbState>) -> Result<(), String> {
ping_connection(state).await
}

#[tauri::command]
pub async fn pg_truncate_table(
state: State<'_, DbState>,
Expand Down Expand Up @@ -523,6 +536,18 @@ pub async fn pg_insert_table_row(
insert_table_row(state, schema, table, values).await
}

// ── Cancel running query ──────────────────────────────────────────────────────

#[tauri::command]
pub async fn cancel_query(state: State<'_, DbState>) -> Result<(), String> {
if let Ok(mut guard) = state.cancel_tx.lock() {
if let Some(tx) = guard.take() {
let _ = tx.send(());
}
}
Ok(())
}

// ── License ───────────────────────────────────────────────────────────────────

#[tauri::command]
Expand Down
7 changes: 6 additions & 1 deletion src-tauri/src/db/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use sqlx::{ConnectOptions, MySqlPool, PgPool, SqlitePool};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tauri::State;
use tokio::sync::oneshot;

use super::ssh_tunnel::{SshConfig, SshTunnel, TunnelState};

Expand Down Expand Up @@ -228,11 +229,15 @@ impl ActiveConnection {

pub struct DbState {
pub conn: Arc<Mutex<Option<ActiveConnection>>>,
pub cancel_tx: Arc<Mutex<Option<oneshot::Sender<()>>>>,
}

impl Default for DbState {
fn default() -> Self {
Self { conn: Arc::new(Mutex::new(None)) }
Self {
conn: Arc::new(Mutex::new(None)),
cancel_tx: Arc::new(Mutex::new(None)),
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions src-tauri/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ pub use explain::{explain_pg, explain_mysql, explain_sqlite, ExplainResult};
pub use ssh_tunnel::TunnelState;
pub use query::{
delete_table_row, delete_table_rows, execute_ddl, execute_sql, execute_sql_multi, execute_sql_on_conn,
get_column_stats, get_table_rows, insert_table_row, update_table_cell,
get_column_stats, get_table_rows, insert_table_row, update_table_cell, ping_connection,
ColumnStats, InsertRowResult, RowFilter, SqlResult, TableRows,
};
pub use schema::{
list_schemas, list_tables, list_schemas_on_conn, list_tables_on_conn,
list_indexes, list_enums, list_triggers, list_sequences,
list_indexes, list_enums, list_functions, list_triggers, list_sequences,
truncate_table, drop_table, get_table_column_structure, get_incoming_foreign_keys,
get_table_ddl, get_table_ddl_on_conn,
TableInfo, IndexInfo, EnumInfo, TriggerInfo, SequenceInfo, ColumnStructureRow, IncomingForeignKey,
TableInfo, IndexInfo, EnumInfo, FunctionInfo, TriggerInfo, SequenceInfo, ColumnStructureRow, IncomingForeignKey,
};
115 changes: 79 additions & 36 deletions src-tauri/src/db/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1632,20 +1632,30 @@ pub async fn execute_ddl(state: State<'_, DbState>, sql: String) -> Result<(), S
}

pub async fn execute_sql(state: State<'_, DbState>, sql: String) -> Result<SqlResult, String> {
let sql_str = sql.trim();
let sql_str = sql.trim().to_string();
if sql_str.is_empty() {
return Err("Query is empty".into());
}
match require_conn(&state)? {
ActiveConnection::Postgres(pool) => execute_sql_pg(&pool, sql_str).await,
ActiveConnection::Sqlite(pool) => super::sqlite::execute_sql(&pool, sql_str).await,
ActiveConnection::D1(cfg) => super::d1::query(&cfg, sql_str, vec![]).await,
ActiveConnection::LibSql(cfg) => super::libsql::query(&cfg, sql_str, vec![]).await,
ActiveConnection::Mysql(pool) => super::mysql::execute_sql(&pool, sql_str).await,
ActiveConnection::Clickhouse(cfg) => super::clickhouse::query(&cfg, sql_str).await,
ActiveConnection::Duckdb(h) => super::duckdb::execute_sql(&h, sql_str).await,
ActiveConnection::Mssql(h) => super::mssql::execute_sql(&h, sql_str).await,
}
let conn = require_conn(&state)?;
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
*state.cancel_tx.lock().map_err(|e| e.to_string())? = Some(cancel_tx);
let result = tokio::select! {
r = async move {
match conn {
ActiveConnection::Postgres(pool) => execute_sql_pg(&pool, &sql_str).await,
ActiveConnection::Sqlite(pool) => super::sqlite::execute_sql(&pool, &sql_str).await,
ActiveConnection::D1(cfg) => super::d1::query(&cfg, &sql_str, vec![]).await,
ActiveConnection::LibSql(cfg) => super::libsql::query(&cfg, &sql_str, vec![]).await,
ActiveConnection::Mysql(pool) => super::mysql::execute_sql(&pool, &sql_str).await,
ActiveConnection::Clickhouse(cfg) => super::clickhouse::query(&cfg, &sql_str).await,
ActiveConnection::Duckdb(h) => super::duckdb::execute_sql(&h, &sql_str).await,
ActiveConnection::Mssql(h) => super::mssql::execute_sql(&h, &sql_str).await,
}
} => r,
_ = async { let _ = cancel_rx.await; } => Err("Query cancelled".to_string()),
};
if let Ok(mut guard) = state.cancel_tx.lock() { *guard = None; }
result
}

/// Execute a SQL query against an arbitrary saved connection without changing
Expand Down Expand Up @@ -1920,34 +1930,44 @@ async fn execute_sql_multi_pg(pool: &sqlx::PgPool, sql: &str) -> Result<Vec<SqlR
}

pub async fn execute_sql_multi(state: State<'_, DbState>, sql: String) -> Result<Vec<SqlResult>, String> {
let sql_str = sql.trim();
let sql_str = sql.trim().to_string();
if sql_str.is_empty() {
return Err("Query is empty".into());
}
match require_conn(&state)? {
ActiveConnection::Postgres(pool) => execute_sql_multi_pg(&pool, sql_str).await,
ActiveConnection::Sqlite(pool) => {
super::sqlite::execute_sql(&pool, sql_str).await.map(|r| vec![r])
}
ActiveConnection::D1(cfg) => {
super::d1::query(&cfg, sql_str, vec![]).await.map(|r| vec![r])
}
ActiveConnection::LibSql(cfg) => {
super::libsql::query(&cfg, sql_str, vec![]).await.map(|r| vec![r])
}
ActiveConnection::Mysql(pool) => {
super::mysql::execute_sql(&pool, sql_str).await.map(|r| vec![r])
}
ActiveConnection::Clickhouse(cfg) => {
super::clickhouse::query(&cfg, sql_str).await.map(|r| vec![r])
}
ActiveConnection::Duckdb(h) => {
super::duckdb::execute_sql(&h, sql_str).await.map(|r| vec![r])
}
ActiveConnection::Mssql(h) => {
super::mssql::execute_sql(&h, sql_str).await.map(|r| vec![r])
}
}
let conn = require_conn(&state)?;
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
*state.cancel_tx.lock().map_err(|e| e.to_string())? = Some(cancel_tx);
let result = tokio::select! {
r = async move {
match conn {
ActiveConnection::Postgres(pool) => execute_sql_multi_pg(&pool, &sql_str).await,
ActiveConnection::Sqlite(pool) => {
super::sqlite::execute_sql(&pool, &sql_str).await.map(|r| vec![r])
}
ActiveConnection::D1(cfg) => {
super::d1::query(&cfg, &sql_str, vec![]).await.map(|r| vec![r])
}
ActiveConnection::LibSql(cfg) => {
super::libsql::query(&cfg, &sql_str, vec![]).await.map(|r| vec![r])
}
ActiveConnection::Mysql(pool) => {
super::mysql::execute_sql(&pool, &sql_str).await.map(|r| vec![r])
}
ActiveConnection::Clickhouse(cfg) => {
super::clickhouse::query(&cfg, &sql_str).await.map(|r| vec![r])
}
ActiveConnection::Duckdb(h) => {
super::duckdb::execute_sql(&h, &sql_str).await.map(|r| vec![r])
}
ActiveConnection::Mssql(h) => {
super::mssql::execute_sql(&h, &sql_str).await.map(|r| vec![r])
}
}
} => r,
_ = async { let _ = cancel_rx.await; } => Err("Query cancelled".to_string()),
};
if let Ok(mut guard) = state.cancel_tx.lock() { *guard = None; }
result
}

// ── D1 helpers (thin wrappers that build SQLite-compatible SQL) ───────────────
Expand Down Expand Up @@ -2591,3 +2611,26 @@ pub async fn get_column_stats(

Ok(ColumnStats { column, count, null_count, distinct_count, min, max, avg })
}

/// Lightweight connection health check — runs `SELECT 1` against the active
/// connection. HTTP-based engines (D1, LibSQL, Clickhouse) are stateless so we
/// return Ok immediately; a real request would validate their tokens but also
/// incur network cost every 30 s.
pub async fn ping_connection(state: State<'_, DbState>) -> Result<(), String> {
let conn = require_conn(&state)?;
match conn {
ActiveConnection::Postgres(pool) => {
sqlx::query("SELECT 1").execute(&pool).await.map(|_| ()).map_err(|e| e.to_string())
}
ActiveConnection::Sqlite(pool) => {
sqlx::query("SELECT 1").execute(&pool).await.map(|_| ()).map_err(|e| e.to_string())
}
ActiveConnection::Mysql(pool) => {
sqlx::query("SELECT 1").execute(&pool).await.map(|_| ()).map_err(|e| e.to_string())
}
// HTTP-based: stateless, no persistent TCP connection to validate
ActiveConnection::D1(_) | ActiveConnection::LibSql(_) | ActiveConnection::Clickhouse(_) => Ok(()),
ActiveConnection::Duckdb(h) => super::duckdb::execute_sql(&h, "SELECT 1").await.map(|_| ()),
ActiveConnection::Mssql(h) => super::mssql::execute_sql(&h, "SELECT 1").await.map(|_| ()),
}
}
Loading
Loading