Files
multica/server/internal/migrations/migrations.go
Multica Eve a91a390d48 fix(cli): recover daemon executable path (MUL-4514)
* fix(cli): recover daemon executable path

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): reuse executable fallback for restart

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-14 14:50:22 +08:00

101 lines
2.4 KiB
Go

package migrations
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/multica-ai/multica/server/internal/selfexec"
)
const maxSearchDepth = 4
var candidateLeaves = []string{
"migrations",
filepath.Join("server", "migrations"),
}
// ResolveDir returns the first migrations directory that exists from the
// current working directory.
func ResolveDir() (string, error) {
seen := make(map[string]bool)
for _, root := range searchRoots() {
base := root
for range maxSearchDepth + 1 {
for _, leaf := range candidateLeaves {
dir := filepath.Clean(filepath.Join(base, leaf))
if seen[dir] {
continue
}
seen[dir] = true
info, err := os.Stat(dir)
if err == nil && info.IsDir() {
return dir, nil
}
}
base = filepath.Join(base, "..")
}
}
return "", fmt.Errorf("migrations directory not found")
}
func searchRoots() []string {
roots := []string{"."}
if exe, err := selfexec.Resolve(); err == nil {
roots = append(roots, filepath.Dir(exe))
}
return roots
}
// Files returns sorted migration files for the given direction ("up" or
// "down").
func Files(direction string) ([]string, error) {
dir, err := ResolveDir()
if err != nil {
return nil, err
}
suffix := "." + direction + ".sql"
files, err := filepath.Glob(filepath.Join(dir, "*"+suffix))
if err != nil {
return nil, err
}
if direction == "down" {
sort.Sort(sort.Reverse(sort.StringSlice(files)))
} else {
sort.Strings(files)
}
return files, nil
}
// AllVersions returns every "up" migration version found on disk, in apply
// order. The readiness check verifies that all of them are recorded in
// schema_migrations — checking only the lexically-last version would miss an
// out-of-order migration (one numbered below an already-applied later one),
// letting a server report ready while running against a schema that lacks it.
func AllVersions() ([]string, error) {
files, err := Files("up")
if err != nil {
return nil, err
}
if len(files) == 0 {
return nil, fmt.Errorf("no up migrations found")
}
versions := make([]string, len(files))
for i, f := range files {
versions[i] = ExtractVersion(f)
}
return versions, nil
}
// ExtractVersion strips the .up.sql / .down.sql suffix from a migration file.
func ExtractVersion(filename string) string {
base := filepath.Base(filename)
base = strings.TrimSuffix(base, ".up.sql")
base = strings.TrimSuffix(base, ".down.sql")
return base
}