fix(mongodb): display array structures as shell-style bson literals

This commit is contained in:
onenewcode
2026-09-29 12:22:42 +08:00
committed by GitHub
parent 27078b8eed
commit cf78eebbc9
8 changed files with 223 additions and 34 deletions
@@ -716,7 +716,9 @@ async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExporte
}
}
const result = mongoDocumentsToQueryResult(exportedDocuments, performance.now() - exportStartedAt, totalRows ?? exportedDocuments.length, exportedCopyDocuments, totalRows !== null);
// Exports consume source text (CSV/SQL), not the live grid's BSON-faithful
// cell encoding, so every document store keeps plain JSON cells here.
const result = mongoDocumentsToQueryResult(exportedDocuments, performance.now() - exportStartedAt, totalRows ?? exportedDocuments.length, exportedCopyDocuments, totalRows !== null, { documentGridValues: false });
if (result.columns.length === 0) result.columns = gridResult.value.columns;
result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : kind === "elasticsearch" ? elasticsearchGridColumnTypesFor(result.columns) : kind === "solr" ? solrGridColumnTypesFor(result.columns) : undefined;
result.affected_rows = exportedDocuments.length;
+22 -16
View File
@@ -108,7 +108,7 @@ import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructur
import { buildTableSelectSql, qualifyTableReferencesInSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
import { uuid } from "@/lib/common/utils";
import { generateCellValues, type CellValueGenerationKind } from "@/lib/dataGrid/cellValueGeneration";
import { MONGO_DOCUMENT_GRID_NULL, mongoDocumentGridClipboardText, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridExternalValue, mongoDocumentGridInputValue } from "@/lib/mongo/mongoDocumentValues";
import { MONGO_DOCUMENT_GRID_NULL, mongoDocumentGridClipboardText, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridExternalValue, mongoDocumentGridInputValue, mongoDocumentRelaxedExtendedJson } from "@/lib/mongo/mongoDocumentValues";
import { compactHeaderColumnType, formatMetadataColumnTypeLabel, isNumericColumnType, resolveDataGridTypeVisualKind, resolveHeaderColumnType, resolveResultColumnType } from "@/lib/dataGrid/dataGridColumnType";
import { dataGridCellTextClass, dataGridTypeVisualClass } from "@/lib/dataGrid/dataGridCellTextVisual";
import { DATA_GRID_TYPE_COLOR_KEYS, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
@@ -745,6 +745,10 @@ const columnTypeMap = computed(() => {
});
const resolvedConnectionConfig = computed(() => connectionStore.getConfig(props.connectionId ?? ""));
const resolvedDatabaseType = computed(() => props.databaseType ?? effectiveDatabaseTypeForConnection(resolvedConnectionConfig.value));
// The collection grid and Mongo query-result grids share the document-grid value
// encoding (BSON null sentinel + JSON-prefixed containers), so every display,
// editor and clipboard path must decode it whenever MongoDB values are on screen.
const usesMongoDocumentGridValues = computed(() => props.mongoCollectionGrid === true || resolvedDatabaseType.value === "mongodb");
const isResultsContext = computed(() => props.context === "results");
const canShowWhereSearch = computed(() => !!props.onExecuteSql && !isResultsContext.value && resolvedDatabaseType.value !== "victoriametrics");
const canUseWhereSearch = computed(() => !!props.tableMeta && canShowWhereSearch.value);
@@ -2210,7 +2214,7 @@ const columnAligns = computed<("left" | "right")[]>(() => {
function gridCellTextColorClass(item: RowItem, actualColIdx: number, visibleColIdx: number): string {
const value = item.data[actualColIdx];
const isGridNull = value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL);
const isGridNull = value === null || (usesMongoDocumentGridValues.value && value === MONGO_DOCUMENT_GRID_NULL);
if (isGridNull) return "text-muted-foreground italic";
if (!colorizeDataGridCellTypes.value) return "text-foreground";
const checkbox = booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && value !== null;
@@ -2233,7 +2237,7 @@ function transposeCellTextColorClass(recordIndex: number, actualColIdx: number):
const item = displayItems.value[recordIndex];
if (!item) return "text-foreground";
const value = item.data[actualColIdx];
const isGridNull = value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL);
const isGridNull = value === null || (usesMongoDocumentGridValues.value && value === MONGO_DOCUMENT_GRID_NULL);
if (isGridNull) return "text-muted-foreground italic";
if (!colorizeDataGridCellTypes.value) return "text-foreground";
return dataGridCellTextClass({
@@ -4019,7 +4023,7 @@ const editor = useDataGridEditor({
includeDatabaseNameInSaveSql: computed(() => settingsStore.editorSettings.generateSqlIncludeDatabaseName),
initialEditColumn: firstVisibleColumnIndex,
cellEditorText: cellEditorTextForValue,
normalizeEditorInput: (value) => (props.mongoCollectionGrid ? mongoDocumentGridInputValue(value) : value),
normalizeEditorInput: (value) => (usesMongoDocumentGridValues.value ? mongoDocumentGridInputValue(value) : value),
getRowItem,
pageSize,
currentPage,
@@ -4303,7 +4307,7 @@ function cellEditContentNeedsExpandedEditor(options: { displayText: string; edit
}
function cellEditorTextForValue(value: CellValue | undefined, columnIndex: number): string {
if (props.mongoCollectionGrid) {
if (usesMongoDocumentGridValues.value) {
const documentGridText = mongoDocumentGridEditorText(value);
if (documentGridText !== undefined) return documentGridText;
}
@@ -4374,7 +4378,7 @@ function isIoTDBTimestampColumn(columnIndex: number): boolean {
}
function inlineCellEditorText(value: CellValue, columnIndex: number): string {
if (props.mongoCollectionGrid) {
if (usesMongoDocumentGridValues.value) {
const documentGridText = mongoDocumentGridEditorText(value);
if (documentGridText !== undefined) return documentGridText;
}
@@ -5981,11 +5985,11 @@ const contextCellDetail = computed(() => {
// The MongoDB collection grid stores an internal sentinel for explicit BSON
// null; detail panes must render display text instead of leaking that marker.
function gridDetailRawValue(value: CellValue): string {
return props.mongoCollectionGrid ? (mongoDocumentGridDisplayText(value) ?? displayCellValue(value)) : displayCellValue(value);
return usesMongoDocumentGridValues.value ? (mongoDocumentGridDisplayText(value) ?? displayCellValue(value)) : displayCellValue(value);
}
function gridDetailIsNullValue(value: CellValue): boolean {
return value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL);
return value === null || (usesMongoDocumentGridValues.value && value === MONGO_DOCUMENT_GRID_NULL);
}
function cellDetailFor(rowIndex: number, columnIndex: number): DataGridCellDetail | null {
@@ -6030,7 +6034,9 @@ const mongoJsonPreviewFullText = computed(() => {
const document = activeMongoJsonDocument.value;
if (document === undefined) return "";
try {
return JSON.stringify(document, null, 2) ?? "";
// Relaxed Extended JSON (`{"$date": "…"}`) like Compass's JSON view, instead
// of the browser form's escaped `ISODate("…")` strings.
return JSON.stringify(mongoDocumentRelaxedExtendedJson(document), null, 2) ?? "";
} catch {
return "";
}
@@ -6324,7 +6330,7 @@ const detailEdit = useDataGridCellDetailEdit({
resultRows: computed(() => props.result.rows),
getColumnInfo: (columnIndex) => tableColumnForGridColumn(columnIndex) ?? resultColumnInfoForGridColumn(columnIndex),
cellEditorText: cellEditorTextForValue,
normalizeEditorInput: (value) => (props.mongoCollectionGrid ? mongoDocumentGridInputValue(value) : value),
normalizeEditorInput: (value) => (usesMongoDocumentGridValues.value ? mongoDocumentGridInputValue(value) : value),
nullValue: () => (props.mongoCollectionGrid ? MONGO_DOCUMENT_GRID_NULL : null),
getRowItem,
hydrateLargeValueCell,
@@ -6656,7 +6662,7 @@ function primitiveCellFormatKey(value: CellValue, columnIndex?: number): string
function formatCell(value: CellValue, columnIndex?: number, originalBytes?: number, limitDisplay = true): string {
const formatter = columnIndex === undefined ? undefined : resolvedColumnFormatters.value[columnIndex];
if (props.mongoCollectionGrid) {
if (usesMongoDocumentGridValues.value) {
const documentGridText = mongoDocumentGridDisplayText(value, formatter);
if (documentGridText !== undefined) return documentGridText;
}
@@ -7603,7 +7609,7 @@ function drawCanvasGrid() {
searchMatchKeys: searchMatchSet.value,
currentSearchMatch: currentSearchMatch.value,
formatCell: (value, columnIndex, row) => formatCellCached(visibleLargeValuePreviewValue(row, columnIndex, value), columnIndex, largeValueOriginalBytes(row, columnIndex)),
isNullValue: (value) => value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL),
isNullValue: (value) => value === null || (usesMongoDocumentGridValues.value && value === MONGO_DOCUMENT_GRID_NULL),
newRowCellPlaceholder,
isRowActive,
rowCellsUseSelectionVisual,
@@ -7935,8 +7941,8 @@ const {
columnComments: visibleColumnComments,
allColumnComments,
displayValue: formatCellCached,
cellClipboardText: (value) => (props.mongoCollectionGrid ? mongoDocumentGridClipboardText(value) : undefined),
externalCellValue: (value) => (props.mongoCollectionGrid ? mongoDocumentGridExternalValue(value) : value),
cellClipboardText: (value) => (usesMongoDocumentGridValues.value ? mongoDocumentGridClipboardText(value) : undefined),
externalCellValue: (value) => (usesMongoDocumentGridValues.value ? mongoDocumentGridExternalValue(value) : value),
mongoDocuments: computed(() => props.result.mongo_copy_documents ?? props.result.mongo_documents),
spatialColumns: computed(() => props.result.spatial_columns),
spatialValues: computed(() => props.result.spatial_values),
@@ -9588,7 +9594,7 @@ async function onGridKeydown(event: KeyboardEvent) {
}
function detailClipboardText(detail: DataGridCellDetail): string {
if (props.mongoCollectionGrid) {
if (usesMongoDocumentGridValues.value) {
const documentGridText = mongoDocumentGridClipboardText(detail.value);
if (documentGridText !== undefined) return documentGridText;
}
@@ -9603,7 +9609,7 @@ function detailClipboardText(detail: DataGridCellDetail): string {
// grid's BSON null marker is restored to a real null instead of leaking the
// internal sentinel into clipboard JSON/TSV.
function gridDetailExternalValue(value: CellValue): CellValue {
return props.mongoCollectionGrid ? mongoDocumentGridExternalValue(value) : value;
return usesMongoDocumentGridValues.value ? mongoDocumentGridExternalValue(value) : value;
}
async function copyDetailValue() {
@@ -24,7 +24,7 @@ describe("MongoDB document date display", () => {
it("formats nested dates without changing non-date BSON values", () => {
const value = mongoDocumentGridValue({ event: { at: { $date: "2026-09-24T02:00:00.000Z" }, sequence: { $numberLong: "9007199254740993" } }, tags: ["a"] });
expect(mongoDocumentGridDisplayText(value, dateFormatter("Asia/Shanghai"))).toBe('{"event":{"at":"2026-09-24 10:00:00","sequence":{"$numberLong":"9007199254740993"}},"tags":["a"]}');
expect(mongoDocumentGridDisplayText(value, dateFormatter("Asia/Shanghai"))).toBe('{"event": {"at": "2026-09-24 10:00:00", "sequence": NumberLong("9007199254740993")}, "tags": ["a"]}');
});
it("uses local time when no timezone override is configured", () => {
@@ -32,9 +32,9 @@ describe("MongoDB document date display", () => {
expect(mongoDocumentGridDisplayText(value, dateFormatter(undefined))).toMatch(/^2026-09-2[34] \d{2}:00:00$/);
});
it("falls back to the original Extended JSON for an invalid timezone", () => {
it("falls back to the shell-style display for an invalid timezone", () => {
const value = mongoDocumentGridValue({ nested: { $date: "2026-09-24T02:00:00.000Z" } });
expect(mongoDocumentGridDisplayText(value, dateFormatter("Invalid/Timezone"))).toBe('{"nested":{"$date":"2026-09-24T02:00:00.000Z"}}');
expect(mongoDocumentGridDisplayText(value, dateFormatter("Invalid/Timezone"))).toBe('{"nested": ISODate("2026-09-24T02:00:00.000Z")}');
});
it("keeps edit, copy, and external values independent from display formatting", () => {
@@ -52,3 +52,38 @@ describe("MongoDB document date display", () => {
expect(mongoDocumentGridDisplayText(value, dateFormatter("Asia/Shanghai"))).toBeUndefined();
});
});
describe("MongoDB array structure display", () => {
it("renders arrays with shell-style BSON literals instead of the internal JSON encoding", () => {
const value = mongoDocumentGridValue({
ids: [{ $oid: "6743e4bfa3f6f84bc3fff6c8" }],
history: [{ at: 'ISODate("2026-06-10T13:59:31.287Z")', by: { $oid: "507f1f77bcf86cd799439011" } }],
counts: [{ $numberLong: "2326645729978441729" }, 2],
});
expect(mongoDocumentGridDisplayText(value)).toBe('{"ids": [ObjectId("6743e4bfa3f6f84bc3fff6c8")], "history": [{"at": ISODate("2026-06-10T13:59:31.287Z"), "by": ObjectId("507f1f77bcf86cd799439011")}], "counts": [NumberLong("2326645729978441729"), 2]}');
});
it("keeps the structure separators readable for scalar arrays and nested containers", () => {
expect(mongoDocumentGridDisplayText(mongoDocumentGridValue(["a", "b"]))).toBe('["a", "b"]');
expect(mongoDocumentGridDisplayText(mongoDocumentGridValue({ tags: [], matrix: [[1, 2], [3]] }))).toBe('{"tags": [], "matrix": [[1, 2], [3]]}');
});
it("still treats JSON-shaped BSON strings as data, not as structure", () => {
expect(mongoDocumentGridDisplayText(mongoDocumentGridValue({ note: "[1,2]" }))).toBe('{"note": "[1,2]"}');
});
it("keeps edit, copy, and external JSON untouched by the shell-style display", () => {
const original = { items: [{ at: { $date: "2026-06-10T13:59:31.287Z" } }] };
const value = mongoDocumentGridValue(original) as string;
const rawJson = JSON.stringify(original);
expect(mongoDocumentGridDisplayText(value)).toBe('{"items": [{"at": ISODate("2026-06-10T13:59:31.287Z")}]}');
expect(mongoDocumentGridEditorText(value)).toBe(rawJson);
expect(mongoDocumentGridClipboardText(value)).toBe(rawJson);
expect(mongoDocumentGridExternalValue(value)).toBe(rawJson);
});
it("renders out-of-range canonical dates as the raw wrapper instead of throwing", () => {
const value = mongoDocumentGridValue({ d: { $date: { $numberLong: "9223372036854775807" } } });
expect(mongoDocumentGridDisplayText(value)).toBe('{"d": {"$date": NumberLong("9223372036854775807")}}');
});
});
@@ -147,6 +147,93 @@ export function mongoDocumentDisplayValue(value: unknown): unknown {
return value;
}
/**
* Strings that are already shell-style BSON display literals. The driver ships
* dates as `ISODate("…")` text and the grid renders other typed scalars the same
* way, so inside arrays and subdocuments they must stay verbatim instead of
* being re-quoted (which produced the escaped `ISODate(\"…\")` mess).
*/
const MONGO_SHELL_DISPLAY_LITERAL_PATTERN = /^(?:ISODate|new Date|ObjectId|NumberLong|NumberInt|NumberDouble|NumberDecimal)\(\s*(["']).+\1\s*\)$/;
/**
* Renders a browser/extended-JSON BSON value the way mongosh prints it, so an
* array cell reads `[ObjectId("…"), ISODate("…")]` instead of the internal
* `[{"$oid":"…"},"ISODate(\\\"…\\\")"]` encoding. Display-only: edit, copy and
* save paths keep the original JSON representation untouched.
*/
export function mongoDocumentDisplayText(value: unknown): string {
if (value === null || value === undefined) return "null";
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (typeof value === "string") return MONGO_SHELL_DISPLAY_LITERAL_PATTERN.test(value) ? value : JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map((item) => mongoDocumentDisplayText(item)).join(", ")}]`;
if (typeof value === "object") {
const object = value as Record<string, unknown>;
const wrapper = mongoShellWrapperDisplayText(object);
if (wrapper !== undefined) return wrapper;
return `{${Object.entries(object)
.map(([key, item]) => `${JSON.stringify(key)}: ${mongoDocumentDisplayText(item)}`)
.join(", ")}}`;
}
return JSON.stringify(String(value));
}
function mongoShellWrapperDisplayText(object: Record<string, unknown>): string | undefined {
const keys = Object.keys(object);
if (keys.length !== 1) return undefined;
const inner = object[keys[0] ?? ""];
switch (keys[0]) {
case "$oid":
return typeof inner === "string" ? `ObjectId(${JSON.stringify(inner)})` : undefined;
case "$numberLong":
return typeof inner === "string" ? `NumberLong(${JSON.stringify(inner)})` : undefined;
case "$numberInt":
return typeof inner === "string" ? `NumberInt(${JSON.stringify(inner)})` : undefined;
case "$numberDouble":
return typeof inner === "string" ? `NumberDouble(${JSON.stringify(inner)})` : undefined;
case "$numberDecimal":
return typeof inner === "string" ? `NumberDecimal(${JSON.stringify(inner)})` : undefined;
case "$date":
return mongoShellDateWrapperDisplayText(inner);
case "$minKey":
return "MinKey()";
case "$maxKey":
return "MaxKey()";
default:
return undefined;
}
}
function mongoShellDateWrapperDisplayText(inner: unknown): string | undefined {
if (typeof inner === "string") return `ISODate(${JSON.stringify(inner)})`;
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
const long = (inner as Record<string, unknown>).$numberLong;
if (typeof long === "string" && /^-?\d+$/.test(long)) {
const millis = Number(long);
// Dates beyond the JS Date range cannot round-trip through toISOString();
// fall back to the raw wrapper instead of throwing past the grid formatter.
if (Number.isFinite(millis) && Math.abs(millis) <= 8.64e15) {
return `ISODate(${JSON.stringify(new Date(millis).toISOString())})`;
}
}
}
return undefined;
}
/**
* Relaxed Extended JSON for read-only document previews: the browser form keeps
* dates as `ISODate("…")` strings, and the preview must show them as
* `{"$date": "…"}` like MongoDB Compass instead of escaped shell text.
*/
export function mongoDocumentRelaxedExtendedJson(value: unknown): unknown {
const date = mongoShellDateToExtendedJson(value);
if (date !== value) return date;
if (Array.isArray(value)) return value.map((item) => mongoDocumentRelaxedExtendedJson(item));
if (value && typeof value === "object") {
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([key, item]) => [key, mongoDocumentRelaxedExtendedJson(item)]));
}
return value;
}
/**
* Maps BSON values into the flat collection-grid representation. The grid
* needs an internal sentinel for an explicit BSON null because an empty cell
@@ -195,14 +282,19 @@ export function mongoDocumentGridDisplayText(value: unknown, formatter?: ColumnF
}
function formatMongoDocumentGridJson(json: string, formatter: ColumnFormatterConfig | undefined): string {
if (formatter?.kind !== "datetime" || !validMongoDisplayTimeZone(formatter.timezone)) return json;
let value: unknown;
try {
const transformed = formatMongoDocumentDates(JSON.parse(json), formatter);
if (!transformed.changed) return json;
return typeof transformed.value === "string" ? transformed.value : JSON.stringify(transformed.value);
value = JSON.parse(json);
} catch {
return json;
}
if (formatter?.kind === "datetime" && validMongoDisplayTimeZone(formatter.timezone)) {
const transformed = formatMongoDocumentDates(value, formatter);
if (transformed.changed) return typeof transformed.value === "string" ? transformed.value : mongoDocumentDisplayText(transformed.value);
}
// Shell-style structure text keeps BSON types inside arrays/subdocuments
// readable (`[ObjectId("…"), ISODate("…")]`) instead of raw JSON wrappers.
return mongoDocumentDisplayText(value);
}
function validMongoDisplayTimeZone(timeZone: string | undefined): boolean {
@@ -454,7 +546,8 @@ export function mongoDocumentIdForGrid(value: unknown): MongoInputValue {
return JSON.stringify(value);
}
function isMongoExtendedJsonId(value: unknown): value is Record<string, unknown> {
/** A single typed scalar wrapper (`{$oid}` / `{$numberLong}`) the grid shows compactly. */
export function isMongoExtendedJsonId(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const object = value as Record<string, unknown>;
const keys = Object.keys(object);
@@ -1,5 +1,5 @@
import type { QueryResult } from "@/types/database";
import { mongoDocumentIdForGrid } from "@/lib/mongo/mongoDocumentValues";
import { isMongoExtendedJsonId, mongoDocumentGridValue, mongoDocumentIdForGrid } from "@/lib/mongo/mongoDocumentValues";
import {
chainedMethodCallPattern,
describeMongoCommandParseFailure as describeMongoCommandParseFailureBasic,
@@ -1136,7 +1136,17 @@ export function evaluateMongoAggregateSafety(command: MongoAggregateCommand, opt
return { allowed: true };
}
export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeMs: number, total: number, copyDocuments?: unknown[], totalIsExact = true): QueryResult {
export interface MongoDocumentsToQueryResultOptions {
/**
* Encode arrays/subdocuments with the collection grid's BSON-faithful values so
* display, editing and clipboard share one representation. Export paths that
* need source text pass `false` and keep plain JSON cells.
*/
documentGridValues?: boolean;
}
export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeMs: number, total: number, copyDocuments?: unknown[], totalIsExact = true, options: MongoDocumentsToQueryResultOptions = {}): QueryResult {
const documentGridValues = options.documentGridValues !== false;
const columns: string[] = [];
for (const doc of documents) {
@@ -1150,8 +1160,8 @@ export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeM
}
const rows = documents.map((doc) => {
if (isRecord(doc)) return columns.map((column) => toCellValue(doc[column]));
return columns.map((column) => (column === "value" ? toCellValue(doc) : null));
if (isRecord(doc)) return columns.map((column) => toCellValue(doc[column], documentGridValues));
return columns.map((column) => (column === "value" ? toCellValue(doc, documentGridValues) : null));
});
return {
@@ -2030,8 +2040,12 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function toCellValue(value: unknown): string | number | boolean | null {
function toCellValue(value: unknown, documentGridValues = false): string | number | boolean | null {
if (value === undefined || value === null) return null;
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
return mongoDocumentIdForGrid(value);
// Typed scalar ids keep their compact grid display; arrays and subdocuments take the
// document-grid encoding so display, editing and clipboard share the collection grid's
// representation instead of dumping the internal JSON encoding into the cell.
if (isMongoExtendedJsonId(value)) return mongoDocumentIdForGrid(value);
return documentGridValues ? (mongoDocumentGridValue(value) as string) : mongoDocumentIdForGrid(value);
}
+3 -1
View File
@@ -9118,7 +9118,9 @@ export const useQueryStore = defineStore("query", () => {
if (pageDocuments.length === 0 || pageDocuments.length < plan.requestLimit || reachedLogicalLimit || reachedExactTotal) break;
}
const result = mongoDocumentsToQueryResult(documents, performance.now() - exportStartedAt, totalRows ?? documents.length, copyDocuments, totalRows !== null);
// Full exports return source rows (CSV/SQL text), not the live grid's
// BSON-faithful cell encoding.
const result = mongoDocumentsToQueryResult(documents, performance.now() - exportStartedAt, totalRows ?? documents.length, copyDocuments, totalRows !== null, { documentGridValues: false });
if (result.columns.length === 0) {
result.columns = tab.result.columns;
result.column_types = tab.result.column_types;
+24
View File
@@ -10,6 +10,7 @@ import {
formatMongoShellLiteral,
MONGO_DOCUMENT_GRID_NULL,
mongoDocumentDisplayValue,
mongoDocumentDisplayText,
mongoDocumentGridDisplayText,
mongoDocumentGridClipboardText,
mongoDocumentGridEditorText,
@@ -18,6 +19,7 @@ import {
mongoDocumentGridValue,
mongoDocumentGridColumnTypes,
mongoDocumentIdForGrid,
mongoDocumentRelaxedExtendedJson,
parseMongoDocumentInputValue,
serializeMongoDocumentId,
} from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts";
@@ -344,6 +346,28 @@ test("keeps normal Mongo values readable and unsafe Int64 editable", () => {
assert.deepEqual(parseMongoDocumentInputValue('NumberLong("9007199254740993")'), { $numberLong: "9007199254740993" });
});
test("renders Mongo structures as shell-style literals like mongosh", () => {
assert.equal(mongoDocumentDisplayText({ $oid: "6743e4bfa3f6f84bc3fff6c8" }), 'ObjectId("6743e4bfa3f6f84bc3fff6c8")');
assert.equal(mongoDocumentDisplayText({ $numberLong: "2326645729978441729" }), 'NumberLong("2326645729978441729")');
assert.equal(mongoDocumentDisplayText({ $numberInt: "42" }), 'NumberInt("42")');
assert.equal(mongoDocumentDisplayText({ $date: "2026-06-10T13:59:31.287Z" }), 'ISODate("2026-06-10T13:59:31.287Z")');
assert.equal(mongoDocumentDisplayText({ $date: { $numberLong: "1781099971287" } }), 'ISODate("2026-06-10T13:59:31.287Z")');
assert.equal(mongoDocumentDisplayText({ $minKey: 1 }), "MinKey()");
assert.equal(mongoDocumentDisplayText(['ISODate("2026-06-10T13:59:31.287Z")', { $oid: "507f1f77bcf86cd799439011" }, 2]), '[ISODate("2026-06-10T13:59:31.287Z"), ObjectId("507f1f77bcf86cd799439011"), 2]');
assert.equal(mongoDocumentDisplayText({ note: "[1,2]", nested: [[1], []] }), '{"note": "[1,2]", "nested": [[1], []]}');
assert.equal(mongoDocumentDisplayText({ $binary: { base64: "AQI=", subType: "00" } }), '{"$binary": {"base64": "AQI=", "subType": "00"}}');
});
test("converts browser-form dates to relaxed Extended JSON for document previews", () => {
assert.deepEqual(mongoDocumentRelaxedExtendedJson({ at: 'ISODate("2026-06-10T13:59:31.287Z")', keep: "ISODate-ish text" }), {
at: { $date: "2026-06-10T13:59:31.287Z" },
keep: "ISODate-ish text",
});
assert.deepEqual(mongoDocumentRelaxedExtendedJson({ items: [{ by: { $oid: "507f1f77bcf86cd799439011" }, at: 'new Date("2026-06-10T13:59:31.287Z")' }] }), {
items: [{ by: { $oid: "507f1f77bcf86cd799439011" }, at: { $date: "2026-06-10T13:59:31.287Z" } }],
});
});
test("builds edits for Int32, Double, Date, and unsafe Int64", () => {
const changes = new Map<number, string | number | boolean | null>([
[1, 42],
+15 -2
View File
@@ -38,7 +38,7 @@ import {
splitMongoCommandRanges,
} from "../../apps/desktop/src/lib/mongo/mongoShellCommand.ts";
import type { MongoWriteCommand } from "../../apps/desktop/src/lib/mongo/mongoShellCommand.ts";
import { buildMongoUpdateDocument as buildMongoDocumentUpdate, formatMongoShellLiteral as formatMongoDocumentShellLiteral } from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts";
import { buildMongoUpdateDocument as buildMongoDocumentUpdate, formatMongoShellLiteral as formatMongoDocumentShellLiteral, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridValue } from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts";
import { normalizeJsonArgument } from "../mongo-shell/src/json.ts";
test("parseMongoFindCommand parses db collection find with an empty JSON filter", () => {
@@ -1751,7 +1751,7 @@ test("mongoDocumentsToQueryResult turns mongo documents into grid rows", () => {
assert.deepEqual(result.columns, ["_id", "name", "profile", "active"]);
assert.deepEqual(result.rows, [
["1", "Ada", '{"role":"admin"}', null],
["1", "Ada", mongoDocumentGridValue({ role: "admin" }), null],
["2", "Lin", null, true],
]);
assert.deepEqual(result.mongo_documents, [
@@ -1763,6 +1763,19 @@ test("mongoDocumentsToQueryResult turns mongo documents into grid rows", () => {
assert.equal(result.truncated, true);
});
test("mongoDocumentsToQueryResult displays array structures with shell-style BSON literals", () => {
const document = {
_id: "1",
history: [{ at: 'ISODate("2026-06-10T13:59:31.287Z")', by: { $oid: "507f1f77bcf86cd799439011" } }],
};
const result = mongoDocumentsToQueryResult([document], 5, 1);
const historyCell = result.rows[0]?.[1];
assert.equal(typeof historyCell, "string");
assert.equal(mongoDocumentGridDisplayText(historyCell), '[{"at": ISODate("2026-06-10T13:59:31.287Z"), "by": ObjectId("507f1f77bcf86cd799439011")}]');
assert.equal(mongoDocumentGridEditorText(historyCell), JSON.stringify(document.history));
});
test("mongoDocumentsToQueryResult keeps aligned extended documents for copying", () => {
const documents = [{ _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, createdAt: 'ISODate("2026-07-24T00:00:00Z")' }];
const copyDocuments = [{ _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, createdAt: { $date: "2026-07-24T00:00:00Z" } }];