fix(mongodb): defer result copy statement generation

This commit is contained in:
t8y2 committed 2026-07-21 17:11:58 +08:00
1 parent 71575c32de
commit 6ace11b097
5 files changed
+125 -67

No files matched your search

@@ -266,14 +266,12 @@ describe("useDataGridExport prepared row statements", () => {
vi.mocked(buildDataGridCopyInsertStatement).mockReturnValueOnce(pending.promise);
const state = useDataGridExport(options);
const prefetch = state.prefetchSelectionAsInsertStatement("merged");
const copy = state.copySelectionAsInsert("merged");
await vi.waitFor(() => expect(buildDataGridCopyInsertStatement).toHaveBeenCalledTimes(1));
expect(state.copySelectionAsInsert("merged")).toBe(false);
expect(copyToClipboard).not.toHaveBeenCalled();
pending.resolve("INSERT INTO users (name, note) VALUES ('Ada', 'math'), ('Grace', 'compiler');");
await prefetch;
await copy;
expect(state.canCopyPreparedSelectionInsert("merged")).toBe(true);
expect(state.copySelectionAsInsert("merged")).toBe(true);
expect(buildDataGridCopyInsertStatement).toHaveBeenCalledWith(
expect.objectContaining({
@@ -353,11 +351,47 @@ describe("useDataGridExport prepared row statements", () => {
},
});
await state.prefetchSelectionAsInsertStatement();
state.copySelectionAsInsert();
await state.copySelectionAsInsert();
expect(copyToClipboard).toHaveBeenCalledWith(`db.getCollection("documents").insert({
"booleanText": "true"
});`);
});
it("does not traverse Mongo documents while checking copy availability", async () => {
let documentReads = 0;
const originalDocument = Object.defineProperty({}, "payload", {
enumerable: true,
get() {
documentReads++;
return "large-value";
},
});
const item = { ...row(["large-value"]), sourceIndex: 0 };
const state = createMongoExportState({ columns: ["payload"], item, mongoDocuments: [originalDocument] });
expect(state.canCopyRowAsInsert.value).toBe(true);
expect(documentReads).toBe(0);
const copy = state.copyRowAsInsert();
expect(documentReads).toBe(0);
expect(copyToClipboard).not.toHaveBeenCalled();
await copy;
expect(documentReads).toBeGreaterThan(0);
expect(copyToClipboard).toHaveBeenCalledWith(expect.stringContaining('"payload": "large-value"'));
});
it("preserves oversized Mongo documents without running the formatter", async () => {
const payload = "x".repeat(1_100_000);
const item = { ...row([payload]), sourceIndex: 0 };
const state = createMongoExportState({ columns: ["payload"], item, mongoDocuments: [{ payload }] });
await state.copyRowAsInsert();
const copied = vi.mocked(copyToClipboard).mock.calls[0]?.[0] ?? "";
expect(copied).toHaveLength(payload.length + 'db.getCollection("documents").insert({"payload":""});'.length);
expect(copied.startsWith('db.getCollection("documents").insert({"payload":"')).toBe(true);
expect(copied.endsWith('"});')).toBe(true);
});
});
@@ -453,17 +453,23 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
});
return;
}
const key = insertCopyKey(excludePrimaryKeys, insertMode);
const current = insertCopyCache(excludePrimaryKeys, insertMode);
if (current.ready && current.key === key) return current.text;
if (current.loading && current.key === key && current.promise) return current.promise;
const data: CopyInsertData = {
columns: columns.value,
sourceColumns: sourceColumns.value,
columnTypes: columnTypes.value?.map((type) => type ?? undefined),
rows,
};
if (databaseType.value === "mongodb") {
// Mongo documents can approach the 16 MiB BSON limit. Yield before the
// synchronous shell serialization so menu close/rendering is never held up.
await yieldToMainThread();
return buildCopyInsertStatement(data, excludePrimaryKeys, insertMode);
}
const key = insertCopyKey(excludePrimaryKeys, insertMode);
const current = insertCopyCache(excludePrimaryKeys, insertMode);
if (current.ready && current.key === key) return current.text;
if (current.loading && current.key === key && current.promise) return current.promise;
const promise = Promise.resolve().then(async () => {
const statement = await buildCopyInsertStatement(data, excludePrimaryKeys, insertMode);
const latest = insertCopyCache(excludePrimaryKeys, insertMode);
@@ -537,6 +543,12 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
});
return;
}
if (databaseType.value === "mongodb") {
// Keep context-menu work bounded; serialize the selected Mongo fields only
// after the user explicitly invokes the copy command.
await yieldToMainThread();
return buildCopyInsertStatement(data, false, insertMode);
}
const key = selectionInsertCopyKey(insertMode);
const current = selectionInsertCopyCache(insertMode);
if (current.ready && current.key === key) return current.text;
@@ -584,10 +596,16 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
return cache.ready && cache.key === selectionInsertCopyKey(insertMode);
}
function copySelectionAsInsert(insertMode: DataGridCopyInsertMode = "merged"): boolean {
if (!canCopyPreparedSelectionInsert(insertMode)) return false;
void copyText(selectionInsertCopyCache(insertMode).text);
return true;
async function copySelectionAsInsert(insertMode: DataGridCopyInsertMode = "merged"): Promise<boolean> {
try {
const statement = await prepareSelectionAsInsertStatement(insertMode);
if (!statement) return false;
await copyText(statement);
return true;
} catch (error: any) {
toast(t("grid.copyFailed", { message: error?.message || String(error) }), 5000);
return false;
}
}
async function prefetchRowAsUpdateStatement() {
@@ -1564,6 +1582,10 @@ function formatMongoCopyInsertStatement(statement: string | undefined): string |
}
}
function yieldToMainThread(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0));
}
function compactLocalTimestamp(date = new Date()): string {
const yy = String(date.getFullYear() % 100).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");