fix(sql): save Unicode file paths natively

This commit is contained in:
t8y2 2026-07-17 13:02:09 +08:00
parent 6f37029060
commit 1dc5c10a6c
6 changed files with 92 additions and 6 deletions

View File

@ -926,13 +926,8 @@ async function saveActiveSqlAsLocalFile() {
const tab = activeTab.value;
if (!tab || !canSaveSqlTab(tab) || !isTauriRuntime()) return;
try {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: defaultSavedSqlName(tab.title),
filters: [{ name: "SQL", extensions: ["sql"] }],
});
const path = await api.saveExternalSqlFile(defaultSavedSqlName(tab.title), tab.sql);
if (!path) return;
await api.writeExternalSqlFile(path, tab.sql);
queryStore.linkExternalSqlPath(tab.id, path, sqlFileTitleFromPath(path));
showSaveSqlDialog.value = false;
closePendingSavedTab();

View File

@ -296,6 +296,7 @@ export const pendingOpenDbFiles = forward("pendingOpenDbFiles");
export const pendingOpenConnectionLinks = forward("pendingOpenConnectionLinks");
export const readExternalSqlFile = forward("readExternalSqlFile");
export const writeExternalSqlFile = forward("writeExternalSqlFile");
export const saveExternalSqlFile = forward("saveExternalSqlFile");
export const listSqlFilesInFolder = forward("listSqlFilesInFolder");
// Nacos

View File

@ -1430,6 +1430,10 @@ export async function writeExternalSqlFile(_path: string, _content: string): Pro
throw new Error("Saving external SQL file paths is only available in the desktop app");
}
export async function saveExternalSqlFile(_defaultFileName: string, _content: string): Promise<string | null> {
throw new Error("Saving SQL files locally is only available in the desktop app");
}
export interface SqlFileEntry {
name: string;
path: string;

View File

@ -613,6 +613,10 @@ export async function writeExternalSqlFile(path: string, content: string): Promi
return invoke("write_external_sql_file", { path, content });
}
export async function saveExternalSqlFile(defaultFileName: string, content: string): Promise<string | null> {
return invoke("save_external_sql_file", { defaultFileName, content });
}
export interface SqlFileEntry {
name: string;
path: string;

View File

@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
use std::sync::Mutex;
use dbx_core::sql::decode_sql_file_bytes;
use tauri_plugin_dialog::DialogExt;
#[tauri::command]
pub fn pending_open_sql_files(state: tauri::State<'_, ExternalSqlOpenState>) -> Vec<String> {
@ -21,6 +22,27 @@ pub async fn write_external_sql_file(path: String, content: String) -> Result<()
write_external_sql_file_content_async(PathBuf::from(path), content).await
}
#[tauri::command]
pub async fn save_external_sql_file(
window: tauri::Window,
default_file_name: String,
content: String,
) -> Result<Option<String>, String> {
let (sender, receiver) = tokio::sync::oneshot::channel();
window.dialog().file().set_file_name(default_file_name).add_filter("SQL", &["sql"]).save_file(move |file_path| {
let _ = sender.send(file_path);
});
let path = receiver
.await
.map_err(|_| "SQL save dialog closed unexpectedly".to_string())?
.map(|file_path| file_path.into_path().map_err(|error| format!("Failed to resolve SQL file path: {error}")))
.transpose()?;
// Keep the native dialog result as a PathBuf until after the write so
// Windows Unicode paths do not cross an extra frontend IPC boundary.
save_external_sql_file_content_async(path, content).await
}
#[derive(Default)]
pub struct ExternalSqlOpenState {
pending: Mutex<Vec<String>>,
@ -99,6 +121,26 @@ async fn write_external_sql_file_content_async(path: PathBuf, content: String) -
tokio::fs::write(&path, content).await.map_err(|e| format!("Failed to save SQL file: {e}"))
}
async fn save_external_sql_file_content_async(
path: Option<PathBuf>,
content: String,
) -> Result<Option<String>, String> {
let Some(path) = path else {
return Ok(None);
};
write_external_sql_file_content_async(path.clone(), content).await?;
Ok(Some(path.to_string_lossy().into_owned()))
}
#[cfg(test)]
fn save_external_sql_file_content(path: Option<&Path>, content: &str) -> Result<Option<String>, String> {
let Some(path) = path else {
return Ok(None);
};
write_external_sql_file_content(path, content)?;
Ok(Some(path.to_string_lossy().into_owned()))
}
fn dedupe_paths(paths: Vec<String>) -> Vec<String> {
let mut unique = Vec::new();
for path in paths {
@ -181,6 +223,45 @@ mod tests {
assert_eq!(content, "select 2;");
}
#[test]
fn saves_external_sql_file_with_unicode_name() {
let path = std::env::temp_dir().join(format!("查询-{}.sql", uuid::Uuid::new_v4()));
let result = save_external_sql_file_content(Some(&path), "select 3;");
let content = std::fs::read_to_string(&path).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(result.unwrap(), Some(path.to_string_lossy().into_owned()));
assert_eq!(content, "select 3;");
}
#[test]
fn saves_external_sql_file_with_ascii_name() {
let path = std::env::temp_dir().join(format!("query-{}.sql", uuid::Uuid::new_v4()));
let result = save_external_sql_file_content(Some(&path), "select 4;");
let _ = std::fs::remove_file(&path);
assert_eq!(result.unwrap(), Some(path.to_string_lossy().into_owned()));
}
#[test]
fn cancelling_external_sql_file_save_does_not_write() {
let result = save_external_sql_file_content(None, "select 5;");
assert_eq!(result.unwrap(), None);
}
#[test]
fn reports_external_sql_file_write_failure() {
let directory = std::env::temp_dir().join(format!("dbx-missing-parent-{}", uuid::Uuid::new_v4()));
let path = directory.join("query.sql");
let result = save_external_sql_file_content(Some(&path), "select 6;");
assert!(result.unwrap_err().contains("Failed to save SQL file"));
}
#[test]
fn rejects_external_non_sql_file_save() {
let path = std::env::temp_dir().join(format!("dbx-test-{}.txt", uuid::Uuid::new_v4()));

View File

@ -1209,6 +1209,7 @@ pub fn run() {
commands::external_sql::pending_open_sql_files,
commands::external_sql::read_external_sql_file,
commands::external_sql::write_external_sql_file,
commands::external_sql::save_external_sql_file,
commands::list_sql_files::list_sql_files_in_folder,
commands::external_db::pending_open_db_files,
commands::keychain::read_keychain_password,