// 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"
"sourcecraft.dev/bigbes/sr-ht-core/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 `<cmd> --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
}