From ff8e53d47bc64f2f9daa7ea9bd0e8487e69a0f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Mon, 6 Jul 2026 18:39:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(query):=20=E4=BC=98=E5=8C=96=E5=A4=9A?= =?UTF-8?q?=E6=9D=A1=E6=9F=A5=E8=AF=A2=E8=AF=AD=E5=8F=A5=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E4=B8=8E=E8=AF=AD=E5=8F=A5=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20(#2697)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/ContentArea.vue | 7 +- apps/desktop/src/lib/tabs/tabPresentation.ts | 16 ++- apps/desktop/src/stores/queryStore.ts | 49 +++++---- packages/app-tests/queryStore.test.ts | 101 ++++++++++++++++++ packages/app-tests/tabPresentation.test.ts | 43 +++++--- 5 files changed, 172 insertions(+), 44 deletions(-) diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 12a0ddc39..cbac95fbf 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -54,7 +54,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState"; -import { databaseDisplayNameForTab, executionSummaryItems, nextExecutionSummaryView, resultGridCacheKey, resultRunItems, tabularResultItems } from "@/lib/tabs/tabPresentation"; +import { databaseDisplayNameForTab, executionSummaryItems, nextExecutionSummaryView, resultGridCacheKey, resultRunItems, resultSqlForGrid, tabularResultItems } from "@/lib/tabs/tabPresentation"; import { defaultQueryResultArchiveFileName } from "@/lib/query/queryResultArchive"; import { saveQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile"; import { isTableDataEditable } from "@/lib/table/tableEditing"; @@ -295,6 +295,7 @@ const allResultExportSheets = computed(() => const resultRuns = computed(() => resultRunItems(props.activeTab)); const activeResultRunItem = computed(() => resultRuns.value.find((run) => run.active)); const activeResultGridCacheKey = computed(() => resultGridCacheKey(props.activeTab)); +const activeResultSql = computed(() => resultSqlForGrid(props.activeTab)); const resultArchiveExporting = ref(false); const canExportResultArchive = computed(() => props.activeTab.mode === "query" && (!!props.activeTab.result || !!props.activeTab.results?.length || !!props.activeTab.resultRuns?.length)); const resultAutoSave = computed(() => props.activeTab.resultAutoSave === true); @@ -775,7 +776,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe size="sm" :variant="activeOutputView === 'result' && (activeTab.activeResultIndex ?? 0) === item.index ? 'default' : 'ghost'" class="h-6 max-w-48 shrink-0 overflow-hidden text-ellipsis whitespace-nowrap px-2 text-xs" - :title="item.label || t('tabs.resultN', { n: item.n })" + :title="item.title || item.label || t('tabs.resultN', { n: item.n })" @click=" queryStore.setActiveResultIndex(activeTab.id, item.index); emit('update:activeOutputView', 'result'); @@ -994,7 +995,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe :sort-direction="activeTab.resultSortDirection" :sort-mode="activeTab.resultSortMode" :initial-order-by-input="activeTab.orderByInput" - :sql="activeTab.lastExecutedSql || activeTab.sql" + :sql="activeResultSql" :loading="activeTab.isExecuting" :editable="!!activeTab.queryAnalysis || !!mongoQueryResultSaveHandler" :source-columns="activeTab.querySourceColumns" diff --git a/apps/desktop/src/lib/tabs/tabPresentation.ts b/apps/desktop/src/lib/tabs/tabPresentation.ts index f96c8368a..f8e55c731 100644 --- a/apps/desktop/src/lib/tabs/tabPresentation.ts +++ b/apps/desktop/src/lib/tabs/tabPresentation.ts @@ -138,12 +138,24 @@ export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; v return lines; } -export function tabularResultItems(results: QueryResult[] | undefined): { result: QueryResult; index: number; n: number; label?: string }[] { +export function queryResultStatementLabel(result: Pick, maxLength = 48): string | undefined { + if (result.sourceLabel) return result.sourceLabel; + const statement = result.sourceStatement?.replace(/\s+/g, " ").trim(); + if (!statement) return undefined; + if (statement.length <= maxLength) return statement; + return `${statement.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`; +} + +export function resultSqlForGrid(tab: Pick): string { + return tab.result?.sourceStatement || tab.resultBaseSql || tab.lastExecutedSql || tab.sql; +} + +export function tabularResultItems(results: QueryResult[] | undefined): { result: QueryResult; index: number; n: number; label?: string; title?: string }[] { if (!results) return []; return results .map((result, index) => ({ result, index })) .filter((item) => item.result.columns.length > 0) - .map((item, ordinal) => ({ ...item, n: ordinal + 1, label: item.result.sourceLabel })); + .map((item, ordinal) => ({ ...item, n: ordinal + 1, label: queryResultStatementLabel(item.result), title: item.result.sourceStatement })); } export function activeResultRun(tab: Pick) { diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 5cfbffb11..ac6257525 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -20,7 +20,7 @@ import { mongoUseToQueryResult, mongoVersionToQueryResult, mongoWriteToQueryResult, - splitMongoCommands, + splitMongoCommandRanges, type MongoAggregateSafetyOptions, } from "@/lib/mongo/mongoShellCommand"; import { redisCommandResultToQueryResult } from "@/lib/redis/redisQueryResult"; @@ -124,15 +124,19 @@ function annotateQueryResultSources(results: QueryResult[], sql: string, databas let statementIndex = 0; for (const result of results) { const statement = statements[statementIndex++]; - if (result.columns.length === 0) continue; if (!statement) continue; - result.sourceStatement = statement.sql; - const label = queryResultSourceLabel(statement.sql, database); - if (label) result.sourceLabel = label; + annotateQueryResultSource(result, statement.sql, database); } return results; } +function annotateQueryResultSource(result: QueryResult, sourceStatement: string, database?: string): QueryResult { + result.sourceStatement = sourceStatement; + const label = queryResultSourceLabel(sourceStatement, database); + if (label) result.sourceLabel = label; + return result; +} + async function withFrontendQueryTimeout(promise: Promise, timeoutSecs: number, message: string): Promise { if (timeoutSecs === 0) return promise; @@ -1994,7 +1998,7 @@ export const useQueryStore = defineStore("query", () => { try { const connStore = useConnectionStore(); let conn = connStore.getConfig(tab.connectionId); - const parsedMongoCommands = conn?.db_type === "mongodb" ? splitMongoCommands(sql) : undefined; + const parsedMongoCommands = conn?.db_type === "mongodb" ? splitMongoCommandRanges(sql) : undefined; let mongoCommands = parsedMongoCommands ?? []; const mongoNeedsConnection = mongoCommands.some(({ command }) => command.kind !== "use"); @@ -2009,7 +2013,7 @@ export const useQueryStore = defineStore("query", () => { } conn = connStore.getConfig(tab.connectionId); if (parsedMongoCommands === undefined && conn?.db_type === "mongodb") { - mongoCommands = splitMongoCommands(sql); + mongoCommands = splitMongoCommandRanges(sql); } const effectiveDbType = effectiveDatabaseTypeForConnection(conn); const useAgentCursor = usesAgentCursorForQuery(conn?.db_type); @@ -2036,7 +2040,7 @@ export const useQueryStore = defineStore("query", () => { for (const command of commands) { try { const result = await api.redisExecuteCommand(tab.connectionId, currentDb, command, skipSafety); - allResults.push(markQueryResultRowsRaw(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, result.command))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, result.command), command))); // Track db switches from SELECT N so later commands in the same batch run on the right db. currentDb = nextRedisCommandDb(currentDb, command, result.value); // Write commands (SET/DEL/...) mutate the key set — drop the cached key-name completion @@ -2046,7 +2050,7 @@ export const useQueryStore = defineStore("query", () => { connStore.invalidateCompletionCache(tab.connectionId, String(currentDb)); } } catch (e: any) { - allResults.push({ columns: ["Error"], rows: [[e?.message ?? String(e)]], affected_rows: 0, execution_time_ms: 0 }); + allResults.push(annotateQueryResultSource({ columns: ["Error"], rows: [[e?.message ?? String(e)]], affected_rows: 0, execution_time_ms: 0 }, command)); } } console.info("[DBX][executeTabSql:redis:done]", { traceId, commandCount: commands.length, elapsed: elapsed() }); @@ -2099,13 +2103,14 @@ export const useQueryStore = defineStore("query", () => { for (const parsedCommand of mongoCommands) { const mongoCommand = parsedCommand.command; + const sourceStatement = parsedCommand.text; const commandStartedAt = performance.now(); try { switch (mongoCommand.kind) { case "find": { console.info("[DBX][executeTabSql:mongo-find:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const result = await api.mongoFindDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.skip, mongoCommand.limit, mongoCommand.filter, mongoCommand.projection, mongoCommand.sort, executionId); - const queryResult = markQueryResultRowsRaw(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total)); + const queryResult = markQueryResultRowsRaw(annotateQueryResultSource(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total), sourceStatement)); allResults.push(queryResult); mongoEditTarget = mongoCommands.length === 1 && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined; console.info("[DBX][executeTabSql:mongo-find:done]", { @@ -2121,7 +2126,7 @@ export const useQueryStore = defineStore("query", () => { case "version": { console.info("[DBX][executeTabSql:mongo-version:start]", { traceId, database: currentDatabase }); const version = await api.mongoServerVersion(tab.connectionId, currentDatabase, executionId); - allResults.push(markQueryResultRowsRaw(mongoVersionToQueryResult(version, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoVersionToQueryResult(version, performance.now() - commandStartedAt), sourceStatement))); mongoEditTarget = undefined; console.info("[DBX][executeTabSql:mongo-version:done]", { traceId, @@ -2134,7 +2139,7 @@ export const useQueryStore = defineStore("query", () => { case "countDocuments": { console.info("[DBX][executeTabSql:mongo-count:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const result = await api.mongoFindDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, 0, 1, mongoCommand.filter, undefined, undefined, executionId); - allResults.push(markQueryResultRowsRaw(mongoCountToQueryResult(result.total, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoCountToQueryResult(result.total, performance.now() - commandStartedAt), sourceStatement))); mongoEditTarget = undefined; console.info("[DBX][executeTabSql:mongo-count:done]", { traceId, @@ -2153,7 +2158,7 @@ export const useQueryStore = defineStore("query", () => { console.info("[DBX][executeTabSql:mongo-aggregate:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const aggregateMaxRows = normalizeResultPageSize(pageLimit ?? options?.pagination?.limit ?? settingsStore.editorSettings.pageSize); const result = await api.mongoAggregateDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.pipeline, aggregateMaxRows, executionId); - allResults.push(markQueryResultRowsRaw(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total), sourceStatement))); mongoEditTarget = undefined; console.info("[DBX][executeTabSql:mongo-aggregate:done]", { traceId, @@ -2168,7 +2173,7 @@ export const useQueryStore = defineStore("query", () => { case "getIndexes": { console.info("[DBX][executeTabSql:mongo-indexes:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase }); const indexes = await api.listIndexes(tab.connectionId, currentDatabase, "", mongoCommand.collection); - allResults.push(markQueryResultRowsRaw(mongoIndexesToQueryResult(indexes, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoIndexesToQueryResult(indexes, performance.now() - commandStartedAt), sourceStatement))); mongoEditTarget = undefined; console.info("[DBX][executeTabSql:mongo-indexes:done]", { traceId, @@ -2187,7 +2192,7 @@ export const useQueryStore = defineStore("query", () => { database: currentDatabase, }); const stats = await api.mongoCollectionStats(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.scale, executionId); - allResults.push(markQueryResultRowsRaw(mongoCollectionStatsToQueryResult(mongoCommand.metric, stats as unknown as Record, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoCollectionStatsToQueryResult(mongoCommand.metric, stats as unknown as Record, performance.now() - commandStartedAt), sourceStatement))); mongoEditTarget = undefined; console.info("[DBX][executeTabSql:mongo-collection-stats:done]", { traceId, @@ -2217,19 +2222,19 @@ export const useQueryStore = defineStore("query", () => { mongoEditTarget = undefined; if (mongoCommand.kind === "insert") { const result = await api.mongoInsertDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.docsJson); - allResults.push(markQueryResultRowsRaw(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt), sourceStatement))); } else if (mongoCommand.kind === "update") { const result = await api.mongoUpdateDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.many); - allResults.push(markQueryResultRowsRaw(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt), sourceStatement))); } else if (mongoCommand.kind === "createIndex") { const result = await api.mongoCreateIndex(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.keys, mongoCommand.options); - allResults.push(markQueryResultRowsRaw(mongoCreateIndexToQueryResult(result.name, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoCreateIndexToQueryResult(result.name, performance.now() - commandStartedAt), sourceStatement))); } else if (mongoCommand.kind === "dropIndex" || mongoCommand.kind === "dropIndexes") { const result = await api.mongoDropIndexes(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.kind === "dropIndex" ? mongoCommand.index : mongoCommand.indexes, mongoCommand.kind === "dropIndex"); - allResults.push(markQueryResultRowsRaw(mongoDroppedIndexesToQueryResult(result.dropped_names, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoDroppedIndexesToQueryResult(result.dropped_names, performance.now() - commandStartedAt), sourceStatement))); } else { const result = await api.mongoDeleteDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.many); - allResults.push(markQueryResultRowsRaw(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt), sourceStatement))); } console.info("[DBX][executeTabSql:mongo-write:done]", { traceId, @@ -2242,7 +2247,7 @@ export const useQueryStore = defineStore("query", () => { } case "use": { currentDatabase = mongoCommand.database; - allResults.push(markQueryResultRowsRaw(mongoUseToQueryResult(currentDatabase, performance.now() - commandStartedAt))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoUseToQueryResult(currentDatabase, performance.now() - commandStartedAt), sourceStatement))); mongoEditTarget = undefined; console.info("[DBX][executeTabSql:mongo-use:done]", { traceId, @@ -2255,7 +2260,7 @@ export const useQueryStore = defineStore("query", () => { } catch (error: any) { // Surface per-command failures inline and continue collecting results // for the rest of the batch, matching the grouped-result UX. - allResults.push(toErrorResult(error)); + allResults.push(annotateQueryResultSource(toErrorResult(error), sourceStatement)); mongoEditTarget = undefined; } } diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 8e75ea2d6..47b179f65 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -3,6 +3,7 @@ import { test } from "vitest"; import { createPinia, setActivePinia } from "pinia"; import { isReactive } from "vue"; import { decodeQueryResultArchive } from "../../apps/desktop/src/lib/query/queryResultArchive.ts"; +import { resultSqlForGrid } from "../../apps/desktop/src/lib/tabs/tabPresentation.ts"; import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts"; import { useQueryStore } from "../../apps/desktop/src/stores/queryStore.ts"; import { useSettingsStore } from "../../apps/desktop/src/stores/settingsStore.ts"; @@ -2487,6 +2488,101 @@ test("mongo multi-command execution runs writes sequentially and keeps grouped r assert.equal(tab?.results?.length, 2); assert.equal(tab?.activeResultIndex, 0); assert.equal(tab?.result?.affected_rows, 1); + assert.deepEqual( + tab?.results?.map((result) => result.sourceStatement), + ['db.users.insertOne({ name: "Ada" })', 'db.users.insertOne({ name: "Grace" })'], + ); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + +test("redis multi-command execution records source statements for each result", async () => { + const restoreStorage = installMemoryStorage(); + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + const store = useQueryStore(); + const originalFetch = globalThis.fetch; + const commandBodies: any[] = []; + + connectionStore.addEphemeralConnection({ + ...conn("redis-1"), + db_type: "redis", + port: 6379, + }); + + globalThis.fetch = withConnectionHealthMock(async (input, init) => { + const url = String(input); + if (url === "/api/redis/execute-command") { + const body = JSON.parse(String(init?.body ?? "{}")); + commandBodies.push(body); + if (body.command === "BAD") return new Response("bad command", { status: 500 }); + return new Response(JSON.stringify({ command: body.command, safety: "allowed", value: body.command === "GET user:1" ? "Ada" : "OK" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response("unexpected request", { status: 500 }); + }); + + try { + const tabId = store.createTab("redis-1", "0", "Redis", "query", ""); + await store.executeTabSql(tabId, "GET user:1\nBAD\nPING"); + const tab = store.tabs.find((item) => item.id === tabId); + + assert.deepEqual( + commandBodies.map((body) => body.command), + ["GET user:1", "BAD", "PING"], + ); + assert.deepEqual( + tab?.results?.map((result) => result.sourceStatement), + ["GET user:1", "BAD", "PING"], + ); + assert.deepEqual(tab?.results?.[1]?.columns, ["Error"]); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + +test("mongo multi-command execution records source statements for error results", async () => { + const restoreStorage = installMemoryStorage(); + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + const store = useQueryStore(); + const originalFetch = globalThis.fetch; + let insertCount = 0; + + connectionStore.addEphemeralConnection({ + ...conn("mongo-1"), + db_type: "mongodb", + port: 27017, + }); + + globalThis.fetch = withConnectionHealthMock(async (input) => { + const url = String(input); + if (url === "/api/mongo/insert-documents") { + insertCount += 1; + if (insertCount === 2) return new Response("duplicate key", { status: 500 }); + return new Response(JSON.stringify({ affected_rows: 1 }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response("unexpected request", { status: 500 }); + }); + + try { + const tabId = store.createTab("mongo-1", "accounting", "Query", "query", ""); + await store.executeTabSql(tabId, 'db.users.insertOne({ name: "Ada" });\ndb.users.insertOne({ name: "Ada" });'); + const tab = store.tabs.find((item) => item.id === tabId); + + assert.deepEqual( + tab?.results?.map((result) => result.sourceStatement), + ['db.users.insertOne({ name: "Ada" })', 'db.users.insertOne({ name: "Ada" })'], + ); + assert.deepEqual(tab?.results?.[1]?.columns, ["Error"]); } finally { globalThis.fetch = originalFetch; restoreStorage(); @@ -3453,6 +3549,10 @@ test("query results keep readable table source labels with active database conte tab?.results?.map((result) => result.sourceStatement), ["select * from users", "select * from orders"], ); + assert.equal(resultSqlForGrid(tab!), "select * from users"); + store.setActiveResultIndex(tabId, 1); + tab = store.tabs.find((item) => item.id === tabId); + assert.equal(resultSqlForGrid(tab!), "select * from orders"); await store.executeTabSql(defaultDatabaseTabId, "SELECT *\nFROM apis AS ap\nLIMIT 10;\n\nSELECT *\nFROM menus AS mn\nLIMIT 10;"); tab = store.tabs.find((item) => item.id === defaultDatabaseTabId); @@ -3473,6 +3573,7 @@ test("query results keep readable table source labels with active database conte await store.executeTabSql(tabId, "update users set active = true; select * from users"); tab = store.tabs.find((item) => item.id === tabId); assert.equal(tab?.results?.[0]?.sourceLabel, undefined); + assert.equal(tab?.results?.[0]?.sourceStatement, "update users set active = true"); assert.equal(tab?.results?.[1]?.sourceLabel, "db.users"); assert.equal(tab?.results?.[1]?.sourceStatement, "select * from users"); } finally { diff --git a/packages/app-tests/tabPresentation.test.ts b/packages/app-tests/tabPresentation.test.ts index 8fa8ffbc3..73078b334 100644 --- a/packages/app-tests/tabPresentation.test.ts +++ b/packages/app-tests/tabPresentation.test.ts @@ -1,17 +1,7 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; import { createPinia, setActivePinia } from "pinia"; -import { - activeResultRun, - databaseDisplayNameForTab, - executionSummaryItems, - nextExecutionSummaryView, - resultGridCacheKey, - resultRunItems, - tabDisplayTitle, - tabModeLabel, - tabularResultItems, -} from "../../apps/desktop/src/lib/tabs/tabPresentation.ts"; +import { activeResultRun, databaseDisplayNameForTab, executionSummaryItems, nextExecutionSummaryView, resultGridCacheKey, resultRunItems, resultSqlForGrid, tabDisplayTitle, tabModeLabel, tabularResultItems } from "../../apps/desktop/src/lib/tabs/tabPresentation.ts"; import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts"; import type { ConnectionConfig, QueryResult, QueryTab } from "../../apps/desktop/src/types/database.ts"; @@ -189,16 +179,32 @@ test("tabular result items hide statement results without returned columns", () }); test("tabular result items expose source labels when available", () => { - const results = [result([]), result(["id"], { sourceLabel: "public.users" }), result(["name"])]; + const results = [result([]), result(["id"], { sourceLabel: "public.users", sourceStatement: "select * from public.users" }), result(["name"], { sourceStatement: "select id, name, email, created_at from users where active = true order by created_at desc" })]; assert.deepEqual( - tabularResultItems(results).map((item) => ({ index: item.index, n: item.n, label: item.label })), + tabularResultItems(results).map((item) => ({ index: item.index, n: item.n, label: item.label, title: item.title })), [ - { index: 1, n: 1, label: "public.users" }, - { index: 2, n: 2, label: undefined }, + { index: 1, n: 1, label: "public.users", title: "select * from public.users" }, + { index: 2, n: 2, label: "select id, name, email, created_at from users...", title: "select id, name, email, created_at from users where active = true order by created_at desc" }, ], ); - assert.deepEqual(tabularResultItems([result(["id"], { sourceLabel: "db.users" })]).map((item) => item.label), ["db.users"]); + assert.deepEqual( + tabularResultItems([result(["id"], { sourceLabel: "db.users" })]).map((item) => item.label), + ["db.users"], + ); +}); + +test("resultSqlForGrid prefers the active result source statement", () => { + const tab = queryTab({ + sql: "select * from users; select * from orders", + lastExecutedSql: "select * from users; select * from orders", + resultBaseSql: "select * from users; select * from orders", + result: result(["id"], { sourceStatement: "select * from orders" }), + }); + + assert.equal(resultSqlForGrid(tab), "select * from orders"); + assert.equal(resultSqlForGrid(queryTab({ sql: "select 1", resultBaseSql: "select 2" })), "select 2"); + assert.equal(resultSqlForGrid(queryTab({ sql: "select 1", lastExecutedSql: "select 3" })), "select 3"); }); test("result run items expose ordered labels and active state", () => { @@ -229,7 +235,10 @@ test("result run items expose ordered labels and active state", () => { { id: "run-2", title: "Run 2", sequence: 2, active: true }, ]); assert.equal(activeResultRun(tab)?.id, "run-2"); - assert.deepEqual(resultRunItems(queryTab()).map((item) => item.title), []); + assert.deepEqual( + resultRunItems(queryTab()).map((item) => item.title), + [], + ); }); test("result grid cache key includes result run id and statement result index", () => {