Files
fn_registry/functions/infra/migration_parse.go
T
egutierrez ec36278c7b feat: tipos Migration/MigrationStatus y funciones puras migration_parse + migration_validate
Fase 1 del issue 0015. Tipos Go en functions/infra/migration.go con metadata en
types/infra/. Funciones puras: MigrationParse (parsea filename NNN_name.sql +
bloques -- +up/-- +down) y MigrationValidate (verifica secuencia, huecos,
duplicados, bloques vacios). 16 tests, todos pasan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:34 +02:00

112 lines
3.1 KiB
Go

package infra
import (
"fmt"
"strconv"
"strings"
)
// MigrationParse parses a migration filename and its SQL content into a Migration.
// The filename must follow the pattern NNN_name.sql (e.g. 001_create_users.sql).
// The content must contain a -- +up marker; -- +down is optional but recommended.
// Returns error if the filename format is invalid or the -- +up block is missing.
func MigrationParse(filename, content string) (Migration, error) {
// Strip path prefix if any
base := filename
if idx := strings.LastIndex(base, "/"); idx >= 0 {
base = base[idx+1:]
}
if idx := strings.LastIndex(base, "\\"); idx >= 0 {
base = base[idx+1:]
}
// Remove .sql extension
name := base
if strings.HasSuffix(strings.ToLower(name), ".sql") {
name = name[:len(name)-4]
}
// Split on first underscore to get version and descriptive name
idx := strings.Index(name, "_")
if idx < 0 {
return Migration{}, fmt.Errorf("migration_parse: filename %q must follow pattern NNN_name.sql", filename)
}
versionStr := name[:idx]
descriptiveName := name[idx+1:]
version, err := strconv.Atoi(versionStr)
if err != nil || version <= 0 {
return Migration{}, fmt.Errorf("migration_parse: filename %q version %q must be a positive integer", filename, versionStr)
}
if descriptiveName == "" {
return Migration{}, fmt.Errorf("migration_parse: filename %q must have a non-empty descriptive name after the version", filename)
}
// Parse up/down blocks
upSQL, downSQL, err := parseMigrationBlocks(content)
if err != nil {
return Migration{}, fmt.Errorf("migration_parse: %w", err)
}
return Migration{
Version: version,
Name: descriptiveName,
UpSQL: upSQL,
DownSQL: downSQL,
}, nil
}
// parseMigrationBlocks splits SQL content by -- +up and -- +down markers.
// Returns (upSQL, downSQL, error). The -- +up block is required.
func parseMigrationBlocks(content string) (string, string, error) {
const markerUp = "-- +up"
const markerDown = "-- +down"
// Normalize line endings
content = strings.ReplaceAll(content, "\r\n", "\n")
upIdx := indexMarker(content, markerUp)
if upIdx < 0 {
return "", "", fmt.Errorf("missing -- +up marker in content")
}
downIdx := indexMarker(content, markerDown)
var upSQL, downSQL string
if downIdx < 0 {
// No down block
upSQL = strings.TrimSpace(content[upIdx+len(markerUp):])
} else if downIdx > upIdx {
// Normal order: up first, then down
upSQL = strings.TrimSpace(content[upIdx+len(markerUp) : downIdx])
downSQL = strings.TrimSpace(content[downIdx+len(markerDown):])
} else {
// Down before up — still valid, just unusual
downSQL = strings.TrimSpace(content[downIdx+len(markerDown) : upIdx])
upSQL = strings.TrimSpace(content[upIdx+len(markerUp):])
}
if upSQL == "" {
return "", "", fmt.Errorf("-- +up block is empty")
}
return upSQL, downSQL, nil
}
// indexMarker finds the index of a marker at the start of any line in content.
func indexMarker(content, marker string) int {
lines := strings.Split(content, "\n")
pos := 0
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == marker {
return pos
}
pos += len(line) + 1 // +1 for the newline
}
return -1
}