fix(editor): avoid metadata wait for wildcard queries

This commit is contained in:
miracle 2026-07-29 00:44:01 +08:00 committed by GitHub
parent e381aa4ee1
commit a8592af97a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 85 additions and 2 deletions

View File

@ -131,6 +131,44 @@ describe("queryStore hidden primary key editing", () => {
expect(tab.queryEditabilityReason).toBeUndefined();
});
it("starts a qualified MySQL star query before slow column metadata finishes", async () => {
const columnsGate = deferred<Awaited<ReturnType<typeof getColumns>>>();
getColumns.mockReturnValue(columnsGate.promise);
analyzeEditableQueryEditability.mockResolvedValue({
editable: true,
analysis: {
schema: undefined,
tableName: "sys_dept",
selectStar: true,
columns: [],
},
});
executeMulti.mockResolvedValue([
{
columns: ["dept_id", "dept_name"],
rows: [[1, "Headquarters"]],
affected_rows: 0,
execution_time_ms: 12,
},
]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "app", "Query");
const execution = store.executeTabSql(tabId, "SELECT sys_dept.* FROM sys_dept");
await vi.waitFor(() => expect(executeMulti).toHaveBeenCalled());
expect(executeMulti).toHaveBeenCalledWith("mysql-1", "app", "SELECT sys_dept.* FROM sys_dept", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
columnsGate.resolve([
{ name: "dept_id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "dept_name", data_type: "varchar", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
await execution;
const tab = store.tabs.find((item) => item.id === tabId)!;
await vi.waitFor(() => expect(tab.tableMeta?.tableName).toBe("sys_dept"));
});
it("loads metadata from the connection default database when the query tab database is empty", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
@ -208,6 +246,45 @@ describe("queryStore hidden primary key editing", () => {
expect(tab.queryEditabilityReason).toBeUndefined();
});
it("starts a MySQL JDBC star query before slow column metadata finishes", async () => {
const columnsGate = deferred<Awaited<ReturnType<typeof getColumns>>>();
getConnectionConfig.mockReturnValue({ id: "jdbc-1", name: "JDBC MySQL", db_type: "jdbc", connection_string: "jdbc:mysql://localhost:3306/app", database: "app", query_timeout_secs: 30 });
getColumns.mockReturnValue(columnsGate.promise);
analyzeEditableQueryEditability.mockResolvedValue({
editable: true,
analysis: {
schema: undefined,
tableName: "sys_dept",
selectStar: true,
columns: [],
},
});
executeMulti.mockResolvedValue([
{
columns: ["dept_id", "dept_name"],
rows: [[1, "Headquarters"]],
affected_rows: 0,
execution_time_ms: 12,
},
]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("jdbc-1", "", "Query");
const execution = store.executeTabSql(tabId, "SELECT * FROM sys_dept");
await vi.waitFor(() => expect(executeMulti).toHaveBeenCalled());
expect(executeMulti).toHaveBeenCalledWith("jdbc-1", "", "SELECT * FROM sys_dept", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
columnsGate.resolve([
{ name: "dept_id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "dept_name", data_type: "varchar", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
await execution;
const tab = store.tabs.find((item) => item.id === tabId)!;
await vi.waitFor(() => expect(tab.tableMeta?.tableName).toBe("sys_dept"));
});
it("keeps an explicitly selected database instead of falling back to the connection default", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();

View File

@ -339,6 +339,10 @@ function editableQuerySources(analysis: EditableQueryInfo): EditableQuerySource[
];
}
function projectsAllColumnsForSource(analysis: EditableQueryInfo, sourceKey: string): boolean {
return analysis.selectStar || analysis.columns.some((column) => column.star && (!column.sourceKey || column.sourceKey === sourceKey));
}
function cloneAnalysisForSource(analysis: EditableQueryInfo, source: EditableQuerySource): EditableQueryInfo {
return {
...analysis,
@ -2729,6 +2733,9 @@ export const useQueryStore = defineStore("query", () => {
const analysis = editability.analysis;
const sources = editableQuerySources(analysis);
if (sources.length !== 1 || analysis.distinct) return unchanged;
// Whole-source projections already include declared primary keys. Only
// Oracle needs preflight metadata here to add ROWID for a keyless table.
if (databaseType !== "oracle" && projectsAllColumnsForSource(analysis, sources[0]!.key)) return unchanged;
const target = resolveEditableSourceMetadataTarget(tab, analysis, sources[0]!, conn, databaseType, executionDatabase);
const cached = getCachedTableMetadata(target.request);
@ -2743,8 +2750,7 @@ export const useQueryStore = defineStore("query", () => {
});
void fullMetadataPromise.catch((error) => queryExecutionLog("warn", "metadata:table-prefetch:failed", { traceId, error, elapsed: elapsed() }));
const indexes = await loadTableIndexes(target.request);
const projectsAllColumns = target.analysis.selectStar || target.analysis.columns.some((column) => column.star && (!column.sourceKey || column.sourceKey === target.source.key));
if (primaryKeyIndex(indexes) && projectsAllColumns) {
if (primaryKeyIndex(indexes) && projectsAllColumnsForSource(target.analysis, target.source.key)) {
return unchanged;
}
loaded = loadedEditableSourceFromMetadata(target, (await fullMetadataPromise).metadata);