From 5052c7fa6f44d62c36ca277164d287cb0bb62c57 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 8 May 2026 21:24:47 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(doris):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=8E?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=8F=98=E6=9B=B4=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #439 - Doris 重命名数据库改走原生 ALTER DATABASE RENAME - Doris 字段/注释预览改为兼容语法,移除 AFTER/FIRST 和无效 NONE - 补充相关回归测试 --- frontend/package.json.md5 | 2 +- .../components/tableDesignerSchemaSql.test.ts | 38 +++++++- .../src/components/tableDesignerSchemaSql.ts | 70 ++++++++++++++ internal/app/methods_db.go | 21 ++++- internal/app/methods_db_rename_test.go | 93 +++++++++++++++++++ 5 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 internal/app/methods_db_rename_test.go diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index bed8925..7396e24 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -0295a42fd931778d85157816d79d29e5 \ No newline at end of file +d0464f9da25e9356e61652e638c99ffe \ No newline at end of file diff --git a/frontend/src/components/tableDesignerSchemaSql.test.ts b/frontend/src/components/tableDesignerSchemaSql.test.ts index 4c7ad91..b1899bb 100644 --- a/frontend/src/components/tableDesignerSchemaSql.test.ts +++ b/frontend/src/components/tableDesignerSchemaSql.test.ts @@ -162,6 +162,40 @@ describe('tableDesignerSchemaSql', () => { expect(sql).not.toContain('MODIFY COLUMN'); }); + it('builds doris alter preview without mysql-only syntax or metadata extra', () => { + const sql = buildAlterTablePreviewSql(buildInput({ + dbType: 'doris', + tableName: 'sales.orders', + originalColumns: [ + baseColumn({ + _key: 'carrier', + name: 'carrier_id', + type: 'bigint', + nullable: 'YES', + extra: 'NONE', + comment: '承运商id', + }), + ], + columns: [ + baseColumn({ + _key: 'carrier', + name: 'carrier_code', + type: 'bigint', + nullable: 'YES', + extra: 'NONE', + comment: '承运商id1', + }), + ], + })); + + expect(sql).toContain('ALTER TABLE `sales`.`orders`\nRENAME COLUMN `carrier_id` `carrier_code`;'); + expect(sql).toContain("ALTER TABLE `sales`.`orders`\nMODIFY COLUMN `carrier_code` bigint NULL COMMENT '承运商id1';"); + expect(sql).not.toContain('CHANGE COLUMN'); + expect(sql).not.toContain('AFTER'); + expect(sql).not.toContain(' FIRST'); + expect(sql).not.toContain('NONE'); + }); + it('uses native limited alter syntax for clickhouse and tdengine instead of mysql syntax', () => { const clickhouseSql = buildAlterTablePreviewSql(buildInput({ dbType: 'clickhouse', @@ -184,8 +218,8 @@ describe('tableDesignerSchemaSql', () => { expect(tdengineSql).not.toContain('AFTER'); }); - it('treats mariadb doris and sphinx as mysql-family only where mysql syntax is intended', () => { - for (const dbType of ['mariadb', 'diros', 'sphinx']) { + it('treats mariadb and sphinx as mysql-family only where mysql syntax is intended', () => { + for (const dbType of ['mariadb', 'sphinx']) { const sql = buildAlterTablePreviewSql(buildInput({ dbType })); expect(sql).toContain('ALTER TABLE `users`'); expect(sql).toContain('ADD COLUMN `age` int NULL'); diff --git a/frontend/src/components/tableDesignerSchemaSql.ts b/frontend/src/components/tableDesignerSchemaSql.ts index 4f67437..a997acb 100644 --- a/frontend/src/components/tableDesignerSchemaSql.ts +++ b/frontend/src/components/tableDesignerSchemaSql.ts @@ -125,6 +125,37 @@ const buildMySqlColumnDefinition = (column: EditableColumnSnapshot, dbType: stri ].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); }; +const DORIS_AGG_TYPES = new Set([ + 'SUM', + 'MIN', + 'MAX', + 'REPLACE', + 'REPLACE_IF_NOT_NULL', + 'HLL_UNION', + 'BITMAP_UNION', + 'QUANTILE_UNION', + 'GENERIC', +]); + +const buildDorisColumnDefinition = (column: EditableColumnSnapshot, dbType: string): string => { + const defaultSql = buildDefaultSql(column.default, dbType); + const autoIncrementSql = column.isAutoIncrement ? 'AUTO_INCREMENT' : ''; + const keyText = String(column.key || '').trim().toUpperCase(); + const extraText = String(column.extra || '').trim().toUpperCase(); + const keyOrAggSql = ['PRI', 'KEY', 'TRUE'].includes(keyText) + ? 'KEY' + : (DORIS_AGG_TYPES.has(extraText) ? extraText : ''); + return [ + quoteIdentifierPart(column.name, dbType), + String(column.type || '').trim(), + keyOrAggSql, + column.nullable === 'NO' ? 'NOT NULL' : 'NULL', + defaultSql, + autoIncrementSql, + `COMMENT '${escapeSqlString(column.comment || '')}'`, + ].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); +}; + const buildStandardColumnDefinition = ( column: EditableColumnSnapshot, dbType: string, @@ -226,6 +257,44 @@ const buildMySqlAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: s return alters.length === 0 ? '' : `ALTER TABLE ${tableName}\n${alters.join(',\n')};`; }; +const buildDorisAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: string): string => { + const tableName = quoteIdentifierPath(input.tableName, dbType); + const statements: string[] = []; + + input.originalColumns.forEach((orig) => { + if (!input.columns.find((col) => col._key === orig._key)) { + statements.push(`ALTER TABLE ${tableName}\nDROP COLUMN ${quoteIdentifierPart(orig.name, dbType)};`); + } + }); + + input.columns.forEach((curr) => { + const orig = input.originalColumns.find((col) => col._key === curr._key); + if (!orig) { + statements.push(`ALTER TABLE ${tableName}\nADD COLUMN ${buildDorisColumnDefinition(curr, dbType)};`); + return; + } + + let currentName = orig.name; + if (curr.name !== orig.name) { + statements.push(`ALTER TABLE ${tableName}\nRENAME COLUMN ${quoteIdentifierPart(orig.name, dbType)} ${quoteIdentifierPart(curr.name, dbType)};`); + currentName = curr.name; + } + + if (definitionChanged(curr, orig)) { + statements.push(`ALTER TABLE ${tableName}\nMODIFY COLUMN ${buildDorisColumnDefinition({ ...curr, name: currentName }, dbType)};`); + } + }); + + const origPKKeys = input.originalColumns.filter((col) => col.key === 'PRI').map((col) => col._key); + const newPKKeys = input.columns.filter((col) => col.key === 'PRI').map((col) => col._key); + const keysChanged = origPKKeys.length !== newPKKeys.length || !origPKKeys.every((key) => newPKKeys.includes(key)); + if (keysChanged) { + statements.push('-- Doris 修改主键/Key 模型需要按表模型手工迁移,已避免生成 MySQL 专属的 DROP/ADD PRIMARY KEY。'); + } + + return statements.join('\n'); +}; + const buildPgLikeAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: string): string => { const tableParts = splitQualifiedName(input.tableName); const baseTableName = tableParts.objectName || stripIdentifierQuotes(input.tableName); @@ -537,6 +606,7 @@ export const buildAlterTablePreviewSql = (input: BuildAlterTablePreviewInput): s if (isSqlServerDialect(dbType)) return buildSqlServerAlterPreviewSql({ ...input, dbType }); if (dbType === 'sqlite') return buildSqliteAlterPreviewSql({ ...input, dbType }); if (dbType === 'duckdb') return buildDuckDbAlterPreviewSql({ ...input, dbType }); + if (dbType === 'diros') return buildDorisAlterPreviewSql({ ...input, dbType }, dbType); if (dbType === 'clickhouse') return buildLimitedBacktickAlterPreviewSql({ ...input, dbType }, dbType, 'ClickHouse'); if (dbType === 'tdengine') return buildLimitedBacktickAlterPreviewSql({ ...input, dbType }, dbType, 'TDengine'); if (isMysqlFamilyDialect(dbType)) return buildMySqlAlterPreviewSql({ ...input, dbType }, dbType); diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 30fb7fd..c469fb2 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -142,6 +142,9 @@ func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string) func resolveDDLDBType(config connection.ConnectionConfig) string { dbType := strings.ToLower(strings.TrimSpace(config.Type)) + if dbType == "doris" { + return "diros" + } if dbType == "oceanbase" && isOceanBaseOracleProtocol(config) { return "oracle" } @@ -275,8 +278,22 @@ func (a *App) RenameDatabase(config connection.ConnectionConfig, oldName string, dbType := resolveDDLDBType(config) switch dbType { - case "mysql", "mariadb", "oceanbase", "diros", "sphinx": - return connection.QueryResult{Success: false, Message: "MySQL/MariaDB/OceanBase/Doris/Sphinx 不支持直接重命名数据库,请新建库后迁移数据"} + case "diros": + runConfig := config + if strings.TrimSpace(runConfig.Database) == "" { + runConfig.Database = oldName + } + dbInst, err := a.getDatabase(runConfig) + if err != nil { + return connection.QueryResult{Success: false, Message: err.Error()} + } + sql := fmt.Sprintf("ALTER DATABASE %s RENAME %s", quoteIdentByType(dbType, oldName), quoteIdentByType(dbType, newName)) + if _, err := dbInst.Exec(sql); err != nil { + return connection.QueryResult{Success: false, Message: err.Error()} + } + return connection.QueryResult{Success: true, Message: "数据库重命名成功"} + case "mysql", "mariadb", "oceanbase", "sphinx": + return connection.QueryResult{Success: false, Message: "MySQL/MariaDB/OceanBase/Sphinx 不支持直接重命名数据库,请新建库后迁移数据"} case "postgres", "kingbase", "highgo", "vastbase", "opengauss": if strings.EqualFold(strings.TrimSpace(config.Database), oldName) { return connection.QueryResult{Success: false, Message: "当前连接正在使用目标数据库,请先连接到其他数据库后再重命名"} diff --git a/internal/app/methods_db_rename_test.go b/internal/app/methods_db_rename_test.go new file mode 100644 index 0000000..952ebf8 --- /dev/null +++ b/internal/app/methods_db_rename_test.go @@ -0,0 +1,93 @@ +package app + +import ( + "testing" + + "GoNavi-Wails/internal/connection" + "GoNavi-Wails/internal/db" + "GoNavi-Wails/internal/secretstore" +) + +type fakeRenameDatabaseDB struct { + connectConfig connection.ConnectionConfig + execQueries []string +} + +func (f *fakeRenameDatabaseDB) Connect(config connection.ConnectionConfig) error { + f.connectConfig = config + return nil +} +func (f *fakeRenameDatabaseDB) Close() error { return nil } +func (f *fakeRenameDatabaseDB) Ping() error { return nil } +func (f *fakeRenameDatabaseDB) Query(query string) ([]map[string]interface{}, []string, error) { + return nil, nil, nil +} +func (f *fakeRenameDatabaseDB) Exec(query string) (int64, error) { + f.execQueries = append(f.execQueries, query) + return 0, nil +} +func (f *fakeRenameDatabaseDB) GetDatabases() ([]string, error) { return nil, nil } +func (f *fakeRenameDatabaseDB) GetTables(dbName string) ([]string, error) { + return nil, nil +} +func (f *fakeRenameDatabaseDB) GetCreateStatement(dbName, tableName string) (string, error) { + return "", nil +} +func (f *fakeRenameDatabaseDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) { + return nil, nil +} +func (f *fakeRenameDatabaseDB) GetAllColumns(dbName string) ([]connection.ColumnDefinitionWithTable, error) { + return nil, nil +} +func (f *fakeRenameDatabaseDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) { + return nil, nil +} +func (f *fakeRenameDatabaseDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) { + return nil, nil +} +func (f *fakeRenameDatabaseDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) { + return nil, nil +} + +var _ db.Database = (*fakeRenameDatabaseDB)(nil) + +func TestResolveDDLDBType_DorisTypeAlias(t *testing.T) { + if got := resolveDDLDBType(connection.ConnectionConfig{Type: "doris"}); got != "diros" { + t.Fatalf("expected Doris type alias to resolve to diros, got %q", got) + } +} + +func TestRenameDatabase_DorisUsesNativeRenameSQL(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc + }) + + fakeDB := &fakeRenameDatabaseDB{} + newDatabaseFunc = func(dbType string) (db.Database, error) { + return fakeDB, nil + } + resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) { + return raw, nil + } + + app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) + result := app.RenameDatabase(connection.ConnectionConfig{ + Type: "custom", + Driver: "doris", + Database: "orders", + }, "orders", "orders_new") + + if !result.Success { + t.Fatalf("expected Doris rename success, got failure: %s", result.Message) + } + if len(fakeDB.execQueries) != 1 { + t.Fatalf("expected one rename statement, got %d: %#v", len(fakeDB.execQueries), fakeDB.execQueries) + } + const want = "ALTER DATABASE `orders` RENAME `orders_new`" + if fakeDB.execQueries[0] != want { + t.Fatalf("unexpected Doris rename SQL, want %q got %q", want, fakeDB.execQueries[0]) + } +}