feat: add column sort, SQL formatter, table stats, insert dialog, saved queries & sessions monitor

- Column sort by header click in table view (ASC/DESC/none cycle, server-side)
- SQL formatter with Format button and Shift+Alt+F keybinding (sql-formatter)
- Table size and row count display in schema tree via pg_class
- Insert row dialog with column type hints and auto-skip for identity columns
- Saved queries (bookmarks) with CRUD backend, sidebar panel, and save dialog
- Active sessions monitor (pg_stat_activity) with auto-refresh, cancel & terminate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 11:52:05 +03:00
parent ab72eeee80
commit 9d54167023
29 changed files with 1223 additions and 18 deletions

View File

@@ -1,5 +1,5 @@
use crate::error::{TuskError, TuskResult};
use crate::models::schema::{ColumnInfo, ConstraintInfo, IndexInfo, SchemaObject};
use crate::models::schema::{ColumnDetail, ColumnInfo, ConstraintInfo, IndexInfo, SchemaObject};
use crate::state::AppState;
use sqlx::Row;
use std::collections::HashMap;
@@ -61,9 +61,14 @@ pub async fn list_tables(
.ok_or(TuskError::NotConnected(connection_id))?;
let rows = sqlx::query(
"SELECT table_name FROM information_schema.tables \
WHERE table_schema = $1 AND table_type = 'BASE TABLE' \
ORDER BY table_name",
"SELECT t.table_name, \
c.reltuples::bigint as row_count, \
pg_total_relation_size(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name))::bigint as size_bytes \
FROM information_schema.tables t \
LEFT JOIN pg_class c ON c.relname = t.table_name \
AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = $1) \
WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE' \
ORDER BY t.table_name",
)
.bind(&schema)
.fetch_all(pool)
@@ -76,6 +81,8 @@ pub async fn list_tables(
name: r.get(0),
object_type: "table".to_string(),
schema: schema.clone(),
row_count: r.get::<Option<i64>, _>(1),
size_bytes: r.get::<Option<i64>, _>(2),
})
.collect())
}
@@ -107,6 +114,8 @@ pub async fn list_views(
name: r.get(0),
object_type: "view".to_string(),
schema: schema.clone(),
row_count: None,
size_bytes: None,
})
.collect())
}
@@ -138,6 +147,8 @@ pub async fn list_functions(
name: r.get(0),
object_type: "function".to_string(),
schema: schema.clone(),
row_count: None,
size_bytes: None,
})
.collect())
}
@@ -169,6 +180,8 @@ pub async fn list_indexes(
name: r.get(0),
object_type: "index".to_string(),
schema: schema.clone(),
row_count: None,
size_bytes: None,
})
.collect())
}
@@ -200,6 +213,8 @@ pub async fn list_sequences(
name: r.get(0),
object_type: "sequence".to_string(),
schema: schema.clone(),
row_count: None,
size_bytes: None,
})
.collect())
}
@@ -378,3 +393,42 @@ pub async fn get_completion_schema(
Ok(result)
}
#[tauri::command]
pub async fn get_column_details(
state: State<'_, AppState>,
connection_id: String,
schema: String,
table: String,
) -> TuskResult<Vec<ColumnDetail>> {
let pools = state.pools.read().await;
let pool = pools
.get(&connection_id)
.ok_or(TuskError::NotConnected(connection_id))?;
let rows = sqlx::query(
"SELECT c.column_name, c.data_type, \
c.is_nullable = 'YES' as is_nullable, \
c.column_default, \
c.is_identity = 'YES' as is_identity \
FROM information_schema.columns c \
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(TuskError::Database)?;
Ok(rows
.iter()
.map(|r| ColumnDetail {
column_name: r.get::<String, _>(0),
data_type: r.get::<String, _>(1),
is_nullable: r.get::<bool, _>(2),
column_default: r.get::<Option<String>, _>(3),
is_identity: r.get::<bool, _>(4),
})
.collect())
}