package infra import ( "fmt" "sort" ) // MigrationValidate checks a slice of migrations for consistency errors. // It verifies that: // - Versions are sequential starting from 1 with no gaps (1, 2, 3...) // - No duplicate versions exist // - Each migration has non-empty UpSQL // - Each migration has a non-empty Name // // Returns a slice of human-readable error strings. An empty slice means all // migrations are valid. The function does not mutate the input slice. func MigrationValidate(migrations []Migration) []string { var errs []string if len(migrations) == 0 { return errs } // Work on a sorted copy to detect gaps and duplicates sorted := make([]Migration, len(migrations)) copy(sorted, migrations) sort.Slice(sorted, func(i, j int) bool { return sorted[i].Version < sorted[j].Version }) // Check individual fields and collect duplicates seen := make(map[int]int) // version -> count for _, m := range sorted { seen[m.Version]++ if m.Name == "" { errs = append(errs, fmt.Sprintf("version %d has empty name", m.Version)) } if m.UpSQL == "" { errs = append(errs, fmt.Sprintf("version %d (%s) has empty up_sql", m.Version, m.Name)) } } // Report duplicates for v, count := range seen { if count > 1 { errs = append(errs, fmt.Sprintf("duplicate version %d appears %d times", v, count)) } } // Check sequential numbering starting from 1 (no gaps) // Build unique sorted versions versions := make([]int, 0, len(seen)) for v := range seen { versions = append(versions, v) } sort.Ints(versions) if len(versions) > 0 && versions[0] != 1 { errs = append(errs, fmt.Sprintf("versions must start at 1, got %d", versions[0])) } for i := 1; i < len(versions); i++ { expected := versions[i-1] + 1 if versions[i] != expected { errs = append(errs, fmt.Sprintf("gap in versions: missing %d (have %d then %d)", expected, versions[i-1], versions[i])) } } return errs }