🐛 fix(doris): 修复数据库重命名与字段变更预览

Refs #439
- Doris 重命名数据库改走原生 ALTER DATABASE RENAME
- Doris 字段/注释预览改为兼容语法,移除 AFTER/FIRST 和无效 NONE
- 补充相关回归测试
This commit is contained in:
Syngnat
2026-05-08 21:24:47 +08:00
parent ab420e3d24
commit 5052c7fa6f
5 changed files with 219 additions and 5 deletions

View File

@@ -1 +1 @@
0295a42fd931778d85157816d79d29e5
d0464f9da25e9356e61652e638c99ffe

View File

@@ -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');

View File

@@ -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);

View File

@@ -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: "当前连接正在使用目标数据库,请先连接到其他数据库后再重命名"}

View File

@@ -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])
}
}