Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Draft
wants to merge 17 commits into
base: master
Choose a base branch
from

Conversation

bevzzz
Copy link
Collaborator

@bevzzz bevzzz commented Nov 19, 2024

🚧 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: #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
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
  1. 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
Copy link
Member

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
Copy link
Collaborator Author

bevzzz commented Nov 21, 2024

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.

Here we introduce bundb auto migrate command to create and run migrations from the CLI.
It works by importing the Models from a pre-build user plugin, similarly to how Migrator
gets the list of migrations by being compiled together with the user's package.
The difference here is that bundb can be distributed as a standalone binary and only requires
users to set up the migrations/main.go file.
This will remove the need for users to do any extra steps before using bundb.
Provide -rebuild flag to force rebuild and -cleanup to remove the dangling plugin file.
Improved error handling during 'go build' step
to output a more user-friendly message.
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.
- migrate
- rollback
- create
- unlock
The bootstrapped CLI which will be compiled along with the rest
of the user migration files does not need the Init command.

On the other hand, the standalone bundb command will provide that
and exclude all other commands to avoid confusion between them.
Experimental commans are the good old [auto, migrate, rollback, etc.]
but adapted to import configuration from a pre-built plugin.

Plugins are known to be unstable and only sometimes work correctly,
so this feature is more of a proof of concept and we should probably
add the option to disable it entirely with build tags (for standalone bundb).

TODO:: bun init --plugin does not initialize a Go module in the
migrations/ directory. Currently one would need to manually run go mod init
and go mod tidy after bootstraping the project.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants