feat: funciones impuras migration_create, migration_up, migration_down, migration_status
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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user