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.
This commit is contained in:
2026-04-06 13:12:52 +03:00
parent 1e002d801a
commit 6b925d6260
11 changed files with 186 additions and 53 deletions

View File

@@ -10,6 +10,7 @@ use std::time::Instant;
use tauri::State; use tauri::State;
#[tauri::command] #[tauri::command]
#[allow(clippy::too_many_arguments)]
pub async fn get_table_data( pub async fn get_table_data(
state: State<'_, Arc<AppState>>, state: State<'_, Arc<AppState>>,
connection_id: String, 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 // Always run table data queries in a read-only transaction to prevent
// writable CTEs or other mutation via the raw filter parameter. // 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") sqlx::query("SET TRANSACTION READ ONLY")
.execute(&mut *tx) .execute(&mut *tx)
.await .await
@@ -129,6 +130,7 @@ pub async fn get_table_data(
} }
#[tauri::command] #[tauri::command]
#[allow(clippy::too_many_arguments)]
pub async fn update_row( pub async fn update_row(
state: State<'_, Arc<AppState>>, state: State<'_, Arc<AppState>>,
connection_id: String, connection_id: String,
@@ -244,7 +246,7 @@ pub async fn delete_rows(
let mut total_affected: u64 = 0; let mut total_affected: u64 = 0;
// Wrap all deletes in a transaction for atomicity // 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() { if pk_columns.is_empty() {
// Fallback: use ctids for row identification // Fallback: use ctids for row identification

View File

@@ -336,10 +336,7 @@ pub async fn alter_role(
options.push(format!("CONNECTION LIMIT {}", limit)); options.push(format!("CONNECTION LIMIT {}", limit));
} }
if let Some(ref valid_until) = params.valid_until { if let Some(ref valid_until) = params.valid_until {
options.push(format!( options.push(format!("VALID UNTIL '{}'", valid_until.replace('\'', "''")));
"VALID UNTIL '{}'",
valid_until.replace('\'', "''")
));
} }
if !options.is_empty() { if !options.is_empty() {

View File

@@ -9,5 +9,5 @@ pub mod management;
pub mod queries; pub mod queries;
pub mod saved_queries; pub mod saved_queries;
pub mod schema; pub mod schema;
pub mod snapshot;
pub mod settings; pub mod settings;
pub mod snapshot;

View File

@@ -43,20 +43,16 @@ pub fn pg_value_to_json(row: &PgRow, index: usize) -> Value {
} }
"DATE" => try_get!(chrono::NaiveDate), "DATE" => try_get!(chrono::NaiveDate),
"TIME" => try_get!(chrono::NaiveTime), "TIME" => try_get!(chrono::NaiveTime),
"BYTEA" => { "BYTEA" => match row.try_get::<Option<Vec<u8>>, _>(index) {
match row.try_get::<Option<Vec<u8>>, _>(index) { Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))),
Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))), Ok(None) => return Value::Null,
Ok(None) => return Value::Null, Err(_) => {}
Err(_) => {} },
} "OID" => match row.try_get::<Option<i32>, _>(index) {
} Ok(Some(v)) => return Value::Number(serde_json::Number::from(v)),
"OID" => { Ok(None) => return Value::Null,
match row.try_get::<Option<i32>, _>(index) { Err(_) => {}
Ok(Some(v)) => return Value::Number(serde_json::Number::from(v)), },
Ok(None) => return Value::Null,
Err(_) => {}
}
}
"VOID" => return Value::Null, "VOID" => return Value::Null,
// Array types (PG prefixes array type names with underscore) // Array types (PG prefixes array type names with underscore)
"_BOOL" => try_get!(Vec<bool>), "_BOOL" => try_get!(Vec<bool>),
@@ -124,7 +120,11 @@ pub async fn execute_query_core(
let result_rows: Vec<Vec<Value>> = rows let result_rows: Vec<Vec<Value>> = rows
.iter() .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(); .collect();
let row_count = result_rows.len(); let row_count = result_rows.len();

View File

@@ -28,10 +28,7 @@ pub async fn list_databases(
Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect()) Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect())
} }
pub async fn list_schemas_core( pub async fn list_schemas_core(state: &AppState, connection_id: &str) -> TuskResult<Vec<String>> {
state: &AppState,
connection_id: &str,
) -> TuskResult<Vec<String>> {
let pool = state.get_pool(connection_id).await?; let pool = state.get_pool(connection_id).await?;
let flavor = state.get_flavor(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<String, ErdTable> = HashMap::new(); let mut tables_map: HashMap<String, ErdTable> = HashMap::new();
for row in &col_rows { for row in &col_rows {
let table_name: String = row.get(0); let table_name: String = row.get(0);
let entry = tables_map.entry(table_name.clone()).or_insert_with(|| ErdTable { let entry = tables_map
schema: schema.clone(), .entry(table_name.clone())
name: table_name, .or_insert_with(|| ErdTable {
columns: Vec::new(), schema: schema.clone(),
}); name: table_name,
columns: Vec::new(),
});
entry.columns.push(ErdColumn { entry.columns.push(ErdColumn {
name: row.get(1), name: row.get(1),
data_type: row.get(2), data_type: row.get(2),

View File

@@ -57,9 +57,13 @@ pub fn run() {
let mcp_state = state.clone(); let mcp_state = state.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
*mcp_state.mcp_running.write().await = true; *mcp_state.mcp_running.write().await = true;
if let Err(e) = if let Err(e) = mcp::start_mcp_server(
mcp::start_mcp_server(mcp_state.clone(), connections_path, mcp_port, shutdown_rx) mcp_state.clone(),
.await connections_path,
mcp_port,
shutdown_rx,
)
.await
{ {
log::error!("MCP server error: {}", e); log::error!("MCP server error: {}", e);
} }

View File

@@ -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<CallToolResult, McpError> { async fn list_connections(&self) -> Result<CallToolResult, McpError> {
let configs: Vec<ConnectionConfig> = if self.connections_path.exists() { let configs: Vec<ConnectionConfig> = if self.connections_path.exists() {
let data = std::fs::read_to_string(&self.connections_path).map_err(|e| { let data = std::fs::read_to_string(&self.connections_path).map_err(|e| {
@@ -110,9 +112,8 @@ impl TuskMcpServer {
}) })
.collect(); .collect();
let json = serde_json::to_string_pretty(&statuses).map_err(|e| { let json = serde_json::to_string_pretty(&statuses)
McpError::internal_error(format!("Serialization error: {}", e), None) .map_err(|e| McpError::internal_error(format!("Serialization error: {}", e), None))?;
})?;
Ok(CallToolResult::success(vec![Content::text(json)])) 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( async fn describe_table(
&self, &self,
Parameters(params): Parameters<DescribeTableParam>, Parameters(params): Parameters<DescribeTableParam>,

View File

@@ -36,8 +36,8 @@ impl ConnectionConfig {
fn urlencoded(s: &str) -> String { fn urlencoded(s: &str) -> String {
s.chars() s.chars()
.map(|c| match c { .map(|c| match c {
':' | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' ':' | '/' | '?' | '#' | '[' | ']' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' | '*'
| '*' | '+' | ',' | ';' | '=' | '%' | ' ' => { | '+' | ',' | ';' | '=' | '%' | ' ' => {
format!("%{:02X}", c as u8) format!("%{:02X}", c as u8)
} }
_ => c.to_string(), _ => c.to_string(),

View File

@@ -1,20 +1,11 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AppSettings { pub struct AppSettings {
pub mcp: McpSettings, pub mcp: McpSettings,
pub docker: DockerSettings, pub docker: DockerSettings,
} }
impl Default for AppSettings {
fn default() -> Self {
Self {
mcp: McpSettings::default(),
docker: DockerSettings::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpSettings { pub struct McpSettings {
pub enabled: bool, pub enabled: bool,

View File

@@ -34,7 +34,11 @@ pub fn topological_sort_tables(
continue; 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; *in_degree.entry(child).or_insert(0) += 1;
} }
} }
@@ -73,3 +77,136 @@ pub fn topological_sort_tables(
result 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);
}
}

View File

@@ -104,7 +104,7 @@ export function TableDataView({ connectionId, schema, table }: Props) {
} }
setIsSaving(true); setIsSaving(true);
try { try {
for (const [_key, change] of pendingChanges) { for (const [, change] of pendingChanges) {
const row = data.rows[change.rowIndex]; const row = data.rows[change.rowIndex];
const colName = data.columns[change.colIndex]; const colName = data.columns[change.colIndex];