fix(query): use tableMeta casing for result tab titles
This commit is contained in:
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user