🐛 fix(external-sql): 修复外部 SQL 文件保存不写回源文件

- 保存逻辑:外部 SQL 文件标签页携带 filePath,保存时写回原始磁盘文件
- 后端接口:新增 WriteSQLFile 能力,支持覆盖已有 SQL 文件并保留原文件权限
- 状态隔离:外部文件保存失败时不创建 savedQuery,避免写入 localStorage 副本
- 兼容行为:非文件标签页继续沿用原有 savedQuery 快速保存逻辑
- 文案优化:将数据库下入口改为“外部 SQL 目录”,减少与单文件打开入口的歧义
- 测试覆盖:补充前端保存分支、后端写文件边界和外部 SQL 目录文案测试
Refs #422
This commit is contained in:
Syngnat
2026-04-28 13:26:55 +08:00
parent a07eea7815
commit ef634075ab
11 changed files with 379 additions and 3 deletions

View File

@@ -96,6 +96,29 @@ func readSQLFileByPath(filePath string) connection.QueryResult {
return connection.QueryResult{Success: true, Data: string(content)}
}
func writeSQLFileByPath(filePath string, content string) connection.QueryResult {
target := strings.TrimSpace(filePath)
if target == "" {
return connection.QueryResult{Success: false, Message: "文件路径不能为空"}
}
if abs, err := filepath.Abs(target); err == nil {
target = abs
}
info, err := os.Stat(target)
if err != nil {
return connection.QueryResult{Success: false, Message: fmt.Sprintf("无法读取文件信息: %v", err)}
}
if info.IsDir() {
return connection.QueryResult{Success: false, Message: "所选路径不是 SQL 文件"}
}
if err := os.WriteFile(target, []byte(content), info.Mode().Perm()); err != nil {
return connection.QueryResult{Success: false, Message: fmt.Sprintf("无法写入 SQL 文件: %v", err)}
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"filePath": target}}
}
func buildSQLDirectoryEntries(directory string) ([]SQLDirectoryEntry, error) {
entries, err := os.ReadDir(directory)
if err != nil {
@@ -215,6 +238,10 @@ func (a *App) ReadSQLFile(filePath string) connection.QueryResult {
return readSQLFileByPath(filePath)
}
func (a *App) WriteSQLFile(filePath string, content string) connection.QueryResult {
return writeSQLFileByPath(filePath, content)
}
// ExecuteSQLFile 在后端流式读取并执行大 SQL 文件,通过事件推送进度。
// 前端通过 EventsOn("sqlfile:progress", ...) 监听进度。
func (a *App) ExecuteSQLFile(config connection.ConnectionConfig, dbName string, filePath string, jobID string) connection.QueryResult {

View File

@@ -41,6 +41,40 @@ func TestBuildSQLDirectoryEntriesKeepsOnlySQLFilesAndNestedFolders(t *testing.T)
}
}
func TestWriteSQLFileByPathOverwritesExistingSQLFile(t *testing.T) {
filePath := filepath.Join(t.TempDir(), "report.sql")
if err := os.WriteFile(filePath, []byte("select 1;"), 0o644); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
result := writeSQLFileByPath(filePath, "select 2;\n")
if !result.Success {
t.Fatalf("expected sql file write to succeed, got %#v", result)
}
content, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("ReadFile returned error: %v", err)
}
if string(content) != "select 2;\n" {
t.Fatalf("expected file content to be overwritten, got %q", string(content))
}
}
func TestWriteSQLFileByPathRejectsDirectories(t *testing.T) {
result := writeSQLFileByPath(t.TempDir(), "select 1;")
if result.Success {
t.Fatalf("expected directory write to fail, got %#v", result)
}
}
func TestWriteSQLFileByPathRejectsEmptyPath(t *testing.T) {
result := writeSQLFileByPath(" ", "select 1;")
if result.Success {
t.Fatalf("expected empty path write to fail, got %#v", result)
}
}
func TestReadSQLFileByPathReturnsLargeFileMetadata(t *testing.T) {
filePath := filepath.Join(t.TempDir(), "big.sql")
file, err := os.Create(filePath)