diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 5cfa10fba..57939afac 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -80,6 +80,7 @@ import { copyNameForTreeNode, objectSourceKindForTreeNode, sidebarSelectionCopyA import { formatSqlInsert } from "@/lib/exportFormats"; import { joinExportedDdls } from "@/lib/ddlExport"; import { fetchTableDataForExport } from "@/lib/tableDataExport"; +import { canActivateExistingDataTableTab } from "@/lib/dataTabActivation"; import { buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, duckDbAttachedDatabaseNameFromPath, supportsCreateDatabaseCharset, uniqueDuckDbAttachedDatabaseName } from "@/lib/createDatabaseSql"; import { buildCreateSchemaSql, @@ -951,12 +952,7 @@ async function openData() { const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema); const tableType = node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : (node.tableType ?? "TABLE"); const isSameDataTableTab = (tab: (typeof queryStore.tabs)[number]) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database && (tab.schema || "") === (tableSchema || "") && (tab.tableMeta?.tableName || tab.title) === node.label; - const activateExistingSameTableTab = () => { - const existing = queryStore.tabs.find(isSameDataTableTab); - if (!existing) return false; - queryStore.activeTabId = existing.id; - return true; - }; + const existingSameTableTab = queryStore.tabs.find(isSameDataTableTab); const resetReusedDataTabState = (tab: (typeof queryStore.tabs)[number]) => { tab.title = node.label; tab.schema = tableSchema; @@ -979,12 +975,18 @@ async function openData() { tab.queryEditabilityReason = undefined; }; - if (activateExistingSameTableTab()) { + if (existingSameTableTab && canActivateExistingDataTableTab(existingSameTableTab)) { + queryStore.activeTabId = existingSameTableTab.id; logPhase("existing-tab-activated", { table: node.label }); return; } const tabId = (() => { + if (existingSameTableTab) { + queryStore.activeTabId = existingSameTableTab.id; + resetReusedDataTabState(existingSameTableTab); + return existingSameTableTab.id; + } if (settingsStore.editorSettings.reuseDataTab) { const existing = queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database); if (existing) { diff --git a/apps/desktop/src/lib/connectionHealth.ts b/apps/desktop/src/lib/connectionHealth.ts index 57527c7c7..74576c329 100644 --- a/apps/desktop/src/lib/connectionHealth.ts +++ b/apps/desktop/src/lib/connectionHealth.ts @@ -21,9 +21,11 @@ const CONNECTION_ERROR_PATTERNS = [ "agent stdout not available", "failed to write to agent stdin", "failed to flush agent stdin", + "input/output error", "关闭的连接", "连接已关闭", "i/o error", + "no route to host", ]; export function staleConnectionMessage(error: unknown): string { @@ -37,7 +39,7 @@ export function shouldMarkDisconnected(error: unknown): boolean { } function hasConnectionOsError(message: string): boolean { - const osErrorCodes = new Set(["10053", "10054", "10057", "10058", "10060", "10061"]); + const osErrorCodes = new Set(["65", "10053", "10054", "10057", "10058", "10060", "10061"]); const match = message.match(/os error\s+(\d+)/); return !!match && osErrorCodes.has(match[1]); } diff --git a/apps/desktop/src/lib/dataTabActivation.ts b/apps/desktop/src/lib/dataTabActivation.ts new file mode 100644 index 000000000..7ab657ea3 --- /dev/null +++ b/apps/desktop/src/lib/dataTabActivation.ts @@ -0,0 +1,11 @@ +import type { QueryResult, QueryTab } from "@/types/database"; + +function isErrorResult(result: QueryResult | undefined): boolean { + return result?.columns.length === 1 && result.columns[0] === "Error"; +} + +export function canActivateExistingDataTableTab(tab: QueryTab): boolean { + if (tab.isExecuting) return true; + if (isErrorResult(tab.result)) return false; + return !!tab.result || !!tab.results?.length; +} diff --git a/packages/app-tests/connectionStoreErrorState.test.ts b/packages/app-tests/connectionStoreErrorState.test.ts index 88b28ce12..e8be78188 100644 --- a/packages/app-tests/connectionStoreErrorState.test.ts +++ b/packages/app-tests/connectionStoreErrorState.test.ts @@ -161,6 +161,7 @@ test("known backend connection errors mark the connection disconnected", async ( "ORA-02396: exceeded maximum idle time, please connect again", "Agent stdin not available", "Failed to write to agent stdin", + "MySQL connection failed: Input/output error: No route to host (os error 65)", ]; try { diff --git a/packages/app-tests/dataTabActivation.test.ts b/packages/app-tests/dataTabActivation.test.ts new file mode 100644 index 000000000..c1b6a5fc8 --- /dev/null +++ b/packages/app-tests/dataTabActivation.test.ts @@ -0,0 +1,59 @@ +import { strict as assert } from "node:assert"; +import { test } from "vitest"; +import { canActivateExistingDataTableTab } from "../../apps/desktop/src/lib/dataTabActivation.ts"; +import type { QueryTab } from "../../apps/desktop/src/types/database.ts"; + +function dataTab(overrides: Partial = {}): QueryTab { + return { + id: "tab-1", + title: "users", + connectionId: "conn-1", + database: "app", + sql: "select * from users", + isExecuting: false, + isCancelling: false, + isExplaining: false, + mode: "data", + ...overrides, + }; +} + +test("activates an existing data table tab while it is still loading", () => { + assert.equal(canActivateExistingDataTableTab(dataTab({ isExecuting: true })), true); +}); + +test("activates an existing data table tab with a usable result", () => { + assert.equal( + canActivateExistingDataTableTab( + dataTab({ + result: { + columns: ["id"], + rows: [[1]], + affected_rows: 0, + execution_time_ms: 1, + }, + }), + ), + true, + ); +}); + +test("reloads restored data table tabs without a result", () => { + assert.equal(canActivateExistingDataTableTab(dataTab()), false); +}); + +test("reloads existing data table tabs showing an error result", () => { + assert.equal( + canActivateExistingDataTableTab( + dataTab({ + result: { + columns: ["Error"], + rows: [["MySQL connection failed: Input/output error: No route to host (os error 65)"]], + affected_rows: 0, + execution_time_ms: 0, + }, + }), + ), + false, + ); +});