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:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user