From 79c682679d3ee2fa2ea022ded41062bac72d714b Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 16 Jul 2026 15:32:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(grid):=20=E5=8D=95=E6=9D=A1=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=BF=AE=E6=94=B9=E4=B8=8D=E5=86=8D=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E5=BC=80=E5=90=AF=E4=BA=8B=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: zipg <4047349+zipg@users.noreply.github.com> --- .../src/composables/useDataGridEditor.ts | 2 +- packages/app-tests/dataGridEditor.test.ts | 87 ++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index b9cff29d0..e4ffeb410 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -1270,7 +1270,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { rollbackStatements: rollbackStmts, }); - if (useTransaction.value && hasBackendSaveTarget.value) { + if (useTransaction.value && stmts.length > 1 && hasBackendSaveTarget.value) { try { apiResult = await api.executeInTransaction(connectionId.value!, database.value ?? "", stmts, preparedSave?.executionSchema); } catch (e: any) { diff --git a/packages/app-tests/dataGridEditor.test.ts b/packages/app-tests/dataGridEditor.test.ts index 673f4a02f..ad79b720d 100644 --- a/packages/app-tests/dataGridEditor.test.ts +++ b/packages/app-tests/dataGridEditor.test.ts @@ -1062,7 +1062,7 @@ test("saving manually typed JSON arrays from a Postgres array column uses array assert.deepEqual(executedSql, [`UPDATE "articles" SET "tags" = '{"draft","发布"}' WHERE "id" = 1;`]); }); -test("failed table data save records a failed history entry", async () => { +test("single-statement table data save uses auto-commit and records a failed history entry", async () => { setActivePinia(createPinia()); installBrowserTestGlobals(); @@ -1082,7 +1082,7 @@ test("failed table data save records a failed history entry", async () => { { status: 200, headers: { "Content-Type": "application/json" } }, ); } - if (url === "/api/query/execute-in-transaction") { + if (url === "/api/query/execute-batch") { return new Response(permissionError, { status: 500 }); } if (url === "/api/history/save") { @@ -1150,6 +1150,89 @@ test("failed table data save records a failed history entry", async () => { assert.equal(historyEntry.sql, `UPDATE "pp_questions" SET "title" = 'New title' WHERE "id" = 1;`); }); +test("multi-statement table data save remains transactional", async () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + + const executionEndpoints: string[] = []; + 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: [], + executionSchema: options.tableMeta.schema, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + if (url === "/api/query/execute-in-transaction" || url === "/api/query/execute-batch") { + executionEndpoints.push(url); + return new Response(JSON.stringify({ affected_rows: 2 }), { status: 200, headers: { "Content-Type": "application/json" } }); + } + if (url === "/api/history/save") { + 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 1"], + [2, "Old title 2"], + ] 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), + customSaveHandler: computed(() => undefined), + sql: computed(() => "SELECT id, title FROM pp_questions"), + searchText: ref(""), + whereFilterInput: ref(""), + currentWhereInput: computed(() => undefined), + orderByInput: ref(""), + rowStatusFilter, + pageSize: ref(50), + currentPage: ref(1), + getRowItem: (rowId) => { + const row = result.value.rows[rowId]; + if (!row) return undefined; + return { + id: rowId, + sourceIndex: rowId, + data: row, + isNew: false, + isDeleted: false, + isDirtyCol: [false, false], + status: "clean", + }; + }, + emit: () => {}, + }); + + editor.applyCellValue(0, 1, "New title 1"); + editor.applyCellValue(1, 1, "New title 2"); + await editor.saveChanges(); + + assert.deepEqual(executionEndpoints, ["/api/query/execute-in-transaction"]); + assert.equal(editor.saveError.value, ""); + assert.equal(editor.dirtyRows.value.size, 0); +}); + test("quick entry off keeps blur edits pending without saving", async () => { setActivePinia(createPinia()); installBrowserTestGlobals();