From 2d73bcc6dedbf10747740b3751abbd027d84330e Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sat, 27 Jun 2026 10:41:28 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E8=B7=B3?= =?UTF-8?q?=E8=BF=87TDengine=E6=89=98=E7=AE=A1=E4=BA=8B=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #592 --- .../QueryEditor.external-sql-save.test.tsx | 35 +++++++++++++++++++ .../src/utils/sqlEditorTransaction.test.ts | 12 +++++-- frontend/src/utils/sqlEditorTransaction.ts | 14 ++++++-- internal/app/methods_db_transaction.go | 11 +++++- internal/app/methods_db_transaction_test.go | 25 ++++++++++--- 5 files changed, 86 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index f8d1336..0867151 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -5429,6 +5429,41 @@ describe('QueryEditor external SQL save', () => { expect(textContent(renderer!.root)).not.toContain('未提交'); }); + it('keeps TDengine insert on the regular query path because it has no managed transaction support', async () => { + storeState.connections[0].config.type = 'tdengine'; + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [ + { columns: ['affectedRows'], rows: [{ affectedRows: 1 }], statementIndex: 1 }, + ], + }); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + await findButton(renderer!, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(backendApp.DBQueryMulti).toHaveBeenCalledWith( + expect.anything(), + 'main', + expect.stringContaining('INSERT INTO meters'), + 'query-1', + ); + expect(backendApp.DBQueryMultiTransactional).not.toHaveBeenCalled(); + expect(messageApi.error).not.toHaveBeenCalledWith(expect.stringContaining('SQL 编辑器托管事务')); + expect(textContent(renderer!.root)).toContain('影响行数:1'); + }); + it('reuses the pending managed transaction for follow-up read-only SQL in the same tab', async () => { backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({ success: true, diff --git a/frontend/src/utils/sqlEditorTransaction.test.ts b/frontend/src/utils/sqlEditorTransaction.test.ts index a2a8d34..1cd2a71 100644 --- a/frontend/src/utils/sqlEditorTransaction.test.ts +++ b/frontend/src/utils/sqlEditorTransaction.test.ts @@ -47,9 +47,15 @@ describe('sqlEditorTransaction', () => { ])).toBe(false); }); - it('keeps Trino DML on the plain multi-statement execution path', () => { - expect(shouldUseSqlEditorManagedTransactionForType('trino', [ - 'UPDATE hive.default.orders SET status = \'done\'', + it.each([ + ['trino', 'UPDATE hive.default.orders SET status = \'done\''], + ['tdengine', 'INSERT INTO meters(ts, current) VALUES (NOW, 10.2)'], + ['clickhouse', 'INSERT INTO events FORMAT JSONEachRow {"id":1}'], + ['iotdb', 'INSERT INTO root.ln.wf01.wt01(timestamp,status) VALUES(1,true)'], + ])('keeps %s writes on the plain multi-statement execution path', (dbType, sql) => { + expect(shouldUseSqlEditorManagedTransactionForType(dbType, [sql])).toBe(false); + expect(canReusePendingSqlEditorTransactionForType(dbType, [ + 'SELECT * FROM users WHERE id = 1', ])).toBe(false); }); diff --git a/frontend/src/utils/sqlEditorTransaction.ts b/frontend/src/utils/sqlEditorTransaction.ts index 5fff33d..6bc189a 100644 --- a/frontend/src/utils/sqlEditorTransaction.ts +++ b/frontend/src/utils/sqlEditorTransaction.ts @@ -1,6 +1,16 @@ const SQL_EDITOR_DML_KEYWORDS = new Set(['insert', 'update', 'delete', 'replace', 'merge', 'upsert']); const SQL_EDITOR_READ_KEYWORDS = new Set(['select', 'with', 'show', 'describe', 'desc', 'explain', 'pragma', 'values']); const SQL_EDITOR_TRANSACTION_CONTROL_KEYWORDS = new Set(['begin', 'commit', 'rollback', 'savepoint', 'release']); +const SQL_EDITOR_MANAGED_TRANSACTION_UNSUPPORTED_TYPES = new Set([ + 'trino', + 'tdengine', + 'clickhouse', + 'iotdb', + 'rocketmq', + 'mqtt', + 'kafka', + 'rabbitmq', +]); type SqlEditorWithAnalysis = { keyword: string; @@ -253,7 +263,7 @@ export const shouldUseSqlEditorManagedTransactionForType = ( type: string, statements: string[], ): boolean => { - if (String(type || '').trim().toLowerCase() === 'trino') { + if (SQL_EDITOR_MANAGED_TRANSACTION_UNSUPPORTED_TYPES.has(String(type || '').trim().toLowerCase())) { return false; } let hasManagedWrite = false; @@ -279,7 +289,7 @@ export const canReusePendingSqlEditorTransactionForType = ( type: string, statements: string[], ): boolean => { - if (String(type || '').trim().toLowerCase() === 'trino') { + if (SQL_EDITOR_MANAGED_TRANSACTION_UNSUPPORTED_TYPES.has(String(type || '').trim().toLowerCase())) { return false; } let hasReadStatement = false; diff --git a/internal/app/methods_db_transaction.go b/internal/app/methods_db_transaction.go index a2f099c..eb1befc 100644 --- a/internal/app/methods_db_transaction.go +++ b/internal/app/methods_db_transaction.go @@ -369,7 +369,7 @@ func executeManagedSQLTransactionStatements(ctx context.Context, session db.Stat } func shouldUseManagedSQLTransaction(dbType string, query string) bool { - if strings.EqualFold(strings.TrimSpace(dbType), "trino") { + if isManagedSQLTransactionUnsupportedType(dbType) { return false } statements := splitSQLStatements(query) @@ -394,6 +394,15 @@ func shouldUseManagedSQLTransaction(dbType string, query string) bool { return hasManagedWrite } +func isManagedSQLTransactionUnsupportedType(dbType string) bool { + switch strings.ToLower(strings.TrimSpace(dbType)) { + case "trino", "tdengine", "clickhouse", "iotdb", "rocketmq", "mqtt", "kafka", "rabbitmq": + return true + default: + return false + } +} + func sqlEditorImplicitTransactionSQL(dbType string) (commitSQL string, rollbackSQL string, ok bool) { switch strings.ToLower(strings.TrimSpace(dbType)) { case "oracle": diff --git a/internal/app/methods_db_transaction_test.go b/internal/app/methods_db_transaction_test.go index 23037e5..fe766cf 100644 --- a/internal/app/methods_db_transaction_test.go +++ b/internal/app/methods_db_transaction_test.go @@ -2,13 +2,28 @@ package app import "testing" -func TestShouldUseManagedSQLTransaction_TrinoAlwaysUsesPlainExecution(t *testing.T) { +func TestShouldUseManagedSQLTransaction_UnsupportedTypesUsePlainExecution(t *testing.T) { t.Parallel() - if shouldUseManagedSQLTransaction("trino", "UPDATE hive.default.orders SET status = 'done'") { - t.Fatal("expected trino DML to skip SQL editor managed transactions") + cases := []struct { + dbType string + query string + }{ + {dbType: "trino", query: "UPDATE hive.default.orders SET status = 'done'"}, + {dbType: "tdengine", query: "INSERT INTO meters(ts, current) VALUES (NOW, 10.2)"}, + {dbType: "clickhouse", query: `INSERT INTO events FORMAT JSONEachRow {"id":1}`}, + {dbType: "iotdb", query: "INSERT INTO root.ln.wf01.wt01(timestamp,status) VALUES(1,true)"}, } - if shouldUseManagedSQLTransaction("trino", "BEGIN; UPDATE hive.default.orders SET status = 'done'; COMMIT;") { - t.Fatal("expected trino explicit transactions to stay unmanaged") + for _, tc := range cases { + tc := tc + t.Run(tc.dbType, func(t *testing.T) { + t.Parallel() + if shouldUseManagedSQLTransaction(tc.dbType, tc.query) { + t.Fatalf("expected %s DML to skip SQL editor managed transactions", tc.dbType) + } + if shouldUseManagedSQLTransaction(tc.dbType, "BEGIN; "+tc.query+"; COMMIT;") { + t.Fatalf("expected %s explicit transactions to stay unmanaged", tc.dbType) + } + }) } }