From 6b925d62601e498c905c0a23bb6033a0d388ea69 Mon Sep 17 00:00:00 2001 From: Aleksey Shakhmatov Date: Mon, 6 Apr 2026 13:12:52 +0300 Subject: [PATCH] style: apply rustfmt, fix clippy warnings, and minor code cleanup Reformat Rust code with rustfmt, suppress clippy::too_many_arguments for Tauri IPC commands, derive Default for AppSettings, fix unused variable pattern in TableDataView, and add unit tests for utils. --- src-tauri/src/commands/data.rs | 6 +- src-tauri/src/commands/management.rs | 5 +- src-tauri/src/commands/mod.rs | 2 +- src-tauri/src/commands/queries.rs | 30 ++-- src-tauri/src/commands/schema.rs | 17 +-- src-tauri/src/lib.rs | 10 +- src-tauri/src/mcp/mod.rs | 13 +- src-tauri/src/models/connection.rs | 4 +- src-tauri/src/models/settings.rs | 11 +- src-tauri/src/utils.rs | 139 +++++++++++++++++- src/components/table-viewer/TableDataView.tsx | 2 +- 11 files changed, 186 insertions(+), 53 deletions(-) diff --git a/src-tauri/src/commands/data.rs b/src-tauri/src/commands/data.rs index 9a6aca6..ce5ee37 100644 --- a/src-tauri/src/commands/data.rs +++ b/src-tauri/src/commands/data.rs @@ -10,6 +10,7 @@ use std::time::Instant; use tauri::State; #[tauri::command] +#[allow(clippy::too_many_arguments)] pub async fn get_table_data( state: State<'_, Arc>, connection_id: String, @@ -55,7 +56,7 @@ pub async fn get_table_data( // Always run table data queries in a read-only transaction to prevent // writable CTEs or other mutation via the raw filter parameter. - let mut tx = (&pool).begin().await.map_err(TuskError::Database)?; + let mut tx = pool.begin().await.map_err(TuskError::Database)?; sqlx::query("SET TRANSACTION READ ONLY") .execute(&mut *tx) .await @@ -129,6 +130,7 @@ pub async fn get_table_data( } #[tauri::command] +#[allow(clippy::too_many_arguments)] pub async fn update_row( state: State<'_, Arc>, connection_id: String, @@ -244,7 +246,7 @@ pub async fn delete_rows( let mut total_affected: u64 = 0; // Wrap all deletes in a transaction for atomicity - let mut tx = (&pool).begin().await.map_err(TuskError::Database)?; + let mut tx = pool.begin().await.map_err(TuskError::Database)?; if pk_columns.is_empty() { // Fallback: use ctids for row identification diff --git a/src-tauri/src/commands/management.rs b/src-tauri/src/commands/management.rs index b6fd918..8347e2c 100644 --- a/src-tauri/src/commands/management.rs +++ b/src-tauri/src/commands/management.rs @@ -336,10 +336,7 @@ pub async fn alter_role( options.push(format!("CONNECTION LIMIT {}", limit)); } if let Some(ref valid_until) = params.valid_until { - options.push(format!( - "VALID UNTIL '{}'", - valid_until.replace('\'', "''") - )); + options.push(format!("VALID UNTIL '{}'", valid_until.replace('\'', "''"))); } if !options.is_empty() { diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index b75c8f9..8bf6058 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -9,5 +9,5 @@ pub mod management; pub mod queries; pub mod saved_queries; pub mod schema; -pub mod snapshot; pub mod settings; +pub mod snapshot; diff --git a/src-tauri/src/commands/queries.rs b/src-tauri/src/commands/queries.rs index be5b8a3..b94526f 100644 --- a/src-tauri/src/commands/queries.rs +++ b/src-tauri/src/commands/queries.rs @@ -43,20 +43,16 @@ pub fn pg_value_to_json(row: &PgRow, index: usize) -> Value { } "DATE" => try_get!(chrono::NaiveDate), "TIME" => try_get!(chrono::NaiveTime), - "BYTEA" => { - match row.try_get::>, _>(index) { - Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))), - Ok(None) => return Value::Null, - Err(_) => {} - } - } - "OID" => { - match row.try_get::, _>(index) { - Ok(Some(v)) => return Value::Number(serde_json::Number::from(v)), - Ok(None) => return Value::Null, - Err(_) => {} - } - } + "BYTEA" => match row.try_get::>, _>(index) { + Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))), + Ok(None) => return Value::Null, + Err(_) => {} + }, + "OID" => match row.try_get::, _>(index) { + Ok(Some(v)) => return Value::Number(serde_json::Number::from(v)), + Ok(None) => return Value::Null, + Err(_) => {} + }, "VOID" => return Value::Null, // Array types (PG prefixes array type names with underscore) "_BOOL" => try_get!(Vec), @@ -124,7 +120,11 @@ pub async fn execute_query_core( let result_rows: Vec> = rows .iter() - .map(|row| (0..columns.len()).map(|i| pg_value_to_json(row, i)).collect()) + .map(|row| { + (0..columns.len()) + .map(|i| pg_value_to_json(row, i)) + .collect() + }) .collect(); let row_count = result_rows.len(); diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs index 4535729..e68e071 100644 --- a/src-tauri/src/commands/schema.rs +++ b/src-tauri/src/commands/schema.rs @@ -28,10 +28,7 @@ pub async fn list_databases( Ok(rows.iter().map(|r| r.get::(0)).collect()) } -pub async fn list_schemas_core( - state: &AppState, - connection_id: &str, -) -> TuskResult> { +pub async fn list_schemas_core(state: &AppState, connection_id: &str) -> TuskResult> { let pool = state.get_pool(connection_id).await?; let flavor = state.get_flavor(connection_id).await; @@ -593,11 +590,13 @@ pub async fn get_schema_erd( let mut tables_map: HashMap = HashMap::new(); for row in &col_rows { let table_name: String = row.get(0); - let entry = tables_map.entry(table_name.clone()).or_insert_with(|| ErdTable { - schema: schema.clone(), - name: table_name, - columns: Vec::new(), - }); + let entry = tables_map + .entry(table_name.clone()) + .or_insert_with(|| ErdTable { + schema: schema.clone(), + name: table_name, + columns: Vec::new(), + }); entry.columns.push(ErdColumn { name: row.get(1), data_type: row.get(2), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e0dc796..40655c9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -57,9 +57,13 @@ pub fn run() { let mcp_state = state.clone(); tauri::async_runtime::spawn(async move { *mcp_state.mcp_running.write().await = true; - if let Err(e) = - mcp::start_mcp_server(mcp_state.clone(), connections_path, mcp_port, shutdown_rx) - .await + if let Err(e) = mcp::start_mcp_server( + mcp_state.clone(), + connections_path, + mcp_port, + shutdown_rx, + ) + .await { log::error!("MCP server error: {}", e); } diff --git a/src-tauri/src/mcp/mod.rs b/src-tauri/src/mcp/mod.rs index d1d8301..33fc2c5 100644 --- a/src-tauri/src/mcp/mod.rs +++ b/src-tauri/src/mcp/mod.rs @@ -79,7 +79,9 @@ impl TuskMcpServer { } } - #[tool(description = "List all configured database connections with their active/read-only status")] + #[tool( + description = "List all configured database connections with their active/read-only status" + )] async fn list_connections(&self) -> Result { let configs: Vec = if self.connections_path.exists() { let data = std::fs::read_to_string(&self.connections_path).map_err(|e| { @@ -110,9 +112,8 @@ impl TuskMcpServer { }) .collect(); - let json = serde_json::to_string_pretty(&statuses).map_err(|e| { - McpError::internal_error(format!("Serialization error: {}", e), None) - })?; + let json = serde_json::to_string_pretty(&statuses) + .map_err(|e| McpError::internal_error(format!("Serialization error: {}", e), None))?; Ok(CallToolResult::success(vec![Content::text(json)])) } @@ -165,7 +166,9 @@ impl TuskMcpServer { } } - #[tool(description = "Describe table columns: name, type, nullable, primary key, default value")] + #[tool( + description = "Describe table columns: name, type, nullable, primary key, default value" + )] async fn describe_table( &self, Parameters(params): Parameters, diff --git a/src-tauri/src/models/connection.rs b/src-tauri/src/models/connection.rs index dca2f03..c8a678c 100644 --- a/src-tauri/src/models/connection.rs +++ b/src-tauri/src/models/connection.rs @@ -36,8 +36,8 @@ impl ConnectionConfig { fn urlencoded(s: &str) -> String { s.chars() .map(|c| match c { - ':' | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' - | '*' | '+' | ',' | ';' | '=' | '%' | ' ' => { + ':' | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' + | '+' | ',' | ';' | '=' | '%' | ' ' => { format!("%{:02X}", c as u8) } _ => c.to_string(), diff --git a/src-tauri/src/models/settings.rs b/src-tauri/src/models/settings.rs index 52e3a7c..f3211c8 100644 --- a/src-tauri/src/models/settings.rs +++ b/src-tauri/src/models/settings.rs @@ -1,20 +1,11 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct AppSettings { pub mcp: McpSettings, pub docker: DockerSettings, } -impl Default for AppSettings { - fn default() -> Self { - Self { - mcp: McpSettings::default(), - docker: DockerSettings::default(), - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpSettings { pub enabled: bool, diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index df85413..e1e0df8 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -34,7 +34,11 @@ pub fn topological_sort_tables( continue; } - if graph.entry(parent.clone()).or_default().insert(child.clone()) { + if graph + .entry(parent.clone()) + .or_default() + .insert(child.clone()) + { *in_degree.entry(child).or_insert(0) += 1; } } @@ -73,3 +77,136 @@ pub fn topological_sort_tables( result } + +#[cfg(test)] +mod tests { + use super::*; + + // ── escape_ident ────────────────────────────────────────── + + #[test] + fn escape_ident_simple_name() { + assert_eq!(escape_ident("users"), "\"users\""); + } + + #[test] + fn escape_ident_with_double_quotes() { + assert_eq!(escape_ident(r#"my"table"#), r#""my""table""#); + } + + #[test] + fn escape_ident_empty_string() { + assert_eq!(escape_ident(""), r#""""#); + } + + #[test] + fn escape_ident_with_spaces() { + assert_eq!(escape_ident("my table"), "\"my table\""); + } + + #[test] + fn escape_ident_with_semicolon() { + assert_eq!(escape_ident("users; DROP TABLE"), "\"users; DROP TABLE\""); + } + + #[test] + fn escape_ident_with_single_quotes() { + assert_eq!(escape_ident("it's"), "\"it's\""); + } + + #[test] + fn escape_ident_with_backslash() { + assert_eq!(escape_ident(r"back\slash"), r#""back\slash""#); + } + + #[test] + fn escape_ident_unicode() { + assert_eq!(escape_ident("таблица"), "\"таблица\""); + } + + #[test] + fn escape_ident_multiple_double_quotes() { + assert_eq!(escape_ident(r#"a""b"#), r#""a""""b""#); + } + + #[test] + fn escape_ident_reserved_word() { + assert_eq!(escape_ident("select"), "\"select\""); + } + + #[test] + fn escape_ident_null_byte() { + assert_eq!(escape_ident("a\0b"), "\"a\0b\""); + } + + #[test] + fn escape_ident_newline() { + assert_eq!(escape_ident("a\nb"), "\"a\nb\""); + } + + // ── topological_sort_tables ─────────────────────────────── + + #[test] + fn topo_sort_no_edges() { + let tables = vec![("public".into(), "b".into()), ("public".into(), "a".into())]; + let result = topological_sort_tables(&[], &tables); + assert_eq!(result.len(), 2); + assert!(result.contains(&("public".into(), "a".into()))); + assert!(result.contains(&("public".into(), "b".into()))); + } + + #[test] + fn topo_sort_simple_dependency() { + let edges = vec![( + "public".into(), + "orders".into(), + "public".into(), + "users".into(), + )]; + let tables = vec![ + ("public".into(), "orders".into()), + ("public".into(), "users".into()), + ]; + let result = topological_sort_tables(&edges, &tables); + let user_pos = result.iter().position(|t| t.1 == "users").unwrap(); + let order_pos = result.iter().position(|t| t.1 == "orders").unwrap(); + assert!(user_pos < order_pos, "users must come before orders"); + } + + #[test] + fn topo_sort_self_reference() { + let edges = vec![( + "public".into(), + "employees".into(), + "public".into(), + "employees".into(), + )]; + let tables = vec![("public".into(), "employees".into())]; + let result = topological_sort_tables(&edges, &tables); + assert_eq!(result.len(), 1); + } + + #[test] + fn topo_sort_cycle() { + let edges = vec![ + ("public".into(), "a".into(), "public".into(), "b".into()), + ("public".into(), "b".into(), "public".into(), "a".into()), + ]; + let tables = vec![("public".into(), "a".into()), ("public".into(), "b".into())]; + let result = topological_sort_tables(&edges, &tables); + assert_eq!(result.len(), 2); + } + + #[test] + fn topo_sort_edge_outside_target_set_ignored() { + let edges = vec![( + "public".into(), + "orders".into(), + "public".into(), + "external".into(), + )]; + let tables = vec![("public".into(), "orders".into())]; + let result = topological_sort_tables(&edges, &tables); + assert_eq!(result.len(), 1); + } +} diff --git a/src/components/table-viewer/TableDataView.tsx b/src/components/table-viewer/TableDataView.tsx index 76730b7..9adf61c 100644 --- a/src/components/table-viewer/TableDataView.tsx +++ b/src/components/table-viewer/TableDataView.tsx @@ -104,7 +104,7 @@ export function TableDataView({ connectionId, schema, table }: Props) { } setIsSaving(true); try { - for (const [_key, change] of pendingChanges) { + for (const [, change] of pendingChanges) { const row = data.rows[change.rowIndex]; const colName = data.columns[change.colIndex];