From 9ab1462ca8e228d9d252ae302d7e66579126cbbc Mon Sep 17 00:00:00 2001 From: slowlyo <94038215+slowlyo@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:10:10 +0800 Subject: [PATCH] fix(query): use tableMeta casing for result tab titles --- .../queryStore.resultSourceLabelCase.spec.ts | 141 ++++++++++++++++++ apps/desktop/src/stores/queryStore.ts | 47 +++++- 2 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/stores/__tests__/queryStore.resultSourceLabelCase.spec.ts diff --git a/apps/desktop/src/stores/__tests__/queryStore.resultSourceLabelCase.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.resultSourceLabelCase.spec.ts new file mode 100644 index 0000000000..27c68a490b --- /dev/null +++ b/apps/desktop/src/stores/__tests__/queryStore.resultSourceLabelCase.spec.ts @@ -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"); + }); + }); +}); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index df993bf925..7a00d7b3ba 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -161,6 +161,8 @@ export interface EditorWorkspacePersistState { sizes: number[]; } +export type QueryMetadataPatch = Pick; + 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; - 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, };