diff --git a/apps/desktop/src/lib/__tests__/sql/queryResultSource.spec.ts b/apps/desktop/src/lib/__tests__/sql/queryResultSource.spec.ts index c72131c5b..9e0d1958b 100644 --- a/apps/desktop/src/lib/__tests__/sql/queryResultSource.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/queryResultSource.spec.ts @@ -1,5 +1,16 @@ import { describe, expect, it } from "vitest"; -import { queryResultSourceLabel } from "@/lib/sql/queryResultSource"; +import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource"; + +describe("queryResultNameFromPreamble", () => { + it("uses the nearest non-empty Name line comment", () => { + expect(queryResultNameFromPreamble("-- Name: Old name\n-- unrelated\r\n -- NAME : Latest name \r\n")).toBe("Latest name"); + expect(queryResultNameFromPreamble("-- Name: kept\n-- Name: \n")).toBe("kept"); + }); + + it("ignores unrelated, malformed, and block comments", () => { + expect(queryResultNameFromPreamble("-- Name without colon\n/*\n-- Name: block\n*/\n-- ordinary comment\n")).toBeUndefined(); + }); +}); describe("queryResultSourceLabel", () => { it("uses the current database for an unqualified table", () => { diff --git a/apps/desktop/src/lib/sql/queryResultSource.ts b/apps/desktop/src/lib/sql/queryResultSource.ts index 6cf8f0309..f221ff080 100644 --- a/apps/desktop/src/lib/sql/queryResultSource.ts +++ b/apps/desktop/src/lib/sql/queryResultSource.ts @@ -7,6 +7,16 @@ export interface QueryResultSourceLabelOptions { databaseType?: DatabaseType; } +export function queryResultNameFromPreamble(preamble: string): string | undefined { + let name: string | undefined; + const withoutBlockComments = preamble.replace(/\/\*[\s\S]*?\*\//g, ""); + for (const line of withoutBlockComments.split(/\r?\n/)) { + const candidate = line.match(/^\s*--\s*name\s*:\s*(.*)$/i)?.[1]?.trim(); + if (candidate) name = candidate; + } + return name; +} + function firstSourceOfKind(sources: SqlSemanticRowSource[], kind: SqlSemanticRowSource["kind"]): SqlSemanticRowSource | undefined { return sources.filter((source) => source.kind === kind).sort((left, right) => left.sourceSpan.start - right.sourceSpan.start)[0]; } diff --git a/apps/desktop/src/stores/__tests__/queryStore.multiStatementError.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.multiStatementError.spec.ts index 3dee40b0c..b4d92d2b4 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.multiStatementError.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.multiStatementError.spec.ts @@ -127,6 +127,21 @@ describe("queryStore multi-statement errors", () => { }); }); + it("uses Name comments for their indexed query results", async () => { + mocks.executeMulti.mockResolvedValue([ + { columns: ["id"], rows: [[2]], affected_rows: 0, execution_time_ms: 1, statement_index: 1 }, + { columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, statement_index: 0 }, + ]); + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("mysql-1", "app", "Query"); + const sql = "-- Name: Users\nSELECT * FROM users;\n-- name : Orders\nSELECT * FROM orders"; + + await store.executeTabSql(tabId, sql); + + expect(store.tabs.find((item) => item.id === tabId)?.results?.map((result) => result.sourceLabel)).toEqual(["Orders", "Users"]); + }); + it("does not promote an unmarked Error alias without type metadata as a batch failure", async () => { mocks.executeMulti.mockResolvedValue([ { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }, diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 7d105866a..a9f568fe3 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -40,7 +40,7 @@ import { loadTableMetadata } from "@/lib/metadata/tableMetadataCache"; import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql"; import { connectionQueryExecutionSchema, effectiveDatabaseTypeForConnection, metadataSchemaForConnection } from "@/lib/database/jdbcDialect"; import { frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout"; -import { queryResultSourceLabel } from "@/lib/sql/queryResultSource"; +import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource"; import { sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort"; import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; import { executableStatementRanges, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges"; @@ -155,6 +155,8 @@ function annotateQueryResultSources(results: QueryResult[], sql: string, databas const statement = statements[sourceIndex]; if (!statement) continue; annotateQueryResultSource(result, statement.sql, database, databaseType, sourceOffset === undefined ? undefined : { from: sourceOffset + statement.from, to: sourceOffset + statement.to }); + const customName = queryResultNameFromPreamble(sql.slice(statement.hitFrom, statement.from)); + if (customName) result.sourceLabel = customName; } return results; }