diff --git a/backend/plugins/tapd/models/migrationscripts/20230411_modify_custom_field_name.go b/backend/plugins/tapd/models/migrationscripts/20230411_modify_custom_field_name.go index fe3703710ec..2120138e3a2 100644 --- a/backend/plugins/tapd/models/migrationscripts/20230411_modify_custom_field_name.go +++ b/backend/plugins/tapd/models/migrationscripts/20230411_modify_custom_field_name.go @@ -19,47 +19,59 @@ package migrationscripts import ( "fmt" + "strings" + "github.com/apache/incubator-devlake/core/context" "github.com/apache/incubator-devlake/core/dal" "github.com/apache/incubator-devlake/core/errors" + "gorm.io/gorm/clause" ) type modifyCustomFieldName struct{} +type columnRename struct { + Old string + New string +} + func (*modifyCustomFieldName) Up(basicRes context.BasicRes) errors.Error { db := basicRes.GetDal() - issuesNameList := []string{"_tool_tapd_stories", "_tool_tapd_bugs", "_tool_tapd_tasks"} - for _, issuesName := range issuesNameList { - switch issuesName { - case "_tool_tapd_bugs": - for i := 6; i < 9; i++ { - oldColumnName := fmt.Sprintf("custom_field%d", i) - newColumnName := fmt.Sprintf("custom_field_%d", i) - if err := renameColumnSafely(db, issuesName, oldColumnName, newColumnName, dal.Text); err != nil { - return err - } - } - case "_tool_tapd_tasks", "_tool_tapd_stories": - tableName := issuesName - renameColumnMap := map[string]string{ - "custom_field6": "custom_field_six", - "custom_field7": "custom_field_seven", - "custom_field8": "custom_field_eight", - } - for oldColumn, newColumn := range renameColumnMap { - if err := renameColumnSafely(db, tableName, oldColumn, newColumn, dal.Text); err != nil { - return err - } - } + + // Define all column renames for each table + tableRenames := map[string][]columnRename{ + "_tool_tapd_bugs": { + {Old: "custom_field6", New: "custom_field_6"}, + {Old: "custom_field7", New: "custom_field_7"}, + {Old: "custom_field8", New: "custom_field_8"}, + }, + "_tool_tapd_stories": { + {Old: "custom_field6", New: "custom_field_six"}, + {Old: "custom_field7", New: "custom_field_seven"}, + {Old: "custom_field8", New: "custom_field_eight"}, + }, + "_tool_tapd_tasks": { + {Old: "custom_field6", New: "custom_field_six"}, + {Old: "custom_field7", New: "custom_field_seven"}, + {Old: "custom_field8", New: "custom_field_eight"}, + }, + } + + // Add custom_field_9 to custom_field_50 for all tables + for i := 9; i <= 50; i++ { + oldCol := fmt.Sprintf("custom_field%d", i) + newCol := fmt.Sprintf("custom_field_%d", i) + for _, table := range []string{"_tool_tapd_bugs", "_tool_tapd_stories", "_tool_tapd_tasks"} { + tableRenames[table] = append(tableRenames[table], columnRename{Old: oldCol, New: newCol}) } - for i := 9; i <= 50; i++ { - oldColumnName := fmt.Sprintf("custom_field%d", i) - newColumnName := fmt.Sprintf("custom_field_%d", i) - if err := renameColumnSafely(db, issuesName, oldColumnName, newColumnName, dal.Text); err != nil { - return err - } + } + + // Execute batch rename for each table + for tableName, renames := range tableRenames { + if err := batchRenameColumns(db, tableName, renames); err != nil { + return err } } + return nil } @@ -71,18 +83,71 @@ func (*modifyCustomFieldName) Name() string { return "modify tapd custom field name" } -func renameColumnSafely(db dal.Dal, table, oldColumn string, newColumn string, newColumnType dal.ColumnType) errors.Error { - if table == "" || oldColumn == "" || newColumn == "" { - return errors.BadInput.New("empty params") +// batchRenameColumns renames multiple columns in a single ALTER TABLE statement +func batchRenameColumns(db dal.Dal, table string, renames []columnRename) errors.Error { + if len(renames) == 0 { + return nil } - if db.HasColumn(table, oldColumn) { - if !db.HasColumn(table, newColumn) { - return db.RenameColumn(table, oldColumn, newColumn) + + // Get all existing column names once to avoid repeated information_schema queries + existingColumns := getExistingColumns(db, table) + + // Filter out renames where old column doesn't exist or new column already exists + validRenames := filterValidRenamesCached(renames, existingColumns) + if len(validRenames) == 0 { + return nil + } + + var sql string + var dialect = db.Dialect() + + if dialect == "postgres" { + // PostgreSQL requires separate ALTER TABLE statements for each RENAME COLUMN + for _, rename := range validRenames { + sql = fmt.Sprintf(`ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"`, table, rename.Old, rename.New) + if err := db.Exec(sql); err != nil { + return err + } } + // Clear PostgreSQL cached plan after all renames + _ = db.Exec("SELECT * FROM ? LIMIT 1", clause.Table{Name: table}) } else { - if !db.HasColumn(table, newColumn) { - return db.AddColumn(table, newColumn, newColumnType) + // MySQL: ALTER TABLE t CHANGE COLUMN a new_name TEXT, CHANGE COLUMN c new_name2 TEXT + clauses := make([]string, 0, len(validRenames)) + for _, rename := range validRenames { + clauses = append(clauses, fmt.Sprintf("CHANGE COLUMN `%s` `%s` %s", rename.Old, rename.New, dal.Text.String())) + } + sql = fmt.Sprintf("ALTER TABLE `%s` %s", table, strings.Join(clauses, ", ")) + if err := db.Exec(sql); err != nil { + return err } } + return nil } + +// getExistingColumns fetches all column names for a table in a single query +func getExistingColumns(db dal.Dal, table string) map[string]bool { + columns := make(map[string]bool) + columnMetas, err := db.GetColumns(&dal.DefaultTabler{Name: table}, nil) + if err != nil { + return columns + } + for _, col := range columnMetas { + columns[col.Name()] = true + } + return columns +} + +// filterValidRenamesCached checks which renames are needed using pre-fetched column map +func filterValidRenamesCached(renames []columnRename, existingColumns map[string]bool) []columnRename { + valid := make([]columnRename, 0, len(renames)) + for _, rename := range renames { + oldExists := existingColumns[rename.Old] + newExists := existingColumns[rename.New] + if oldExists && !newExists { + valid = append(valid, rename) + } + } + return valid +}