perf: full-stack performance optimization

Frontend:
- Debounce queryStore tab persistence (remove flush:sync deep watcher)
- Eliminate unnecessary object spreading in connectionStore error handling
- Use in-place delete for completion cache invalidation
- Deduplicate schema cache prefix API calls
- Split echarts, reka-ui, marked, @codemirror/commands into separate chunks
- Lazy-load xlsxExport and markdownTable in DataGrid
- Lazy-load marked parser in UpdateDialog

Backend:
- Replace tokio::sync::Mutex with RwLock for AppState connections/configs
  (60+ concurrent reads no longer block each other)
- Parallelize PostgreSQL DDL metadata queries with tokio::try_join!
This commit is contained in:
t8y2 2026-05-10 23:23:22 +08:00
parent dbfe69572e
commit bafb424b23
13 changed files with 126 additions and 99 deletions

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::RwLock;
use crate::db;
use crate::db::ssh_tunnel::TunnelManager;
@ -89,8 +89,8 @@ async fn connect_oracle_pool(
}
pub struct AppState {
pub connections: Mutex<HashMap<String, PoolKind>>,
pub configs: Mutex<HashMap<String, ConnectionConfig>>,
pub connections: RwLock<HashMap<String, PoolKind>>,
pub configs: RwLock<HashMap<String, ConnectionConfig>>,
pub running_queries: RunningQueries,
pub tunnels: TunnelManager,
pub storage: Storage,
@ -122,8 +122,8 @@ impl AppState {
pub fn new_with_plugin_dir(storage: Storage, plugin_dir: PathBuf) -> Self {
Self {
connections: Mutex::new(HashMap::new()),
configs: Mutex::new(HashMap::new()),
connections: RwLock::new(HashMap::new()),
configs: RwLock::new(HashMap::new()),
running_queries: RunningQueries::default(),
tunnels: TunnelManager::new(),
storage,
@ -154,7 +154,7 @@ impl AppState {
pub async fn get_or_create_pool(&self, connection_id: &str, database: Option<&str>) -> Result<String, String> {
let db_type = {
let configs = self.configs.lock().await;
let configs = self.configs.read().await;
configs.get(connection_id).map(|c| c.db_type.clone())
};
@ -175,7 +175,7 @@ impl AppState {
}
};
let conns = self.connections.lock().await;
let conns = self.connections.read().await;
if conns.contains_key(&pool_key) {
if let Some(PoolKind::Oracle(pool)) = conns.get(&pool_key) {
let client = pool.primary();
@ -184,7 +184,7 @@ impl AppState {
drop(conn);
drop(conns);
log::info!("[oracle] connection closed, reconnecting...");
self.connections.lock().await.remove(&pool_key);
self.connections.write().await.remove(&pool_key);
} else {
return Ok(pool_key);
}
@ -195,7 +195,7 @@ impl AppState {
drop(conns);
}
let configs = self.configs.lock().await;
let configs = self.configs.read().await;
let config = configs.get(connection_id).ok_or("Connection config not found")?.clone();
drop(configs);
@ -292,7 +292,7 @@ impl AppState {
DatabaseType::Jdbc => self.external_driver_pool("jdbc", &db_config).await?,
};
self.connections.lock().await.insert(pool_key.clone(), pool);
self.connections.write().await.insert(pool_key.clone(), pool);
Ok(pool_key)
}
@ -342,7 +342,7 @@ impl AppState {
pub async fn reconnect_pool(&self, connection_id: &str, database: Option<&str>) -> Result<String, String> {
let is_single_conn = {
let configs = self.configs.lock().await;
let configs = self.configs.read().await;
configs
.get(connection_id)
.map(|c| {
@ -360,7 +360,7 @@ impl AppState {
None => connection_id.to_string(),
}
};
self.connections.lock().await.remove(&pool_key);
self.connections.write().await.remove(&pool_key);
self.get_or_create_pool(connection_id, database).await
}
}
@ -506,7 +506,7 @@ mod tests {
config.host = db_path.to_string_lossy().to_string();
config.port = 0;
state.configs.lock().await.insert(config.id.clone(), config);
state.configs.write().await.insert(config.id.clone(), config);
let pool_key = state.get_or_create_pool("sqlite-conn", None).await.unwrap();
assert_eq!(pool_key, "sqlite-conn");

View File

@ -3,7 +3,7 @@ use crate::db::elasticsearch_driver;
use crate::db::mongo_driver::{self, MongoDocumentResult};
pub async fn mongo_list_databases_core(state: &AppState, connection_id: &str) -> Result<Vec<String>, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::list_databases(client).await,
PoolKind::Elasticsearch(_) => Ok(vec!["default".to_string()]),
@ -16,7 +16,7 @@ pub async fn mongo_list_collections_core(
connection_id: &str,
database: &str,
) -> Result<Vec<String>, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::list_collections(client, database).await,
PoolKind::Elasticsearch(client) => elasticsearch_driver::list_indices(client).await,
@ -32,7 +32,7 @@ pub async fn mongo_find_documents_core(
skip: u64,
limit: i64,
) -> Result<MongoDocumentResult, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::find_documents(client, database, collection, skip, limit).await,
PoolKind::Elasticsearch(client) => {
@ -51,7 +51,7 @@ pub async fn mongo_insert_document_core(
collection: &str,
doc_json: &str,
) -> Result<String, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::insert_document(client, database, collection, doc_json).await,
PoolKind::Elasticsearch(client) => {
@ -71,7 +71,7 @@ pub async fn mongo_update_document_core(
id: &str,
doc_json: &str,
) -> Result<u64, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::update_document(client, database, collection, id, doc_json).await,
PoolKind::Elasticsearch(client) => {
@ -90,7 +90,7 @@ pub async fn mongo_delete_document_core(
collection: &str,
id: &str,
) -> Result<u64, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::delete_document(client, database, collection, id).await,
PoolKind::Elasticsearch(client) => {

View File

@ -130,7 +130,7 @@ pub async fn do_execute(
schema: Option<&str>,
cancel_token: Option<CancellationToken>,
) -> Result<db::QueryResult, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(pool_key).ok_or("Connection not found")?;
match pool {
@ -417,7 +417,7 @@ pub async fn execute_statements_in_transaction(
// Clone the pool handle within the lock, then drop it before any async work.
let path = {
let conns = state.connections.lock().await;
let conns = state.connections.read().await;
conns.get(&pool_key).map(|p| match p {
PoolKind::Postgres(pg) => TxPath::Pg(pg.clone()),
PoolKind::Mysql(mp, _mode) => TxPath::Mysql(mp.clone(), false),

View File

@ -2,7 +2,7 @@ use crate::connection::{AppState, PoolKind};
use crate::db::redis_driver::{self, RedisScanResult, RedisValue};
pub async fn redis_list_databases_core(state: &AppState, connection_id: &str) -> Result<Vec<u32>, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(connection_id).ok_or("Connection not found")?;
match pool {
PoolKind::Redis(con) => {
@ -21,7 +21,7 @@ pub async fn redis_scan_keys_core(
pattern: &str,
count: usize,
) -> Result<RedisScanResult, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(connection_id).ok_or("Connection not found")?;
match pool {
PoolKind::Redis(con) => {
@ -43,7 +43,7 @@ pub async fn redis_get_value_in_db_core(
db: u32,
key_raw: &str,
) -> Result<RedisValue, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(connection_id).ok_or("Connection not found")?;
match pool {
PoolKind::Redis(con) => {
@ -74,7 +74,7 @@ pub async fn redis_set_string_in_db_core(
value: &str,
ttl: Option<i64>,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(connection_id).ok_or("Connection not found")?;
match pool {
PoolKind::Redis(con) => {
@ -97,7 +97,7 @@ pub async fn redis_delete_key_in_db_core(
db: u32,
key_raw: &str,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(connection_id).ok_or("Connection not found")?;
match pool {
PoolKind::Redis(con) => {
@ -128,7 +128,7 @@ pub async fn redis_hash_set_in_db_core(
field: &str,
value: &str,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -151,7 +151,7 @@ pub async fn redis_hash_del_in_db_core(
key_raw: &str,
field: &str,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -174,7 +174,7 @@ pub async fn redis_list_push_in_db_core(
key_raw: &str,
value: &str,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -202,7 +202,7 @@ pub async fn redis_list_remove_in_db_core(
key_raw: &str,
index: i64,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -225,7 +225,7 @@ pub async fn redis_set_add_in_db_core(
key_raw: &str,
member: &str,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -253,7 +253,7 @@ pub async fn redis_set_remove_in_db_core(
key_raw: &str,
member: &str,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -273,7 +273,7 @@ pub async fn redis_zadd_in_db_core(
member: &str,
score: f64,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -292,7 +292,7 @@ pub async fn redis_zrem_in_db_core(
key_raw: &str,
member: &str,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -311,7 +311,7 @@ pub async fn redis_set_ttl_in_db_core(
key_raw: &str,
ttl: i64,
) -> Result<(), String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -329,7 +329,7 @@ pub async fn redis_delete_keys_in_db_core(
db: u32,
key_raws: &[String],
) -> Result<u64, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;
@ -351,7 +351,7 @@ pub async fn redis_load_more_in_db_core(
cursor: u64,
count: usize,
) -> Result<redis_driver::RedisValue, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::Redis(con) => {
let mut con = con.lock().await;

View File

@ -131,7 +131,7 @@ pub fn extract_gaussdb(
pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Result<Vec<db::DatabaseInfo>, String> {
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if extract_external(&connections, connection_id).is_some() {
return Ok(vec![db::DatabaseInfo { name: "main".to_string() }]);
}
@ -170,7 +170,7 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul
}
}
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(connection_id).ok_or("Connection not found")?;
match pool {
@ -192,7 +192,7 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database:
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(PoolKind::ExternalDriver { config, session, .. }) = connections.get(&pool_key) {
let config = config.clone();
let session = session.clone();
@ -224,7 +224,7 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database:
}
}
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
@ -242,7 +242,7 @@ pub async fn list_tables_core(
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(ext_pool) = extract_external(&connections, &pool_key) {
drop(connections);
let cache = ext_pool.cache.clone();
@ -296,7 +296,7 @@ pub async fn list_tables_core(
}
}
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
@ -322,7 +322,7 @@ pub async fn list_objects_core(
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(ext_pool) = extract_external(&connections, &pool_key) {
drop(connections);
let cache = ext_pool.cache.clone();
@ -376,7 +376,7 @@ pub async fn get_columns_core(
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(ext_pool) = extract_external(&connections, &pool_key) {
drop(connections);
let cache = ext_pool.cache.clone();
@ -436,7 +436,7 @@ pub async fn get_columns_core(
}
}
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
@ -463,7 +463,7 @@ pub async fn list_indexes_core(
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -487,7 +487,7 @@ pub async fn list_indexes_core(
}
}
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
@ -514,7 +514,7 @@ pub async fn list_foreign_keys_core(
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -538,7 +538,7 @@ pub async fn list_foreign_keys_core(
}
}
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
@ -565,7 +565,7 @@ pub async fn list_triggers_core(
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -589,7 +589,7 @@ pub async fn list_triggers_core(
}
}
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
@ -616,7 +616,7 @@ pub async fn get_table_ddl_core(
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
{
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(con) = extract_duckdb(&connections, &pool_key) {
drop(connections);
let tbl = table.replace('\'', "''");
@ -666,7 +666,7 @@ pub async fn get_table_ddl_core(
}
}
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
match pool {
@ -697,9 +697,11 @@ pub async fn sqlite_ddl(pool: &sqlx::sqlite::SqlitePool, table: &str) -> Result<
}
pub async fn pg_ddl(pool: &sqlx::postgres::PgPool, schema: &str, table: &str) -> Result<String, String> {
let columns = db::postgres::get_columns(pool, schema, table).await?;
let indexes = db::postgres::list_indexes(pool, schema, table).await?;
let fkeys = db::postgres::list_foreign_keys(pool, schema, table).await?;
let (columns, indexes, fkeys) = tokio::try_join!(
db::postgres::get_columns(pool, schema, table),
db::postgres::list_indexes(pool, schema, table),
db::postgres::list_foreign_keys(pool, schema, table),
)?;
let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n");
let col_lines: Vec<String> = columns

View File

@ -469,7 +469,7 @@ pub fn count_sql(table: &str, schema: &str, db_type: &DatabaseType) -> String {
}
pub async fn execute_on_pool(state: &AppState, pool_key: &str, sql: &str) -> Result<db::QueryResult, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
let pool = connections.get(pool_key).ok_or("Connection not found")?;
match pool {
@ -585,7 +585,7 @@ pub async fn execute_on_pool(state: &AppState, pool_key: &str, sql: &str) -> Res
}
pub async fn get_db_type(state: &AppState, connection_id: &str) -> Result<DatabaseType, String> {
let configs = state.configs.lock().await;
let configs = state.configs.read().await;
configs
.get(connection_id)
.map(|c| c.db_type.clone())
@ -600,7 +600,7 @@ pub async fn get_columns_for_transfer(
schema: &str,
table: &str,
) -> Result<Vec<db::ColumnInfo>, String> {
let connections = state.connections.lock().await;
let connections = state.connections.read().await;
if let Some(PoolKind::DuckDb(con)) = connections.get(pool_key) {
let con = con.clone();

View File

@ -234,15 +234,15 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
DatabaseType::Jdbc => state.external_driver_pool("jdbc", &db_config).await?,
};
state.connections.lock().await.insert(id.clone(), pool);
state.configs.lock().await.insert(id.clone(), config);
state.connections.write().await.insert(id.clone(), pool);
state.configs.write().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 mut conns = state.connections.write().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 {
@ -266,7 +266,7 @@ pub async fn disconnect_db(state: State<'_, Arc<AppState>>, connection_id: Strin
}
}
drop(conns);
state.configs.lock().await.remove(&connection_id);
state.configs.write().await.remove(&connection_id);
state.tunnels.stop_tunnel(&connection_id).await;
Ok(())
}

View File

@ -35,20 +35,20 @@ pub async fn test_connection(
// Store config temporarily
let temp_id = format!("__test_{}", uuid::Uuid::new_v4());
app.configs.lock().await.insert(temp_id.clone(), config.clone());
app.configs.write().await.insert(temp_id.clone(), config.clone());
// Try to connect
let result = app.get_or_create_pool(&temp_id, config.database.as_deref()).await;
// Clean up any pool keys created for the temporary connection, including
// database-scoped keys like "__test_uuid:database".
let mut connections = app.connections.lock().await;
let mut connections = app.connections.write().await;
let temp_keys: Vec<String> = connections.keys().filter(|key| key.starts_with(&temp_id)).cloned().collect();
for key in temp_keys {
connections.remove(&key);
}
drop(connections);
app.configs.lock().await.remove(&temp_id);
app.configs.write().await.remove(&temp_id);
match result {
Ok(_) => Ok(Json("Connection successful".to_string())),
@ -64,7 +64,7 @@ pub async fn connect_db(
let app = &state.app;
let connection_id = config.id.clone();
app.configs.lock().await.insert(connection_id.clone(), config.clone());
app.configs.write().await.insert(connection_id.clone(), config.clone());
app.get_or_create_pool(&connection_id, None).await.map_err(AppError)?;
@ -76,7 +76,7 @@ pub async fn disconnect_db(
Json(body): Json<DisconnectRequest>,
) -> Result<Json<()>, AppError> {
let app = &state.app;
let mut connections = app.connections.lock().await;
let mut connections = app.connections.write().await;
let pool_prefix = format!("{}:", body.connection_id);
let keys_to_remove: Vec<String> =
@ -86,7 +86,7 @@ pub async fn disconnect_db(
}
drop(connections);
app.configs.lock().await.remove(&body.connection_id);
app.configs.write().await.remove(&body.connection_id);
app.tunnels.stop_tunnel(&body.connection_id).await;
Ok(Json(()))
@ -108,7 +108,7 @@ pub async fn load_connections(State(state): State<Arc<WebState>>) -> Result<Json
}
async fn cache_connection_configs(state: &WebState, configs: &[ConnectionConfig]) {
let mut runtime_configs = state.app.configs.lock().await;
let mut runtime_configs = state.app.configs.write().await;
for config in configs {
runtime_configs.insert(config.id.clone(), config.clone());
}
@ -186,7 +186,7 @@ mod tests {
.await;
assert!(result.is_ok());
let configs = state.app.configs.lock().await;
let configs = state.app.configs.read().await;
assert_eq!(configs.get("sqlite-conn").map(|c| c.host.as_str()), Some(config.host.as_str()));
let _ = std::fs::remove_dir_all(dir);
@ -203,7 +203,7 @@ mod tests {
let conn2_pool = dbx_core::db::sqlite::connect_path(&conn2_path.to_string_lossy()).await.unwrap();
{
let mut connections = state.app.connections.lock().await;
let mut connections = state.app.connections.write().await;
connections.insert("conn".to_string(), PoolKind::Sqlite(conn_pool));
connections.insert("conn2".to_string(), PoolKind::Sqlite(conn2_pool));
}
@ -212,7 +212,7 @@ mod tests {
disconnect_db(State(state.clone()), Json(DisconnectRequest { connection_id: "conn".to_string() })).await;
assert!(result.is_ok());
let connections = state.app.connections.lock().await;
let connections = state.app.connections.read().await;
assert!(!connections.contains_key("conn"));
assert!(connections.contains_key("conn2"));

View File

@ -74,8 +74,6 @@ import {
} from "@/lib/gridSelection";
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { buildDataGridRollbackStatements, buildDataGridSaveStatements, formatGridSqlLiteral } from "@/lib/dataGridSql";
import { formatMarkdownTable } from "@/lib/markdownTable";
import { buildXlsxWorkbook } from "@/lib/xlsxExport";
import {
matchesRowStatusFilter,
rowStatusFilterAfterAddingRow,
@ -2019,6 +2017,7 @@ async function exportMarkdown() {
try {
const cols = props.result.columns;
const visibleRows = displayItems.value.map((item) => item.data);
const { formatMarkdownTable } = await import("@/lib/markdownTable");
const md = formatMarkdownTable({ columns: cols, rows: visibleRows });
if (await saveFileContent(md, "export.md", "Markdown", "md")) {
toast(t("grid.exported"));
@ -2030,6 +2029,7 @@ async function exportMarkdown() {
async function exportXlsx() {
try {
const { buildXlsxWorkbook } = await import("@/lib/xlsxExport");
const workbook = buildXlsxWorkbook({
sheetName: props.tableMeta?.tableName || "Export",
columns: props.result.columns,

View File

@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed } from "vue";
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Marked } from "marked";
import { Loader2 } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@ -27,12 +26,21 @@ const emit = defineEmits<{
const { t } = useI18n();
const isDesktop = isTauriRuntime();
const marked = new Marked({ breaks: true, gfm: true });
const renderedNotes = ref("");
const renderedNotes = computed(() => {
if (!props.updateInfo?.release_notes) return "";
return marked.parse(props.updateInfo.release_notes) as string;
});
watch(
() => props.updateInfo?.release_notes,
async (notes) => {
if (!notes) {
renderedNotes.value = "";
return;
}
const { Marked } = await import("marked");
const marked = new Marked({ breaks: true, gfm: true });
renderedNotes.value = marked.parse(notes) as string;
},
{ immediate: true },
);
</script>
<template>

View File

@ -121,17 +121,12 @@ export const useConnectionStore = defineStore("connection", () => {
}
function setConnectionError(connectionId: string, message: string) {
connectionErrors.value = {
...connectionErrors.value,
[connectionId]: message,
};
connectionErrors.value[connectionId] = message;
}
function clearConnectionError(connectionId: string) {
if (!connectionErrors.value[connectionId]) return;
const next = { ...connectionErrors.value };
delete next[connectionId];
connectionErrors.value = next;
delete connectionErrors.value[connectionId];
}
function recordConnectionError(connectionId: string, error: unknown): string {
@ -323,8 +318,15 @@ export const useConnectionStore = defineStore("connection", () => {
loadedTreeNodeChildrenIds.value.delete(id);
}
}
api.deleteSchemaCachePrefix(`${prefix}:`).catch(() => undefined);
api.deleteSchemaCachePrefix(`${schemaCacheKey(prefix)}:`).catch(() => undefined);
const rawPrefix = `${prefix}:`;
const encodedPrefix = `${schemaCacheKey(prefix)}:`;
if (rawPrefix === encodedPrefix) {
api.deleteSchemaCachePrefix(rawPrefix).catch(() => undefined);
} else {
Promise.all([api.deleteSchemaCachePrefix(rawPrefix), api.deleteSchemaCachePrefix(encodedPrefix)]).catch(
() => undefined,
);
}
}
function schemaCachePrefixForNode(node: TreeNode): string | null {
@ -399,12 +401,12 @@ export const useConnectionStore = defineStore("connection", () => {
function invalidateCompletionCache(connectionId: string) {
const cachePrefix = `${connectionId}:`;
completionTablesCache.value = Object.fromEntries(
Object.entries(completionTablesCache.value).filter(([key]) => !key.startsWith(cachePrefix)),
);
completionColumnsCache.value = Object.fromEntries(
Object.entries(completionColumnsCache.value).filter(([key]) => !key.startsWith(cachePrefix)),
);
for (const key of Object.keys(completionTablesCache.value)) {
if (key.startsWith(cachePrefix)) delete completionTablesCache.value[key];
}
for (const key of Object.keys(completionColumnsCache.value)) {
if (key.startsWith(cachePrefix)) delete completionColumnsCache.value[key];
}
}
async function removeConnection(id: string) {

View File

@ -39,7 +39,18 @@ export const useQueryStore = defineStore("query", () => {
const activeTabId = ref<string | null>(restored.activeTabId);
const MAX_CACHED_RESULTS = 10;
watch([tabs, activeTabId], () => saveTabs(tabs.value, activeTabId.value), { deep: true, flush: "sync" });
let _persistTimer: ReturnType<typeof setTimeout> | null = null;
watch(
[tabs, activeTabId],
() => {
if (_persistTimer) clearTimeout(_persistTimer);
_persistTimer = setTimeout(() => {
saveTabs(tabs.value, activeTabId.value);
_persistTimer = null;
}, 300);
},
{ deep: true },
);
function findTabByTitle(connectionId: string, database: string, title: string) {
return tabs.value.find((t) => t.connectionId === connectionId && t.database === database && t.title === title);

View File

@ -24,9 +24,13 @@ export default defineConfig(async () => ({
"@codemirror/view",
"@codemirror/state",
"@codemirror/autocomplete",
"@codemirror/commands",
"@codemirror/theme-one-dark",
],
"sql-formatter": ["sql-formatter"],
echarts: ["echarts", "vue-echarts"],
ui: ["reka-ui"],
marked: ["marked"],
},
},
},