package swmigration import ( "context" "database/sql" "fmt" "log/slog" "sort" "strings" "gorm.io/gorm" scripts "kra/deploy/migrations" ) func migrateFramework(ctx context.Context, db *gorm.DB, conn *sql.Conn, logger *slog.Logger) error { tables, err := tableSet(db) if err != nil { return err } if tables["sw_kra_state"] { var previous struct{ Version, Phase string } if err := db.Table("sw_kra_state").Where("id=1").Take(&previous).Error; err != nil { return err } if previous.Version != "20261103_framework_migration_v5" { return fmt.Errorf("unsupported legacy migration version %q", previous.Version) } switch previous.Phase { case "completed": return nil case "data_validated": return finishArchives(ctx, db, conn) case "backup": // All framework data changes and the data_validated marker commit // together. In this phase only empty target DDL may survive a crash. if err := resetIncompleteFramework(ctx, db, conn, tables); err != nil { return err } default: return fmt.Errorf("unsupported partial migration phase %q", previous.Phase) } } else if !tables["sys_user"] { return fmt.Errorf("legacy source users and migration evidence are missing") } return executeScript(ctx, conn, scripts.FrameworkSQL(), false, logger) } func resetIncompleteFramework(ctx context.Context, db *gorm.DB, conn *sql.Conn, tables map[string]bool) error { for source, archive := range scripts.LegacyArchives() { if !tables[source] || tables[archive] { return fmt.Errorf("incomplete framework sources are not recoverable: %s", source) } } created := scripts.FrameworkCreatedTables() sort.SliceStable(created, func(i, j int) bool { priority := func(name string) int { if name == "sw_kra_state" { return 2 } if name == "sw_kra_checks" { return 1 } return 0 } return priority(created[i]) < priority(created[j]) }) for _, name := range created { if !tables[name] || strings.HasPrefix(name, "bak_sw2kra_") || name == "sw_kra_state" || name == "sw_kra_checks" { continue } var count int64 if err := db.Table(name).Count(&count).Error; err != nil { return err } if count != 0 { return fmt.Errorf("partial framework target %s contains committed data; automatic reset refused", name) } } for _, name := range created { if tables[name] { if _, err := conn.ExecContext(ctx, "DROP TABLE "+quoteName(name)); err != nil { return err } } } return nil } func finishArchives(ctx context.Context, db *gorm.DB, conn *sql.Conn) error { tables, err := tableSet(db) if err != nil { return err } for _, name := range scripts.FrameworkCreatedTables() { if !tables[name] { return fmt.Errorf("validated framework table is missing: %s", name) } } if err := verifyCheckRows(db); err != nil { return err } var moves []string for source, archive := range scripts.LegacyArchives() { if tables[source] == tables[archive] { return fmt.Errorf("ambiguous legacy archive state: %s", source) } if tables[source] { moves = append(moves, quoteName(source)+" TO "+quoteName(archive)) } } sort.Strings(moves) if len(moves) > 0 { if _, err := conn.ExecContext(ctx, "RENAME TABLE "+strings.Join(moves, ",")); err != nil { return err } } _, err = conn.ExecContext(ctx, "UPDATE sw_kra_state SET phase='completed',completed_at=NOW(3) WHERE id=1") return err }