35a49174ca
Fase 2 del issue 0015. MigrationCreate (crea archivo .sql template con version auto-calculada), MigrationUp (aplica migraciones pendientes en transacciones individuales), MigrationDown (revierte ultimas N via down_sql de _migrations), MigrationGetStatus (cruza disco con BD, detecta orphaned). Tests de integracion: ciclo completo create->up->status->down->status. 26 tests, todos pasan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
84 lines
2.0 KiB
Go
84 lines
2.0 KiB
Go
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
|
|
}
|