mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-16 10:07:36 +08:00
🐛 fix(doris): 修复数据库重命名与字段变更预览
Refs #439 - Doris 重命名数据库改走原生 ALTER DATABASE RENAME - Doris 字段/注释预览改为兼容语法,移除 AFTER/FIRST 和无效 NONE - 补充相关回归测试
This commit is contained in:
@@ -1 +1 @@
|
||||
0295a42fd931778d85157816d79d29e5
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: "当前连接正在使用目标数据库,请先连接到其他数据库后再重命名"}
|
||||
|
||||
93
internal/app/methods_db_rename_test.go
Normal file
93
internal/app/methods_db_rename_test.go
Normal 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])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user