fix(export): preserve MongoDB Extended JSON

MongoDB JSON exports now use mongo_copy_documents so nested values and BSON date and object wrappers survive serialization without changing SQL export rows.

Closes #8558
This commit is contained in:
t8y2 committed 2026-09-09 15:27:31 +08:00
1 parent 23ebc67e6c
commit 918ff69338
2 files changed
+57 -8

No files matched your search

@@ -7,7 +7,7 @@ import { copyToClipboard } from "@/lib/common/clipboard";
import type { DataGridTableMeta } from "@/lib/dataGrid/dataGridSql";
import type { CellSelectionMatrix, SelectionData } from "@/lib/dataGrid/gridSelection";
import type { CellValue } from "@/lib/dataGrid/cellValue";
import { extractDataGridSelection } from "@/lib/backend/api";
import { extractDataGridSelection, exportQueryResultJson } from "@/lib/backend/api";
import { DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS } from "@/lib/dataGrid/dataGridCopyExtractor";
import { clearDataGridClipboardCopy, parseDataGridClipboard } from "@/lib/dataGrid/dataGridClipboard";
@@ -46,6 +46,7 @@ vi.mock("@/lib/backend/api", async (importOriginal) => {
return {
...original,
extractDataGridSelection: vi.fn(),
exportQueryResultJson: vi.fn(),
};
});
@@ -70,6 +71,7 @@ function createMongoExportState(options: {
mongoUpdateTarget?: false;
contextColumn?: number;
syntheticContext?: boolean;
fullExportResult?: UseDataGridExportOptions["fullExportResult"];
}) {
const items = options.items ?? [options.item];
const selectedRowIds = options.selectedRowIds ?? new Set<number>();
@@ -99,6 +101,7 @@ function createMongoExportState(options: {
getRowItem: (rowId) => items.find((item) => item.id === rowId),
selectedRowIds: ref(selectedRowIds),
hasRowSelection: computed(() => selectedRowIds.size > 0),
fullExportResult: options.fullExportResult,
};
return useDataGridExport(state);
}
@@ -1509,6 +1512,33 @@ describe("useDataGridExport prepared row statements", () => {
expect(extractDataGridSelection).not.toHaveBeenCalled();
expect(copyToClipboard).not.toHaveBeenCalled();
});
it("preserves Mongo Extended JSON objects and dates in JSON exports", async () => {
const columns = ["_id", "valueMap", "createdTime"];
const mongoCopyDocument = {
_id: { $oid: "6a9fb51db2f0c46b94002f26" },
valueMap: { field1: "10147E", field2: "0" },
createdTime: { $date: "2026-09-08T07:11:25.458Z" },
};
const item = { ...row(["6a9fb51db2f0c46b94002f26", JSON.stringify(mongoCopyDocument.valueMap), 'ISODate("2026-09-08T07:11:25.458Z")']), sourceIndex: 0 };
const state = createMongoExportState({
columns,
item,
mongoDocuments: [mongoCopyDocument],
fullExportResult: async () => ({
columns,
column_types: ["", "", "datetime"],
rows: [item.data as [string, string, string]],
mongo_copy_documents: [mongoCopyDocument],
affected_rows: 1,
execution_time_ms: 1,
}),
});
await state.exportJson();
expect(exportQueryResultJson).toHaveBeenCalledWith(expect.any(String), columns, [[mongoCopyDocument._id, mongoCopyDocument.valueMap, mongoCopyDocument.createdTime]]);
});
});
// issue #7471:文本型 MySQL VARBINARY 复制单元格/多选/整行时,外部剪贴板应呈现原始字符串;
@@ -331,7 +331,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
});
}
function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; columnComments: Array<string | undefined>; rows: CellValue[][] } {
function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; columnComments: Array<string | undefined>; rows: CellValue[][]; mongoCopyDocuments?: unknown[] } {
const hiddenColumnIndexes = new Set(result.hidden_column_indexes ?? []);
const exportedColumnIndexes = result.columns.map((_, index) => index).filter((index) => !hiddenColumnIndexes.has(index));
const hasHiddenColumns = exportedColumnIndexes.length !== result.columns.length;
@@ -345,15 +345,34 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
columnTypes: hasHiddenColumns ? exportedColumnIndexes.map((index) => result.column_types?.[index] ?? "") : (result.column_types ?? []),
columnComments: hasHiddenColumns ? exportedColumnIndexes.map((index) => allXlsxColumnComments.value[index]) : [...allXlsxColumnComments.value],
rows: hasHiddenColumns ? rows.map((row) => exportedColumnIndexes.map((index) => row[index])) : rows,
mongoCopyDocuments: result.mongo_copy_documents?.slice(0, rows.length),
};
}
function mongoDocumentRowsForJson(columnsToExport: string[], rows: CellValue[][], documents: unknown[] | undefined): CellValue[][] {
if (databaseType.value !== "mongodb" || !documents || documents.length !== rows.length) return rows;
return rows.map((row, rowIndex) => {
const document = documents[rowIndex];
if (!document || typeof document !== "object" || Array.isArray(document)) return row;
const source = document as Record<string, unknown>;
return columnsToExport.map((column, columnIndex) => (Object.prototype.hasOwnProperty.call(source, column) ? (source[column] as CellValue) : (row[columnIndex] ?? null)));
});
}
function mongoLocalRowsForJson(items: RowItem[]): CellValue[][] {
return items.map((item) => {
const document = rowToJsonObject(item);
return columns.value.map((column) => (document[column] as CellValue) ?? null);
});
}
async function resultToExport(
rowIds?: number[],
onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void,
useFullExport = true,
formatDateTime = true,
headerMode: XlsxHeaderMode = "name",
preserveMongoExtendedJson = false,
): Promise<{
columns: string[];
columnTypes: string[];
@@ -367,7 +386,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
if (result) {
const columnComments = buildXlsxHeaderOverrides(result.columns, commentsForExportColumns(result.columns), headerMode);
return {
...applyGlobalDateTimeExportFormat({ columns: result.columns, columnTypes: result.column_types ?? [], rows: result.rows }, formatDateTime),
...applyGlobalDateTimeExportFormat({ columns: result.columns, columnTypes: result.column_types ?? [], rows: preserveMongoExtendedJson ? mongoDocumentRowsForJson(result.columns, result.rows, result.mongo_copy_documents) : result.rows }, formatDateTime && !preserveMongoExtendedJson),
columnComments,
spatialColumns: result.spatial_columns,
spatialValues: result.spatial_values,
@@ -383,7 +402,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
const normalized = normalizeCompleteLocalResult(completeLocalResult.value);
const columnComments = buildXlsxHeaderOverrides(normalized.columns, normalized.columnComments, headerMode);
return {
...applyGlobalDateTimeExportFormat({ columns: normalized.columns, columnTypes: normalized.columnTypes, rows: normalized.rows }, formatDateTime),
...applyGlobalDateTimeExportFormat({ columns: normalized.columns, columnTypes: normalized.columnTypes, rows: preserveMongoExtendedJson ? mongoDocumentRowsForJson(normalized.columns, normalized.rows, normalized.mongoCopyDocuments) : normalized.rows }, formatDateTime && !preserveMongoExtendedJson),
columnComments,
spatialColumns: completeLocalResult.value.spatial_columns,
spatialValues: completeLocalResult.value.spatial_values,
@@ -399,9 +418,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
{
columns: columns.value,
columnTypes: (columnTypes.value ?? []).map((type) => type ?? ""),
rows: exportItems.map((item) => item.data),
rows: preserveMongoExtendedJson && databaseType.value === "mongodb" ? mongoLocalRowsForJson(exportItems) : exportItems.map((item) => item.data),
},
formatDateTime,
formatDateTime && !preserveMongoExtendedJson,
),
columnComments: commentHeader,
...(spatialColumns?.length ? { spatialColumns } : {}),
@@ -842,7 +861,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
if (!path) return;
outputPath = path as string;
}
const result = await resultToExport(rowIds);
const result = await resultToExport(rowIds, undefined, true, true, "name", true);
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
toast(t("grid.exported"));
} catch (e: any) {
@@ -864,7 +883,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
if (!path) return;
outputPath = path as string;
}
const result = await resultToExport(undefined, undefined, false);
const result = await resultToExport(undefined, undefined, false, true, "name", true);
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
toast(t("grid.exported"));
} catch (e: any) {