···11+Copyright (c) 2026 Doug Tangren
22+33+Permission is hereby granted, free of charge, to any person obtaining
44+a copy of this software and associated documentation files (the
55+"Software"), to deal in the Software without restriction, including
66+without limitation the rights to use, copy, modify, merge, publish,
77+distribute, sublicense, and/or sell copies of the Software, and to
88+permit persons to whom the Software is furnished to do so, subject to
99+the following conditions:
1010+1111+The above copyright notice and this permission notice shall be
1212+included in all copies or substantial portions of the Software.
1313+1414+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1515+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1616+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
1717+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
1818+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
1919+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
2020+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+9
README.md
···11+# moose
22+33+Moose is a Go-focused [MongoDB](https://www.mongodb.com/) database migration tool, inspired by [Goose](https://github.com/pressly/goose).
44+55+Features:
66+77+* <img height="14" src="https://octicons-col.vercel.app/tools/00ED64"/> A CLI and a library for writing customized migration tooling
88+* <img height="14" src="https://octicons-col.vercel.app/file-binary/00ED64"/> Write migrations in Go or JSON
99+* <img height="14" src="https://octicons-col.vercel.app/container/00ED64"/> Supports embedded migrations, simplifying packaging and deployment in CI/CD pipelines
+308
cmd/moose/main.go
···11+package main
22+33+import (
44+ "cmp"
55+ "context"
66+ "errors"
77+ "flag"
88+ "fmt"
99+ "log"
1010+ "os"
1111+ "path/filepath"
1212+ "runtime/debug"
1313+ "sort"
1414+ "strconv"
1515+ "strings"
1616+ "text/tabwriter"
1717+ "time"
1818+1919+ "tangled.org/softprops.tngl.sh/moose"
2020+ "tangled.org/softprops.tngl.sh/moose/internal/stats"
2121+ "go.mongodb.org/mongo-driver/v2/mongo"
2222+ "go.mongodb.org/mongo-driver/v2/mongo/options"
2323+)
2424+2525+var version string // assigned at compile time with `Go build -ldflags "-X main.version=v0.1.0"`
2626+2727+func main() {
2828+ log.SetFlags(0) // clear log default prefix
2929+3030+ var dir string
3131+ var uri string
3232+ var db string
3333+ var sequential bool
3434+ var allowMissing bool
3535+ var printVersion bool
3636+3737+ flags := flag.NewFlagSet("moose", flag.ExitOnError)
3838+ flags.StringVar(&dir, "dir", cmp.Or(os.Getenv("MOOSE_MIGRATION_DIR"), "migrations"), "directory with migration files, (MOOSE_MIGRATION_DIR env variable supported)")
3939+ flags.StringVar(&uri, "uri", cmp.Or(os.Getenv("MOOSE_MONGODB_URI"), "mongodb://127.0.0.1:27017"), "MongoDB connection string, (MONGODB_URI env variable supported)")
4040+ flags.StringVar(&db, "db", os.Getenv("MOOSE_MIGRATION_DB"), "database to apply migration files to, (MOOSE_MIGRATION_DB env variable supported)")
4141+ flags.BoolVar(&printVersion, "version", false, "print version")
4242+ flags.BoolVar(&sequential, "s", false, "use seqential number for new migrations")
4343+ flags.BoolVar(&allowMissing, "allow-missing", false, "allow missing (out-of-order) migrations")
4444+ flags.Usage = usage(flags)
4545+ if err := flags.Parse(os.Args[1:]); err != nil {
4646+ log.Fatal(err)
4747+ }
4848+4949+ if printVersion {
5050+ if info, ok := debug.ReadBuildInfo(); version == "" && ok && info != nil && info.Main.Version == "" {
5151+ version = info.Main.Version
5252+ }
5353+ fmt.Printf("moose version: %s\n", strings.TrimSpace(version))
5454+ return
5555+ }
5656+5757+ args := flags.Args()
5858+5959+ if len(args) == 0 {
6060+ flags.Usage()
6161+ os.Exit(1)
6262+ }
6363+6464+ client, err := mongo.Connect(options.Client().ApplyURI(uri))
6565+ if err != nil {
6666+ _, _ = fmt.Fprintf(os.Stderr, "moose: %v\n", err)
6767+ os.Exit(1)
6868+ }
6969+ defer func() { _ = client.Disconnect(context.Background()) }()
7070+7171+ if err := Run(args[0], dir, client.Database(db), args[1:], moose.WithAllowMissing(allowMissing), moose.WithSequential(sequential)); err != nil {
7272+7373+ _, _ = fmt.Fprintf(os.Stderr, "moose: %v\n", err)
7474+ os.Exit(1)
7575+ }
7676+}
7777+7878+func Run(cmd, dir string, db *mongo.Database, args []string, opts ...moose.ProviderOption) error {
7979+ provider, err := moose.NewProvider(
8080+ db, os.DirFS(dir), opts...,
8181+ )
8282+ if err != nil {
8383+ return err
8484+ }
8585+8686+ switch cmd {
8787+ case "create":
8888+ if len(args) == 0 {
8989+ return errors.New("create must be of the form: moose [OPTIONS] create NAME [go|json]")
9090+ }
9191+ migrationType := moose.MigrationTypeGo
9292+ if len(args) == 2 {
9393+ migrationType = moose.MigrationType(args[1])
9494+ }
9595+ file, err := moose.Create(dir, args[0], migrationType, true)
9696+ if err != nil {
9797+ return err
9898+ }
9999+ fmt.Printf("Created new migration file: %s\n", file)
100100+ case "up":
101101+ results, err := provider.Up(context.Background())
102102+ if err != nil {
103103+ return err
104104+ }
105105+ if len(results) == 0 {
106106+ version, err := provider.DBVersion(context.Background())
107107+ if err != nil {
108108+ return err
109109+ }
110110+ fmt.Printf("no new migrations to apply. current version %d", version)
111111+ }
112112+ for _, r := range results {
113113+ fmt.Println(r)
114114+ }
115115+ case "up-by-one":
116116+ result, err := provider.UpByOne(context.Background())
117117+ if err != nil {
118118+ if errors.Is(err, moose.ErrNoNextVersion) {
119119+ version, err := provider.DBVersion(context.Background())
120120+ if err != nil {
121121+ return err
122122+ }
123123+ fmt.Printf("no new migrations to apply. current version %d", version)
124124+ return nil
125125+ }
126126+ return err
127127+ }
128128+ fmt.Println(result)
129129+ case "up-to":
130130+ if len(args) == 0 {
131131+ return errors.New("up-to must be of the form: moose [OPTIONS] up-to VERSION")
132132+ }
133133+134134+ version, err := strconv.ParseInt(args[0], 10, 64)
135135+ if err != nil {
136136+ return errors.New("version must be an integer")
137137+ }
138138+ results, err := provider.UpTo(context.Background(), version)
139139+ if err != nil {
140140+ return err
141141+ }
142142+ if len(results) == 0 {
143143+ version, err := provider.DBVersion(context.Background())
144144+ if err != nil {
145145+ return err
146146+ }
147147+ fmt.Printf("no new migrations to apply. current version %d", version)
148148+ }
149149+150150+ for _, r := range results {
151151+ fmt.Println(r)
152152+ }
153153+ case "down":
154154+ results, err := provider.Down(context.Background())
155155+ if err != nil {
156156+ if errors.Is(err, moose.ErrNoNextVersion) {
157157+ version, err := provider.DBVersion(context.Background())
158158+ if err != nil {
159159+ return err
160160+ }
161161+ fmt.Printf("no new migrations to apply. current version %d", version)
162162+ return nil
163163+ }
164164+ return err
165165+ }
166166+ fmt.Printf("results %s", results)
167167+ case "down-to":
168168+ if len(args) == 0 {
169169+ return errors.New("down-to must be of the form: moose [OPTIONS] down-to VERSION")
170170+ }
171171+ version, err := strconv.ParseInt(args[0], 10, 64)
172172+ if err != nil {
173173+ return errors.New("version must be an integer")
174174+ }
175175+ results, err := provider.DownTo(context.Background(), version)
176176+ if err != nil {
177177+ return err
178178+ }
179179+180180+ if errors.Is(err, moose.ErrNoNextVersion) {
181181+ version, err := provider.DBVersion(context.Background())
182182+ if err != nil {
183183+ return err
184184+ }
185185+ fmt.Printf("no new migrations to apply. current version %d", version)
186186+ return nil
187187+ }
188188+ fmt.Printf("results %s", results)
189189+ case "validate":
190190+ if err := validate(dir); err != nil {
191191+ return err
192192+ }
193193+ case "status":
194194+ results, err := provider.Status(context.Background())
195195+ if err != nil {
196196+ return err
197197+ }
198198+199199+ log.Printf(" Applied At Migration")
200200+ log.Printf(" =======================================")
201201+ for _, r := range results {
202202+ appliedAt := "Pending"
203203+ if !r.AppliedAt.IsZero() {
204204+ appliedAt = r.AppliedAt.Format(time.ANSIC)
205205+ }
206206+ log.Printf(" %-24s -- %v", appliedAt, filepath.Base(r.Source.Path))
207207+ }
208208+ case "version":
209209+ version, err := provider.DBVersion(context.Background())
210210+ if err != nil {
211211+ return err
212212+ }
213213+ log.Printf("moose: version %v", version)
214214+ default:
215215+ return fmt.Errorf("%q: no such command", cmd)
216216+ }
217217+ return nil
218218+}
219219+220220+func validate(dir string) error {
221221+ names, err := filenames(dir)
222222+ if err != nil {
223223+ return err
224224+ }
225225+ stats, err := stats.Collect(
226226+ stats.NewFileWalker(names...),
227227+ false,
228228+ )
229229+ if err != nil {
230230+ return err
231231+ }
232232+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent)
233233+ fmtPattern := "%v\t%v\t%v\t%v\t%v\t\n"
234234+ _, _ = fmt.Fprintf(w, fmtPattern, "Type", "Txn", "Up", "Down", "Name")
235235+ _, _ = fmt.Fprintf(w, fmtPattern, "────", "───", "──", "────", "────")
236236+ for _, m := range stats {
237237+ txnStr := "✔"
238238+ if !m.Tx {
239239+ txnStr = "✘"
240240+ }
241241+ _, _ = fmt.Fprintf(w, fmtPattern,
242242+ strings.TrimPrefix(filepath.Ext(m.FileName), "."),
243243+ txnStr,
244244+ m.UpCount,
245245+ m.DownCount,
246246+ filepath.Base(m.FileName),
247247+ )
248248+ }
249249+250250+ return w.Flush()
251251+}
252252+253253+func filenames(dir string) ([]string, error) {
254254+ stat, err := os.Stat(dir)
255255+ if err != nil {
256256+ return nil, err
257257+ }
258258+ var filenames []string
259259+ if stat.IsDir() {
260260+ for _, pattern := range []string{"*.json", "*.go"} {
261261+ file, err := filepath.Glob(filepath.Join(dir, pattern))
262262+ if err != nil {
263263+ return nil, err
264264+ }
265265+ filenames = append(filenames, file...)
266266+ }
267267+ } else {
268268+ filenames = append(filenames, dir)
269269+ }
270270+ sort.Strings(filenames)
271271+ return filenames, nil
272272+}
273273+274274+func usage(flags *flag.FlagSet) func() {
275275+ return func() {
276276+ fmt.Println(usagePrefix)
277277+ flags.PrintDefaults()
278278+ fmt.Println(usageCommands)
279279+ }
280280+}
281281+282282+var (
283283+ usagePrefix = `Usage: moose [OPTIONS] COMMAND
284284+285285+Examples:
286286+ moose -db example status
287287+ moose -db example ./foo.db create init json
288288+ moose -db example ./foo.db up
289289+290290+Options:
291291+`
292292+293293+ usageCommands = `
294294+Commands:
295295+ up Migrate the DB to the most recent version available
296296+ up-by-one Migrate the DB up by 1
297297+ up-to VERSION Migrate the DB to a specific VERSION
298298+ down Roll back the version by 1
299299+ down-to VERSION Roll back to a specific VERSION
300300+ redo Re-run the latest migration
301301+ reset Roll back all migrations
302302+ status Dump the migration status for the current DB
303303+ version Print the current version of the database
304304+ create NAME [json|go] Creates new migration file with the current timestamp
305305+ fix Apply sequential ordering to migrations
306306+ validate Check migration files without running them
307307+`
308308+)