package infra import ( "fmt" "os" "path/filepath" "regexp" "strconv" "strings" ) var migrationFilePattern = regexp.MustCompile(`^(\d+)_[a-zA-Z0-9_]+\.sql$`) var migrationNamePattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`) // MigrationCreate creates a new migration file in dir with the given name. // It calculates the next version by scanning existing .sql files in dir. // The filename follows the pattern NNN_name.sql (e.g. 003_add_index.sql). // Returns the absolute path of the created file. func MigrationCreate(dir, name string) (string, error) { if !migrationNamePattern.MatchString(name) { return "", fmt.Errorf("migration_create: name %q must match [a-zA-Z][a-zA-Z0-9_]*", name) } if err := os.MkdirAll(dir, 0o755); err != nil { return "", fmt.Errorf("migration_create: cannot create directory %q: %w", dir, err) } next, err := nextMigrationVersion(dir) if err != nil { return "", fmt.Errorf("migration_create: %w", err) } filename := fmt.Sprintf("%03d_%s.sql", next, name) path := filepath.Join(dir, filename) template := fmt.Sprintf("-- %s\n\n-- +up\n\n\n-- +down\n\n", filename) if err := os.WriteFile(path, []byte(template), 0o644); err != nil { return "", fmt.Errorf("migration_create: cannot write file %q: %w", path, err) } return path, nil } // nextMigrationVersion returns the next version number by scanning .sql files in dir. // Returns 1 if the directory is empty or has no migration files. func nextMigrationVersion(dir string) (int, error) { entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return 1, nil } return 0, fmt.Errorf("cannot read directory: %w", err) } max := 0 for _, e := range entries { if e.IsDir() { continue } name := e.Name() if !strings.HasSuffix(strings.ToLower(name), ".sql") { continue } if !migrationFilePattern.MatchString(name) { continue } idx := strings.Index(name, "_") if idx < 0 { continue } v, err := strconv.Atoi(name[:idx]) if err != nil { continue } if v > max { max = v } } return max + 1, nil }