fix(query): use tableMeta casing for result tab titles

This commit is contained in:
slowlyo authored and GitHub committed 2026-09-24 12:10:10 +08:00
1 parent 3899aea866
commit 9ab1462ca8
2 files changed
+184 -4

No files matched your search

@@ -0,0 +1,141 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it } from "vitest";
import { canonicalizeQueryResultSourceLabel, useQueryStore } from "@/stores/queryStore";
import type { ColumnInfo, QueryResult } from "@/types/database";
function sampleResult(sourceLabel?: string, sourceStatement?: string): QueryResult {
return {
columns: ["id"],
rows: [[1]],
affected_rows: 0,
execution_time_ms: 1,
sourceLabel,
sourceStatement,
};
}
describe("result tab sourceLabel canonical casing (#10092)", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
describe("canonicalizeQueryResultSourceLabel helper", () => {
it("canonicalizes table name to tableMeta casing when SQL had uppercase", () => {
const next = canonicalizeQueryResultSourceLabel("amis_base.ADMIN_PAGES", "SELECT * FROM ADMIN_PAGES", { tableName: "admin_pages", database: "amis_base" }, { database: "amis_base", databaseType: "mysql" });
expect(next).toBe("amis_base.admin_pages");
});
it("canonicalizes table name to tableMeta casing when SQL had lowercase", () => {
const next = canonicalizeQueryResultSourceLabel("amis_base.admin_pages", "SELECT * FROM admin_pages", { tableName: "admin_pages", database: "amis_base" }, { database: "amis_base", databaseType: "mysql" });
expect(next).toBe("amis_base.admin_pages");
});
it("normalizes qualifier casing when matching tableMeta database or schema", () => {
const next = canonicalizeQueryResultSourceLabel("AMIS_BASE.ADMIN_PAGES", "SELECT * FROM AMIS_BASE.ADMIN_PAGES", { tableName: "admin_pages", database: "amis_base" }, { database: "amis_base", databaseType: "mysql" });
expect(next).toBe("amis_base.admin_pages");
});
it("supports schema qualifier in postgres / oracle dialects", () => {
const next = canonicalizeQueryResultSourceLabel("PUBLIC.USERS", "SELECT * FROM PUBLIC.USERS", { tableName: "users", schema: "public" }, { database: "app", databaseType: "postgres" });
expect(next).toBe("public.users");
});
it("handles unqualified tables", () => {
const next = canonicalizeQueryResultSourceLabel("ADMIN_PAGES", "SELECT * FROM ADMIN_PAGES", { tableName: "admin_pages" }, { databaseType: "sqlite" });
expect(next).toBe("admin_pages");
});
it("leaves preamble custom names untouched", () => {
const next = canonicalizeQueryResultSourceLabel("MyCustomReport", "SELECT * FROM ADMIN_PAGES", { tableName: "admin_pages", database: "amis_base" }, { database: "amis_base", databaseType: "mysql" });
expect(next).toBeUndefined();
});
it("keeps existing qualifier text when it does not match schema or database", () => {
const next = canonicalizeQueryResultSourceLabel("other_qualifier.ADMIN_PAGES", "SELECT * FROM other_qualifier.ADMIN_PAGES", { tableName: "admin_pages", database: "amis_base" }, { database: "amis_base", databaseType: "mysql" });
expect(next).toBe("other_qualifier.admin_pages");
});
});
describe("applyQueryMetadataPatch in queryStore", () => {
it("updates sourceLabel on tab.result and matching tab.results entry", () => {
const store = useQueryStore();
const tabId = store.createTab("conn-1", "amis_base", "Query", "query");
const tab = store.tabs.find((t) => t.id === tabId)!;
const result = sampleResult("amis_base.ADMIN_PAGES", "SELECT * FROM ADMIN_PAGES");
tab.result = result;
tab.results = [result];
store.applyQueryMetadataPatch(
tab,
{
tableMeta: {
tableName: "admin_pages",
database: "amis_base",
columns: [] as ColumnInfo[],
primaryKeys: [],
},
} as never,
"mysql",
"amis_base",
);
expect(tab.result.sourceLabel).toBe("amis_base.admin_pages");
expect(tab.results[0].sourceLabel).toBe("amis_base.admin_pages");
});
it("updates only matching entry in multi-result tab", () => {
const store = useQueryStore();
const tabId = store.createTab("conn-1", "amis_base", "Query", "query");
const tab = store.tabs.find((t) => t.id === tabId)!;
const firstResult = sampleResult("amis_base.other_table", "SELECT * FROM other_table");
const secondResult = sampleResult("amis_base.ADMIN_PAGES", "SELECT * FROM ADMIN_PAGES");
tab.results = [firstResult, secondResult];
tab.activeResultIndex = 1;
tab.result = secondResult;
store.applyQueryMetadataPatch(
tab,
{
tableMeta: {
tableName: "admin_pages",
database: "amis_base",
columns: [] as ColumnInfo[],
primaryKeys: [],
},
} as never,
"mysql",
"amis_base",
);
expect(tab.result.sourceLabel).toBe("amis_base.admin_pages");
expect(tab.results[1].sourceLabel).toBe("amis_base.admin_pages");
expect(tab.results[0].sourceLabel).toBe("amis_base.other_table");
});
it("does not overwrite custom preamble sourceLabel during patch", () => {
const store = useQueryStore();
const tabId = store.createTab("conn-1", "amis_base", "Query", "query");
const tab = store.tabs.find((t) => t.id === tabId)!;
const result = sampleResult("CustomTitle", "SELECT * FROM ADMIN_PAGES");
tab.result = result;
tab.results = [result];
store.applyQueryMetadataPatch(
tab,
{
tableMeta: {
tableName: "admin_pages",
database: "amis_base",
columns: [] as ColumnInfo[],
primaryKeys: [],
},
} as never,
"mysql",
"amis_base",
);
expect(tab.result.sourceLabel).toBe("CustomTitle");
expect(tab.results[0].sourceLabel).toBe("CustomTitle");
});
});
});
+43 -4
View File
@@ -161,6 +161,8 @@ export interface EditorWorkspacePersistState {
sizes: number[];
}
export type QueryMetadataPatch = Pick<QueryTab, "queryAnalysis" | "querySourceColumns" | "queryWriteTargets" | "queryEditabilityReason" | "tableMeta" | "resultColumnComments" | "queryDisplaySourceColumns">;
interface BatchSqlResumeOptions {
batch: BatchSqlExecution;
previousResults: QueryResult[];
@@ -651,6 +653,32 @@ function annotateQueryResultSource(result: QueryResult, sourceStatement: string,
return result;
}
export function canonicalizeQueryResultSourceLabel(currentLabel: string, sourceStatement: string, tableMeta: { tableName: string; schema?: string; database?: string }, options: { database?: string; databaseType?: DatabaseType } = {}): string | undefined {
const canonicalTableName = tableMeta.tableName?.trim();
if (!canonicalTableName) return undefined;
const autoLabel = queryResultSourceLabel(sourceStatement, options);
if (!autoLabel || autoLabel.toLowerCase() !== currentLabel.toLowerCase()) {
return undefined;
}
const lastDot = currentLabel.lastIndexOf(".");
if (lastDot < 0) {
return canonicalTableName;
}
const existingQualifier = currentLabel.slice(0, lastDot);
const lowerQualifier = existingQualifier.toLowerCase();
let qualifier = existingQualifier;
if (tableMeta.schema?.trim() && tableMeta.schema.trim().toLowerCase() === lowerQualifier) {
qualifier = tableMeta.schema.trim();
} else if (tableMeta.database?.trim() && tableMeta.database.trim().toLowerCase() === lowerQualifier) {
qualifier = tableMeta.database.trim();
}
return `${qualifier}.${canonicalTableName}`;
}
function elasticsearchHttpErrorStatus(result: QueryResult): number | undefined {
const statusIndex = result.columns.findIndex((column) => column.toLowerCase() === "status");
if (statusIndex < 0) return undefined;
@@ -5600,8 +5628,6 @@ export const useQueryStore = defineStore("query", () => {
return producedResult;
}
type QueryMetadataPatch = Pick<QueryTab, "queryAnalysis" | "querySourceColumns" | "queryWriteTargets" | "queryEditabilityReason" | "tableMeta" | "resultColumnComments" | "queryDisplaySourceColumns">;
type LoadedEditableSource = {
source: EditableQuerySource;
analysis: EditableQueryInfo;
@@ -5710,7 +5736,7 @@ export const useQueryStore = defineStore("query", () => {
return databaseType !== "oracle" || !!loaded.tableMeta.tableType?.trim();
}
function applyQueryMetadataPatch(tab: QueryTab, patch: QueryMetadataPatch) {
function applyQueryMetadataPatch(tab: QueryTab, patch: QueryMetadataPatch, databaseType?: DatabaseType, database?: string) {
tab.queryAnalysis = patch.queryAnalysis;
tab.querySourceColumns = patch.querySourceColumns;
tab.queryWriteTargets = patch.queryWriteTargets;
@@ -5719,6 +5745,18 @@ export const useQueryStore = defineStore("query", () => {
tab.tableMeta = patch.tableMeta;
tab.resultColumnComments = patch.resultColumnComments;
tab.queryDisplaySourceColumns = patch.queryDisplaySourceColumns;
if (patch.tableMeta?.tableName && tab.result?.sourceStatement && tab.result.sourceLabel) {
const nextLabel = canonicalizeQueryResultSourceLabel(tab.result.sourceLabel, tab.result.sourceStatement, patch.tableMeta, {
database: database ?? tab.database,
databaseType,
});
if (nextLabel) {
tab.result.sourceLabel = nextLabel;
const matching = tab.results?.find((result) => result === tab.result);
if (matching) matching.sourceLabel = nextLabel;
}
}
}
function resolveEditableSourceMetadataTarget(tab: QueryTab, analysis: EditableQueryInfo, source: EditableQuerySource, conn: ConnectionConfig | undefined, dbType: string, executionDatabase: string): EditableSourceMetadataTarget {
@@ -6331,7 +6369,7 @@ export const useQueryStore = defineStore("query", () => {
}
const current = tabs.value.find((t) => t.id === tabId);
if (patch && current?.result === result) {
applyQueryMetadataPatch(current, patch);
applyQueryMetadataPatch(current, patch, databaseType, executionDatabase);
syncActiveResultRunFromDisplayed(current);
queryExecutionLog("info", "metadata:done", { traceId, elapsed: elapsed() });
} else {
@@ -9248,6 +9286,7 @@ export const useQueryStore = defineStore("query", () => {
countTabResultRows,
buildQueryResultExportRequest,
exportQuerySqlDirect,
applyQueryMetadataPatch,
getResourceLifecycleDiagnostics: () => resourceLifecycleDiagnostics(tabs.value),
notifyConnectionMayBeLost,
};