fix(sql-library): preserve folders during directory import

Closes #5877
This commit is contained in:
t8y2 committed 2026-08-11 17:03:10 +08:00
1 parent d71091f262
commit 8482ef25ec
4 files changed
+101 -18

No files matched your search

@@ -17,6 +17,7 @@ import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { focusSidebarRenameInput } from "@/lib/sidebar/sidebarRenameFocus";
import { savedSqlFolderBranchFileCount } from "@/lib/savedSql/savedSqlFolderCounts";
import { collectSavedSqlDirectoryImportFiles } from "@/lib/savedSql/savedSqlDirectoryImport";
import { ensureSqlExtension, stripSqlExtension } from "@/lib/savedSql/savedSqlFileName";
import { savedSqlExecutionTargetFromTab, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
import type { SavedSqlFile, SavedSqlFolder } from "@/types/database";
@@ -76,14 +77,6 @@ function sanitizeFileSystemSegment(name: string) {
return name.replace(/[<>:"/\\|?*\p{Cc}]/gu, "_").trim() || "untitled";
}
function relativeImportName(baseDir: string, filePath: string) {
const normalizedBase = baseDir.replace(/\\/g, "/").replace(/\/+$/, "");
const normalizedFile = filePath.replace(/\\/g, "/");
const relative = normalizedFile.startsWith(`${normalizedBase}/`) ? normalizedFile.slice(normalizedBase.length + 1) : normalizedFile.split("/").pop() || "import.sql";
const pretty = relative.replace(/\//g, " - ");
return ensureSqlExtension(pretty);
}
function uniqueImportedName(name: string, takenNames: Set<string>) {
const normalized = ensureSqlExtension(name);
if (!takenNames.has(normalized)) {
@@ -201,10 +194,27 @@ async function exportFolderContents(folder?: SavedSqlFolder) {
}
}
async function collectSqlFilesRecursively(dir: string): Promise<string[]> {
const collectPaths = (entries: Awaited<ReturnType<typeof api.listSqlFilesInFolder>>): string[] => entries.flatMap((entry) => (entry.is_dir ? collectPaths(entry.children) : [entry.path]));
async function collectSqlFilesRecursively(dir: string) {
return collectSavedSqlDirectoryImportFiles(await api.listSqlFilesInFolder(dir));
}
return collectPaths(await api.listSqlFilesInFolder(dir));
function importedFolderCacheKey(parentFolderId: string | undefined, name: string) {
return JSON.stringify([parentFolderId || "", name]);
}
async function resolveImportedFolder(connectionId: string, rootFolderId: string | undefined, folderNames: string[], folderCache: Map<string, SavedSqlFolder>) {
let parentFolderId = rootFolderId;
for (const name of folderNames) {
const cacheKey = importedFolderCacheKey(parentFolderId, name);
let folder = folderCache.get(cacheKey);
if (!folder) {
folder = savedSqlStore.listChildFolders(connectionId, parentFolderId).find((candidate) => candidate.name === name);
if (!folder) folder = await savedSqlStore.createFolder(connectionId, name, parentFolderId);
folderCache.set(cacheKey, folder);
}
parentFolderId = folder.id;
}
return parentFolderId;
}
async function importDirectoryIntoLibrary(targetFolder?: SavedSqlFolder) {
@@ -229,27 +239,36 @@ async function importDirectoryIntoLibrary(targetFolder?: SavedSqlFolder) {
});
if (!selected || Array.isArray(selected)) return;
const sqlPaths = await collectSqlFilesRecursively(selected);
if (sqlPaths.length === 0) {
const importFiles = await collectSqlFilesRecursively(selected);
if (importFiles.length === 0) {
toast(t("sqlLibrary.importNone"), 3000);
return;
}
const takenNames = new Set((targetFolder ? savedSqlStore.filesInFolder(targetFolder.id) : savedSqlStore.filesWithoutFolder()).map((file) => file.name));
const folderCache = new Map<string, SavedSqlFolder>();
const takenNamesByFolder = new Map<string, Set<string>>();
for (const path of sqlPaths) {
for (const file of importFiles) {
const folderId = await resolveImportedFolder(connectionId, targetFolder?.id, file.folderNames, folderCache);
const folderKey = folderId || "";
let takenNames = takenNamesByFolder.get(folderKey);
if (!takenNames) {
takenNames = new Set(savedSqlStore.listFiles(connectionId, folderId).map((savedFile) => savedFile.name));
takenNamesByFolder.set(folderKey, takenNames);
}
const path = file.path;
const content = await api.readExternalSqlFile(path);
const displayName = uniqueImportedName(relativeImportName(selected, path), takenNames);
const displayName = uniqueImportedName(file.name, takenNames);
await savedSqlStore.saveFile({
connectionId,
folderId: targetFolder?.id,
folderId,
name: displayName,
database: "",
sql: content,
});
}
toast(t("sqlLibrary.imported", { count: sqlPaths.length }), 2500);
toast(t("sqlLibrary.imported", { count: importFiles.length }), 2500);
} catch (e: any) {
toast(t("sqlLibrary.importFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
}
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { collectSavedSqlDirectoryImportFiles } from "@/lib/savedSql/savedSqlDirectoryImport";
describe("saved SQL directory import", () => {
it("preserves nested directory paths without flattening file names", () => {
const files = collectSavedSqlDirectoryImportFiles([
{ name: "database", path: "/scripts/database", is_dir: true, children: [{ name: "daily.sql", path: "/scripts/database/daily.sql", is_dir: false, children: [] }] },
{
name: "incidents",
path: "/scripts/incidents",
is_dir: true,
children: [
{
name: "postgres",
path: "/scripts/incidents/postgres",
is_dir: true,
children: [{ name: "repair.sql", path: "/scripts/incidents/postgres/repair.sql", is_dir: false, children: [] }],
},
],
},
{ name: "root.sql", path: "/scripts/root.sql", is_dir: false, children: [] },
]);
expect(files).toEqual([
{ name: "daily.sql", path: "/scripts/database/daily.sql", folderNames: ["database"] },
{ name: "repair.sql", path: "/scripts/incidents/postgres/repair.sql", folderNames: ["incidents", "postgres"] },
{ name: "root.sql", path: "/scripts/root.sql", folderNames: [] },
]);
});
});
@@ -0,0 +1,24 @@
export interface SavedSqlDirectoryImportEntry {
name: string;
path: string;
is_dir: boolean;
children: SavedSqlDirectoryImportEntry[];
}
export interface SavedSqlDirectoryImportFile {
name: string;
path: string;
folderNames: string[];
}
export function collectSavedSqlDirectoryImportFiles(entries: SavedSqlDirectoryImportEntry[], folderNames: string[] = []): SavedSqlDirectoryImportFile[] {
const files: SavedSqlDirectoryImportFile[] = [];
for (const entry of entries) {
if (entry.is_dir) {
files.push(...collectSavedSqlDirectoryImportFiles(entry.children, [...folderNames, entry.name]));
} else {
files.push({ name: entry.name, path: entry.path, folderNames });
}
}
return files;
}