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;
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub async fn get_table_data(
state: State<'_, Arc<AppState>>,
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<AppState>>,
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

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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::<Option<Vec<u8>>, _>(index) {
Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))),
Ok(None) => return Value::Null,
Err(_) => {}
}
}
"OID" => {
match row.try_get::<Option<i32>, _>(index) {
Ok(Some(v)) => return Value::Number(serde_json::Number::from(v)),
Ok(None) => return Value::Null,
Err(_) => {}
}
}
"BYTEA" => match row.try_get::<Option<Vec<u8>>, _>(index) {
Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))),
Ok(None) => return Value::Null,
Err(_) => {}
},
"OID" => match row.try_get::<Option<i32>, _>(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<bool>),
@@ -124,7 +120,11 @@ pub async fn execute_query_core(
let result_rows: Vec<Vec<Value>> = 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();

View File

@@ -28,10 +28,7 @@ pub async fn list_databases(
Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect())
}
pub async fn list_schemas_core(
state: &AppState,
connection_id: &str,
) -> TuskResult<Vec<String>> {
pub async fn list_schemas_core(state: &AppState, connection_id: &str) -> TuskResult<Vec<String>> {
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<String, ErdTable> = 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),

View File

@@ -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);
}

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> {
let configs: Vec<ConnectionConfig> = 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<DescribeTableParam>,

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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);
}
}