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

feat: ftl migrate command #2033

Merged
merged 7 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions backend/controller/sql/database_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package sql_test

import (
"fmt"
"testing"

in "github.com/TBD54566975/ftl/integration"
Expand All @@ -23,3 +24,19 @@ func TestDatabase(t *testing.T) {
in.QueryRow("testdb", "SELECT data FROM requests", "hello"),
)
}

func TestMigrate(t *testing.T) {
dbName := "ftl_test"
dbUri := fmt.Sprintf("postgres://postgres:secret@localhost:15432/%s?sslmode=disable", dbName)

q := func() in.Action {
return in.QueryRow(dbName, "SELECT version FROM schema_migrations WHERE version = '20240704103403'", "20240704103403")
}

in.RunWithoutController(t, "",
in.DropDBAction(t, dbName),
in.Fail(q(), "Should fail because the database does not exist."),
in.Exec("ftl", "migrate", "--dsn", dbUri),
q(),
)
}
2 changes: 1 addition & 1 deletion backend/controller/sql/databasetesting/devel.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func CreateForDevel(ctx context.Context, dsn string, recreate bool) (*pgxpool.Po

_, _ = conn.Exec(ctx, fmt.Sprintf("CREATE DATABASE %q", config.Database)) //nolint:errcheck // PG doesn't support "IF NOT EXISTS" so instead we just ignore any error.

err = sql.Migrate(ctx, dsn)
err = sql.Migrate(ctx, dsn, log.Debug)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions backend/controller/sql/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
var migrationSchema embed.FS

// Migrate the database.
func Migrate(ctx context.Context, dsn string) error {
func Migrate(ctx context.Context, dsn string, logLevel log.Level) error {
u, err := url.Parse(dsn)
if err != nil {
return fmt.Errorf("invalid DSN: %w", err)
Expand All @@ -31,7 +31,7 @@ func Migrate(ctx context.Context, dsn string) error {

db := dbmate.New(u)
db.FS = migrationSchema
db.Log = log.FromContext(ctx).Scope("migrate").WriterAt(log.Debug)
db.Log = log.FromContext(ctx).Scope("migrate").WriterAt(logLevel)
db.MigrationsDir = []string{"schema"}
err = db.CreateAndMigrate()
if err != nil {
Expand Down
23 changes: 23 additions & 0 deletions cmd/ftl/cmd_migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"context"
"fmt"

"github.com/TBD54566975/ftl/backend/controller/sql"
"github.com/TBD54566975/ftl/internal/log"
)

type migrateCmd struct {
DSN string `help:"DSN for the database." default:"postgres://localhost:15432/ftl?sslmode=disable&user=postgres&password=secret" env:"DATABASE_URL"`
}

func (c *migrateCmd) Run(ctx context.Context) error {
logger := log.FromContext(ctx)
logger.Infof("Migrating database")
err := sql.Migrate(ctx, c.DSN, log.Info)
if err != nil {
return fmt.Errorf("failed to migrate database: %w", err)
}
return nil
}
1 change: 1 addition & 0 deletions cmd/ftl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type CLI struct {
Box boxCmd `cmd:"" help:"Build a self-contained Docker container for running a set of module."`
BoxRun boxRunCmd `cmd:"" hidden:"" help:"Run FTL inside an ftl-in-a-box container"`
Deploy deployCmd `cmd:"" help:"Build and deploy all modules found in the specified directories."`
Migrate migrateCmd `cmd:"" help:"Run a database migration, if required, based on the migration table."`
Download downloadCmd `cmd:"" help:"Download a deployment."`
Secret secretCmd `cmd:"" help:"Manage secrets."`
Config configCmd `cmd:"" help:"Manage configuration."`
Expand Down
41 changes: 34 additions & 7 deletions integration/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,17 @@ func CreateDBAction(module, dbName string, isTest bool) Action {
}
}

func terminateDanglingConnections(t testing.TB, db *sql.DB, dbName string) {
t.Helper()

_, err := db.Exec(`
SELECT pid, pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = $1 AND pid <> pg_backend_pid()`,
dbName)
assert.NoError(t, err)
}

func CreateDB(t testing.TB, module, dbName string, isTestDb bool) {
// insert test suffix if needed when actually setting up db
if isTestDb {
Expand All @@ -368,18 +379,34 @@ func CreateDB(t testing.TB, module, dbName string, isTestDb bool) {
assert.NoError(t, err, "failed to create database")

t.Cleanup(func() {
// Terminate any dangling connections.
_, err := db.Exec(`
SELECT pid, pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = $1 AND pid <> pg_backend_pid()`,
dbName)
assert.NoError(t, err)
terminateDanglingConnections(t, db, dbName)
_, err = db.Exec("DROP DATABASE " + dbName)
assert.NoError(t, err)
})
}

func DropDBAction(t testing.TB, dbName string) Action {
return func(t testing.TB, ic TestContext) {
DropDB(t, dbName)
}
}

func DropDB(t testing.TB, dbName string) {
Infof("Dropping database %s", dbName)
db, err := sql.Open("pgx", "postgres://postgres:secret@localhost:15432/postgres?sslmode=disable")
assert.NoError(t, err, "failed to open database connection")

terminateDanglingConnections(t, db, dbName)

_, err = db.Exec("DROP DATABASE IF EXISTS " + dbName)
assert.NoError(t, err, "failed to delete existing database")

t.Cleanup(func() {
err := db.Close()
assert.NoError(t, err)
})
}

// Create a directory in the working directory
func Mkdir(dir string) Action {
return func(t testing.TB, ic TestContext) {
Expand Down
Loading