fix(source): open pending tab while loading object source

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-09-14 19:59:21 +08:00
1 parent eba6853480
commit 760d2c6409
14 files changed
+824 -149

No files matched your search

@@ -0,0 +1,272 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
// issue #9035:源码 tab 必须先出现再加载。这批用例锁定 pending 占位 tab 的
// 行为契约:立即出现、原地填充、就地重试、加载中被关闭则丢弃、重复点击去重。
const mocks = vi.hoisted(() => {
const connectionStore = {
activeConnectionId: null as string | null,
ensureConnected: vi.fn(async () => {}),
getConfig: (connectionId: string) => (connectionId ? { id: connectionId, name: connectionId, db_type: "oracle" } : undefined),
};
return {
connectionStore,
ensureConnected: connectionStore.ensureConnected,
getObjectSource: vi.fn(),
buildEditableObjectSource: vi.fn(async (input: { source: string }) => input.source),
closeQuerySession: vi.fn(async () => {}),
closeClientConnectionSession: vi.fn(async () => {}),
saveOpenTabsState: vi.fn(async () => {}),
};
});
vi.mock("@/lib/backend/api", () => ({
getObjectSource: (...args: unknown[]) => mocks.getObjectSource(...args),
buildEditableObjectSource: (input: { source: string }) => mocks.buildEditableObjectSource(input),
closeQuerySession: mocks.closeQuerySession,
closeClientConnectionSession: mocks.closeClientConnectionSession,
saveOpenTabsState: mocks.saveOpenTabsState,
}));
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => mocks.connectionStore,
}));
const CONNECTION_ID = "ora-1";
const DATABASE = "ORCL";
const SCHEMA = "APP";
const pendingOptions = (request: { name: string; objectType: "VIEW" | "PROCEDURE" | "FUNCTION"; signature?: string }) => ({
connectionId: CONNECTION_ID,
database: DATABASE,
title: `Source - ${request.name}`,
schema: SCHEMA,
request,
});
const objectSource = (source: string, editable?: boolean) => ({
name: "ignored",
object_type: "VIEW" as const,
source,
...(editable === undefined ? {} : { editable }),
});
/** 让 loader 的 await 链跑完(ensureConnected → getObjectSource → 落地)。 */
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
describe("queryStore pending object source tab", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("localStorage", { getItem: vi.fn(() => null), setItem: vi.fn(), removeItem: vi.fn() });
setActivePinia(createPinia());
});
it("creates a visible loading tab before the source arrives", async () => {
let resolveSource: ((value: unknown) => void) | undefined;
mocks.getObjectSource.mockReturnValue(new Promise((resolve) => (resolveSource = resolve)));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const id = store.openObjectSourceTabPending(pendingOptions({ name: "v_orders", objectType: "VIEW" }));
// 请求还没回来,tab 已经在界面上:这正是 #9035 缺的那一步
expect(store.tabs).toHaveLength(1);
const tab = store.tabs[0]!;
expect(tab.id).toBe(id);
expect(tab.title).toBe("Source - v_orders");
expect(tab.sourceView).toBe(true);
expect(tab.sql).toBe("");
expect(tab.objectSource).toBeUndefined();
expect(tab.sourceLoad?.error).toBeUndefined();
expect(tab.sourceLoad?.request).toEqual({ name: "v_orders", objectType: "VIEW", signature: undefined });
expect(tab.sourceLoad?.startedAt).toBeTypeOf("number");
resolveSource?.(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 1 FROM dual"));
await settle();
});
it("fills the same tab in place once the source resolves", async () => {
mocks.getObjectSource.mockResolvedValue(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 1 FROM dual"));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const id = store.openObjectSourceTabPending(pendingOptions({ name: "v_orders", objectType: "VIEW" }));
await settle();
expect(store.tabs).toHaveLength(1);
const tab = store.tabs[0]!;
expect(tab.id).toBe(id);
expect(tab.sql).toBe("CREATE OR REPLACE VIEW app.v_orders AS SELECT 1 FROM dual");
expect(tab.sourceView).toBe(true);
expect(tab.sourceLoad).toBeUndefined();
expect(tab.objectSource).toMatchObject({ schema: SCHEMA, name: "v_orders", objectType: "VIEW" });
});
it("keeps the resolved (fallback) object type on the tab, not the requested one", async () => {
// 请求 PROCEDURE,后端返回空 → routine fallback 回落到 FUNCTION
mocks.getObjectSource.mockImplementation(async (_connectionId, _database, _schema, _name, objectType: string) => objectSource(objectType === "FUNCTION" ? "CREATE OR REPLACE FUNCTION app.foo RETURN NUMBER IS BEGIN RETURN 1; END;" : ""));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
store.openObjectSourceTabPending(pendingOptions({ name: "foo", objectType: "PROCEDURE" }));
await settle();
expect(store.tabs[0]?.objectSource).toMatchObject({ name: "foo", objectType: "FUNCTION" });
});
it("surfaces a failure inside the tab and retries in place", async () => {
mocks.getObjectSource.mockRejectedValueOnce(new Error("ORA-00942: table or view does not exist"));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const id = store.openObjectSourceTabPending(pendingOptions({ name: "v_orders", objectType: "VIEW" }));
await settle();
const failed = store.tabs[0]!;
expect(failed.id).toBe(id);
expect(failed.sourceLoad?.error).toContain("ORA-00942");
expect(failed.sql).toBe("");
// 失败不吞掉 tab:用户就在这个 tab 里重试,而不是只收到一个 toast
mocks.getObjectSource.mockResolvedValueOnce(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 2 FROM dual"));
store.retryObjectSourceTab(id);
// 重试立即回到加载态(错误清空 → tab 栏重新转圈)
expect(store.tabs[0]?.sourceLoad?.error).toBeUndefined();
await settle();
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.sql).toBe("CREATE OR REPLACE VIEW app.v_orders AS SELECT 2 FROM dual");
expect(store.tabs[0]?.sourceLoad).toBeUndefined();
});
it("drops the result when the tab was closed while loading", async () => {
let resolveSource: ((value: unknown) => void) | undefined;
mocks.getObjectSource.mockReturnValue(new Promise((resolve) => (resolveSource = resolve)));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const id = store.openObjectSourceTabPending(pendingOptions({ name: "v_orders", objectType: "VIEW" }));
store.closeTab(id);
expect(store.tabs).toHaveLength(0);
resolveSource?.(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 1 FROM dual"));
await settle();
// 关掉就是放弃:不重建 tab、不写入已关闭的容器
expect(store.tabs).toHaveLength(0);
});
it("reuses the pending tab and the in-flight request on a repeat click", async () => {
let resolveSource: ((value: unknown) => void) | undefined;
mocks.getObjectSource.mockReturnValue(new Promise((resolve) => (resolveSource = resolve)));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const options = pendingOptions({ name: "v_orders", objectType: "VIEW" });
const first = store.openObjectSourceTabPending(options);
const second = store.openObjectSourceTabPending(options);
expect(second).toBe(first);
expect(store.tabs).toHaveLength(1);
// 第二次点击发生在请求真正发出之前(loader 先 await ensureConnected),
// 所以这里要等微任务落定后再确认只发了一次请求
await settle();
expect(mocks.getObjectSource).toHaveBeenCalledTimes(1);
resolveSource?.(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 1 FROM dual"));
await settle();
expect(store.tabs).toHaveLength(1);
});
it("reuses an already loaded tab instantly and revalidates it in the background", async () => {
mocks.getObjectSource.mockResolvedValue(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 1 FROM dual"));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const options = pendingOptions({ name: "v_orders", objectType: "VIEW" });
const first = store.openObjectSourceTabPending(options);
await settle();
expect(mocks.getObjectSource).toHaveBeenCalledTimes(1);
expect(store.activeTabId).toBe(first);
// 重复打开:不出现第二个转圈 tab,立刻回到同一个 tab,也不进加载态
mocks.getObjectSource.mockResolvedValueOnce(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 2 FROM dual"));
expect(store.openObjectSourceTabPending(options)).toBe(first);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.sourceLoad).toBeUndefined();
// 复用路径不经过 ensureConnected,但树上动作的惯例仍是把该连接设为当前连接
expect(mocks.connectionStore.activeConnectionId).toBe(CONNECTION_ID);
await settle();
// 后台重新校验:重开拿到的是最新 DDL(改动前每次打开都会重新取源,
// 而源码 tab 没有别的刷新入口,所以复用不能省掉这次校验)
expect(mocks.getObjectSource).toHaveBeenCalledTimes(2);
expect(store.tabs[0]?.sql).toBe("CREATE OR REPLACE VIEW app.v_orders AS SELECT 2 FROM dual");
expect(store.tabs[0]?.sourceLoad).toBeUndefined();
});
it("never clobbers unsaved edits when revalidating in the background", async () => {
mocks.getObjectSource.mockResolvedValue(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 1 FROM dual"));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const options = pendingOptions({ name: "v_orders", objectType: "VIEW" });
const id = store.openObjectSourceTabPending(options);
await settle();
// 用户在源码 tab 里改了内容(未保存)
store.updateSql(id, "CREATE OR REPLACE VIEW app.v_orders AS SELECT 999 FROM dual");
mocks.getObjectSource.mockResolvedValueOnce(objectSource("CREATE OR REPLACE VIEW app.v_orders AS SELECT 2 FROM dual"));
store.openObjectSourceTabPending(options);
await settle();
expect(store.tabs[0]?.sql).toContain("SELECT 999");
expect(store.tabs[0]?.originalSql).not.toBe(store.tabs[0]?.sql);
});
it("hands the loaded source to an existing tab that already matches the resolved identity", async () => {
// 已有一个解析为 FUNCTION 的 tab;本次请求的是 PROCEDURE(树的类型可能是错的),
// 前置复用检查匹配不上,只能先开 pending,等 fallback 解析出 FUNCTION 后再交接。
mocks.getObjectSource.mockImplementation(async (_connectionId, _database, _schema, _name, objectType: string) => objectSource(objectType === "FUNCTION" ? "CREATE OR REPLACE FUNCTION app.foo RETURN NUMBER IS BEGIN RETURN 1; END;" : ""));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const existingId = store.openObjectSourceTab({
connectionId: CONNECTION_ID,
database: DATABASE,
title: "Source - foo",
schema: SCHEMA,
sql: "CREATE OR REPLACE FUNCTION app.foo RETURN NUMBER IS BEGIN RETURN 0; END;",
objectSource: { schema: SCHEMA, name: "foo", objectType: "FUNCTION" },
});
const pendingId = store.openObjectSourceTabPending(pendingOptions({ name: "foo", objectType: "PROCEDURE" }));
expect(store.tabs).toHaveLength(2);
expect(pendingId).not.toBe(existingId);
await settle();
// pending 占位交接给既有 tab,不留下同一个对象的第二个 tab
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.id).toBe(existingId);
expect(store.tabs[0]?.sql).toBe("CREATE OR REPLACE FUNCTION app.foo RETURN NUMBER IS BEGIN RETURN 1; END;");
expect(store.tabs[0]?.sourceLoad).toBeUndefined();
});
it("honors backend editability: read-only sources load as a plain source tab", async () => {
mocks.getObjectSource.mockResolvedValue(objectSource("CREATE SEQUENCE app.seq_users", false));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
store.openObjectSourceTabPending(pendingOptions({ name: "seq_users", objectType: "VIEW" }));
await settle();
const tab = store.tabs[0]!;
expect(tab.sql).toBe("CREATE SEQUENCE app.seq_users");
expect(tab.sourceView).toBe(true);
expect(tab.objectSource).toBeUndefined();
expect(tab.sourceLoad).toBeUndefined();
});
});
+251 -3
View File
@@ -5,7 +5,7 @@ import { uuid } from "@/lib/common/utils";
import { computed, markRaw, nextTick, onScopeDispose, reactive, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { sanitizeTabPageUiState } from "@/lib/tabs/tabUiState";
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, NacosConfigEditorViewport, ObjectBrowserFilter, ObjectBrowserViewport, QueryResult, QueryResultSourceColumnRef, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, NacosConfigEditorViewport, ObjectBrowserFilter, ObjectBrowserViewport, ObjectSource, ObjectSourceKind, QueryResult, QueryResultSourceColumnRef, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
import { orderPinnedFirst } from "@/lib/app/pinnedItems";
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
import { buildExplainSql, parseExplainResult, parseDamengExplainText, parseOracleExplainText, sqlServerExplainResult, type BuildExplainSqlResult, type ExplainPlanDatabaseType } from "@/lib/diagram/explainPlan";
@@ -92,6 +92,8 @@ import { ensureSqlExtension } from "@/lib/savedSql/savedSqlFileName";
import { resolveSavedSqlExecutionTarget, savedSqlExecutionTargetFromTab, type SavedSqlExecutionTarget, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
import { loadEditableObjectSourceForEditor } from "@/lib/table/objectSourceLoad";
import { buildEditableObjectSource } from "@/lib/table/objectSourceEditor";
import { disposeAllSqlServerActivityTraces, disposeSqlServerActivityTrace } from "@/lib/sqlserver/sqlServerActivityTraceRuntime";
import type { SavedSqlFile } from "@/types/database";
import i18n, { currentLocale } from "@/i18n";
@@ -176,6 +178,32 @@ interface OpenObjectSourceTabOptions {
objectSource: NonNullable<QueryTab["objectSource"]>;
}
/**
* 请求身份:`objectSource.objectType` 要等 routine fallback 跑完才知道,所以
* pending 去重只能按请求时的身份判定。请求身份必须含 objectType —— PACKAGE 与
* PACKAGE_BODY 同名同 schema,仅靠 name+schema 会错误合并。
*/
interface ObjectSourceRequestIdentity {
name: string;
objectType: ObjectSourceKind;
signature?: string;
}
interface OpenPendingObjectSourceTabOptions {
connectionId: string;
database: string;
title: string;
schema?: string;
catalog?: string;
request: ObjectSourceRequestIdentity;
}
/**
* 拿到源码也没有可编辑形态的对象类型:只填内容,不挂 objectSource。
* (`App.vue` 的 Ctrl+click 路径有一份少了 `JOB` 的旧副本,是既有不一致。)
*/
const OBJECT_SOURCE_READ_ONLY_TYPES: readonly ObjectSourceKind[] = ["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY", "JOB"];
interface UpdateExecutionTargetOptions {
persistSavedSqlTarget?: boolean;
}
@@ -2560,9 +2588,17 @@ export const useQueryStore = defineStore("query", () => {
});
}
function openObjectSourceTab(options: OpenObjectSourceTabOptions) {
const existing = tabs.value.find(
/**
* 对象源码 tab 的判重键。裁决点是**解析后**的 objectSource.objectType:
* routine fallback 会把 PROCEDURE↔FUNCTION、PACKAGE↔PACKAGE_BODY 归一,
* 因此「PROCEDURE foo」与「FUNCTION foo」应当共用一个 tab。pending 阶段
* 拿不到解析结果,只能做请求身份去重(见 openObjectSourceTabPending),
* 解析完成后再回到这里落定,避免改变既有语义。
*/
function findMatchingObjectSourceTab(options: OpenObjectSourceTabOptions, excludeTabId?: string): QueryTab | undefined {
return tabs.value.find(
(tab) =>
tab.id !== excludeTabId &&
tab.mode === "query" &&
tab.connectionId === options.connectionId &&
tab.database === options.database &&
@@ -2573,6 +2609,10 @@ export const useQueryStore = defineStore("query", () => {
(tab.objectSource.schema || "") === (options.objectSource.schema || "") &&
(tab.objectSource.signature || "") === (options.objectSource.signature || ""),
);
}
function openObjectSourceTab(options: OpenObjectSourceTabOptions) {
const existing = findMatchingObjectSourceTab(options);
if (existing) {
existing.sourceView = true;
switchTab(existing.id);
@@ -2588,6 +2628,212 @@ export const useQueryStore = defineStore("query", () => {
return id;
}
/**
* 正在后台重新校验源码的 tab。非响应式:仅用于避免同一个 tab 上叠起多次
* 取源请求(Oracle 的 GET_DDL 正是慢的那一步)。
*/
const sourceRevalidateInFlight = new Set<string>();
function findPendingObjectSourceTab(options: OpenPendingObjectSourceTabOptions): QueryTab | undefined {
return tabs.value.find(
(tab) =>
!!tab.sourceLoad &&
tab.connectionId === options.connectionId &&
tab.database === options.database &&
(tab.schema || "") === (options.schema || "") &&
(tab.catalog || "") === (options.catalog || "") &&
tab.sourceLoad.request.name === options.request.name &&
tab.sourceLoad.request.objectType === options.request.objectType &&
(tab.sourceLoad.request.signature || "") === (options.request.signature || ""),
);
}
/**
* 立即建出源码 tab 并挂上加载态,再异步取源码(issue #9035)。
* 此前是「等连接 + 等源码都完成才建 tab」,等待期间没有任何可见 UI,
* 用户看到的是点击后毫无反应。
*/
function openObjectSourceTabPending(options: OpenPendingObjectSourceTabOptions): string {
// 这个对象已经打开过:立刻切过去,再在后台重新校验源码。
// 两条弯路都要避开 —— 再建一个 pending tab 会让界面上多出一个转圈 tab,
// 随后又被交接逻辑关掉;而只切过去不校验,会让重开看到的是旧 DDL
// (改动前每次打开都会重新取源,源码 tab 没有其它刷新入口)。
const loaded = findMatchingObjectSourceTab({
connectionId: options.connectionId,
database: options.database,
title: options.title,
schema: options.schema,
catalog: options.catalog,
sql: "",
objectSource: { schema: options.schema, name: options.request.name, objectType: options.request.objectType, signature: options.request.signature },
});
if (loaded) {
loaded.sourceView = true;
switchTab(loaded.id);
// 这条路径不经过 ensureConnected,但树上的动作会把该连接设为当前连接
useConnectionStore().activeConnectionId = options.connectionId;
void revalidateObjectSourceTab(loaded.id);
return loaded.id;
}
const existing = findPendingObjectSourceTab(options);
if (existing) {
switchTab(existing.id);
// 再次点击同一对象 = 再试一次,不新开 tab、不重复占用一个 tab 位
if (existing.sourceLoad?.error) retryObjectSourceTab(existing.id);
return existing.id;
}
const id = createTab(options.connectionId, options.database, options.title, "query", options.schema, "", options.catalog, { forceNew: true, sourceView: true });
const tab = tabs.value.find((candidate) => candidate.id === id);
if (tab) tab.sourceLoad = { startedAt: Date.now(), request: { ...options.request } };
void loadObjectSourceIntoTab(id);
return id;
}
/**
* 后台重新校验一个已加载源码 tab 的 DDL:不占用加载态、不打断编辑,
* 失败就保持原内容(用户并没有在等这次请求)。
*/
async function revalidateObjectSourceTab(id: string) {
if (sourceRevalidateInFlight.has(id)) return;
const tab = tabs.value.find((candidate) => candidate.id === id);
const objectSource = tab?.objectSource;
if (!tab || !objectSource) return;
const { connectionId } = tab;
const { database } = tab;
const schema = objectSource.schema || tab.schema || database;
sourceRevalidateInFlight.add(id);
try {
const connectionStore = useConnectionStore();
await connectionStore.ensureConnected(connectionId);
const databaseType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(connectionId));
if (!databaseType) return;
const {
raw,
editableSource,
objectType: resolvedType,
} = await loadEditableObjectSourceForEditor(api.getObjectSource, buildEditableObjectSource, {
connectionId,
database,
schema,
name: objectSource.name,
objectType: objectSource.objectType,
databaseType,
signature: objectSource.signature,
});
// 期间 tab 可能被关闭、被复用或已被编辑:身份没变且用户没改过内容时才回填
const current = tabs.value.find((candidate) => candidate.id === id);
if (current?.objectSource !== objectSource || resolvedType !== objectSource.objectType) return;
if (raw.editable === false || OBJECT_SOURCE_READ_ONLY_TYPES.includes(resolvedType)) return;
if (isTabDirty(current)) return;
updateSql(id, editableSource);
markTabClean(current);
} catch {
// 已有的源码依然可用,静默保留
} finally {
sourceRevalidateInFlight.delete(id);
}
}
function retryObjectSourceTab(id: string) {
const tab = tabs.value.find((candidate) => candidate.id === id);
if (!tab?.sourceLoad) return;
tab.sourceLoad.error = undefined;
tab.sourceLoad.startedAt = Date.now();
void loadObjectSourceIntoTab(id);
}
function clearObjectSourceLoad(tab: QueryTab) {
tab.sourceLoad = undefined;
}
async function loadObjectSourceIntoTab(id: string) {
const tab = tabs.value.find((candidate) => candidate.id === id);
if (!tab?.sourceLoad || !tab.connectionId) return;
const { connectionId } = tab;
const { database } = tab;
const schema = tab.schema || database;
const { request } = tab.sourceLoad;
try {
const connectionStore = useConnectionStore();
await connectionStore.ensureConnected(connectionId);
connectionStore.activeConnectionId = connectionId;
const databaseType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(connectionId));
if (!databaseType) throw new Error("Connection type is unavailable.");
const {
raw,
editableSource,
objectType: resolvedType,
} = await loadEditableObjectSourceForEditor(api.getObjectSource, buildEditableObjectSource, {
connectionId,
database,
schema,
name: request.name,
objectType: request.objectType,
databaseType,
signature: request.signature,
});
applyLoadedObjectSource(id, { connectionId, database, schema, catalog: tab.catalog, title: tab.title, request, editableSource, raw, resolvedType });
} catch (e: any) {
// 就地显示错误 + Retry:用户此刻正看着这个 tab,比 toast 更可发现
const failed = tabs.value.find((candidate) => candidate.id === id);
if (failed?.sourceLoad) failed.sourceLoad.error = e?.message || String(e);
}
}
function applyLoadedObjectSource(
id: string,
loaded: {
connectionId: string;
database: string;
schema?: string;
catalog?: string;
title: string;
request: ObjectSourceRequestIdentity;
editableSource: string;
raw: ObjectSource;
resolvedType: ObjectSourceKind;
},
) {
// 加载期间 tab 被关掉(用户放弃)或连接被断开:静默丢弃,不重建、不写库
const tab = tabs.value.find((candidate) => candidate.id === id);
if (!tab?.sourceLoad) return;
const sourceIsEditable = loaded.raw.editable !== false && !OBJECT_SOURCE_READ_ONLY_TYPES.includes(loaded.resolvedType);
if (sourceIsEditable) {
const options: OpenObjectSourceTabOptions = {
connectionId: loaded.connectionId,
database: loaded.database,
title: loaded.title,
schema: loaded.schema,
catalog: loaded.catalog,
sql: loaded.editableSource,
objectSource: { schema: loaded.schema, name: loaded.request.name, objectType: loaded.resolvedType, signature: loaded.request.signature },
};
// 解析后的身份可能命中已存在的 tab(例:先按 FUNCTION 打开过,这次请求的是 PROCEDURE)。
// 有则交接给它并关掉 pending 占位,避免同一个对象出现两个 tab。
const existing = findMatchingObjectSourceTab(options, id);
if (existing) {
clearObjectSourceLoad(tab);
closeTab(id);
existing.sourceView = true;
switchTab(existing.id);
if (!isTabDirty(existing)) {
updateSql(existing.id, loaded.editableSource);
markTabClean(existing);
}
return;
}
updateSql(id, loaded.editableSource);
setObjectSource(id, options.objectSource);
} else {
updateSql(id, loaded.editableSource);
}
tab.sourceView = true;
markTabClean(tab);
clearObjectSourceLoad(tab);
}
function showExecutedQueryResults(connectionId: string, database: string, sql: string, queryResults: QueryResult[]) {
const id = createTab(connectionId, database, undefined, "query", undefined, sql);
const tab = tabs.value.find((item) => item.id === id);
@@ -8155,6 +8401,8 @@ export const useQueryStore = defineStore("query", () => {
isConfirmingAppClose,
createTab,
openObjectSourceTab,
openObjectSourceTabPending,
retryObjectSourceTab,
showExecutedQueryResults,
focusGroup,
activateTab,