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:
parent
dbfe69572e
commit
bafb424b23
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue