Skip to content

Commit 4967e7c

Browse files
committed
refactor: provide CLI app in a standalone package
Users can start the tool from their own entrypoints by using buncli.New/Run etc. They are also responsible for configuring the DB connection and AutoMigrator. bundb/bunctl will eventually use FromPlugin() to read config from a pre-built plugin.
1 parent 83bea5a commit 4967e7c

File tree

7 files changed

+181
-354
lines changed

7 files changed

+181
-354
lines changed

cmd/bundb/main.go

+3-354
Original file line numberDiff line numberDiff line change
@@ -25,366 +25,15 @@ Although... this way we are moving towards a .bundb.config or something.
2525
package main
2626

2727
import (
28-
"bytes"
29-
"database/sql"
30-
"fmt"
3128
"log"
3229
"os"
33-
"os/exec"
34-
"path"
35-
"plugin"
36-
"strings"
3730

38-
"github.com/uptrace/bun"
39-
"github.com/uptrace/bun/dialect/mssqldialect"
40-
"github.com/uptrace/bun/dialect/mysqldialect"
41-
"github.com/uptrace/bun/dialect/oracledialect"
42-
"github.com/uptrace/bun/dialect/pgdialect"
43-
"github.com/uptrace/bun/dialect/sqlitedialect"
44-
"github.com/uptrace/bun/driver/pgdriver"
45-
"github.com/uptrace/bun/driver/sqliteshim"
46-
"github.com/uptrace/bun/migrate"
47-
"github.com/uptrace/bun/schema"
48-
"github.com/urfave/cli/v2"
31+
"github.com/uptrace/bun/extra/buncli"
4932
)
5033

51-
const (
52-
defaultMigrationsDirectory = "./migrations"
53-
pluginName = "plugin.so"
54-
)
55-
56-
var (
57-
supportedDrivers = []string{"postgres", "sqlserver", "mysql", "oci8", "file"}
58-
autoMigratorOptions []migrate.AutoMigratorOption
59-
migrationsDirectory string
60-
)
61-
62-
var (
63-
cleanup = &cli.BoolFlag{
64-
Name: "cleanup",
65-
}
66-
)
67-
68-
var app = &cli.App{
69-
Name: "bundb",
70-
Usage: "Database migration tool for uptrace/bun",
71-
Commands: cli.Commands{
72-
// bundb init --create-directory
73-
// bundb create --sql --go --tx [-d | --dir]
74-
// bundb migrate
75-
// bundb auto create --tx
76-
// bundb auto migrate
77-
&cli.Command{
78-
Name: "auto",
79-
Usage: "manage database schema with AutoMigrator",
80-
Subcommands: cli.Commands{
81-
&cli.Command{
82-
Name: "create",
83-
Usage: "Generate SQL migration files",
84-
Flags: []cli.Flag{
85-
&cli.StringFlag{
86-
Name: "uri",
87-
Aliases: []string{"database-uri", "dsn"},
88-
Required: true,
89-
EnvVars: []string{"BUNDB_URI"},
90-
},
91-
&cli.StringFlag{
92-
Name: "driver",
93-
},
94-
&cli.StringFlag{
95-
Name: "d",
96-
Aliases: []string{"migrations-directory"},
97-
Destination: &migrationsDirectory,
98-
Value: defaultMigrationsDirectory,
99-
Action: func(ctx *cli.Context, dir string) error {
100-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithMigrationsDirectoryAuto(dir))
101-
return nil
102-
},
103-
},
104-
&cli.StringFlag{
105-
Name: "t",
106-
Aliases: []string{"migrations-table"},
107-
Action: func(ctx *cli.Context, migrationsTable string) error {
108-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithTableNameAuto(migrationsTable))
109-
return nil
110-
},
111-
},
112-
&cli.StringFlag{
113-
Name: "l",
114-
Aliases: []string{"locks", "migration-locks-table"},
115-
Action: func(ctx *cli.Context, locksTable string) error {
116-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithLocksTableNameAuto(locksTable))
117-
return nil
118-
},
119-
},
120-
&cli.StringFlag{
121-
Name: "s",
122-
Aliases: []string{"schema"},
123-
Action: func(ctx *cli.Context, schemaName string) error {
124-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithSchemaName(schemaName))
125-
return nil
126-
},
127-
},
128-
&cli.StringSliceFlag{
129-
Name: "exclude",
130-
Action: func(ctx *cli.Context, tables []string) error {
131-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithExcludeTable(tables...))
132-
return nil
133-
},
134-
},
135-
&cli.BoolFlag{
136-
Name: "rebuild",
137-
},
138-
cleanup,
139-
&cli.BoolFlag{
140-
Name: "tx",
141-
Aliases: []string{"transactional"},
142-
},
143-
},
144-
Action: func(ctx *cli.Context) error {
145-
if err := buildPlugin(ctx.Bool("rebuild")); err != nil {
146-
return err
147-
}
148-
149-
if cleanup.Get(ctx) {
150-
defer deletePlugin()
151-
}
152-
153-
db, err := connect(ctx.String("uri"), ctx.String("driver"), !ctx.IsSet("driver"))
154-
if err != nil {
155-
return err
156-
157-
}
158-
159-
if !ctx.IsSet("migrations-directory") {
160-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithMigrationsDirectoryAuto(defaultMigrationsDirectory))
161-
162-
}
163-
m, err := automigrator(db)
164-
if err != nil {
165-
return err
166-
}
167-
168-
if ctx.Bool("tx") {
169-
_, err = m.CreateTxSQLMigrations(ctx.Context)
170-
} else {
171-
_, err = m.CreateSQLMigrations(ctx.Context)
172-
}
173-
if err != nil {
174-
return err
175-
}
176-
return nil
177-
},
178-
},
179-
&cli.Command{
180-
Name: "migrate",
181-
Usage: "Generate SQL migrations and apply them right away",
182-
Flags: []cli.Flag{
183-
&cli.StringFlag{
184-
Name: "uri",
185-
Aliases: []string{"database-uri", "dsn"},
186-
Required: true,
187-
EnvVars: []string{"BUNDB_URI"},
188-
},
189-
&cli.StringFlag{
190-
Name: "driver",
191-
},
192-
&cli.StringFlag{
193-
Name: "d",
194-
Aliases: []string{"migrations-directory"},
195-
Destination: &migrationsDirectory,
196-
Value: defaultMigrationsDirectory,
197-
Action: func(ctx *cli.Context, dir string) error {
198-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithMigrationsDirectoryAuto(dir))
199-
return nil
200-
},
201-
},
202-
&cli.StringFlag{
203-
Name: "t",
204-
Aliases: []string{"migrations-table"},
205-
Action: func(ctx *cli.Context, migrationsTable string) error {
206-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithTableNameAuto(migrationsTable))
207-
return nil
208-
},
209-
},
210-
&cli.StringFlag{
211-
Name: "l",
212-
Aliases: []string{"locks", "migration-locks-table"},
213-
Action: func(ctx *cli.Context, locksTable string) error {
214-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithLocksTableNameAuto(locksTable))
215-
return nil
216-
},
217-
},
218-
&cli.StringFlag{
219-
Name: "s",
220-
Aliases: []string{"schema"},
221-
Action: func(ctx *cli.Context, schemaName string) error {
222-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithSchemaName(schemaName))
223-
return nil
224-
},
225-
},
226-
&cli.StringSliceFlag{
227-
Name: "exclude",
228-
Action: func(ctx *cli.Context, tables []string) error {
229-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithExcludeTable(tables...))
230-
return nil
231-
},
232-
},
233-
&cli.BoolFlag{
234-
Name: "rebuild",
235-
},
236-
cleanup,
237-
},
238-
Action: func(ctx *cli.Context) error {
239-
if err := buildPlugin(ctx.Bool("rebuild")); err != nil {
240-
return err
241-
}
242-
243-
if cleanup.Get(ctx) {
244-
defer deletePlugin()
245-
}
246-
247-
db, err := connect(ctx.String("uri"), ctx.String("driver"), !ctx.IsSet("driver"))
248-
if err != nil {
249-
return err
250-
251-
}
252-
253-
if !ctx.IsSet("migrations-directory") {
254-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithMigrationsDirectoryAuto(defaultMigrationsDirectory))
255-
256-
}
257-
m, err := automigrator(db)
258-
if err != nil {
259-
return err
260-
}
261-
262-
group, err := m.Migrate(ctx.Context)
263-
if err != nil {
264-
return err
265-
}
266-
if group.IsZero() {
267-
log.Print("ok, nothing to migrate")
268-
}
269-
return nil
270-
},
271-
},
272-
},
273-
},
274-
},
275-
}
276-
277-
func pluginPath() string {
278-
return path.Join(migrationsDirectory, pluginName)
279-
}
280-
281-
// TODO: wrap Build and Open steps into a sync.OnceFunc, so that we could use the Plugin object in multiple places
282-
// without having to worry if it has been compiled or not.
283-
func buildPlugin(force bool) error {
284-
if force {
285-
if err := deletePlugin(); err != nil {
286-
return err
287-
}
288-
}
289-
290-
// Cmd.Run returns *exec.ExitError which will only contain the exit code message in case of an error.
291-
// Rather than logging "exit code 1" we want to output a more informative error, so we redirect the Stderr.
292-
var errBuf bytes.Buffer
293-
294-
cmd := exec.Command("go", "build", "-C", migrationsDirectory, "-buildmode", "plugin", "-o", pluginName)
295-
cmd.Stderr = &errBuf
296-
297-
err := cmd.Run()
298-
if err != nil {
299-
// TODO: if errBuf contains "no such file or directory" add the following to the error message:
300-
// "Create 'migrations/' directory by running: bundb init --create-directory migrations/"
301-
return fmt.Errorf("build %s: %s", pluginPath(), &errBuf)
302-
}
303-
return nil
304-
}
305-
306-
func deletePlugin() error {
307-
return os.RemoveAll(pluginPath())
308-
}
309-
310-
// connect to the database under the URI. A driver must be one of the supported drivers.
311-
// If not set explicitly, the name of the driver is guessed from the URI.
312-
//
313-
// Example:
314-
//
315-
// "postgres://root:@localhost:5432/test" -> "postgres"
316-
func connect(uri, driverName string, guessDriver bool) (*bun.DB, error) {
317-
var sqldb *sql.DB
318-
var dialect schema.Dialect
319-
var err error
320-
321-
if guessDriver {
322-
driver, _, found := strings.Cut(uri, ":")
323-
if !found {
324-
return nil, fmt.Errorf("driver cannot be guessed from connection string; pass -driver option explicitly")
325-
}
326-
driverName = driver
327-
}
328-
329-
switch driverName {
330-
case "postgres":
331-
sqldb = sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(uri)))
332-
dialect = pgdialect.New()
333-
case "sqlserver":
334-
sqldb, err = sql.Open(driverName, uri)
335-
dialect = mssqldialect.New()
336-
case "file":
337-
sqldb, err = sql.Open(sqliteshim.ShimName, uri)
338-
dialect = sqlitedialect.New()
339-
case "mysql":
340-
sqldb, err = sql.Open(driverName, uri)
341-
dialect = mysqldialect.New()
342-
case "oci8":
343-
sqldb, err = sql.Open(driverName, uri)
344-
dialect = oracledialect.New()
345-
default:
346-
err = fmt.Errorf("driver %q not recognized, supported drivers are %+v", driverName, supportedDrivers)
347-
}
348-
349-
if err != nil {
350-
return nil, err
351-
}
352-
353-
return bun.NewDB(sqldb, dialect), nil
354-
}
355-
356-
// automigrator creates AutoMigrator for models from user's 'migrations' package.
357-
func automigrator(db *bun.DB) (*migrate.AutoMigrator, error) {
358-
sym, err := lookup("Models")
359-
if err != nil {
360-
return nil, err
361-
}
362-
363-
models, ok := sym.(*[]interface{})
364-
if !ok {
365-
return nil, fmt.Errorf("migrations plugin must export Models as []interface{}, got %T", models)
366-
}
367-
autoMigratorOptions = append(autoMigratorOptions, migrate.WithModel(*models...))
368-
369-
auto, err := migrate.NewAutoMigrator(db, autoMigratorOptions...)
370-
if err != nil {
371-
return nil, err
372-
}
373-
return auto, nil
374-
}
375-
376-
// lookup a symbol from user's migrations plugin.
377-
func lookup(symbol string) (plugin.Symbol, error) {
378-
p, err := plugin.Open(pluginPath())
379-
if err != nil {
380-
return nil, err
381-
}
382-
return p.Lookup(symbol)
383-
}
384-
38534
func main() {
38635
log.SetPrefix("bundb: ")
387-
if err := app.Run(os.Args); err != nil {
388-
log.Fatal(err)
36+
// TODO: use buncli.New(buncli.FromPlugin()) to read config from plugin
37+
if err := buncli.Run(os.Args, nil); err != nil {
38938
}
39039
}

0 commit comments

Comments
 (0)