From 3ef9639dbff15efadc2bd02db0d22ea8694a7f6b Mon Sep 17 00:00:00 2001 From: lxk955 <164191365+lxk955@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:27:02 +0800 Subject: [PATCH] fix(duckdb): save grid batch edits without a transaction --- .../__tests__/useDataGridEditor.spec.ts | 17 +++++++++++++++++ .../lib/__tests__/table/tableEditing.spec.ts | 10 ++++++++++ .../database/databaseTableDataCapabilities.ts | 10 ++++++++++ 3 files changed, 37 insertions(+) diff --git a/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts index 6987f6ecac..7e6a9e5678 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts @@ -1725,6 +1725,23 @@ describe("useDataGridEditor saveChanges reload", () => { expect(mocks.prepareDataGridSave.mock.calls[1]![0].dirtyRows).toEqual([[0, [[3, "new title"]]]]); }); + it("routes multi-statement edits through executeBatch for non-transactional DuckDB (#12306)", async () => { + mocks.prepareDataGridSave.mockResolvedValue({ + statements: ["UPDATE users SET name='alpha' WHERE id=1", "UPDATE users SET name='beta' WHERE id=2"], + rollbackStatements: [], + }); + mocks.executeBatch.mockResolvedValue({ affected_rows: 2 }); + const { editor } = createSaveTestEditor({ databaseType: "duckdb" }); + expect(editor.useTransaction.value).toBe(false); + editor.dirtyRows.value.set(0, new Map([[1, "alpha"]])); + editor.dirtyRows.value.set(1, new Map([[1, "beta"]])); + await editor.saveChanges(); + expect(mocks.executeInTransaction).not.toHaveBeenCalled(); + expect(mocks.executeBatch).toHaveBeenCalledTimes(1); + expect(mocks.executeBatch).toHaveBeenCalledWith("connection-1", "app", ["UPDATE users SET name='alpha' WHERE id=1", "UPDATE users SET name='beta' WHERE id=2"], undefined); + expect(editor.saveError.value).toBe(""); + }); + it("keeps both joined edits pending if the transaction fails", async () => { const targets = [ { tableMeta: { tableName: "users", primaryKeys: ["id"], columns: [] }, sourceColumns: ["id", "name", undefined, undefined] }, diff --git a/apps/desktop/src/lib/__tests__/table/tableEditing.spec.ts b/apps/desktop/src/lib/__tests__/table/tableEditing.spec.ts index 92dd3a5a77..629263f820 100644 --- a/apps/desktop/src/lib/__tests__/table/tableEditing.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableEditing.spec.ts @@ -263,4 +263,14 @@ describe("tableEditing", () => { expect(supportsDataGridTransaction("iris")).toBe(true); expect(canUseKeylessRowPredicate("iris", [])).toBe(true); }); + + it("edits DuckDB table data row by row without a transaction (#12306)", () => { + expect(isTableDataEditable("duckdb", ["id"], "TABLE")).toBe(true); + expect(isTableDataEditable("duckdb", [], "TABLE")).toBe(true); + expect(isTableDataEditable("duckdb", ["id"], "VIEW")).toBe(false); + expect(canInsertTableRows("duckdb")).toBe(true); + expect(canEditExistingTableRows("duckdb")).toBe(true); + expect(supportsDataGridTransaction("duckdb")).toBe(false); + expect(canUseKeylessRowPredicate("duckdb", [])).toBe(true); + }); }); diff --git a/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts b/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts index 94f911584c..8dc8030b6a 100644 --- a/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts +++ b/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts @@ -92,6 +92,16 @@ const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set([ ]); const DATABASE_CAPABILITY_OVERRIDES: Partial>> = { + duckdb: { + tableData: { + insert: true, + updateRequiresPrimaryKey: false, + deleteRequiresPrimaryKey: false, + keylessRowPredicate: true, + requiresTransactionalTableForExistingRows: false, + transaction: false, + }, + }, kyuubi: { tableData: { insert: false,