// Command doltsrht-migrate runs the dolt.sr.ht Postgres migrations. It is a thin // single-service wrapper around git.sr.ht/~bitfehler/brant, modeled on // sourcehut-migrate: the brant subcommands (up, down, current, list, stamp, // validate, ping) apply the files under the migrations directory, and an extra // `init` subcommand loads the full schema.sql in one shot and stamps the // database to head (used for a fresh install instead of replaying every // migration). // // The connection string comes from [dolt.sr.ht]connection-string unless // overridden with --dsn. Migrations are read from ./migrations in a dev checkout // or from the installed assets path (/usr/share/sourcehut/migrations/dolt.sr.ht) // otherwise. The lib/pq "postgres" driver this module already links is used in // preference to brant's default pgx driver. package main import ( "context" "errors" "fmt" "log" "os" "path/filepath" "git.sr.ht/~bitfehler/brant" "git.sr.ht/~bitfehler/brant/cli" "github.com/alexflint/go-arg" _ "github.com/lib/pq" // registers the "postgres" database/sql driver "github.com/vaughan0/go-ini" "git.sr.ht/~sircmpwn/core-go/config" ) // serviceName is the SourceHut service identifier and config section name. const serviceName = "dolt.sr.ht" // driverName is the database/sql driver this binary links (lib/pq). It overrides // brant's postgres-dialect default of "pgx", which this module does not import. const driverName = "postgres" // InitArgs configures the `init` subcommand: load the full DDL and stamp to head. type InitArgs struct { Schema string `arg:"--schema" default:"schema.sql" placeholder:"FILE" help:"schema file to initialize the database with"` } // Args embeds brant's CLI arguments (the up/down/... subcommands and shared // flags such as --dir and --dsn) and adds the init subcommand and the -a // migrate-on-upgrade gate. type Args struct { cli.Args Init *InitArgs `arg:"subcommand:init" help:"initialize the database from the schema file and stamp to head"` Auto bool `arg:"-a" help:"honor [dolt.sr.ht]migrate-on-upgrade; exit early when it is disabled"` } func (Args) Epilogue() string { return "Use ` --help` for help with individual commands" } func main() { var a Args p := arg.MustParse(&a) if p.Subcommand() == nil { p.WriteHelp(os.Stderr) os.Exit(1) } conf := config.LoadConfig() if a.Auto && !config.GetBool(conf, serviceName, "migrate-on-upgrade", false) { log.Printf("doltsrht-migrate: [%s]migrate-on-upgrade disabled, exiting", serviceName) return } if a.DataSourceName == "" { dsn, ok := conf.Get(serviceName, "connection-string") if !ok || dsn == "" { log.Fatalf("doltsrht-migrate: no [%s]connection-string configured", serviceName) } a.DataSourceName = dsn } // Use lib/pq's "postgres" driver rather than brant's default "pgx". drv := driverName a.Driver = &drv resolvePaths(conf, &a) log.Printf("doltsrht-migrate: loading migrations from %s", a.Directory) if a.Init != nil { log.Printf("doltsrht-migrate: initializing schema from %s", a.Init.Schema) if err := initDatabase(&a); err != nil { log.Fatalf("doltsrht-migrate: init failed: %v", err) } return } cli.RunWithArgs(&a.Args) } // resolvePaths picks the migrations directory and schema file when the user did // not override --dir: a ./migrations directory in the working tree (dev // checkout) wins, otherwise the installed assets path is used, mirroring // sourcehut-migrate. func resolvePaths(conf ini.File, a *Args) { if a.Directory != "./migrations" { return // user overrode --dir; respect it verbatim } info, err := os.Stat("migrations") if err != nil && !errors.Is(err, os.ErrNotExist) { log.Fatalf("doltsrht-migrate: checking ./migrations: %v", err) } if err == nil && info.IsDir() { log.Println("doltsrht-migrate: found ./migrations, using it") return } assetsDir := config.GetString(conf, "sr.ht", "assets", "/usr/share/sourcehut") a.Directory = filepath.Join(assetsDir, "migrations", serviceName) if a.Init != nil && a.Init.Schema == "schema.sql" { a.Init.Schema = filepath.Join(assetsDir, serviceName+".sql") } } // initDatabase applies the schema file wholesale and stamps the version table to // head, so a fresh install skips replaying the incremental migrations. func initDatabase(a *Args) error { p, err := cli.ProviderFromArgs(&a.Args) if err != nil { return fmt.Errorf("creating provider: %w", err) } defer p.Close() statements, err := os.ReadFile(a.Init.Schema) if err != nil { return fmt.Errorf("reading schema %s: %w", a.Init.Schema, err) } db, err := p.DB() if err != nil { return fmt.Errorf("connecting to database: %w", err) } if _, err := db.Exec(string(statements)); err != nil { return fmt.Errorf("executing %s: %w", a.Init.Schema, err) } if err := p.Stamp(context.Background(), brant.VERSION_HEAD, false); err != nil { return fmt.Errorf("stamping database: %w", err) } return nil }