uptrace / bun

SQL-first Golang ORM
https://bun.uptrace.dev
BSD 2-Clause "Simplified" License
3.83k stars 230 forks source link

cmd/bundb: Bun CLI for database schema management #1062

Open bevzzz opened 6 days ago

bevzzz commented 6 days ago

🚧 This PR is work in progress. Any feedback is welcome.

Summary

$ bundb -h
NAME:
   bundb - Database migration tool for uptrace/bun

USAGE:
   bundb [global options] command [command options]

COMMANDS:
   auto     manage database schema with AutoMigrator
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help

Motivation

To provide a default CLI utility for Bun users to interact with (Auto-)Migrator. Our Migrations guide currently has users build their own binary, but it would be great to eliminate this step for those that do not have any special logic on top of "my models have changed, please generate migrations for it".

Related proposal: https://github.com/uptrace/bun/issues/875

Implementation

As per the current docs, the migration binary has to be co-located with the user's migrations/ package and compiled together to access the registered / discovered migrations:

package migrations
var Migrations = migrate.NewMigrations()

// Then somewhere in cmd/mybun/main.go
migrator := migrate.NewMigrator(migrations.Migrations)

To distribute bundb as a standalone binary, we need to be able to load objects from the users' packages dynamically. This is achieved by leveraging plugin package from the Go's standard library. The implementation details are handled by bundb (it will build the user's package in -buildmode=plugin to then be able to import symbols from it at runtime), so that the users won't have to deal with any of it.

The setup required to use AutoMigrator is only a little different from the current on:

package migrations
var Models = []interface{ (*Book)(nil), (*Author)(nil) }

Bonus: this method works for accessing Migrations and using them with the usual Migrator pretty much out of the box.


Besides that, this is a just a plain old CLI built with urfave/cli/v2.

How to test locally

After checking out this branch locally:

  1. Build the binary
go install ./cmd/bundb
  1. Run a local Postgres instance (currently the only dialect supporting inspection / migration)
docker run --name pg-library -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres
  1. Store the connection as environment variable:
export BUNDB_URI="postgres://postgres:@localhost:5432/postgres?sslmode=disable"
  1. Create a new project elsewhere and define some Bun models in its main.go file:
mkdir example/ && cd example/
go mod init example.com/library

# Use the latest version of uptrace/bun
go work init
go work use . <path/to/uptrace/bun>
Example main.go ```go package library import ( "github.com/uptrace/bun" ) type Book struct { ISBN string `bun:",pk"` } type Author struct { bun.BaseModel `bun:"table:writers"` ID int64 `bun:",pk,identity"` FirstName string `bun:",notnull"` LastName string `bun:",notnull"` } ```

Declare Bun models which AutoMigrator should target:

mkdir migrations/
touch migrations/main.go
// migrations/main.go
package main

import "example.com/library"

var Models = []interface{}{
        // From the example package above
        (*library.Book)(nil),
        (*library.Author)(nil),
}
  1. Finally, try bundb out:
bundb auto migrate

Discussion

  1. Currently Models []interface{} is the only AutoMigrator configuration that's expressed "in Go" and other options can be passed via command-line flags. Do we want to also allow the users to configure their own AutoMigrator completely (including the database connections, etc) themselves and just export it in their migrations package? I.e. use migrations.AutoMigrator if one is exported., ignoring all related CLI options?

  2. How much logging do we want? I'm thinking:

    • By default, print some useful messages like "created 2 migration files: 20240301_public.up.sql and 20240301_public.down.sql" or `"nothing to migrate, ok"
    • Provide -v | --verbose to enable query logging (set BUNDEBUG=2) or something like that
    • Provide --silent to disable logging altogether
  3. Do we want to have a mechanism for persisting config locally? For example, the user runs bundb init --create-directory=db-migrations. If we store directory=db-migrations in a local config file, they can run subsequent commands without passing any flags: bundb auto migrate.
    If a config file sounds like an overkill, we could store these to the env variables (those won't be persisted between shell sessions)


On the naming

We didn't want to potentially clash with bun.js CLI, we decided to add some sort of a qualified, e.g. "bungo". Then I saw that migrate example calls its application bun db, so I borrowed it directly from there, omitting the space.

vmihailenco commented 5 days ago

Using plugin looks interesting, but risky and it does not work on Windows :( But I actually had similar idea.

An alternative can be to provide buncli as a package that contains the cli.App, e,g.

package buncli

func New(options ...Option) *cli.App {}

And then we can provide some command that creates an initial app structure/dirs or just some repo with a bunch of folders/files that uses the buncli package, e.g.

package main

import "github.com/uptace/bun/buncli"

func main() {
    app := buncli.New(buncli.WithAutoMigrator(...))
    app.Run()
}

Then user can directly modify main.go to register models etc.

WDYT?

bevzzz commented 4 days ago

Using plugin looks interesting, but risky

Sharing this really useful summary of caveats to look out for from someone who tried using the plugins productively.
I agree that making this the default solution would prove too quite tricky.

[...] does not work on Windows. An alternative can be to provide buncli as a package that contains the cli.App ... some command that creates an initial app structure/dirs

Yep, that's the way to go, I'll focus on this approach.