1 parent
c27a4dd83d
commit
143143a88d
9 files changed
+111
-12
No files matched your search
@@ -358,6 +358,7 @@ interface DataGridProps {
|
||||
countTotalRows?: () => Promise<number | undefined>;
|
||||
loading?: boolean;
|
||||
cacheKey?: string;
|
||||
pendingStateKey?: string;
|
||||
exportSql?: string;
|
||||
onExecuteSql?: (sql: string) => Promise<void>;
|
||||
fullExportResult?: (onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => Promise<QueryResult | undefined>;
|
||||
@@ -3115,7 +3116,7 @@ const editor = useDataGridEditor({
|
||||
getRowItem,
|
||||
pageSize,
|
||||
currentPage,
|
||||
cacheKey: computed(() => props.cacheKey),
|
||||
cacheKey: computed(() => props.pendingStateKey ?? props.cacheKey),
|
||||
onResultPayloadMutated: () => queryStore.invalidateResultEstimateForPayload(props.result),
|
||||
emit,
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGr
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, queryResultExecutionSql, resultGridCacheKey, resultRunItems, resultSourceRange, resultSqlForGrid, statementExecutionMarkers, tabularResultItems, type ExecutionSummaryItem } from "@/lib/tabs/tabPresentation";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, queryResultExecutionSql, resultGridCacheKey, resultGridInstanceKey, resultRunItems, resultSourceRange, resultSqlForGrid, statementExecutionMarkers, tabularResultItems, type ExecutionSummaryItem } from "@/lib/tabs/tabPresentation";
|
||||
import { defaultQueryResultArchiveFileName } from "@/lib/query/queryResultArchive";
|
||||
import { saveQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile";
|
||||
import { isTableDataEditable } from "@/lib/table/tableEditing";
|
||||
@@ -392,6 +392,7 @@ const allResultExportSheets = computed(() =>
|
||||
);
|
||||
const resultRuns = computed(() => resultRunItems(props.activeTab));
|
||||
const activeResultGridCacheKey = computed(() => resultGridCacheKey(props.activeTab));
|
||||
const activeResultGridInstanceKey = computed(() => resultGridInstanceKey(props.activeTab));
|
||||
const activeResultSql = computed(() => resultSqlForGrid(props.activeTab));
|
||||
const activeResultExportSql = computed(() => queryResultExecutionSql(props.activeTab));
|
||||
const activeStatementExecutionMarkers = computed(() =>
|
||||
@@ -1510,8 +1511,9 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
<DataGrid
|
||||
v-else-if="activeTab.result && hasTabularResult"
|
||||
ref="dataGridRef"
|
||||
:key="activeResultGridCacheKey"
|
||||
:key="activeResultGridInstanceKey"
|
||||
:cache-key="activeResultGridCacheKey"
|
||||
:pending-state-key="activeResultGridInstanceKey"
|
||||
class="flex-1 min-h-0"
|
||||
:result="activeTab.result"
|
||||
:sort-column="activeTab.resultSortColumn"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID, useDataGridEditor } from "@/composables/useDataGridEditor";
|
||||
import { clearDataGridPendingSnapshot, DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID, useDataGridEditor } from "@/composables/useDataGridEditor";
|
||||
import type { CellValue } from "@/lib/dataGrid/cellValue";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -18,7 +18,7 @@ vi.mock("@/stores/productionSafetyStore", () => ({
|
||||
useProductionSafetyStore: () => ({}),
|
||||
}));
|
||||
|
||||
function createEditor(sourceColumns?: Array<string | undefined>, confirmDangerousRowDeletion = true) {
|
||||
function createEditor(sourceColumns?: Array<string | undefined>, confirmDangerousRowDeletion = true, cacheKey?: string) {
|
||||
let editor: ReturnType<typeof useDataGridEditor>;
|
||||
const result = ref<{ columns: string[]; rows: CellValue[][] }>({
|
||||
columns: ["first", "hidden", "last"],
|
||||
@@ -51,6 +51,7 @@ function createEditor(sourceColumns?: Array<string | undefined>, confirmDangerou
|
||||
confirmDangerousRowDeletion: computed(() => confirmDangerousRowDeletion),
|
||||
pageSize: ref(100),
|
||||
currentPage: ref(1),
|
||||
cacheKey: computed(() => cacheKey),
|
||||
getRowItem: (rowId) => {
|
||||
if (rowId === DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID) {
|
||||
return {
|
||||
@@ -83,6 +84,46 @@ function createEditor(sourceColumns?: Array<string | undefined>, confirmDangerou
|
||||
return editor;
|
||||
}
|
||||
|
||||
describe("useDataGridEditor result snapshots", () => {
|
||||
it("does not restore scroll or editing state into a replacement result identity", () => {
|
||||
class TestScroller {
|
||||
scrollTop = 0;
|
||||
scrollLeft = 0;
|
||||
scrollTo({ top, left }: ScrollToOptions) {
|
||||
if (typeof top === "number") this.scrollTop = top;
|
||||
if (typeof left === "number") this.scrollLeft = left;
|
||||
}
|
||||
}
|
||||
vi.stubGlobal("HTMLElement", TestScroller);
|
||||
const oldKey = "tab-current-0-execution-1";
|
||||
const previous = createEditor(undefined, true, oldKey);
|
||||
const previousScroller = new TestScroller();
|
||||
previousScroller.scrollTop = 6_400;
|
||||
previousScroller.scrollLeft = 24;
|
||||
previous.scrollerRef.value = previousScroller as unknown as NonNullable<typeof previous.scrollerRef.value>;
|
||||
previous.editingCell.value = { rowId: -1, col: 0 };
|
||||
previous.editValue.value = "Ada";
|
||||
previous.savePendingSnapshot(true, true);
|
||||
|
||||
const replacement = createEditor(undefined, true, "tab-current-0-execution-2");
|
||||
const replacementScroller = new TestScroller();
|
||||
replacement.scrollerRef.value = replacementScroller as unknown as NonNullable<typeof replacement.scrollerRef.value>;
|
||||
replacement.restorePendingSnapshotFocus();
|
||||
expect(replacement.editingCell.value).toBeNull();
|
||||
expect(replacementScroller.scrollTop).toBe(0);
|
||||
expect(replacementScroller.scrollLeft).toBe(0);
|
||||
|
||||
clearDataGridPendingSnapshot(oldKey);
|
||||
const oldIdentity = createEditor(undefined, true, oldKey);
|
||||
const oldIdentityScroller = new TestScroller();
|
||||
oldIdentity.scrollerRef.value = oldIdentityScroller as unknown as NonNullable<typeof oldIdentity.scrollerRef.value>;
|
||||
oldIdentity.restorePendingSnapshotFocus();
|
||||
expect(oldIdentity.editingCell.value).toBeNull();
|
||||
expect(oldIdentityScroller.scrollTop).toBe(0);
|
||||
expect(oldIdentityScroller.scrollLeft).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useDataGridEditor row deletion confirmation", () => {
|
||||
it("keeps the row pending until confirmation when confirmation is enabled", () => {
|
||||
const editor = createEditor(undefined, true);
|
||||
|
||||
@@ -183,6 +183,10 @@ export function clearDataGridPendingSnapshotsForTab(tabId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function clearDataGridPendingSnapshot(cacheKey: string) {
|
||||
pendingChangesCache.delete(cacheKey);
|
||||
}
|
||||
|
||||
export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
const connectionStore = useConnectionStore();
|
||||
const historyStore = useHistoryStore();
|
||||
|
||||
+13
-1
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { connectionGroupDisplayName, executionSummaryItems, middleEllipsis, queryResultBaseSql, queryResultExecutionSql, resultSourceRange, statementExecutionMarkers, tabTooltipLines, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import { connectionGroupDisplayName, executionSummaryItems, middleEllipsis, queryResultBaseSql, queryResultExecutionSql, resultGridCacheKey, resultGridInstanceKey, resultSourceRange, statementExecutionMarkers, tabTooltipLines, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
|
||||
import type { ConnectionConfig, QueryTab } from "@/types/database";
|
||||
|
||||
@@ -133,6 +133,18 @@ describe("query result labels", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("query result grid identity", () => {
|
||||
it("separates rerun payloads while retaining run and result-set identity", () => {
|
||||
const first = queryTab({ activeResultRunId: "run-1", activeResultIndex: 2, resultGridRevision: "execution-1" });
|
||||
const rerun = queryTab({ activeResultRunId: "run-1", activeResultIndex: 2, resultGridRevision: "execution-2" });
|
||||
|
||||
expect(resultGridInstanceKey(first)).toBe("tab-1-run-1-2-execution-1");
|
||||
expect(resultGridInstanceKey(rerun)).not.toBe(resultGridInstanceKey(first));
|
||||
expect(resultGridInstanceKey({ ...first, activeResultIndex: 1 })).toBe("tab-1-run-1-1-execution-1");
|
||||
expect(resultGridCacheKey(rerun)).toBe(resultGridCacheKey(first));
|
||||
});
|
||||
});
|
||||
|
||||
describe("tab group presentation", () => {
|
||||
it("adds the full, live group path to tab tooltips", () => {
|
||||
const store = useConnectionStore();
|
||||
|
||||
@@ -349,6 +349,10 @@ export function resultGridCacheKey(tab: Pick<QueryTab, "id" | "activeResultRunId
|
||||
return `${tab.id}-${tab.activeResultRunId ?? "current"}-${tab.activeResultIndex ?? 0}`;
|
||||
}
|
||||
|
||||
export function resultGridInstanceKey(tab: Pick<QueryTab, "id" | "activeResultRunId" | "activeResultIndex" | "resultGridRevision">): string {
|
||||
return `${resultGridCacheKey(tab)}-${tab.resultGridRevision ?? "initial"}`;
|
||||
}
|
||||
|
||||
export function nextExecutionSummaryView(currentView: OutputView, canShowResult: boolean): OutputView {
|
||||
if (currentView === "summary" && canShowResult) return "result";
|
||||
return "summary";
|
||||
|
||||
@@ -13,6 +13,8 @@ const mocks = vi.hoisted(() => ({
|
||||
getConnectionConfig: vi.fn(),
|
||||
prepareQueryPaginationExecutionPlan: vi.fn(),
|
||||
saveOpenTabsState: vi.fn(),
|
||||
clearDataGridPendingSnapshot: vi.fn(),
|
||||
clearDataGridPendingSnapshotsForTab: vi.fn(),
|
||||
tabResultSnapshots: new Map<string, unknown>(),
|
||||
}));
|
||||
|
||||
@@ -42,6 +44,11 @@ vi.mock("@/stores/settingsStore", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/composables/useDataGridEditor", () => ({
|
||||
clearDataGridPendingSnapshot: mocks.clearDataGridPendingSnapshot,
|
||||
clearDataGridPendingSnapshotsForTab: mocks.clearDataGridPendingSnapshotsForTab,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/tabs/tabResultCache", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/tabs/tabResultCache")>();
|
||||
return {
|
||||
@@ -630,6 +637,12 @@ describe("queryStore multi-statement errors", () => {
|
||||
expect(tab.resultAutoSave).toBeUndefined();
|
||||
expect(tab.resultRuns).toHaveLength(2);
|
||||
expect(tab.activeResultRunId).toBe(tab.resultRuns?.[1]?.id);
|
||||
const firstRunRevision = tab.resultRuns?.[0]?.resultGridRevision;
|
||||
const secondRunRevision = tab.resultRuns?.[1]?.resultGridRevision;
|
||||
expect(firstRunRevision).toBeTruthy();
|
||||
expect(secondRunRevision).toBeTruthy();
|
||||
expect(secondRunRevision).not.toBe(firstRunRevision);
|
||||
expect(mocks.clearDataGridPendingSnapshot).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(await store.setActiveResultRun(tabId, tab.resultRuns![0]!.id)).toBe(true);
|
||||
expect(tab.result?.rows[0]?.[0]).toBe(1);
|
||||
@@ -640,6 +653,9 @@ describe("queryStore multi-statement errors", () => {
|
||||
|
||||
expect(tab.resultRuns).toHaveLength(2);
|
||||
expect(tab.activeResultRunId).toBe(tab.resultRuns?.[1]?.id);
|
||||
expect(tab.resultGridRevision).toBe(tab.resultRuns?.[1]?.resultGridRevision);
|
||||
expect(tab.resultGridRevision).not.toBe(secondRunRevision);
|
||||
expect(mocks.clearDataGridPendingSnapshot).toHaveBeenCalledTimes(2);
|
||||
expect(await store.setActiveResultRun(tabId, tab.resultRuns![0]!.id)).toBe(true);
|
||||
expect(tab.result?.rows[0]?.[0]).toBe(1);
|
||||
expect(await store.setActiveResultRun(tabId, tab.resultRuns![1]!.id)).toBe(true);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { computed, markRaw, onScopeDispose, reactive, ref, watch } from "vue";
|
||||
import { computed, markRaw, nextTick, onScopeDispose, reactive, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, ObjectBrowserViewport, QueryResult, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
|
||||
import { orderPinnedFirst } from "@/lib/app/pinnedItems";
|
||||
@@ -52,10 +52,10 @@ import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { agentProtocolQueryResultMaxRows, capQueryResultTotal, effectiveQueryResultMaxRows, limitQueryPagination, queryResultLimitReached } from "@/lib/dataGrid/queryResultRowLimit";
|
||||
import { elasticsearchRestRequestRanges, executableStatementRanges, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
|
||||
import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen";
|
||||
import { clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor";
|
||||
import { clearDataGridPendingSnapshot, clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor";
|
||||
import { buildTabResultSnapshot, deleteTabResultSnapshot, pruneTabResultSnapshots, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
|
||||
import { estimateQueryResultsBytes, selectInactiveResultEvictions } from "@/lib/tabs/queryResultSize";
|
||||
import { queryResultBaseSql, queryResultExecutionSql } from "@/lib/tabs/tabPresentation";
|
||||
import { queryResultBaseSql, queryResultExecutionSql, resultGridInstanceKey } from "@/lib/tabs/tabPresentation";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { decodeQueryResultArchive, encodeQueryResultArchive, type DecodedQueryResultArchive } from "@/lib/query/queryResultArchive";
|
||||
import * as api from "@/lib/backend/api";
|
||||
@@ -1005,6 +1005,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.result = run.result ?? run.results?.[activeIndex];
|
||||
tab.results = run.results;
|
||||
tab.activeResultIndex = run.activeResultIndex;
|
||||
tab.resultGridRevision = run.resultGridRevision;
|
||||
tab.batchSqlExecution = cloneBatchSqlExecution(run.batchSqlExecution);
|
||||
tab.resultBaseSql = run.resultBaseSql;
|
||||
tab.resultEditorFingerprint = run.resultEditorFingerprint;
|
||||
@@ -1242,6 +1243,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
result: tab.result,
|
||||
results: tab.results,
|
||||
activeResultIndex: tab.activeResultIndex,
|
||||
resultGridRevision: tab.resultGridRevision,
|
||||
batchSqlExecution: cloneBatchSqlExecution(tab.batchSqlExecution),
|
||||
resultBaseSql: tab.resultBaseSql,
|
||||
resultEditorFingerprint: tab.resultEditorFingerprint,
|
||||
@@ -1339,6 +1341,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
result: tab.result,
|
||||
results: tab.results,
|
||||
activeResultIndex: tab.activeResultIndex,
|
||||
resultGridRevision: tab.resultGridRevision,
|
||||
batchSqlExecution: cloneBatchSqlExecution(tab.batchSqlExecution),
|
||||
resultBaseSql: tab.resultBaseSql,
|
||||
resultEditorFingerprint: tab.resultEditorFingerprint,
|
||||
@@ -3266,8 +3269,10 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
|
||||
async function executeCurrentSql(sql: string, options?: { skipRedisSafetyCheck?: boolean; sourceOffset?: number; openInNewResultTab?: boolean }) {
|
||||
if (!activeTabId.value) return;
|
||||
const tab = tabs.value.find((item) => item.id === activeTabId.value);
|
||||
const executionTabId = activeTabId.value;
|
||||
if (!executionTabId) return;
|
||||
const tab = tabs.value.find((item) => item.id === executionTabId);
|
||||
const previousGridKey = tab ? resultGridInstanceKey(tab) : undefined;
|
||||
if (tab?.mode === "query") {
|
||||
tab.resultSortColumn = undefined;
|
||||
tab.resultSortColumnIndex = undefined;
|
||||
@@ -3275,7 +3280,17 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.resultSortMode = undefined;
|
||||
tab.resultSortedSql = undefined;
|
||||
}
|
||||
return await executeTabSql(activeTabId.value, sql, { resultBaseSql: sql, resultSortedSql: undefined, ...options });
|
||||
const producedResult = await executeTabSql(executionTabId, sql, { resultBaseSql: sql, resultSortedSql: undefined, ...options });
|
||||
if (producedResult !== false) {
|
||||
const current = tabs.value.find((item) => item.id === executionTabId);
|
||||
if (current?.result) {
|
||||
current.resultGridRevision = uuid();
|
||||
if (current.activeResultRunId) syncActiveResultRunFromDisplayed(current);
|
||||
await nextTick();
|
||||
if (previousGridKey && options?.openInNewResultTab !== true) clearDataGridPendingSnapshot(previousGridKey);
|
||||
}
|
||||
}
|
||||
return producedResult;
|
||||
}
|
||||
|
||||
type QueryMetadataPatch = Pick<QueryTab, "queryAnalysis" | "querySourceColumns" | "queryEditabilityReason" | "tableMeta">;
|
||||
|
||||
@@ -767,6 +767,8 @@ export interface QueryResultRun {
|
||||
sequence: number;
|
||||
sql: string;
|
||||
createdAt: number;
|
||||
/** Distinguishes successive result payloads that reuse the same run slot. */
|
||||
resultGridRevision?: string;
|
||||
result?: QueryResult;
|
||||
results?: QueryResult[];
|
||||
activeResultIndex?: number;
|
||||
@@ -1108,6 +1110,8 @@ export interface QueryTab {
|
||||
result?: QueryResult;
|
||||
results?: QueryResult[];
|
||||
activeResultIndex?: number;
|
||||
/** Distinguishes successive result payloads that reuse the current result slot. */
|
||||
resultGridRevision?: string;
|
||||
resultRuns?: QueryResultRun[];
|
||||
activeResultRunId?: string;
|
||||
resultAutoSave?: boolean;
|
||||
|
||||
Reference in new issue
Block a user