Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ node_modules
.superpowers/
.trellis/
.worktrees/
.trae/
openspec/
.mcp.json
AGENTS.md
Expand Down
109 changes: 109 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 93 additions & 7 deletions apps/desktop/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useProductionSafetyStore } from "@/stores/productionSafetyStore";
import { enforceRightSidebarPanelExclusivity, RIGHT_SIDEBAR_PANEL_IDS, transitionRightSidebarPanels, useSettingsStore, type RightSidebarPanelId, type RightSidebarPanelState } from "@/stores/settingsStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { usePromptTemplateStore } from "@/stores/promptTemplateStore";
import { useProjectStore } from "@/stores/projectStore";
import { useToast } from "@/composables/useToast";
import { useTheme } from "@/composables/useTheme";
import { useAppUpdater } from "@/composables/useAppUpdater";
Expand Down Expand Up @@ -60,7 +61,8 @@ 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, unassociatedExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
import { rememberExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
import { resolveProjectFileTarget, type ProjectLike } from "@/lib/sql/projectFileTarget";
import { externalSqlFileOpenErrorMessage, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen";
import type { ConnectionConfig, DatabaseType, ObjectSourceKind, QueryTab, TreeNode } from "@/types/database";
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
Expand Down Expand Up @@ -156,6 +158,7 @@ const settingsStore = useSettingsStore();
const savedSqlStore = useSavedSqlStore();
const promptTemplateStore = usePromptTemplateStore();
const recentConnectionIds = ref<readonly string[]>(parseRecentConnectionIds(safeLocalStorageGet(RECENT_CONNECTION_IDS_STORAGE_KEY)));
const projectStore = useProjectStore();
connectionStore.setBeforeConnectHandler(async (config) => {
await ensureJdbcxRuntimeDrivers(config, api);
const jdbcProductRuntimeBefore = JSON.stringify({
Expand Down Expand Up @@ -509,6 +512,7 @@ const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({
openSqlFilePath,
openDbFilePath,
openConnectionDeepLink,
openSqlProjectPaths,
});
const { showCloseActionPrompt, chooseQuit, chooseMinimize, cancelCloseActionPrompt, performCloseAction, setupCloseActionPromptListener, cleanupCloseActionPromptListener } = useCloseActionPrompt({ requestClose: requestAppClose });
useVisibilityChange();
Expand Down Expand Up @@ -1123,9 +1127,19 @@ function handleCloseActionPromptOpenChange(open: boolean) {
async function writeExternalSqlTab(tab: QueryTab, options: { closeAfterSave?: boolean; expectedContentHash?: string; expectedMissing?: boolean } = {}): Promise<"saved" | "retry" | "failed"> {
if (!tab.externalSqlPath || !isTauriRuntime()) return "failed";
try {
// Local History 保底:写回前把磁盘当前内容记入项目快照(仅项目内文件)。
if (tab.projectId) {
try {
await api.snapshotSqlFileBeforeSave(tab.projectId, tab.externalSqlPath);
} catch {
// 快照失败不阻断保存(快照仅为保底,非关键路径)
}
}
const result = await api.writeExternalSqlFile(tab.externalSqlPath, tab.sql, {
expectedContentHash: options.expectedContentHash,
expectedMissing: options.expectedMissing,
encoding: tab.fileEncoding,
lineEnding: tab.fileLineEnding,
});
if (result.kind !== "written") return "retry";
rememberExternalSqlFileTarget(tab.externalSqlPath, { connectionId: tab.connectionId, database: tab.database, catalog: tab.catalog });
Expand Down Expand Up @@ -1439,15 +1453,29 @@ async function saveActiveSqlAsLocalFile() {
if (tab) await saveExternalSqlTabAs(tab);
}

/** 统一的项目上下文解析选项(连接存在性 + 项目列表)。 */
function projectFileTargetOptions(projects: ProjectLike[]) {
return {
connectionExists: (connectionId: string) => !!connectionStore.getConfig(connectionId),
getConnection: (connectionId: string) => connectionStore.getConfig(connectionId),
projects,
activeConnectionId: connectionStore.activeConnectionId,
firstConnectionId: connectionStore.connections[0]?.id,
};
}

function applyExternalSqlFileTarget(tab: QueryTab, path: string) {
const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget());
const target = resolveProjectFileTarget(path, projectFileTargetOptions(projectStore.projects));
if (target.connectionId !== tab.connectionId) {
queryStore.updateConnection(tab.id, target.connectionId, target.database);
}
if (target.catalog !== tab.catalog || target.database !== tab.database) {
if (target.catalog !== undefined || tab.catalog !== undefined) queryStore.updateCatalog(tab.id, target.catalog, target.database);
else queryStore.updateDatabase(tab.id, target.database);
}
if (target.schema !== undefined && tab.schema !== target.schema) {
queryStore.updateSchema(tab.id, target.schema);
}
}

async function openSqlFile() {
Expand Down Expand Up @@ -1517,8 +1545,21 @@ async function openSqlFilePath(path: string) {
try {
await desktopOpenTabsRestorationBarrier?.settled;
const snapshot = await api.readExternalSqlFileSnapshot(path);
const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget());
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, target.catalog);
let projects: ProjectLike[] = [];
try {
await projectStore.ensureLoaded();
projects = projectStore.projects;
} catch {
projects = [];
}
const target = resolveProjectFileTarget(path, projectFileTargetOptions(projects));
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, {
catalog: target.catalog,
schema: target.schema,
projectId: target.projectId,
fileEncoding: snapshot.encoding,
fileLineEnding: snapshot.lineEnding,
});
} catch (e: any) {
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
}
Expand All @@ -1536,6 +1577,32 @@ async function openPendingSqlFiles() {
}
}

async function openSqlProjectPaths(paths: string[]) {
if (!isTauriRuntime() || paths.length === 0) return;
try {
const opened = await projectStore.openProjects(paths);
if (opened.length === 0) return;
openRightSidebarPanel("sqlFile");
if (opened.length === 1) {
toast(t("toolbar.sqlProjectOpened", { name: opened[0].name }));
} else {
toast(t("toolbar.sqlProjectsOpened", { name: opened[0].name, count: opened.length - 1 }));
}
} catch (e: any) {
toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000);
}
}

async function openPendingSqlProjects() {
if (!isTauriRuntime()) return;
try {
const paths = await api.pendingOpenSqlProjects();
await openSqlProjectPaths(paths);
} catch {
/* ignore startup project-open probing errors */
}
}

async function openDbFilePath(path: string) {
if (!isTauriRuntime()) return;
await connectionStore.initFromDisk();
Expand Down Expand Up @@ -2112,15 +2179,27 @@ function onAiOpenExplainPlan(sql: string) {
}

async function handleQuickOpenSelect(item: any) {
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();

// Handle SQL file types first — they don't require a database connection
if (item.type === "sql_file" && item.filePath) {
try {
const snapshot = await api.readExternalSqlFileSnapshot(item.filePath);
const target = resolveExternalSqlFileTarget(item.filePath, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget());
queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, target.catalog);
let projects: ProjectLike[] = [];
try {
await projectStore.ensureLoaded();
projects = projectStore.projects;
} catch {
projects = [];
}
const target = resolveProjectFileTarget(item.filePath, projectFileTargetOptions(projects));
queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, {
catalog: target.catalog,
schema: target.schema,
projectId: target.projectId,
fileEncoding: snapshot.encoding,
fileLineEnding: snapshot.lineEnding,
});
} catch (e: any) {
toast(
externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)),
Expand Down Expand Up @@ -2643,6 +2722,10 @@ function openDriverStoreFromEvent(event: Event) {
openDriverStorePage(((event as CustomEvent).detail as DriverStoreFocus | undefined) ?? null);
}

function showSqlFilePanelFromEvent() {
openRightSidebarPanel("sqlFile");
}

function runUpdateNotificationChecks() {
if (!updateNotificationsEnabled.value) return;
checkUpdates({ silent: true });
Expand Down Expand Up @@ -2681,6 +2764,7 @@ onMounted(async () => {
window.addEventListener("blur", handleTabSwitcherWindowBlur);
document.addEventListener("visibilitychange", handleTabSwitcherVisibilityChange);
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
window.addEventListener("dbx-show-sql-file-panel", showSqlFilePanelFromEvent);
window.addEventListener("dbx-mcp-status-changed", handleMcpStatusChanged);
if (isDesktop) {
document.addEventListener("contextmenu", handleContextMenu);
Expand Down Expand Up @@ -2737,6 +2821,7 @@ onMounted(async () => {
void openPendingSqlFiles();
void openPendingDbFiles();
void openPendingConnectionLinks();
void openPendingSqlProjects();
console.log(`[STARTUP] onMounted sync done: ${(performance.now() - mountStart).toFixed(0)}ms`);
});

Expand All @@ -2754,6 +2839,7 @@ onUnmounted(() => {
document.removeEventListener("visibilitychange", handleTabSwitcherVisibilityChange);
tabSwitcherKeyboard.reset();
window.removeEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
window.removeEventListener("dbx-show-sql-file-panel", showSqlFilePanelFromEvent);
window.removeEventListener("dbx-mcp-status-changed", handleMcpStatusChanged);
document.removeEventListener("contextmenu", handleContextMenu);
window.clearTimeout(sqlLibraryFlyAnimationTimer);
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/components/layout/AppDialogs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,13 @@ watch(
:prefill-schema="dialogs.dataComparePrefillSchema.value"
:prefill-table="dialogs.dataComparePrefillTable.value"
/>
<SqlFileExecutionDialog v-model:open="dialogs.showSqlFileDialog.value" :prefill-connection-id="dialogs.sqlFilePrefillConnectionId.value" :prefill-database="dialogs.sqlFilePrefillDatabase.value" :prefill-file-path="dialogs.sqlFilePrefillFilePath.value" />
<SqlFileExecutionDialog
v-model:open="dialogs.showSqlFileDialog.value"
:prefill-connection-id="dialogs.sqlFilePrefillConnectionId.value"
:prefill-database="dialogs.sqlFilePrefillDatabase.value"
:prefill-file-path="dialogs.sqlFilePrefillFilePath.value"
:prefill-file-paths="dialogs.sqlFilePrefillFilePaths.value"
/>
<SchemaDiagramDialog
v-if="dialogs.showDiagramDialog.value"
v-model:open="dialogs.showDiagramDialog.value"
Expand Down
Loading
Loading