274 lines
5.9 KiB
Go
274 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"kjol/config"
|
|
"kjol/dbutil"
|
|
|
|
_ "time/tzdata"
|
|
|
|
_ "github.com/lib/pq"
|
|
)
|
|
|
|
func invalidInput() {
|
|
fmt.Println("Usage: [up, down, drop, goto {V}, new {migration name}]")
|
|
os.Exit(1)
|
|
}
|
|
|
|
const (
|
|
defaultTimeFormat = "20060102150405"
|
|
defaultTimezone = "UTC"
|
|
)
|
|
|
|
var (
|
|
errInvalidSequenceWidth = errors.New("Digits must be positive")
|
|
errIncompatibleSeqAndFormat = errors.New("The seq and format options are mutually exclusive")
|
|
errInvalidTimeFormat = errors.New("Time format may not be empty")
|
|
)
|
|
|
|
func createFile(filename string) error {
|
|
// create exclusive (fails if file already exists)
|
|
// os.Create() specifies 0666 as the FileMode, so we're doing the same
|
|
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return f.Close()
|
|
}
|
|
|
|
func nextSeqVersion(matches []string, seqDigits int) (string, error) {
|
|
if seqDigits <= 0 {
|
|
return "", errInvalidSequenceWidth
|
|
}
|
|
|
|
nextSeq := uint64(1)
|
|
|
|
if len(matches) > 0 {
|
|
filename := matches[len(matches)-1]
|
|
matchSeqStr := filepath.Base(filename)
|
|
idx := strings.Index(matchSeqStr, "_")
|
|
|
|
if idx < 1 { // Using 1 instead of 0 since there should be at least 1 digit
|
|
return "", fmt.Errorf("Malformed migration filename: %s", filename)
|
|
}
|
|
|
|
var err error
|
|
matchSeqStr = matchSeqStr[0:idx]
|
|
nextSeq, err = strconv.ParseUint(matchSeqStr, 10, 64)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
nextSeq++
|
|
}
|
|
|
|
version := fmt.Sprintf("%0[2]*[1]d", nextSeq, seqDigits)
|
|
|
|
if len(version) > seqDigits {
|
|
return "", fmt.Errorf("Next sequence number %s too large. At most %d digits are allowed", version, seqDigits)
|
|
}
|
|
|
|
return version, nil
|
|
}
|
|
|
|
func timeVersion(startTime time.Time, format string) (version string, err error) {
|
|
switch format {
|
|
case "":
|
|
err = errInvalidTimeFormat
|
|
case "unix":
|
|
version = strconv.FormatInt(startTime.Unix(), 10)
|
|
case "unixNano":
|
|
version = strconv.FormatInt(startTime.UnixNano(), 10)
|
|
default:
|
|
version = startTime.Format(format)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func newCmd(dir string, startTime time.Time, format string, name string, ext string, seq bool, seqDigits int, print bool) error {
|
|
if seq && format != defaultTimeFormat {
|
|
return errIncompatibleSeqAndFormat
|
|
}
|
|
|
|
var version string
|
|
var err error
|
|
|
|
dir = filepath.Clean(dir)
|
|
ext = "." + strings.TrimPrefix(ext, ".")
|
|
|
|
if seq {
|
|
matches, err := filepath.Glob(filepath.Join(dir, "*"+ext))
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
version, err = nextSeqVersion(matches, seqDigits)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
version, err = timeVersion(startTime, format)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
versionGlob := filepath.Join(dir, version+"_*"+ext)
|
|
matches, err := filepath.Glob(versionGlob)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(matches) > 0 {
|
|
return fmt.Errorf("duplicate migration version: %s", version)
|
|
}
|
|
|
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, direction := range []string{"up", "down"} {
|
|
basename := fmt.Sprintf("%s_%s.%s%s", version, name, direction, ext)
|
|
filename := filepath.Join(dir, basename)
|
|
|
|
if err = createFile(filename); err != nil {
|
|
return err
|
|
}
|
|
|
|
if print {
|
|
absPath, _ := filepath.Abs(filename)
|
|
log.Println(absPath)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
envfile := flag.String("env-file", "", "Load environment variables from this file instead of the implicit ./.env. Real environment variables always take precedence.")
|
|
dir := flag.String("dir", "./migrations", "Directory containing migration files.")
|
|
|
|
flag.Parse()
|
|
|
|
var dbc struct {
|
|
Username string `env:"DATABASE_USERNAME"`
|
|
Password string `env:"DATABASE_PASSWORD"`
|
|
Host string `env:"DATABASE_HOST"`
|
|
Port int `env:"DATABASE_PORT"`
|
|
Name string `env:"DATABASE_NAME"`
|
|
Schema string `env:"DATABASE_SCHEMA"`
|
|
SSLMode string `env:"DATABASE_SSL_MODE"`
|
|
}
|
|
if err := config.Load(*envfile, &dbc); err != nil {
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
connectionString := dbutil.BuildConnectionString(dbutil.ConnConfig{
|
|
Username: dbc.Username,
|
|
Password: dbc.Password,
|
|
Host: dbc.Host,
|
|
Port: dbc.Port,
|
|
Name: dbc.Name,
|
|
Schema: dbc.Schema,
|
|
SSLMode: dbc.SSLMode,
|
|
})
|
|
|
|
// parse CLI args and do actions
|
|
args := flag.Args()
|
|
|
|
if len(args) < 1 {
|
|
invalidInput()
|
|
}
|
|
|
|
// handle "new" command early — it doesn't need a database connection
|
|
if args[0] == "new" {
|
|
if len(args) < 2 {
|
|
fmt.Println("Please provide a name for the new migration.")
|
|
os.Exit(1)
|
|
}
|
|
newCmd(*dir, time.Now(), defaultTimeFormat, args[1], "sql", true, 7, true)
|
|
return
|
|
}
|
|
|
|
eng, err := newEngine(*dir, connectionString, dbc.Schema)
|
|
if err != nil {
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer eng.close()
|
|
|
|
migrateNum := 0
|
|
|
|
if len(args) >= 2 {
|
|
var parseErr error
|
|
migrateNum, parseErr = strconv.Atoi(args[1])
|
|
if parseErr != nil {
|
|
fmt.Println("Please provide a valid migration number.")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
switch args[0] {
|
|
case "up":
|
|
if err := eng.up(); err != nil {
|
|
if errors.Is(err, errNoChange) {
|
|
fmt.Println("No change")
|
|
os.Exit(0)
|
|
}
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Database successfully migrated to latest version")
|
|
|
|
case "down":
|
|
if err := eng.down(); err != nil {
|
|
if errors.Is(err, errNoChange) {
|
|
fmt.Println("No change")
|
|
os.Exit(0)
|
|
}
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Database successfully migrated to previous version")
|
|
|
|
case "goto":
|
|
if err := eng.goTo(uint(migrateNum)); err != nil {
|
|
if errors.Is(err, errNoChange) {
|
|
fmt.Println("No change")
|
|
os.Exit(0)
|
|
}
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Database successfully migrated to version `%d`\n", migrateNum)
|
|
|
|
case "drop":
|
|
if err := eng.dropAll(); err != nil {
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Database tables, views, and types successfully dropped")
|
|
|
|
default:
|
|
invalidInput()
|
|
}
|
|
}
|