diff --git a/cli/commands.go b/cli/commands.go index 2cfa9d5..aecbd85 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -1,24 +1,81 @@ package main import ( + "errors" + "fmt" "github.com/golang-migrate/migrate" _ "github.com/golang-migrate/migrate/database/stub" // TODO remove again _ "github.com/golang-migrate/migrate/source/file" "os" - "fmt" + "path/filepath" + "strconv" + "strings" ) -func createCmd(dir string, timestamp int64, name string, ext string) { - base := fmt.Sprintf("%v%v_%v.", dir, timestamp, name) +func nextSeq(matches []string, dir string, seqDigits int) (string, error) { + if seqDigits <= 0 { + return "", errors.New("Digits must be positive") + } + + nextSeq := 1 + if len(matches) > 0 { + filename := matches[len(matches)-1] + matchSeqStr := strings.TrimPrefix(filename, dir) + idx := strings.Index(matchSeqStr, "_") + if idx < 1 { // Using 1 instead of 0 since there should be at least 1 digit + return "", errors.New("Malformed migration filename: " + filename) + } + matchSeqStr = matchSeqStr[0:idx] + var err error + nextSeq, err = strconv.Atoi(matchSeqStr) + if err != nil { + return "", err + } + nextSeq++ + } + if nextSeq <= 0 { + return "", errors.New("Next sequence number must be positive") + } + + nextSeqStr := strconv.Itoa(nextSeq) + if len(nextSeqStr) > seqDigits { + return "", fmt.Errorf("Next sequence number %s too large. At most %d digits are allowed", nextSeqStr, seqDigits) + } + padding := seqDigits - len(nextSeqStr) + if padding > 0 { + nextSeqStr = strings.Repeat("0", padding) + nextSeqStr + } + return nextSeqStr, nil +} + +func createCmd(dir string, timestamp int64, name string, ext string, seq bool, seqDigits int) { + var base string + if seq { + if seqDigits <= 0 { + log.fatalErr(errors.New("Digits must be positive")) + } + matches, err := filepath.Glob(dir + "*" + ext) + if err != nil { + log.fatalErr(err) + } + nextSeqStr, err := nextSeq(matches, dir, seqDigits) + if err != nil { + log.fatalErr(err) + } + base = fmt.Sprintf("%v%v_%v.", dir, nextSeqStr, name) + } else { + base = fmt.Sprintf("%v%v_%v.", dir, timestamp, name) + } + os.MkdirAll(dir, os.ModePerm) createFile(base + "up" + ext) createFile(base + "down" + ext) } func createFile(fname string) { - if _, err := os.Create(fname); err != nil { - log.fatalErr(err) - } + if _, err := os.Create(fname); err != nil { + log.fatalErr(err) + } } func gotoCmd(m *migrate.Migrate, v uint) { diff --git a/cli/commands_test.go b/cli/commands_test.go new file mode 100644 index 0000000..6ab0ed4 --- /dev/null +++ b/cli/commands_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "testing" +) + +func TestNextSeq(t *testing.T) { + cases := []struct { + name string + matches []string + dir string + seqDigits int + expected string + expectedErrStr string + }{ + {"Bad digits", []string{}, "migrationDir", 0, "", "Digits must be positive"}, + {"Single digit initialize", []string{}, "migrationDir", 1, "1", ""}, + {"Single digit malformed", []string{"bad"}, "migrationDir", 1, "", "Malformed migration filename: bad"}, + {"Single digit no int", []string{"bad_bad"}, "migrationDir", 1, "", "strconv.Atoi: parsing \"bad\": invalid syntax"}, + {"Single digit negative seq", []string{"-5_test"}, "migrationDir", 1, "", "Next sequence number must be positive"}, + {"Single digit increment", []string{"3_test", "4_test"}, "migrationDir", 1, "5", ""}, + {"Single digit overflow", []string{"9_test"}, "migrationDir", 1, "", "Next sequence number 10 too large. At most 1 digits are allowed"}, + {"Zero-pad initialize", []string{}, "migrationDir", 6, "000001", ""}, + {"Zero-pad malformed", []string{"bad"}, "migrationDir", 6, "", "Malformed migration filename: bad"}, + {"Zero-pad no int", []string{"bad_bad"}, "migrationDir", 6, "", "strconv.Atoi: parsing \"bad\": invalid syntax"}, + {"Zero-pad negative seq", []string{"-000005_test"}, "migrationDir", 6, "", "Next sequence number must be positive"}, + {"Zero-pad increment", []string{"000003_test", "000004_test"}, "migrationDir", 6, "000005", ""}, + {"Zero-pad overflow", []string{"999999_test"}, "migrationDir", 6, "", "Next sequence number 1000000 too large. At most 6 digits are allowed"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + nextSeq, err := nextSeq(c.matches, c.dir, c.seqDigits) + if nextSeq != c.expected { + t.Error("Incorrect nextSeq: " + nextSeq + " != " + c.expected) + } + if err != nil { + if err.Error() != c.expectedErrStr { + t.Error("Incorrect error: " + err.Error() + " != " + c.expectedErrStr) + } + } else if c.expectedErrStr != "" { + t.Error("Expected error: " + c.expectedErrStr + " but got nil instead") + } + }) + } +} diff --git a/cli/main.go b/cli/main.go index ec563c2..75b9b39 100644 --- a/cli/main.go +++ b/cli/main.go @@ -42,8 +42,9 @@ Options: -help Print usage Commands: - create [-ext E] [-dir D] NAME - Create a set of timestamped up/down migrations titled NAME, in directory D with extension E + create [-ext E] [-dir D] [-seq] [-digits N] NAME + Create a set of timestamped up/down migrations titled NAME, in directory D with extension E. + Use -seq option to generate sequential up/down migrations with N digits. goto V Migrate to version V up [N] Apply all or N up migrations down [N] Apply all or N down migrations @@ -106,10 +107,14 @@ Commands: switch flag.Arg(0) { case "create": args := flag.Args()[1:] + seq := false + seqDigits := 6 createFlagSet := flag.NewFlagSet("create", flag.ExitOnError) extPtr := createFlagSet.String("ext", "", "File extension") dirPtr := createFlagSet.String("dir", "", "Directory to place file in (default: current working directory)") + createFlagSet.BoolVar(&seq, "seq", seq, "Use sequential numbers instead of timestamps (default: false)") + createFlagSet.IntVar(&seqDigits, "digits", seqDigits, "The number of digits to use in sequences (default: 6)") createFlagSet.Parse(args) if createFlagSet.NArg() == 0 { @@ -126,7 +131,7 @@ Commands: timestamp := startTime.Unix() - createCmd(*dirPtr, timestamp, name, *extPtr) + createCmd(*dirPtr, timestamp, name, *extPtr, seq, seqDigits) case "goto": if migraterErr != nil {