mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(grid): support result source labels
This commit is contained in:
@@ -4746,6 +4746,7 @@ const {
|
||||
fullExportResult: props.fullExportResult,
|
||||
queryResultExportRequest: props.queryResultExportRequest,
|
||||
allExportResults: computed(() => props.allExportResults),
|
||||
currentResultLabel: computed(() => props.result.sourceLabel),
|
||||
exportFileBaseName: computed(() => props.exportFileBaseName),
|
||||
exportProgressDialog,
|
||||
exportProgressState,
|
||||
|
||||
@@ -245,10 +245,11 @@ const activeQueryError = computed(() => {
|
||||
return String(result.rows[0]?.[0] ?? "");
|
||||
});
|
||||
const hasQueryOutput = computed(() => !!props.activeTab.result || !!props.activeTab.explainPlan || !!props.activeTab.explainError || props.activeTab.isExecuting === true || props.activeTab.isExplaining === true);
|
||||
const visibleResultItems = computed(() => tabularResultItems(props.activeTab.results ?? (props.activeTab.result ? [props.activeTab.result] : undefined)));
|
||||
const tabularResults = computed(() => tabularResultItems(props.activeTab.results));
|
||||
const allResultExportSheets = computed(() =>
|
||||
tabularResults.value.map((item) => ({
|
||||
sheetName: t("tabs.resultN", { n: item.n }),
|
||||
sheetName: item.label || t("tabs.resultN", { n: item.n }),
|
||||
result: item.result,
|
||||
})),
|
||||
);
|
||||
@@ -259,7 +260,7 @@ 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);
|
||||
watch(
|
||||
() => tabularResults.value.map((item) => item.index).join(","),
|
||||
() => visibleResultItems.value.map((item) => item.index).join(","),
|
||||
() => {
|
||||
nextTick(updateResultTabsScrollbar);
|
||||
},
|
||||
@@ -268,7 +269,7 @@ const summaryItems = computed(() => executionSummaryItems(props.activeTab));
|
||||
const hasExecutionSummary = computed(() => summaryItems.value.length > 0 || props.activeTab.isExecuting);
|
||||
const hasTabularResult = computed(() => {
|
||||
if (props.activeTab.result?.columns.length) return true;
|
||||
return tabularResults.value.length > 0;
|
||||
return visibleResultItems.value.length > 0;
|
||||
});
|
||||
const canShowResultOutput = computed(() => hasTabularResult.value || props.activeTab.isExecuting);
|
||||
const resultsPaneOpen = ref(false);
|
||||
@@ -655,7 +656,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
<template v-if="tabularResults.length > 1">
|
||||
<template v-if="visibleResultItems.length > 0">
|
||||
<span class="mx-1 h-4 w-px shrink-0 bg-border" />
|
||||
<div class="relative min-w-0 flex-1 self-stretch">
|
||||
<div v-if="hasResultTabOverflow" class="result-tab-scrollbar" :class="{ 'result-tab-scrollbar--dragging': isResultTabsScrollbarDragging }" @pointerdown="startResultTabsScrollbarDrag">
|
||||
@@ -663,17 +664,18 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
|
||||
</div>
|
||||
<div ref="resultTabsScrollerRef" class="result-tab-scroll flex h-full items-center gap-1 overflow-x-auto overflow-y-hidden px-1" :style="resultTabsScrollerStyle" @scroll="updateResultTabsScrollbar" @wheel="onResultTabsWheel">
|
||||
<Button
|
||||
v-for="item in tabularResults"
|
||||
v-for="item in visibleResultItems"
|
||||
:key="item.index"
|
||||
size="sm"
|
||||
:variant="activeOutputView === 'result' && activeTab.activeResultIndex === item.index ? 'default' : 'ghost'"
|
||||
class="h-6 px-2 text-xs shrink-0"
|
||||
: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 })"
|
||||
@click="
|
||||
queryStore.setActiveResultIndex(activeTab.id, item.index);
|
||||
emit('update:activeOutputView', 'result');
|
||||
"
|
||||
>
|
||||
{{ t("tabs.resultN", { n: item.n }) }}
|
||||
{{ item.label || t("tabs.resultN", { n: item.n }) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface UseDataGridExportOptions {
|
||||
fullExportResult?: (onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => Promise<QueryResult | undefined>;
|
||||
queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" }) => Promise<QueryResultExportRequest | undefined>;
|
||||
allExportResults?: ComputedRef<Array<{ sheetName: string; result: QueryResult }> | undefined>;
|
||||
currentResultLabel?: ComputedRef<string | undefined>;
|
||||
exportFileBaseName?: ComputedRef<string | undefined>;
|
||||
exportProgressDialog?: Ref<boolean>;
|
||||
exportProgressState?: Ref<{
|
||||
@@ -117,6 +118,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
fullExportResult,
|
||||
queryResultExportRequest,
|
||||
allExportResults,
|
||||
currentResultLabel,
|
||||
exportFileBaseName,
|
||||
exportProgressDialog,
|
||||
exportProgressState,
|
||||
@@ -149,6 +151,10 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
};
|
||||
}
|
||||
|
||||
function currentXlsxSheetName(): string {
|
||||
return currentResultLabel?.value || tableMeta.value?.tableName || "Export";
|
||||
}
|
||||
|
||||
function targetedRows(): RowItem[] {
|
||||
if (hasRowSelection.value && selectedRowIds.value.size > 0) {
|
||||
return displayItems.value.filter((item) => selectedRowIds.value.has(item.id));
|
||||
@@ -715,7 +721,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
totalRows: result.rows.length,
|
||||
};
|
||||
}
|
||||
await api.exportQueryResultXlsx(outputPath, tableMeta.value?.tableName || "Export", result.columns, result.rows);
|
||||
await api.exportQueryResultXlsx(outputPath, currentXlsxSheetName(), result.columns, result.rows);
|
||||
if (needsFullExport && exportProgressState) {
|
||||
exportProgressState.value = {
|
||||
...exportProgressState.value,
|
||||
@@ -752,7 +758,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(undefined, undefined, false);
|
||||
await api.exportQueryResultXlsx(outputPath, tableMeta.value?.tableName || "Export", result.columns, result.rows);
|
||||
await api.exportQueryResultXlsx(outputPath, currentXlsxSheetName(), result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
|
||||
@@ -123,12 +123,12 @@ export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; v
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function tabularResultItems(results: QueryResult[] | undefined): { result: QueryResult; index: number; n: number }[] {
|
||||
export function tabularResultItems(results: QueryResult[] | undefined): { result: QueryResult; index: number; n: number; label?: 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 }));
|
||||
.map((item, ordinal) => ({ ...item, n: ordinal + 1, label: item.result.sourceLabel }));
|
||||
}
|
||||
|
||||
export function activeResultRun(tab: Pick<QueryTab, "resultRuns" | "activeResultRunId">) {
|
||||
|
||||
@@ -38,6 +38,8 @@ interface ColumnarQueryResult {
|
||||
execution_time_ms: number;
|
||||
truncated?: boolean;
|
||||
has_more?: boolean;
|
||||
sourceLabel?: string;
|
||||
sourceStatement?: string;
|
||||
}
|
||||
|
||||
type QueryResultRunSnapshot = NonNullable<QueryTab["resultRuns"]>[number];
|
||||
@@ -129,6 +131,8 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin
|
||||
truncated: result.truncated,
|
||||
session_id: undefined,
|
||||
has_more: result.has_more,
|
||||
sourceLabel: result.sourceLabel,
|
||||
sourceStatement: result.sourceStatement,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,6 +161,8 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
|
||||
execution_time_ms: result.execution_time_ms,
|
||||
truncated: result.truncated,
|
||||
has_more: result.has_more,
|
||||
sourceLabel: result.sourceLabel,
|
||||
sourceStatement: result.sourceStatement,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,6 +178,8 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul
|
||||
truncated: result.truncated,
|
||||
session_id: undefined,
|
||||
has_more: result.has_more,
|
||||
sourceLabel: result.sourceLabel,
|
||||
sourceStatement: result.sourceStatement,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { orderPinnedFirst } from "@/lib/pinnedItems";
|
||||
import { canCancelQueryExecution } from "@/lib/queryExecutionState";
|
||||
import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
|
||||
import { buildExplainSql, parseExplainResult, parseDamengExplainText } from "@/lib/explainPlan";
|
||||
import { allEditableColumnsWriteable, allPrimaryKeysPresent, sourceColumnsForResult, type EditableQueryInfo } from "@/lib/sqlAnalysis";
|
||||
import { allEditableColumnsWriteable, allPrimaryKeysPresent, analyzeEditableQuery, sourceColumnsForResult, type EditableQueryInfo } from "@/lib/sqlAnalysis";
|
||||
import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence";
|
||||
import {
|
||||
evaluateMongoAggregateSafety,
|
||||
@@ -36,6 +36,7 @@ import { connectionUsesDatabaseObjectTreeMode, connectionUsesSchemaExecutionCont
|
||||
import { queryTimeoutSecsForConnection } from "@/lib/queryTimeout";
|
||||
import { sortDataGridRows, type DataGridSortDirection } from "@/lib/dataGridSort";
|
||||
import { normalizeResultPageSize } from "@/lib/paginationPageSize";
|
||||
import { splitSqlStatementRanges } from "@/lib/sqlStatementRanges";
|
||||
import { clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor";
|
||||
import { buildTabResultSnapshot, deleteTabResultSnapshot, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabResultCache";
|
||||
import { decodeQueryResultArchive, encodeQueryResultArchive, type DecodedQueryResultArchive } from "@/lib/queryResultArchive";
|
||||
@@ -83,6 +84,27 @@ function markQueryResultRunsRowsRaw(resultRuns: NonNullable<QueryTab["resultRuns
|
||||
return resultRuns;
|
||||
}
|
||||
|
||||
function queryResultSourceLabel(sql: string, database: string | undefined): string | undefined {
|
||||
const analysis = analyzeEditableQuery(sql);
|
||||
if (!analysis) return undefined;
|
||||
const qualifier = analysis.schema || database?.trim();
|
||||
return qualifier ? `${qualifier}.${analysis.tableName}` : analysis.tableName;
|
||||
}
|
||||
|
||||
function annotateQueryResultSources(results: QueryResult[], sql: string, database: string | undefined, databaseType?: DatabaseType): QueryResult[] {
|
||||
const statements = splitSqlStatementRanges(sql, databaseType);
|
||||
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;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function withFrontendQueryTimeout<T>(promise: Promise<T>, timeoutSecs: number, message: string): Promise<T> {
|
||||
if (timeoutSecs === 0) return promise;
|
||||
|
||||
@@ -1762,7 +1784,8 @@ export const useQueryStore = defineStore("query", () => {
|
||||
});
|
||||
const executionPromise = api.executeMulti(tab.connectionId, tab.database, sqlToExecute, executionSchema, executionId, executionOptions);
|
||||
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
|
||||
const results = markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, queryTimeoutSecs === 0 ? 0 : frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs })));
|
||||
const sourceLabelDatabase = tab.database || conn?.database;
|
||||
const results = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, queryTimeoutSecs === 0 ? 0 : frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType);
|
||||
console.info("[DBX][executeTabSql:execute-multi:done]", {
|
||||
traceId,
|
||||
resultCount: results.length,
|
||||
|
||||
@@ -395,6 +395,8 @@ export interface QueryResult {
|
||||
truncated?: boolean;
|
||||
session_id?: string | null;
|
||||
has_more?: boolean;
|
||||
sourceLabel?: string;
|
||||
sourceStatement?: string;
|
||||
}
|
||||
|
||||
export interface QueryResultRun {
|
||||
|
||||
@@ -2466,6 +2466,92 @@ test("multi statement execution shows the first result set by default", async ()
|
||||
}
|
||||
});
|
||||
|
||||
test("query results keep readable table source labels with active database context", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
const connectionStore = useConnectionStore();
|
||||
const store = useQueryStore();
|
||||
const originalFetch = globalThis.fetch;
|
||||
let currentSql = "";
|
||||
|
||||
connectionStore.addEphemeralConnection({ ...conn("conn-1"), database: "aaa" });
|
||||
const tabId = store.createTab("conn-1", "db", "Query");
|
||||
const defaultDatabaseTabId = store.createTab("conn-1", "", "Default database query");
|
||||
|
||||
globalThis.fetch = withConnectionHealthMock(async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/query/prepare-pagination-plan") {
|
||||
const body = JSON.parse(String(init?.body ?? "{}"));
|
||||
currentSql = body.options.sql;
|
||||
return new Response(JSON.stringify({ sqlToExecute: currentSql, useAgentResultSession: false }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (url === "/api/query/execute-multi") {
|
||||
const results =
|
||||
currentSql === "select * from users; select * from orders"
|
||||
? [
|
||||
{ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
|
||||
{ columns: ["id"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 },
|
||||
]
|
||||
: currentSql === "SELECT *\nFROM apis AS ap\nLIMIT 10;\n\nSELECT *\nFROM menus AS mn\nLIMIT 10;"
|
||||
? [
|
||||
{ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
|
||||
{ columns: ["id"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 },
|
||||
]
|
||||
: currentSql === "select * from public.users"
|
||||
? [{ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]
|
||||
: currentSql === "select u.id from users u join orders o on o.user_id = u.id"
|
||||
? [{ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]
|
||||
: [
|
||||
{ columns: [], rows: [], affected_rows: 1, execution_time_ms: 1 },
|
||||
{ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
|
||||
];
|
||||
return new Response(JSON.stringify(results), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url === "/api/query/analyze-editability") {
|
||||
return new Response(JSON.stringify({ editable: false, reason: "complex-source" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response("unexpected request", { status: 500 });
|
||||
});
|
||||
|
||||
try {
|
||||
await store.executeTabSql(tabId, "select * from users; select * from orders");
|
||||
let tab = store.tabs.find((item) => item.id === tabId);
|
||||
assert.deepEqual(tab?.results?.map((result) => result.sourceLabel), ["db.users", "db.orders"]);
|
||||
assert.deepEqual(tab?.results?.map((result) => result.sourceStatement), ["select * from users", "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);
|
||||
assert.deepEqual(tab?.results?.map((result) => result.sourceLabel), ["aaa.apis", "aaa.menus"]);
|
||||
|
||||
await store.executeTabSql(tabId, "select * from public.users");
|
||||
tab = store.tabs.find((item) => item.id === tabId);
|
||||
assert.equal(tab?.result?.sourceLabel, "public.users");
|
||||
|
||||
await store.executeTabSql(tabId, "select u.id from users u join orders o on o.user_id = u.id");
|
||||
tab = store.tabs.find((item) => item.id === tabId);
|
||||
assert.equal(tab?.result?.sourceLabel, undefined);
|
||||
assert.equal(tab?.result?.sourceStatement, "select u.id from users u join orders o on o.user_id = u.id");
|
||||
|
||||
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?.[1]?.sourceLabel, "db.users");
|
||||
assert.equal(tab?.results?.[1]?.sourceStatement, "select * from users");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
restoreStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("tab reuse is scoped by mode and schema instead of title alone", () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
try {
|
||||
|
||||
@@ -58,12 +58,13 @@ function queryTab(overrides: Partial<QueryTab> = {}): QueryTab {
|
||||
};
|
||||
}
|
||||
|
||||
function result(columns: string[]): QueryResult {
|
||||
function result(columns: string[], overrides: Partial<QueryResult> = {}): QueryResult {
|
||||
return {
|
||||
columns,
|
||||
rows: [],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,6 +149,19 @@ test("tabular result items hide statement results without returned columns", ()
|
||||
assert.deepEqual(tabularResultItems(undefined), []);
|
||||
});
|
||||
|
||||
test("tabular result items expose source labels when available", () => {
|
||||
const results = [result([]), result(["id"], { sourceLabel: "public.users" }), result(["name"])];
|
||||
|
||||
assert.deepEqual(
|
||||
tabularResultItems(results).map((item) => ({ index: item.index, n: item.n, label: item.label })),
|
||||
[
|
||||
{ index: 1, n: 1, label: "public.users" },
|
||||
{ index: 2, n: 2, label: undefined },
|
||||
],
|
||||
);
|
||||
assert.deepEqual(tabularResultItems([result(["id"], { sourceLabel: "db.users" })]).map((item) => item.label), ["db.users"]);
|
||||
});
|
||||
|
||||
test("result run items expose ordered labels and active state", () => {
|
||||
const tab = queryTab({
|
||||
activeResultRunId: "run-2",
|
||||
|
||||
@@ -24,6 +24,8 @@ test("result snapshots strip live session handles and clone result rows", () =>
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
session_id: "live-session",
|
||||
sourceLabel: "public.users",
|
||||
sourceStatement: "select * from public.users",
|
||||
},
|
||||
results: [
|
||||
{
|
||||
@@ -40,6 +42,8 @@ test("result snapshots strip live session handles and clone result rows", () =>
|
||||
const snapshot = buildTabResultSnapshot(tab);
|
||||
|
||||
assert.equal(snapshot?.result?.session_id, undefined);
|
||||
assert.equal(snapshot?.result?.sourceLabel, "public.users");
|
||||
assert.equal(snapshot?.result?.sourceStatement, "select * from public.users");
|
||||
assert.equal(snapshot?.results?.[0]?.session_id, undefined);
|
||||
assert.deepEqual(snapshot?.result?.rows, [[1]]);
|
||||
tab.result!.rows[0]![0] = 2;
|
||||
@@ -61,6 +65,8 @@ test("result snapshots strip session handles from result runs", () => {
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
session_id: "live-run-session",
|
||||
sourceLabel: "users",
|
||||
sourceStatement: "select * from users",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -69,6 +75,8 @@ test("result snapshots strip session handles from result runs", () => {
|
||||
const snapshot = buildTabResultSnapshot(tab);
|
||||
|
||||
assert.equal(snapshot?.resultRuns?.[0]?.result?.session_id, undefined);
|
||||
assert.equal(snapshot?.resultRuns?.[0]?.result?.sourceLabel, "users");
|
||||
assert.equal(snapshot?.resultRuns?.[0]?.result?.sourceStatement, "select * from users");
|
||||
assert.deepEqual(snapshot?.resultRuns?.[0]?.result?.rows, [[1]]);
|
||||
});
|
||||
|
||||
@@ -85,6 +93,8 @@ test("result snapshots encode as binary columnar payloads and decode back to row
|
||||
execution_time_ms: 3,
|
||||
session_id: "live-session",
|
||||
has_more: true,
|
||||
sourceLabel: "public.users",
|
||||
sourceStatement: "select id, name, active from public.users",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -101,5 +111,7 @@ test("result snapshots encode as binary columnar payloads and decode back to row
|
||||
]);
|
||||
assert.equal(decoded?.result?.session_id, undefined);
|
||||
assert.equal(decoded?.result?.has_more, true);
|
||||
assert.equal(decoded?.result?.sourceLabel, "public.users");
|
||||
assert.equal(decoded?.result?.sourceStatement, "select id, name, active from public.users");
|
||||
assert.equal(decoded?.cachedAt, snapshot.cachedAt);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ function installMemoryStorage() {
|
||||
};
|
||||
}
|
||||
|
||||
function buildExportHarness(options: { exportFileBaseName?: string } = {}) {
|
||||
function buildExportHarness(options: { currentResultLabel?: string; exportFileBaseName?: string } = {}) {
|
||||
const exportProgressDialog = ref(false);
|
||||
const exportProgressState = ref({
|
||||
title: "",
|
||||
@@ -106,6 +106,7 @@ function buildExportHarness(options: { exportFileBaseName?: string } = {}) {
|
||||
hasRowSelection: computed(() => false),
|
||||
fullExportResult,
|
||||
queryResultExportRequest,
|
||||
currentResultLabel: computed(() => options.currentResultLabel),
|
||||
exportFileBaseName: computed(() => options.exportFileBaseName),
|
||||
exportProgressDialog,
|
||||
exportProgressState,
|
||||
@@ -326,6 +327,19 @@ test("selected query result CSV export keeps the existing in-memory path", async
|
||||
assert.deepEqual(apiMock.exportQueryResultCsv.mock.calls[0][2], [[1, "Ada"]]);
|
||||
});
|
||||
|
||||
test("selected query result XLSX export uses the current source label as the sheet name", async () => {
|
||||
const { composable, queryResultExportRequest } = buildExportHarness({ currentResultLabel: "aaa.apis" });
|
||||
|
||||
await composable.exportXlsx([1]);
|
||||
|
||||
assert.equal(queryResultExportRequest.mock.calls.length, 0);
|
||||
assert.equal(apiMock.startQueryResultExport.mock.calls.length, 0);
|
||||
assert.equal(apiMock.exportQueryResultXlsx.mock.calls.length, 1);
|
||||
assert.equal(apiMock.exportQueryResultXlsx.mock.calls[0][1], "aaa.apis");
|
||||
assert.deepEqual(apiMock.exportQueryResultXlsx.mock.calls[0][2], ["id", "name"]);
|
||||
assert.deepEqual(apiMock.exportQueryResultXlsx.mock.calls[0][3], [[1, "Ada"]]);
|
||||
});
|
||||
|
||||
test("cancelled query result CSV export clears the cancel handler without using the in-memory path", async () => {
|
||||
const { composable, fullExportResult, exportProgressState, exportCancelHandler } = buildExportHarness();
|
||||
apiMock.startQueryResultExport.mockImplementationOnce(async (_request, onProgress) => {
|
||||
|
||||
Reference in New Issue
Block a user