feat(mongo): add document JSON preview

This commit is contained in:
二丫讲梵 2026-07-11 01:00:47 +08:00 committed by GitHub
parent ee4658bac2
commit 264909aaa9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 387 additions and 18 deletions

View File

@ -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<CellDetailTab>(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<HTMLElement>();
const valueEditorContainer = ref<HTMLElement>();
const sideJsonPreviewContainer = ref<HTMLElement>();
const dialogJsonPreviewContainer = ref<HTMLElement>();
const mongoJsonPreviewContainer = ref<HTMLElement>();
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<IndexInfo[]>([]);
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<ContextMenuItem[]>(() => {
{{ t("grid.keylessEditWarningHint") }}
</TooltipContent>
</Tooltip>
<Tooltip v-if="canShowMongoJsonPreview">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="sm"
:class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '', mongoJsonPreviewOpen ? 'text-primary bg-primary/10 hover:bg-primary/15' : '']"
:aria-pressed="mongoJsonPreviewOpen"
@click="toggleMongoJsonPreview"
>
<Code2 class="data-grid-topbar-action-icon w-3 h-3" />
<span class="data-grid-topbar-action-label" :class="{ 'data-grid-topbar-action-label--compact': compactDataGridToolbar }">{{ t("grid.mongoJsonPreview") }}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{{ t("grid.mongoJsonPreview") }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '', isSaving ? '' : '']" :disabled="isSaving" @click="onToolbarRefresh">
@ -10575,6 +10708,30 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</TabsContent>
</Tabs>
</div>
<!-- MongoDB document JSON preview -->
<div v-else-if="mongoJsonPreviewOpen" class="relative col-start-3 row-start-1 flex min-w-0 flex-col border-l bg-background" :class="{ 'detail-drawer-resizing': isResizingMongoJsonPreview }" :style="mongoJsonPreviewStyle" @contextmenu="onDrawerContextMenu">
<div class="absolute bottom-0 left-0 top-0 z-20 w-1.5 -translate-x-1/2 cursor-col-resize hover:bg-primary/30" @mousedown.prevent="onMongoJsonPreviewResizeStart" />
<div class="flex h-9 shrink-0 items-center gap-2 border-b bg-muted/20 px-3">
<Code2 class="h-3.5 w-3.5 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate text-xs font-medium">{{ t("grid.mongoJsonPreview") }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!mongoJsonPreviewFullText" :title="t('grid.copyJson')" @click="copyMongoJsonPreview">
<Copy class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="closeMongoJsonPreview">
<X class="h-3 w-3" />
</Button>
</div>
<div v-if="mongoJsonPreviewText" class="flex min-h-0 flex-1 flex-col overflow-hidden p-2">
<div v-if="mongoJsonPreviewUsesCodeEditor" ref="mongoJsonPreviewContainer" data-cell-detail-editor-root class="min-h-0 flex-1 overflow-hidden" />
<template v-else>
<pre class="min-h-0 flex-1 overflow-auto rounded border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap break-words">{{ mongoJsonPreviewText }}</pre>
<div class="mt-1 text-[11px] text-muted-foreground">{{ t("grid.largeValuePreviewHint", { count: mongoJsonPreviewText.length }) }}</div>
</template>
</div>
<div v-else class="flex min-h-0 flex-1 items-center justify-center px-6 text-center text-xs text-muted-foreground">
{{ t("grid.mongoJsonPreviewEmpty") }}
</div>
</div>
</div>
</div>
</CustomContextMenu>

View File

@ -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<CustomSaveHandler | undefined>(() =
return stmts;
};
return { save, preview, canInsert: false, canDelete: false, supportsInsert: false, readonlyColumns: [target.idColumn], targetLabel: target.collection };
const applySavedChanges: NonNullable<CustomSaveHandler["applySavedChanges"]> = ({ 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<unknown, unknown>();
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);

View File

@ -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;

View File

@ -53,6 +53,7 @@ type GridScrollerRef =
export interface CustomSaveHandler {
save: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; newRows: CellValue[][]; deletedRows: Set<number>; columns: string[]; rows: CellValue[][] }) => Promise<void>;
applySavedChanges?: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; columns: string[] }) => void;
preview?: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; newRows: CellValue[][]; deletedRows: Set<number>; columns: string[]; rows: CellValue[][] }) => Promise<string[]>;
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();

View File

@ -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)",

View File

@ -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)",

View File

@ -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)",

View File

@ -631,6 +631,9 @@ export default withEnglishFallback({
noFilteredRows: "フィルターに一致する行がありません",
noFilteredRowsDescription: "検索テキストまたは行ステータスフィルターを調整してください。",
copy: "コピー",
mongoJsonPreview: "JSON プレビュー",
mongoJsonPreviewEmpty: "プレビューする MongoDB ドキュメントを選択してください。",
copyJson: "JSON をコピー",
copyDdl: "DDLをコピー",
copyCell: "セルをコピー",
copyRow: "行をコピー (JSON)",

View File

@ -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)",

View File

@ -686,6 +686,9 @@ export default withEnglishFallback({
noFilteredRows: "没有符合条件的行",
noFilteredRowsDescription: "调整搜索词或状态筛选试试。",
copy: "复制",
mongoJsonPreview: "JSON 预览",
mongoJsonPreviewEmpty: "选择一条 MongoDB 文档以预览。",
copyJson: "复制 JSON",
copyDdl: "复制 DDL",
copyCell: "复制单元格",
copyRow: "复制行 (JSON)",

View File

@ -633,6 +633,9 @@ export default withEnglishFallback({
noFilteredRows: "沒有符合條件的資料",
noFilteredRowsDescription: "調整搜尋詞或狀態篩選試試。",
copy: "複製",
mongoJsonPreview: "JSON 預覽",
mongoJsonPreviewEmpty: "選擇一筆 MongoDB 文件以預覽。",
copyJson: "複製 JSON",
copyDdl: "複製 DDL",
copyCell: "複製儲存格",
copyRow: "複製整筆 (JSON)",

View File

@ -23,6 +23,10 @@ type DataGridRow = DataGridCellValue[];
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
export function sortDataGridRows<T extends DataGridRow>(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<T extends DataGridRow>(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 {

View File

@ -56,6 +56,22 @@ export function buildMongoUpdateDocument(changes: Map<number, MongoInputValue>,
return doc;
}
export function applyMongoGridChangesToDocument(document: unknown, changes: Map<number, MongoInputValue>, columns: string[]): unknown {
if (!document || typeof document !== "object" || Array.isArray(document)) return document;
const updated = { ...(document as Record<string, unknown>) };
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<string, unknown> {
const doc: Record<string, unknown> = {};
for (let ci = 0; ci < columns.length; ci++) {

View File

@ -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,

View File

@ -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,

View File

@ -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<QueryTab["resultRuns
for (const run of resultRuns) {
if (run.result) markQueryResultRowsRaw(run.result);
if (run.results) markQueryResultsRowsRaw(run.results);
if (run.resultLocalSortOriginalRows) markRaw(run.resultLocalSortOriginalRows);
if (run.resultLocalSortOriginalMongoDocuments) markRaw(run.resultLocalSortOriginalMongoDocuments);
}
return resultRuns;
}
@ -388,6 +391,7 @@ export const useQueryStore = defineStore("query", () => {
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;

View File

@ -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;

View File

@ -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]);
});

View File

@ -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<number, string | number | boolean | null>([
[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" },

View File

@ -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);

View File

@ -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 () => {

View File

@ -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);
});

View File

@ -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");