From 302f48ec8bcdd1f99f8eb975e64d41cdac946fbb Mon Sep 17 00:00:00 2001 From: vrustx <279631638@qq.com> Date: Mon, 1 Jun 2026 18:31:06 +0800 Subject: [PATCH] fix: record failed data grid saves in history --- .../src/composables/useDataGridEditor.ts | 37 ++++++-- packages/app-tests/dataGridEditor.test.ts | 87 +++++++++++++++++++ 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index b077076553..85cfd474f9 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -577,10 +577,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { statements: string[], rollbackStatements: string[], elapsed: number, - historyResult?: { affected_rows?: number }, + historyResult?: { affected_rows?: number; success?: boolean; error?: string }, ) { if (!connectionId.value || !database.value || !tableMeta.value) return; const connName = connectionStore.getConfig(connectionId.value)?.name || ""; + const success = historyResult?.success ?? true; const details = { schema: tableMeta.value.schema, table: tableMeta.value.tableName, @@ -588,7 +589,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { updated_rows: dirtyRows.value.size, deleted_rows: deletedRows.value.size, statements, - rollback_statements: rollbackStatements, + rollback_statements: success ? rollbackStatements : [], + error: success ? undefined : historyResult?.error, }; await historyStore.add({ connection_id: connectionId.value, @@ -596,16 +598,35 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { database: database.value, sql: statements.join("\n"), execution_time_ms: elapsed, - success: true, + success, + error: success ? undefined : historyResult?.error, activity_kind: "data_change", operation: dataChangeOperation(), target: tableHistoryTarget(), - affected_rows: historyResult?.affected_rows ?? statements.length, - rollback_sql: rollbackStatements.length ? rollbackStatements.join("\n") : undefined, + affected_rows: success ? (historyResult?.affected_rows ?? statements.length) : undefined, + rollback_sql: success && rollbackStatements.length ? rollbackStatements.join("\n") : undefined, details_json: JSON.stringify(details), }); } + async function recordFailedDataGridHistory( + statements: string[], + rollbackStatements: string[], + start: number, + error: unknown, + ) { + const message = normalizeDataGridSaveError(databaseType.value, error); + try { + await recordDataGridHistory(statements, rollbackStatements, Date.now() - start, { + success: false, + error: message, + }); + } catch (historyError) { + console.warn("[DBX] failed to record data grid history", historyError); + } + return message; + } + function reloadCurrentData() { options.emit( "reload", @@ -691,7 +712,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { preparedSave?.executionSchema, ); } catch (e: any) { - saveError.value = normalizeDataGridSaveError(databaseType.value, e); + saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e); isSaving.value = false; return; } @@ -699,7 +720,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { try { apiResult = await api.executeBatch(connectionId.value, database.value, stmts); } catch (e: any) { - saveError.value = normalizeDataGridSaveError(databaseType.value, e); + saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e); isSaving.value = false; return; } @@ -709,7 +730,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { await onExecuteSql.value(sqlStmt); } } catch (e: any) { - saveError.value = normalizeDataGridSaveError(databaseType.value, e); + saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e); isSaving.value = false; return; } diff --git a/packages/app-tests/dataGridEditor.test.ts b/packages/app-tests/dataGridEditor.test.ts index 0e2a2dded1..5f03326f99 100644 --- a/packages/app-tests/dataGridEditor.test.ts +++ b/packages/app-tests/dataGridEditor.test.ts @@ -443,3 +443,90 @@ test("saving manually typed JSON from a MySQL grid normalizes smart quotes", asy `UPDATE "settings" SET "payload" = '{"2:3":"3:4","3:2":"4:3","21:9":"16:9"}' WHERE "id" = 1;`, ]); }); + +test("failed table data save records a failed history entry", async () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + + const permissionError = "Statement 1 failed: Server error: ERROR 42000 (1142): UPDATE command denied to user"; + const savedHistoryEntries: Array> = []; + globalThis.fetch = (async (input, init) => { + const url = String(input); + if (url === "/api/query/prepare-data-grid-save") { + const body = JSON.parse(String(init?.body ?? "{}")); + const options = body.options as DataGridSaveStatementOptions; + return new Response( + JSON.stringify({ + statements: mockPreparedSaveStatements(options), + rollbackStatements: [`UPDATE "pp_questions" SET "title" = 'Old title' WHERE "id" = 1;`], + executionSchema: options.tableMeta.schema, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + if (url === "/api/query/execute-in-transaction") { + return new Response(permissionError, { status: 500 }); + } + if (url === "/api/history/save") { + savedHistoryEntries.push(JSON.parse(String(init?.body ?? "{}")).entry); + return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } }); + } + return new Response(`unexpected request: ${url}`, { status: 500 }); + }) as typeof fetch; + + const result = computed(() => ({ + columns: ["id", "title"], + rows: [[1, "Old title"] as CellValue[]], + })); + const rowStatusFilter = ref<"all" | "changed" | "edited" | "new" | "deleted">("all"); + const editor = useDataGridEditor({ + result, + editable: computed(() => true), + databaseType: computed(() => "mysql"), + connectionId: computed(() => "conn-1"), + database: computed(() => "app_db"), + tableMeta: computed(() => ({ + tableName: "pp_questions", + columns: [column("id", true), column("title")], + primaryKeys: ["id"], + })), + onExecuteSql: computed(() => undefined), + customSave: computed(() => undefined), + sql: computed(() => "SELECT id, title FROM pp_questions"), + searchText: ref(""), + whereFilterInput: ref(""), + orderByInput: ref(""), + rowStatusFilter, + pageSize: ref(50), + currentPage: ref(1), + getRowItem: (rowId) => { + if (rowId !== 0) return undefined; + return { + id: 0, + sourceIndex: 0, + data: result.value.rows[0], + isNew: false, + isDeleted: false, + isDirtyCol: [false, false], + status: "clean", + }; + }, + emit: () => {}, + }); + + editor.applyCellValue(0, 1, "New title"); + await editor.saveChanges(); + + assert.equal(editor.saveError.value, permissionError); + assert.equal(editor.dirtyRows.value.size, 1); + assert.equal(savedHistoryEntries.length, 1); + const historyEntry = savedHistoryEntries[0]; + assert.equal(historyEntry.success, false); + assert.equal(historyEntry.error, permissionError); + assert.equal(historyEntry.activity_kind, "data_change"); + assert.equal(historyEntry.operation, "UPDATE"); + assert.equal(historyEntry.target, "pp_questions"); + assert.equal(historyEntry.rollback_sql, undefined); + assert.equal(historyEntry.affected_rows, undefined); + assert.equal(historyEntry.sql, `UPDATE "pp_questions" SET "title" = 'New title' WHERE "id" = 1;`); +});