diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index ee9204769..1f6b8581f 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -119,7 +119,18 @@ import { BINARY_CELL_DOWNLOAD_MODES, binaryCellDisplayText, binaryCellDownloadFi import { buildBinaryHexViewRows } from "@/lib/dataGrid/binaryHexViewer"; import { canFormatCellDetailJson, cellDetailEditorText, compactJsonText, defaultCellDetailTab, formatJsonText, isGeometryColumnType, linkedCellDetailTarget, looksLikeJsonContainerText, valueEditorActions, visibleCellDetailTabs, type CellDetailTab } from "@/lib/dataGrid/cellDetailPresentation"; import { renderWktOnCanvas, isHexGeometry } from "@/lib/dataGrid/geometryPreview"; -import { buildDataGridCellDetail, buildDataGridColumnDetail, buildDataGridRowDetail, dataGridColumnDetailJson, dataGridColumnDetailTsv, dataGridRowDetailJson, dataGridRowDetailTsv, filterDataGridDetailFields, type DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail"; +import { + buildDataGridCellDetail, + buildDataGridColumnDetail, + buildDataGridRowDetail, + CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH, + dataGridColumnDetailJson, + dataGridColumnDetailTsv, + dataGridRowDetailJson, + dataGridRowDetailTsv, + filterDataGridDetailFields, + type DataGridCellDetail, +} from "@/lib/dataGrid/dataGridDetail"; import { applyColumnFormatter, buildColumnFormatterKey, normalizeColumnFormatter, resolveColumnFormatter, type ColumnFormatterConfig, type DateTimeFormatterUnit, DateTimePatterns } from "@/lib/dataGrid/columnFormatter"; import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor"; import { isCancelSearchShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isFocusSearchShortcut, isModRShortcut, isSaveShortcut, isToggleTransposeShortcut } from "@/lib/editor/keyboardShortcuts"; @@ -485,6 +496,7 @@ const detailCell = ref<{ rowIndex: number; col: number } | null>(null); const hoveredDetailCell = ref<{ rowIndex: number; col: number } | null>(null); const quickDownloadMenuCell = ref<{ rowIndex: number; col: number } | null>(null); const showCellDetail = ref(false); +const showMongoJsonPreview = ref(false); const activeCellDetailTab = ref(defaultCellDetailTab()); const cellDetailDialogOpen = ref(false); const cellDetailDialogTarget = ref<{ rowIndex: number; col: number } | null>(null); @@ -4787,6 +4799,42 @@ const activeCellDetail = computed(() => { return cell ? cellDetailFor(cell.rowIndex, cell.col) : null; }); +const canShowMongoJsonPreview = computed(() => props.databaseType === "mongodb" && !!props.result.mongo_documents && props.result.mongo_documents.length === props.result.rows.length); +const mongoJsonPreviewOpen = computed(() => showMongoJsonPreview.value && canShowMongoJsonPreview.value); +const activeMongoJsonDocument = computed(() => { + if (!mongoJsonPreviewOpen.value) return undefined; + const selectedCell = currentSelectedCellPosition(); + if (!selectedCell) return undefined; + const item = displayItemAt(selectedCell.rowIndex); + return item?.sourceIndex === undefined ? undefined : props.result.mongo_documents?.[item.sourceIndex]; +}); +const mongoJsonPreviewFullText = computed(() => { + const document = activeMongoJsonDocument.value; + if (document === undefined) return ""; + try { + return JSON.stringify(document, null, 2) ?? ""; + } catch { + return ""; + } +}); +const mongoJsonPreviewText = computed(() => mongoJsonPreviewFullText.value.slice(0, CELL_DETAIL_VALUE_PREVIEW_MAX_LENGTH)); +const mongoJsonPreviewTruncated = computed(() => mongoJsonPreviewText.value.length < mongoJsonPreviewFullText.value.length); +const mongoJsonPreviewUsesCodeEditor = computed(() => !!mongoJsonPreviewText.value && !mongoJsonPreviewTruncated.value); + +watch(canShowMongoJsonPreview, (available) => { + if (!available) showMongoJsonPreview.value = false; +}); + +// Result-set switches remount the grid, but re-executing the same result set +// keeps this component alive. Clear the ephemeral preview before fresh query +// data arrives so it cannot retain a stale row selection or drawer state. +watch( + () => props.loading, + (loading) => { + if (loading) showMongoJsonPreview.value = false; + }, +); + const dialogCellDetail = computed(() => { const target = cellDetailDialogTarget.value; return target ? cellDetailFor(target.rowIndex, target.col) : null; @@ -5041,10 +5089,12 @@ const detailsEditorContainer = ref(); const valueEditorContainer = ref(); const sideJsonPreviewContainer = ref(); const dialogJsonPreviewContainer = ref(); +const mongoJsonPreviewContainer = ref(); let detailsDetailEditor: UseCellDetailEditorReturn | null = null; let valueDetailEditor: UseCellDetailEditorReturn | null = null; let sideJsonPreviewEditor: UseCellDetailEditorReturn | null = null; let dialogJsonPreviewEditor: UseCellDetailEditorReturn | null = null; +let mongoJsonPreviewEditor: UseCellDetailEditorReturn | null = null; const editorThemeAccessor = () => settingsStore.editorSettings.theme; const editorAppAppearance = () => (isDark.value ? "dark" : "light") as import("@/lib/app/appTheme").AppThemeAppearance; @@ -5143,10 +5193,32 @@ watch(dialogJsonPreviewContainer, async (el) => { } }); +watch(mongoJsonPreviewContainer, async (el) => { + if (el && !mongoJsonPreviewEditor) { + mongoJsonPreviewEditor = useCellDetailEditor({ + language: "json", + readOnly: true, + editorTheme: editorThemeAccessor, + appAppearance: editorAppAppearance, + appPalette: editorAppPalette, + fontSize: editorFontSize, + fontFamily: editorFontFamily, + }); + await mongoJsonPreviewEditor.create(el, mongoJsonPreviewText.value, "json"); + } else if (!el && mongoJsonPreviewEditor) { + mongoJsonPreviewEditor.destroy(); + mongoJsonPreviewEditor = null; + } +}); + watch(sideJsonPreviewText, (value) => { sideJsonPreviewEditor?.setValue(value, "json"); }); +watch(mongoJsonPreviewText, (value) => { + mongoJsonPreviewEditor?.setValue(value, "json"); +}); + watch( () => dialogCellDetail.value?.formattedJson ?? "", (value) => { @@ -5165,6 +5237,20 @@ function closeCellDetails() { detailCell.value = null; } +function toggleMongoJsonPreview() { + if (!canShowMongoJsonPreview.value) return; + showMongoJsonPreview.value = !showMongoJsonPreview.value; + if (showMongoJsonPreview.value) closeCellDetails(); +} + +function closeMongoJsonPreview() { + showMongoJsonPreview.value = false; +} + +function copyMongoJsonPreview() { + if (mongoJsonPreviewFullText.value) copyText(mongoJsonPreviewFullText.value); +} + function cellDetailEditText(detail: DataGridCellDetail): string { if (sideDetailJsonView.value && detail.formattedJson) return detail.formattedJson; return dataGridCellEditorText({ @@ -6349,6 +6435,7 @@ function selectExportMenuItem(value: string) { // --- Cell selection and detail --- function showCellDetails(rowIndex: number, colIndex: number) { + closeMongoJsonPreview(); resetDetailEdit(); detailCell.value = { rowIndex, col: colIndex }; activeCellDetailTab.value = defaultCellDetailTab(); @@ -7733,6 +7820,7 @@ const CELL_DETAIL_TABLE_MIN_VISIBLE_ROWS = 1.5; const CELL_DETAIL_TABLE_HORIZONTAL_SCROLLBAR_HEIGHT = 10; const CELL_DETAIL_TABLE_MIN_VISIBLE_HEIGHT = Math.ceil(CELL_DETAIL_TABLE_HEADER_HEIGHT + CANVAS_DATA_GRID_ROW_HEIGHT * CELL_DETAIL_TABLE_MIN_VISIBLE_ROWS + CELL_DETAIL_TABLE_HORIZONTAL_SCROLLBAR_HEIGHT); const DRAWER_MAX_WIDTH = 900; +const MONGO_JSON_PREVIEW_DEFAULT_WIDTH = 420; function clampCellDetailPanelSize(value: number, layout = cellDetailPanelLayout.value): number { const min = layout === "bottom" ? CELL_DETAIL_PANEL_MIN_HEIGHT : CELL_DETAIL_PANEL_MIN_WIDTH; const max = layout === "bottom" ? CELL_DETAIL_PANEL_MAX_HEIGHT : DRAWER_MAX_WIDTH; @@ -7759,12 +7847,16 @@ function onDdlKeydown(e: KeyboardEvent) { const ddlLoading = ref(false); const ddlWidth = ref(settingsStore.editorSettings.tableInfoDrawerWidth); const detailPanelHeight = ref(settingsStore.editorSettings.cellDetailDrawerWidth); +const mongoJsonPreviewWidth = ref(MONGO_JSON_PREVIEW_DEFAULT_WIDTH); const ddlWrap = ref(true); const isResizingDdl = ref(false); +const isResizingMongoJsonPreview = ref(false); let ddlResizeStartX = 0; let ddlResizeStartWidth = 0; let detailResizeStartY = 0; let detailResizeStartHeight = 0; +let mongoJsonPreviewResizeStartX = 0; +let mongoJsonPreviewResizeStartWidth = 0; const indexes = ref([]); const indexesLoaded = ref(false); const indexesLoading = ref(false); @@ -7836,6 +7928,10 @@ const detailPanelStyle = computed(() => : { width: `${detailPanelHeight.value}px` }, ); +const mongoJsonPreviewStyle = computed(() => ({ + width: `${mongoJsonPreviewWidth.value}px`, +})); + const contentGridStyle = computed(() => cellDetailPanelIsBottom.value && showCellDetail.value && activeCellDetail.value ? { @@ -8126,6 +8222,27 @@ function onDetailResizeEnd() { window.removeEventListener("mouseup", onDetailResizeEnd); } +function onMongoJsonPreviewResizeStart(event: MouseEvent) { + isResizingMongoJsonPreview.value = true; + mongoJsonPreviewResizeStartX = event.clientX; + mongoJsonPreviewResizeStartWidth = mongoJsonPreviewWidth.value; + document.body.classList.add("select-none", "cursor-col-resize"); + window.addEventListener("mousemove", onMongoJsonPreviewResizeMove); + window.addEventListener("mouseup", onMongoJsonPreviewResizeEnd); +} + +function onMongoJsonPreviewResizeMove(event: MouseEvent) { + if (!isResizingMongoJsonPreview.value) return; + mongoJsonPreviewWidth.value = clampCellDetailPanelSize(mongoJsonPreviewResizeStartWidth + mongoJsonPreviewResizeStartX - event.clientX, "right"); +} + +function onMongoJsonPreviewResizeEnd() { + isResizingMongoJsonPreview.value = false; + document.body.classList.remove("select-none", "cursor-col-resize"); + window.removeEventListener("mousemove", onMongoJsonPreviewResizeMove); + window.removeEventListener("mouseup", onMongoJsonPreviewResizeEnd); +} + const loadingElapsed = ref(0); let _loadingFrame: number | undefined; let _loadingStart = 0; @@ -8198,6 +8315,7 @@ onUnmounted(() => { onSearchSplitResizeEnd(); onDdlResizeEnd(); onDetailResizeEnd(); + onMongoJsonPreviewResizeEnd(); finishCellSelection(); clearTimeout(highlightedColumnTimer); clearTimeout(_searchTimer); @@ -9047,6 +9165,21 @@ const gridContextMenuItems = computed(() => { {{ t("grid.keylessEditWarningHint") }} + + + + + {{ t("grid.mongoJsonPreview") }} + + + +
+
+ +
+
+ {{ t("grid.mongoJsonPreviewEmpty") }} +
+
diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 93e0ce351..3fd2192c9 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -69,7 +69,7 @@ import { formatShortcut } from "@/lib/editor/shortcutRegistry"; import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { chartableColumnIndexes } from "@/lib/dataGrid/chartData"; import * as api from "@/lib/backend/api"; -import { buildMongoUpdateDocument, formatMongoShellLiteral, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues"; +import { applyMongoGridChangesToDocument, buildMongoUpdateDocument, formatMongoShellLiteral, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues"; import type { SqlExecutionOverride } from "@/lib/sql/sqlExecutionTarget"; import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; import { useTabScroll } from "@/composables/useTabScroll"; @@ -452,7 +452,26 @@ const mongoQueryResultSaveHandler = computed(() = return stmts; }; - return { save, preview, canInsert: false, canDelete: false, supportsInsert: false, readonlyColumns: [target.idColumn], targetLabel: target.collection }; + const applySavedChanges: NonNullable = ({ dirtyRows, columns }) => { + const documents = tab.result?.mongo_documents; + if (!documents) return; + + // Replace the raw array only after every backend update succeeds, keeping + // the grid and JSON preview atomic when a multi-row save partially fails. + const replacements = new Map(); + tab.result!.mongo_documents = documents.map((document, rowIdx) => { + const changes = dirtyRows.get(rowIdx); + if (!changes) return document; + const updated = applyMongoGridChangesToDocument(document, changes, columns); + replacements.set(document, updated); + return updated; + }); + if (tab.resultLocalSortOriginalMongoDocuments) { + tab.resultLocalSortOriginalMongoDocuments = tab.resultLocalSortOriginalMongoDocuments.map((document) => replacements.get(document) ?? document); + } + }; + + return { save, preview, applySavedChanges, canInsert: false, canDelete: false, supportsInsert: false, readonlyColumns: [target.idColumn], targetLabel: target.collection }; }); const resultsPaneOpen = ref(false); const resultsPaneSize = ref(Number(safeLocalStorageGet("dbx-results-pane-size")) || DEFAULT_QUERY_RESULTS_PANE_SIZE); diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 0288074ca..aba275651 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1202,6 +1202,7 @@ async function openData() { tab.resultSortDirection = undefined; tab.resultSortMode = undefined; tab.resultLocalSortOriginalRows = undefined; + tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultSortedSql = undefined; tab.resultPageSql = undefined; tab.resultPageLimit = undefined; diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index cdf3b0ff7..d1b6501d1 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -53,6 +53,7 @@ type GridScrollerRef = export interface CustomSaveHandler { save: (changes: { dirtyRows: Map>; newRows: CellValue[][]; deletedRows: Set; columns: string[]; rows: CellValue[][] }) => Promise; + applySavedChanges?: (changes: { dirtyRows: Map>; columns: string[] }) => void; preview?: (changes: { dirtyRows: Map>; newRows: CellValue[][]; deletedRows: Set; columns: string[]; rows: CellValue[][] }) => Promise; canInsert?: boolean; canDelete?: boolean; @@ -1184,6 +1185,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { return; } snapshot.newRowRefs.forEach((row) => savingNewRows.delete(row)); + customHandler.applySavedChanges?.({ dirtyRows: snapshot.dirtyRows, columns: result.value.columns }); applyDirtyRowsToResult(snapshot); clearSavedPendingChanges(snapshot); if (!hasPendingChanges.value) exitTransaction(); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 01fec541f..55b3b5a70 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -685,6 +685,9 @@ export default { noFilteredRows: "No rows match the filter", noFilteredRowsDescription: "Adjust the search text or row status filter.", copy: "Copy", + mongoJsonPreview: "JSON Preview", + mongoJsonPreviewEmpty: "Select a MongoDB document to preview.", + copyJson: "Copy JSON", copyDdl: "Copy DDL", copyCell: "Copy Cell", copyRow: "Copy Row (JSON)", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 6fb02b864..ae3bb6b88 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -635,6 +635,9 @@ export default withEnglishFallback({ noFilteredRows: "Ninguna fila coincide con el filtro", noFilteredRowsDescription: "Ajusta el texto de búsqueda o el filtro de estado de filas.", copy: "Copiar", + mongoJsonPreview: "Vista previa de JSON", + mongoJsonPreviewEmpty: "Selecciona un documento de MongoDB para obtener una vista previa.", + copyJson: "Copiar JSON", copyDdl: "Copiar DDL", copyCell: "Copiar celda", copyRow: "Copiar fila (JSON)", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index cf56914ac..e26f83b2a 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -633,6 +633,9 @@ export default withEnglishFallback({ noFilteredRows: "Nessuna riga corrisponde al filtro", noFilteredRowsDescription: "Regola il testo di ricerca o il filtro dello stato della riga.", copy: "Copia", + mongoJsonPreview: "Anteprima JSON", + mongoJsonPreviewEmpty: "Seleziona un documento MongoDB da visualizzare.", + copyJson: "Copia JSON", copyDdl: "Copia DDL", copyCell: "Copia Cella", copyRow: "Copia Riga (JSON)", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index d1150e18d..1a9de2f61 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -631,6 +631,9 @@ export default withEnglishFallback({ noFilteredRows: "フィルターに一致する行がありません", noFilteredRowsDescription: "検索テキストまたは行ステータスフィルターを調整してください。", copy: "コピー", + mongoJsonPreview: "JSON プレビュー", + mongoJsonPreviewEmpty: "プレビューする MongoDB ドキュメントを選択してください。", + copyJson: "JSON をコピー", copyDdl: "DDLをコピー", copyCell: "セルをコピー", copyRow: "行をコピー (JSON)", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 325ce15be..313743b0c 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -634,6 +634,9 @@ export default withEnglishFallback({ noFilteredRows: "Nenhuma linha corresponde ao filtro", noFilteredRowsDescription: "Ajuste o texto de pesquisa ou o filtro de status das linhas.", copy: "Copiar", + mongoJsonPreview: "Visualização JSON", + mongoJsonPreviewEmpty: "Selecione um documento MongoDB para visualizar.", + copyJson: "Copiar JSON", copyDdl: "Copiar DDL", copyCell: "Copiar Célula", copyRow: "Copiar Linha (JSON)", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 47cf1f4bb..bf212257a 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -686,6 +686,9 @@ export default withEnglishFallback({ noFilteredRows: "没有符合条件的行", noFilteredRowsDescription: "调整搜索词或状态筛选试试。", copy: "复制", + mongoJsonPreview: "JSON 预览", + mongoJsonPreviewEmpty: "选择一条 MongoDB 文档以预览。", + copyJson: "复制 JSON", copyDdl: "复制 DDL", copyCell: "复制单元格", copyRow: "复制行 (JSON)", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 543e6884c..ef084e956 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -633,6 +633,9 @@ export default withEnglishFallback({ noFilteredRows: "沒有符合條件的資料", noFilteredRowsDescription: "調整搜尋詞或狀態篩選試試。", copy: "複製", + mongoJsonPreview: "JSON 預覽", + mongoJsonPreviewEmpty: "選擇一筆 MongoDB 文件以預覽。", + copyJson: "複製 JSON", copyDdl: "複製 DDL", copyCell: "複製儲存格", copyRow: "複製整筆 (JSON)", diff --git a/apps/desktop/src/lib/dataGrid/dataGridSort.ts b/apps/desktop/src/lib/dataGrid/dataGridSort.ts index c1329f9af..7909840ab 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridSort.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridSort.ts @@ -23,6 +23,10 @@ type DataGridRow = DataGridCellValue[]; const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); export function sortDataGridRows(rows: readonly T[], columnIndex: number, direction: DataGridSortDirection): T[] { + return sortDataGridRowIndexes(rows, columnIndex, direction).map((index) => rows[index]!); +} + +export function sortDataGridRowIndexes(rows: readonly DataGridRow[], columnIndex: number, direction: DataGridSortDirection): number[] { const directionMultiplier = direction === "asc" ? 1 : -1; return rows .map((row, index) => ({ row, index })) @@ -33,7 +37,7 @@ export function sortDataGridRows(rows: readonly T[], colu if (compared !== 0) return compared * directionMultiplier; return left.index - right.index; }) - .map((item) => item.row); + .map((item) => item.index); } export function compareDataGridValues(left: DataGridCellValue, right: DataGridCellValue): number { diff --git a/apps/desktop/src/lib/mongo/mongoDocumentValues.ts b/apps/desktop/src/lib/mongo/mongoDocumentValues.ts index e45dc4193..7320b6eb8 100644 --- a/apps/desktop/src/lib/mongo/mongoDocumentValues.ts +++ b/apps/desktop/src/lib/mongo/mongoDocumentValues.ts @@ -56,6 +56,22 @@ export function buildMongoUpdateDocument(changes: Map, return doc; } +export function applyMongoGridChangesToDocument(document: unknown, changes: Map, columns: string[]): unknown { + if (!document || typeof document !== "object" || Array.isArray(document)) return document; + + const updated = { ...(document as Record) }; + for (const [colIdx, newVal] of changes) { + const column = columns[colIdx]; + if (!column || column === "_id") continue; + if (newVal === null) { + delete updated[column]; + } else { + updated[column] = parseMongoDocumentInputValue(newVal); + } + } + return updated; +} + export function buildMongoInsertDocument(row: MongoInputValue[], columns: string[]): Record { const doc: Record = {}; for (let ci = 0; ci < columns.length; ci++) { diff --git a/apps/desktop/src/lib/mongo/mongoShellCommand.ts b/apps/desktop/src/lib/mongo/mongoShellCommand.ts index f7ea5fa69..9404d290e 100644 --- a/apps/desktop/src/lib/mongo/mongoShellCommand.ts +++ b/apps/desktop/src/lib/mongo/mongoShellCommand.ts @@ -498,6 +498,7 @@ export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeM return { columns, rows, + mongo_documents: documents, affected_rows: total, execution_time_ms: Math.max(0, Math.round(executionTimeMs)), truncated: total > documents.length, diff --git a/apps/desktop/src/lib/tabs/tabResultCache.ts b/apps/desktop/src/lib/tabs/tabResultCache.ts index 1826cc652..4a056531b 100644 --- a/apps/desktop/src/lib/tabs/tabResultCache.ts +++ b/apps/desktop/src/lib/tabs/tabResultCache.ts @@ -16,6 +16,13 @@ export interface TabResultSnapshot { result?: QueryResult; results?: QueryResult[]; activeResultIndex?: number; + /** + * Source ordering retained while a local grid sort is active. It must travel + * with the snapshot so clearing the sort after a cache/archive restore can + * still return to the original result order. + */ + resultLocalSortOriginalRows?: QueryResult["rows"]; + resultLocalSortOriginalMongoDocuments?: QueryResult["mongo_documents"]; resultRuns?: QueryTab["resultRuns"]; activeResultRunId?: string; queryAnalysis?: QueryTab["queryAnalysis"]; @@ -36,6 +43,7 @@ interface ColumnarQueryResult { column_types?: string[]; columnValues: CellValue[][]; rowCount: number; + mongo_documents?: unknown[]; affected_rows: number; execution_time_ms: number; truncated?: boolean; @@ -128,6 +136,7 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin columns: [...result.columns], column_types: result.column_types ? [...result.column_types] : undefined, rows: result.rows.map((row) => [...row]), + mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined, affected_rows: result.affected_rows, execution_time_ms: result.execution_time_ms, truncated: result.truncated, @@ -147,6 +156,8 @@ function stripResultRunSessionIds(resultRuns: QueryTab["resultRuns"]): QueryTab[ ...run, result: stripSessionIds(run.result), results: stripResultSessionIds(run.results), + resultLocalSortOriginalRows: run.resultLocalSortOriginalRows?.map((row) => [...row]), + resultLocalSortOriginalMongoDocuments: run.resultLocalSortOriginalMongoDocuments ? clonePlain(run.resultLocalSortOriginalMongoDocuments) : undefined, resultSessionId: undefined, })); } @@ -159,6 +170,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult column_types: result.column_types ? [...result.column_types] : undefined, columnValues, rowCount: result.rows.length, + mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined, affected_rows: result.affected_rows, execution_time_ms: result.execution_time_ms, truncated: result.truncated, @@ -175,6 +187,7 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul columns: [...result.columns], column_types: result.column_types ? [...result.column_types] : undefined, rows, + mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined, affected_rows: result.affected_rows, execution_time_ms: result.execution_time_ms, truncated: result.truncated, @@ -364,6 +377,8 @@ export function buildTabResultSnapshot(tab: QueryTab): TabResultSnapshot | undef result: stripSessionIds(tab.result), results: stripResultSessionIds(tab.results), activeResultIndex: tab.activeResultIndex, + resultLocalSortOriginalRows: tab.resultLocalSortOriginalRows?.map((row) => [...row]), + resultLocalSortOriginalMongoDocuments: tab.resultLocalSortOriginalMongoDocuments ? clonePlain(tab.resultLocalSortOriginalMongoDocuments) : undefined, resultRuns: stripResultRunSessionIds(tab.resultRuns), activeResultRunId: tab.activeResultRunId, queryAnalysis: tab.queryAnalysis ? clonePlain(tab.queryAnalysis) : undefined, diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index c1bacbb2a..1cbd11480 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -38,7 +38,7 @@ import { quoteTableIdentifier } 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 { sortDataGridRows, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort"; +import { sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort"; import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges"; import { clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor"; @@ -102,6 +102,7 @@ function droppedTableObjectSchemaCandidates(target: DroppedTableObjectTarget): S function markQueryResultRowsRaw(result: QueryResult): QueryResult { markRaw(result.rows); + if (result.mongo_documents) markRaw(result.mongo_documents); return result; } @@ -114,6 +115,8 @@ function markQueryResultRunsRowsRaw(resultRuns: NonNullable { tab.results = undefined; tab.activeResultIndex = undefined; tab.resultLocalSortOriginalRows = undefined; + tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultSortMode = undefined; tab.resultSessionId = undefined; tab.resultAccessedAt = undefined; @@ -414,6 +418,7 @@ export const useQueryStore = defineStore("query", () => { run.result = undefined; run.results = undefined; run.resultLocalSortOriginalRows = undefined; + run.resultLocalSortOriginalMongoDocuments = undefined; run.resultSessionId = undefined; run.queryAnalysis = undefined; run.querySourceColumns = undefined; @@ -436,7 +441,8 @@ export const useQueryStore = defineStore("query", () => { tab.resultSortColumnIndex = run.resultSortColumnIndex; tab.resultSortDirection = run.resultSortDirection; tab.resultSortMode = run.resultSortMode; - tab.resultLocalSortOriginalRows = undefined; + tab.resultLocalSortOriginalRows = run.resultLocalSortOriginalRows; + tab.resultLocalSortOriginalMongoDocuments = run.resultLocalSortOriginalMongoDocuments; tab.orderByInput = run.orderByInput; tab.resultPageSql = run.resultPageSql; tab.resultPageLimit = run.resultPageLimit; @@ -468,13 +474,15 @@ export const useQueryStore = defineStore("query", () => { const snapshotRun = snapshot?.resultRuns?.find((item) => item.id === runId); if (!snapshotRun) return run; - const restoredRun = { - ...run, - ...snapshotRun, - result: snapshotRun.result ? markQueryResultRowsRaw(snapshotRun.result) : undefined, - results: snapshotRun.results ? markQueryResultsRowsRaw(snapshotRun.results) : undefined, - resultCacheState: "memory" as const, - }; + const restoredRun = markQueryResultRunsRowsRaw([ + { + ...run, + ...snapshotRun, + result: snapshotRun.result ? markQueryResultRowsRaw(snapshotRun.result) : undefined, + results: snapshotRun.results ? markQueryResultsRowsRaw(snapshotRun.results) : undefined, + resultCacheState: "memory" as const, + }, + ])[0]!; tab.resultRuns = tab.resultRuns?.map((item) => (item.id === runId ? restoredRun : item)); return restoredRun; } @@ -575,6 +583,8 @@ export const useQueryStore = defineStore("query", () => { resultSortColumnIndex: tab.resultSortColumnIndex, resultSortDirection: tab.resultSortDirection, resultSortMode: tab.resultSortMode, + resultLocalSortOriginalRows: tab.resultLocalSortOriginalRows, + resultLocalSortOriginalMongoDocuments: tab.resultLocalSortOriginalMongoDocuments, orderByInput: tab.orderByInput, resultPageSql: tab.resultPageSql, resultPageLimit: tab.resultPageLimit, @@ -624,6 +634,8 @@ export const useQueryStore = defineStore("query", () => { resultSortColumnIndex: tab.resultSortColumnIndex, resultSortDirection: tab.resultSortDirection, resultSortMode: tab.resultSortMode, + resultLocalSortOriginalRows: tab.resultLocalSortOriginalRows, + resultLocalSortOriginalMongoDocuments: tab.resultLocalSortOriginalMongoDocuments, orderByInput: tab.orderByInput, resultPageSql: tab.resultPageSql, resultPageLimit: tab.resultPageLimit, @@ -671,17 +683,25 @@ export const useQueryStore = defineStore("query", () => { if (!tab.resultLocalSortOriginalRows) { tab.resultLocalSortOriginalRows = tab.result.rows.slice(); + tab.resultLocalSortOriginalMongoDocuments = tab.result.mongo_documents?.slice(); } - const rows = direction ? sortDataGridRows(tab.resultLocalSortOriginalRows, columnIndex, direction) : tab.resultLocalSortOriginalRows; - assignDisplayedResult(tab, { ...tab.result, rows }); + const originalRows = tab.resultLocalSortOriginalRows; + const rowIndexes = direction ? sortDataGridRowIndexes(originalRows, columnIndex, direction) : originalRows.map((_, index) => index); + const rows = rowIndexes.map((index) => originalRows[index]!); + const originalMongoDocuments = tab.resultLocalSortOriginalMongoDocuments; + const mongo_documents = originalMongoDocuments ? rowIndexes.map((index) => originalMongoDocuments[index]) : undefined; + assignDisplayedResult(tab, { ...tab.result, rows, mongo_documents }); tab.resultSortColumn = direction ? column : undefined; tab.resultSortColumnIndex = direction ? columnIndex : undefined; tab.resultSortDirection = direction ?? undefined; tab.resultSortMode = direction ? "local" : undefined; tab.resultSortedSql = undefined; - if (!direction) tab.resultLocalSortOriginalRows = undefined; + if (!direction) { + tab.resultLocalSortOriginalRows = undefined; + tab.resultLocalSortOriginalMongoDocuments = undefined; + } touchResult(tab); syncDisplayedResultRun(tab, tab.resultBaseSql ?? tab.lastExecutedSql ?? tab.sql); @@ -1408,6 +1428,7 @@ export const useQueryStore = defineStore("query", () => { resultSortDirection: undefined, resultSortMode: undefined, resultLocalSortOriginalRows: undefined, + resultLocalSortOriginalMongoDocuments: undefined, orderByInput: undefined, resultPageSql: undefined, resultPageLimit: undefined, @@ -2295,6 +2316,7 @@ export const useQueryStore = defineStore("query", () => { tab.executionId = executionId; tab.lastExecutedSql = sql; tab.resultLocalSortOriginalRows = undefined; + tab.resultLocalSortOriginalMongoDocuments = undefined; const updateActiveResultRun = !!tab.activeResultRunId && options?.preserveResultDuringExecution === true; if (!updateActiveResultRun) { tab.activeResultRunId = undefined; @@ -3039,6 +3061,7 @@ export const useQueryStore = defineStore("query", () => { tab.activeResultIndex = index; tab.result = tab.results[index]; tab.resultLocalSortOriginalRows = undefined; + tab.resultLocalSortOriginalMongoDocuments = undefined; tab.resultSortColumn = undefined; tab.resultSortColumnIndex = undefined; tab.resultSortDirection = undefined; @@ -3139,6 +3162,8 @@ export const useQueryStore = defineStore("query", () => { tab.results = results; tab.activeResultIndex = snapshot.activeResultIndex; tab.result = snapshot.result ? markQueryResultRowsRaw(snapshot.result) : results?.[activeIndex] ? markQueryResultRowsRaw(results[activeIndex]) : undefined; + tab.resultLocalSortOriginalRows = snapshot.resultLocalSortOriginalRows ? markRaw(snapshot.resultLocalSortOriginalRows) : undefined; + tab.resultLocalSortOriginalMongoDocuments = snapshot.resultLocalSortOriginalMongoDocuments ? markRaw(snapshot.resultLocalSortOriginalMongoDocuments) : undefined; tab.resultRuns = snapshot.resultRuns ? markQueryResultRunsRowsRaw(snapshot.resultRuns) : tab.resultRuns; tab.activeResultRunId = snapshot.activeResultRunId ?? tab.activeResultRunId; if (!tab.result && !tab.results && !tab.resultRuns) return false; diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 241b5c79d..66581508f 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -459,6 +459,11 @@ export interface QueryResult { */ column_sortables?: boolean[]; rows: (string | number | boolean | null)[][]; + /** + * Original MongoDB documents, kept in lockstep with `rows` for document + * preview. This is populated only for MongoDB document query results. + */ + mongo_documents?: unknown[]; affected_rows: number; execution_time_ms: number; truncated?: boolean; @@ -484,6 +489,7 @@ export interface QueryResultRun { resultSortDirection?: "asc" | "desc"; resultSortMode?: "database" | "local"; resultLocalSortOriginalRows?: QueryResult["rows"]; + resultLocalSortOriginalMongoDocuments?: QueryResult["mongo_documents"]; orderByInput?: string; resultPageSql?: string; resultPageLimit?: number; @@ -698,6 +704,7 @@ export interface QueryTab { resultSortDirection?: "asc" | "desc"; resultSortMode?: "database" | "local"; resultLocalSortOriginalRows?: QueryResult["rows"]; + resultLocalSortOriginalMongoDocuments?: QueryResult["mongo_documents"]; orderByInput?: string; resultPageSql?: string; resultPageLimit?: number; diff --git a/packages/app-tests/dataGridSort.test.ts b/packages/app-tests/dataGridSort.test.ts index 7164146aa..c9378dc11 100644 --- a/packages/app-tests/dataGridSort.test.ts +++ b/packages/app-tests/dataGridSort.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; -import { sortDataGridRows } from "../../apps/desktop/src/lib/dataGrid/dataGridSort.ts"; +import { sortDataGridRowIndexes, sortDataGridRows } from "../../apps/desktop/src/lib/dataGrid/dataGridSort.ts"; test("sortDataGridRows sorts numbers numerically and keeps null values last", () => { const rows = [ @@ -43,3 +43,10 @@ test("sortDataGridRows sorts ISO date strings by time", () => { assert.deepEqual(sortDataGridRows(rows, 0, "asc"), [["2025-12-31"], ["2026-01-01"], ["2026-02-01"]]); }); + +test("sortDataGridRowIndexes preserves stable source ordering", () => { + const rows = [["item-10"], ["item-2"], ["item-2"]]; + + assert.deepEqual(sortDataGridRowIndexes(rows, 0, "asc"), [1, 2, 0]); + assert.deepEqual(sortDataGridRowIndexes(rows, 0, "desc"), [0, 1, 2]); +}); diff --git a/packages/app-tests/mongoDocumentValues.test.ts b/packages/app-tests/mongoDocumentValues.test.ts index 03fc47bbf..5695c7e92 100644 --- a/packages/app-tests/mongoDocumentValues.test.ts +++ b/packages/app-tests/mongoDocumentValues.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { test } from "vitest"; -import { buildMongoCopyInsertDocument, buildMongoInsertDocument, buildMongoUpdateDocument, formatMongoShellLiteral, parseMongoDocumentInputValue } from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts"; +import { applyMongoGridChangesToDocument, buildMongoCopyInsertDocument, buildMongoInsertDocument, buildMongoUpdateDocument, formatMongoShellLiteral, parseMongoDocumentInputValue } from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts"; test("parses Mongo shell ISODate literals as extended JSON dates", () => { assert.deepEqual(parseMongoDocumentInputValue('ISODate("2026-06-10T13:59:31.287Z")'), { @@ -36,6 +36,32 @@ test("builds Mongo grid updates with set and unset operators", () => { }); }); +test("applies saved Mongo grid changes to the raw preview document", () => { + const original = { + _id: "1", + name: "Ada", + profile: { role: "admin" }, + archivedAt: "2026-01-01", + }; + const changes = new Map([ + [1, "Lin"], + [2, '{"role":"maintainer"}'], + [3, null], + ]); + + assert.deepEqual(applyMongoGridChangesToDocument(original, changes, ["_id", "name", "profile", "archivedAt"]), { + _id: "1", + name: "Lin", + profile: { role: "maintainer" }, + }); + assert.deepEqual(original, { + _id: "1", + name: "Ada", + profile: { role: "admin" }, + archivedAt: "2026-01-01", + }); +}); + test("builds Mongo inserts with parsed date values", () => { assert.deepEqual(buildMongoInsertDocument(["ignored", 'new Date("2026-06-10T13:59:31.287Z")'], ["_id", "createdAt"]), { createdAt: { $date: "2026-06-10T13:59:31.287Z" }, diff --git a/packages/app-tests/mongoShellCommand.test.ts b/packages/app-tests/mongoShellCommand.test.ts index 27fd5f8c0..878f1501a 100644 --- a/packages/app-tests/mongoShellCommand.test.ts +++ b/packages/app-tests/mongoShellCommand.test.ts @@ -480,6 +480,10 @@ test("mongoDocumentsToQueryResult turns mongo documents into grid rows", () => { ["1", "Ada", '{"role":"admin"}', null], ["2", "Lin", null, true], ]); + assert.deepEqual(result.mongo_documents, [ + { _id: "1", name: "Ada", profile: { role: "admin" } }, + { _id: "2", active: true, name: "Lin" }, + ]); assert.equal(result.affected_rows, 12); assert.equal(result.execution_time_ms, 5); assert.equal(result.truncated, true); diff --git a/packages/app-tests/queryResultArchive.test.ts b/packages/app-tests/queryResultArchive.test.ts index c782e0f41..dec9c87c7 100644 --- a/packages/app-tests/queryResultArchive.test.ts +++ b/packages/app-tests/queryResultArchive.test.ts @@ -34,6 +34,10 @@ test("query result archives round-trip query tab metadata and result runs", asyn [1, "Ada"], [2, "Linus"], ], + mongo_documents: [ + { _id: "1", profile: { role: "admin" } }, + { _id: "2", profile: { role: "maintainer" } }, + ], affected_rows: 0, execution_time_ms: 3, session_id: "live-session", @@ -60,6 +64,8 @@ test("query result archives round-trip query tab metadata and result runs", asyn affected_rows: 0, execution_time_ms: 5, }, + resultLocalSortOriginalRows: [[1, "pending"]], + resultLocalSortOriginalMongoDocuments: [{ _id: "1", status: "pending" }], }); const snapshot = buildTabResultSnapshot(tab); assert.ok(snapshot); @@ -79,7 +85,13 @@ test("query result archives round-trip query tab metadata and result runs", asyn [1, "Ada"], [2, "Linus"], ]); + assert.deepEqual(decoded?.snapshot.resultRuns?.[0]?.result?.mongo_documents, [ + { _id: "1", profile: { role: "admin" } }, + { _id: "2", profile: { role: "maintainer" } }, + ]); assert.equal(decoded?.snapshot.resultRuns?.[0]?.result?.session_id, undefined); + assert.deepEqual(decoded?.snapshot.resultLocalSortOriginalRows, [[1, "pending"]]); + assert.deepEqual(decoded?.snapshot.resultLocalSortOriginalMongoDocuments, [{ _id: "1", status: "pending" }]); }); test("query result archives reject invalid files", async () => { diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 0713cf8de..81befc7e0 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -705,6 +705,11 @@ test("sortTabResultLocally sorts current rows and restores original order", () = [1, "Ada"], [3, "Linus"], ], + mongo_documents: [ + { id: 2, name: "Grace", nested: { level: 2 } }, + { id: 1, name: "Ada", nested: { level: 1 } }, + { id: 3, name: "Linus", nested: { level: 3 } }, + ], affected_rows: 0, execution_time_ms: 1, }; @@ -716,12 +721,22 @@ test("sortTabResultLocally sorts current rows and restores original order", () = [2, "Grace"], [3, "Linus"], ]); + assert.deepEqual(tab.result?.mongo_documents?.map((document) => (document as { id: number }).id), [1, 2, 3]); assert.equal(tab.resultSortColumn, "name"); assert.equal(tab.resultSortColumnIndex, 1); assert.equal(tab.resultSortDirection, "asc"); assert.equal(tab.resultSortMode, "local"); assert.equal(tab.resultSortedSql, undefined); + store.sortTabResultLocally(tabId, "name", 1, "desc"); + + assert.deepEqual(tab.result?.rows, [ + [3, "Linus"], + [2, "Grace"], + [1, "Ada"], + ]); + assert.deepEqual(tab.result?.mongo_documents?.map((document) => (document as { id: number }).id), [3, 2, 1]); + store.sortTabResultLocally(tabId, "name", 1, null); assert.deepEqual(tab.result?.rows, [ @@ -729,6 +744,7 @@ test("sortTabResultLocally sorts current rows and restores original order", () = [1, "Ada"], [3, "Linus"], ]); + assert.deepEqual(tab.result?.mongo_documents?.map((document) => (document as { id: number }).id), [2, 1, 3]); assert.equal(tab.resultSortColumn, undefined); assert.equal(tab.resultSortMode, undefined); }); diff --git a/packages/app-tests/tabResultCache.test.ts b/packages/app-tests/tabResultCache.test.ts index db1accb25..2d289bfec 100644 --- a/packages/app-tests/tabResultCache.test.ts +++ b/packages/app-tests/tabResultCache.test.ts @@ -21,6 +21,7 @@ test("result snapshots strip live session handles and clone result rows", () => result: { columns: ["id"], rows: [[1]], + mongo_documents: [{ _id: "1", profile: { role: "admin" } }], affected_rows: 0, execution_time_ms: 1, session_id: "live-session", @@ -37,6 +38,8 @@ test("result snapshots strip live session handles and clone result rows", () => }, ], activeResultIndex: 0, + resultLocalSortOriginalRows: [[2]], + resultLocalSortOriginalMongoDocuments: [{ _id: "2", profile: { role: "maintainer" } }], }); const snapshot = buildTabResultSnapshot(tab); @@ -46,8 +49,13 @@ test("result snapshots strip live session handles and clone result rows", () => assert.equal(snapshot?.result?.sourceStatement, "select * from public.users"); assert.equal(snapshot?.results?.[0]?.session_id, undefined); assert.deepEqual(snapshot?.result?.rows, [[1]]); + assert.deepEqual(snapshot?.result?.mongo_documents, [{ _id: "1", profile: { role: "admin" } }]); + assert.deepEqual(snapshot?.resultLocalSortOriginalRows, [[2]]); + assert.deepEqual(snapshot?.resultLocalSortOriginalMongoDocuments, [{ _id: "2", profile: { role: "maintainer" } }]); tab.result!.rows[0]![0] = 2; + tab.resultLocalSortOriginalRows![0]![0] = 3; assert.deepEqual(snapshot?.result?.rows, [[1]]); + assert.deepEqual(snapshot?.resultLocalSortOriginalRows, [[2]]); }); test("result snapshots strip session handles from result runs", () => { @@ -68,6 +76,8 @@ test("result snapshots strip session handles from result runs", () => { sourceLabel: "users", sourceStatement: "select * from users", }, + resultLocalSortOriginalRows: [[2]], + resultLocalSortOriginalMongoDocuments: [{ _id: "2", role: "maintainer" }], }, ], }); @@ -78,6 +88,8 @@ test("result snapshots strip session handles from result runs", () => { 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]]); + assert.deepEqual(snapshot?.resultRuns?.[0]?.resultLocalSortOriginalRows, [[2]]); + assert.deepEqual(snapshot?.resultRuns?.[0]?.resultLocalSortOriginalMongoDocuments, [{ _id: "2", role: "maintainer" }]); }); test("result snapshots encode as binary columnar payloads and decode back to rows", () => { @@ -89,6 +101,10 @@ test("result snapshots encode as binary columnar payloads and decode back to row [1, "Ada", true], [2, "Linus", false], ], + mongo_documents: [ + { _id: "1", name: "Ada", tags: ["admin"] }, + { _id: "2", name: "Linus", tags: ["maintainer"] }, + ], affected_rows: 0, execution_time_ms: 3, session_id: "live-session", @@ -96,6 +112,14 @@ test("result snapshots encode as binary columnar payloads and decode back to row sourceLabel: "public.users", sourceStatement: "select id, name, active from public.users", }, + resultLocalSortOriginalRows: [ + [2, "Linus", false], + [1, "Ada", true], + ], + resultLocalSortOriginalMongoDocuments: [ + { _id: "2", name: "Linus", tags: ["maintainer"] }, + { _id: "1", name: "Ada", tags: ["admin"] }, + ], }), ); assert.ok(snapshot); @@ -109,6 +133,18 @@ test("result snapshots encode as binary columnar payloads and decode back to row [1, "Ada", true], [2, "Linus", false], ]); + assert.deepEqual(decoded?.result?.mongo_documents, [ + { _id: "1", name: "Ada", tags: ["admin"] }, + { _id: "2", name: "Linus", tags: ["maintainer"] }, + ]); + assert.deepEqual(decoded?.resultLocalSortOriginalRows, [ + [2, "Linus", false], + [1, "Ada", true], + ]); + assert.deepEqual(decoded?.resultLocalSortOriginalMongoDocuments, [ + { _id: "2", name: "Linus", tags: ["maintainer"] }, + { _id: "1", name: "Ada", tags: ["admin"] }, + ]); assert.equal(decoded?.result?.session_id, undefined); assert.equal(decoded?.result?.has_more, true); assert.equal(decoded?.result?.sourceLabel, "public.users");