5 files changed
+162
-2
No files matched your search
@@ -52,6 +52,7 @@ import { uuid } from "@/lib/common/utils";
|
||||
import { isMacOS, isWindows } from "@/lib/backend/platform";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { openQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile";
|
||||
import { rememberExternalSqlFileTarget, resolveExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
|
||||
import { externalSqlFileOpenErrorMessage, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen";
|
||||
import type { ConnectionConfig, ObjectSourceKind, QueryTab, SavedSqlFile } from "@/types/database";
|
||||
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
|
||||
@@ -818,6 +819,7 @@ async function saveExternalSqlPath(tab: QueryTab, options: { closeAfterSave?: bo
|
||||
if (!tab.externalSqlPath || !isTauriRuntime()) return false;
|
||||
try {
|
||||
await api.writeExternalSqlFile(tab.externalSqlPath, tab.sql);
|
||||
rememberExternalSqlFileTarget(tab.externalSqlPath, { connectionId: tab.connectionId, database: tab.database });
|
||||
queryStore.markTabClean(tab);
|
||||
toast(t("savedSql.saved"), 2000);
|
||||
if (options.closeAfterSave) queryStore.closeTab(tab.id, { force: true });
|
||||
@@ -1081,6 +1083,7 @@ async function saveActiveSqlAsLocalFile() {
|
||||
const path = await api.saveExternalSqlFile(defaultSavedSqlName(tab.title), tab.sql);
|
||||
if (!path) return;
|
||||
queryStore.linkExternalSqlPath(tab.id, path, sqlFileTitleFromPath(path));
|
||||
rememberExternalSqlFileTarget(path, { connectionId: tab.connectionId, database: tab.database });
|
||||
invalidateSaveSqlFolderSelection();
|
||||
showSaveSqlDialog.value = false;
|
||||
closePendingSavedTab();
|
||||
@@ -1090,6 +1093,18 @@ async function saveActiveSqlAsLocalFile() {
|
||||
}
|
||||
}
|
||||
|
||||
function applyExternalSqlFileTarget(tab: QueryTab, path: string) {
|
||||
const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), {
|
||||
connectionId: tab.connectionId,
|
||||
database: tab.database,
|
||||
});
|
||||
if (target.connectionId !== tab.connectionId) {
|
||||
queryStore.updateConnection(tab.id, target.connectionId, target.database);
|
||||
} else if (target.database !== tab.database) {
|
||||
queryStore.updateDatabase(tab.id, target.database);
|
||||
}
|
||||
}
|
||||
|
||||
async function openSqlFile() {
|
||||
const tab = activeTab.value;
|
||||
if (!tab) return;
|
||||
@@ -1105,6 +1120,7 @@ async function openSqlFile() {
|
||||
const content = await api.readExternalSqlFile(sqlPath);
|
||||
queryStore.updateSql(tab.id, content);
|
||||
queryStore.linkExternalSqlPath(tab.id, sqlPath, sqlFileTitleFromPath(sqlPath));
|
||||
applyExternalSqlFileTarget(tab, sqlPath);
|
||||
}
|
||||
} else {
|
||||
const input = document.createElement("input");
|
||||
@@ -1159,7 +1175,8 @@ async function openSqlFilePath(path: string) {
|
||||
const connectionId = connectionStore.activeConnectionId || activeTab.value?.connectionId || connectionStore.connections[0]?.id || "";
|
||||
const connection = connectionId ? connectionStore.getConfig(connectionId) : undefined;
|
||||
const database = activeTab.value?.database || (connection ? resolveDefaultDatabase(connection, []) : "");
|
||||
queryStore.openExternalSqlFile(connectionId, database, path, content);
|
||||
const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), { connectionId, database });
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, content);
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { resolveExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
|
||||
import { externalSqlFileOpenErrorMessage, formatSqlFileSize, isExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { SqlFileEntry } from "@/lib/backend/api";
|
||||
@@ -211,7 +212,8 @@ async function openFile(path: string) {
|
||||
const connectionId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id || "";
|
||||
const connection = connectionId ? connectionStore.getConfig(connectionId) : undefined;
|
||||
const database = connection ? resolveDefaultDatabase(connection, []) : "";
|
||||
queryStore.openExternalSqlFile(connectionId, database, path, content);
|
||||
const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), { connectionId, database });
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, content);
|
||||
} catch (e: any) {
|
||||
if (isExternalSqlFileTooLargeError(e)) {
|
||||
executeFile(path);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY, MAX_EXTERNAL_SQL_FILE_TARGETS, rememberExternalSqlFileTarget, resolveExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
|
||||
|
||||
describe("external SQL file targets", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("restores the saved data source across normalized file paths", () => {
|
||||
rememberExternalSqlFileTarget(" C:\\work\\report.sql ", { connectionId: "saved-connection", database: "analytics" });
|
||||
|
||||
expect(resolveExternalSqlFileTarget("C:/work/report.sql", () => true, { connectionId: "fallback", database: "default" })).toEqual({
|
||||
connectionId: "saved-connection",
|
||||
database: "analytics",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back when the saved connection no longer exists", () => {
|
||||
rememberExternalSqlFileTarget("/work/report.sql", { connectionId: "deleted-connection", database: "analytics" });
|
||||
|
||||
expect(resolveExternalSqlFileTarget("/work/report.sql", () => false, { connectionId: "fallback", database: "default" })).toEqual({
|
||||
connectionId: "fallback",
|
||||
database: "default",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores malformed persisted state", () => {
|
||||
localStorage.setItem(EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY, "not-json");
|
||||
|
||||
expect(resolveExternalSqlFileTarget("/work/report.sql", () => true, { connectionId: "fallback", database: "default" })).toEqual({
|
||||
connectionId: "fallback",
|
||||
database: "default",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps only the most recently saved targets", () => {
|
||||
for (let index = 0; index <= MAX_EXTERNAL_SQL_FILE_TARGETS; index += 1) {
|
||||
rememberExternalSqlFileTarget(`/work/report-${index}.sql`, { connectionId: `connection-${index}`, database: `database-${index}` });
|
||||
}
|
||||
|
||||
expect(resolveExternalSqlFileTarget("/work/report-0.sql", () => true, { connectionId: "fallback", database: "default" })).toEqual({
|
||||
connectionId: "fallback",
|
||||
database: "default",
|
||||
});
|
||||
expect(resolveExternalSqlFileTarget(`/work/report-${MAX_EXTERNAL_SQL_FILE_TARGETS}.sql`, () => true, { connectionId: "fallback", database: "default" })).toEqual({
|
||||
connectionId: `connection-${MAX_EXTERNAL_SQL_FILE_TARGETS}`,
|
||||
database: `database-${MAX_EXTERNAL_SQL_FILE_TARGETS}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen";
|
||||
|
||||
export const EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY = "dbx-external-sql-file-targets-v1";
|
||||
export const MAX_EXTERNAL_SQL_FILE_TARGETS = 200;
|
||||
|
||||
export interface ExternalSqlFileTarget {
|
||||
connectionId: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
interface StoredExternalSqlFileTarget extends ExternalSqlFileTarget {
|
||||
path: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
function loadExternalSqlFileTargets(): StoredExternalSqlFileTarget[] {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY) || "[]");
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((item): item is StoredExternalSqlFileTarget => typeof item?.path === "string" && typeof item?.connectionId === "string" && typeof item?.database === "string" && typeof item?.updatedAt === "number");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveExternalSqlFileTargets(targets: StoredExternalSqlFileTarget[]) {
|
||||
try {
|
||||
localStorage.setItem(EXTERNAL_SQL_FILE_TARGETS_STORAGE_KEY, JSON.stringify(targets));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function rememberExternalSqlFileTarget(path: string, target: ExternalSqlFileTarget) {
|
||||
const normalizedPath = normalizeExternalSqlPath(path);
|
||||
if (!normalizedPath) return;
|
||||
const remaining = loadExternalSqlFileTargets().filter((item) => item.path !== normalizedPath);
|
||||
if (!target.connectionId) {
|
||||
saveExternalSqlFileTargets(remaining);
|
||||
return;
|
||||
}
|
||||
saveExternalSqlFileTargets([{ path: normalizedPath, ...target, updatedAt: Date.now() }, ...remaining].slice(0, MAX_EXTERNAL_SQL_FILE_TARGETS));
|
||||
}
|
||||
|
||||
export function resolveExternalSqlFileTarget(path: string, connectionExists: (connectionId: string) => boolean, fallback: ExternalSqlFileTarget): ExternalSqlFileTarget {
|
||||
const normalizedPath = normalizeExternalSqlPath(path);
|
||||
const saved = loadExternalSqlFileTargets().find((item) => item.path === normalizedPath);
|
||||
if (!saved || !connectionExists(saved.connectionId)) return fallback;
|
||||
return { connectionId: saved.connectionId, database: saved.database };
|
||||
}
|
||||
Reference in new issue
Block a user