package main import ( "context" "flag" "fmt" "os" "strings" "github.com/loyaly/behavision-server/internal/migrate" "github.com/loyaly/behavision-server/internal/store" "github.com/loyaly/behavision-server/migrations" ) // runMigrate is the operator's view of the schema. // // The server applies pending migrations itself at boot, so this exists for the // two things it cannot do: SAY what a database is on without changing it, and // adopt a database whose schema predates the tracking table. func runMigrate(args []string) error { fs := flag.NewFlagSet("migrate", flag.ExitOnError) status := fs.Bool("status", false, "print what this database has recorded and exit") baseline := fs.Int("baseline", 0, "record migrations up to this version as applied WITHOUT running them") fs.Usage = func() { fmt.Fprint(os.Stderr, `usage: behavision-server migrate [flags] no flags apply everything this database has not recorded -status print the recorded versions and what is pending -baseline N adopt a database built before migration tracking existed: record 1..N as applied without running them. Use it once, on a database whose schema you know already matches those files. DATABASE_URL is required. `) fs.PrintDefaults() } if err := fs.Parse(args); err != nil { return err } dsn := os.Getenv("DATABASE_URL") if dsn == "" { return fmt.Errorf("DATABASE_URL is required") } ctx := context.Background() st, err := store.Open(ctx, dsn) if err != nil { return err } defer st.Close() files, err := migrate.Load(migrations.FS) if err != nil { return err } if *baseline > 0 { marked, err := migrate.Baseline(ctx, st.Pool(), migrations.FS, *baseline) if err != nil { return err } if len(marked) == 0 { fmt.Println("nothing to adopt - every migration up to that version was already recorded") return nil } fmt.Printf("adopted without running: %s\n", strings.Join(marked, ", ")) fmt.Println("run `migrate` (no flags) to apply anything after that.") return nil } if *status { done, err := migrate.Status(ctx, st.Pool()) if err != nil { return err } seen := map[int]migrate.Record{} for _, r := range done { seen[r.Version] = r } for _, f := range files { r, ok := seen[f.Version] switch { case !ok: fmt.Printf(" pending %s\n", f.Name) case r.Baselined: fmt.Printf(" adopted %s (recorded, never run here)\n", f.Name) case r.Checksum != f.Checksum: fmt.Printf(" CHANGED %s (the file differs from what was applied)\n", f.Name) default: fmt.Printf(" applied %s\n", f.Name) } } return nil } applied, err := migrate.Apply(ctx, st.Pool(), migrations.FS) if err != nil { return err } if len(applied) == 0 { fmt.Println("schema up to date") return nil } fmt.Printf("applied: %s\n", strings.Join(applied, ", ")) return nil }