sqldb: add support for custom in-code migrations

This commit introduces support for custom, in-code migrations, allowing
a specific Go function to be executed at a designated database version
during sqlc migrations. If the current database version surpasses the
specified version, the migration will be skipped.
This commit is contained in:
Andras Banki-Horvath
2024-11-22 18:29:54 +01:00
parent 9acd06d296
commit b789fb2db3
6 changed files with 565 additions and 20 deletions

View File

@@ -2,8 +2,15 @@ package sqldb
import (
"context"
"database/sql"
"fmt"
"path/filepath"
"testing"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
pgx_migrate "github.com/golang-migrate/migrate/v4/database/pgx/v5"
sqlite_migrate "github.com/golang-migrate/migrate/v4/database/sqlite"
"github.com/lightningnetwork/lnd/sqldb/sqlc"
"github.com/stretchr/testify/require"
)
@@ -152,3 +159,289 @@ func testInvoiceExpiryMigration(t *testing.T, makeDB makeMigrationTestDB) {
require.NoError(t, err)
require.Equal(t, expected, invoices)
}
// TestCustomMigration tests that a custom in-code migrations are correctly
// executed during the migration process.
func TestCustomMigration(t *testing.T) {
var customMigrationLog []string
logMigration := func(name string) {
customMigrationLog = append(customMigrationLog, name)
}
// Some migrations to use for both the failure and success tests. Note
// that the migrations are not in order to test that they are executed
// in the correct order.
migrations := []MigrationConfig{
{
Name: "1",
Version: 1,
SchemaVersion: 1,
MigrationFn: func(*sqlc.Queries) error {
logMigration("1")
return nil
},
},
{
Name: "2",
Version: 2,
SchemaVersion: 1,
MigrationFn: func(*sqlc.Queries) error {
logMigration("2")
return nil
},
},
{
Name: "3",
Version: 3,
SchemaVersion: 2,
MigrationFn: func(*sqlc.Queries) error {
logMigration("3")
return nil
},
},
}
tests := []struct {
name string
migrations []MigrationConfig
expectedSuccess bool
expectedMigrationLog []string
expectedSchemaVersion int
expectedVersion int
}{
{
name: "success",
migrations: migrations,
expectedSuccess: true,
expectedMigrationLog: []string{"1", "2", "3"},
expectedSchemaVersion: 2,
expectedVersion: 3,
},
{
name: "unordered migrations",
migrations: append([]MigrationConfig{
{
Name: "4",
Version: 4,
SchemaVersion: 3,
MigrationFn: func(*sqlc.Queries) error {
logMigration("4")
return nil
},
},
}, migrations...),
expectedSuccess: false,
expectedMigrationLog: nil,
expectedSchemaVersion: 0,
},
{
name: "failure of migration 4",
migrations: append(migrations, MigrationConfig{
Name: "4",
Version: 4,
SchemaVersion: 3,
MigrationFn: func(*sqlc.Queries) error {
return fmt.Errorf("migration 4 failed")
},
}),
expectedSuccess: false,
expectedMigrationLog: []string{"1", "2", "3"},
// Since schema migration is a separate step we expect
// that migrating up to 3 succeeded.
expectedSchemaVersion: 3,
// We still remain on version 3 though.
expectedVersion: 3,
},
{
name: "success of migration 4",
migrations: append(migrations, MigrationConfig{
Name: "4",
Version: 4,
SchemaVersion: 3,
MigrationFn: func(*sqlc.Queries) error {
logMigration("4")
return nil
},
}),
expectedSuccess: true,
expectedMigrationLog: []string{"1", "2", "3", "4"},
expectedSchemaVersion: 3,
expectedVersion: 4,
},
}
ctxb := context.Background()
for _, test := range tests {
// checkSchemaVersion checks the database schema version against
// the expected version.
getSchemaVersion := func(t *testing.T,
driver database.Driver, dbName string) int {
sqlMigrate, err := migrate.NewWithInstance(
"migrations", nil, dbName, driver,
)
require.NoError(t, err)
version, _, err := sqlMigrate.Version()
if err != migrate.ErrNilVersion {
require.NoError(t, err)
}
return int(version)
}
t.Run("SQLite "+test.name, func(t *testing.T) {
customMigrationLog = nil
// First instantiate the database and run the migrations
// including the custom migrations.
t.Logf("Creating new SQLite DB for testing migrations")
dbFileName := filepath.Join(t.TempDir(), "tmp.db")
var (
db *SqliteStore
err error
)
// Run the migration 3 times to test that the migrations
// are idempotent.
for i := 0; i < 3; i++ {
db, err = NewSqliteStore(&SqliteConfig{
SkipMigrations: false,
}, dbFileName, test.migrations)
if db != nil {
dbToCleanup := db.DB
t.Cleanup(func() {
require.NoError(
t, dbToCleanup.Close(),
)
})
}
if test.expectedSuccess {
require.NoError(t, err)
} else {
require.Error(t, err)
// Also repoen the DB without migrations
// so we can read versions.
db, err = NewSqliteStore(&SqliteConfig{
SkipMigrations: true,
}, dbFileName, nil)
require.NoError(t, err)
}
require.Equal(t,
test.expectedMigrationLog,
customMigrationLog,
)
// Create the migration executor to be able to
// query the current schema version.
driver, err := sqlite_migrate.WithInstance(
db.DB, &sqlite_migrate.Config{},
)
require.NoError(t, err)
require.Equal(
t, test.expectedSchemaVersion,
getSchemaVersion(t, driver, ""),
)
// Check the migraton version in the database.
version, err := db.GetDatabaseVersion(ctxb)
if test.expectedSchemaVersion != 0 {
require.NoError(t, err)
} else {
require.Equal(t, sql.ErrNoRows, err)
}
require.Equal(
t, test.expectedVersion, int(version),
)
}
})
t.Run("Postgres "+test.name, func(t *testing.T) {
customMigrationLog = nil
// First create a temporary Postgres database to run
// the migrations on.
fixture := NewTestPgFixture(
t, DefaultPostgresFixtureLifetime,
)
t.Cleanup(func() {
fixture.TearDown(t)
})
dbName := randomDBName(t)
// Next instantiate the database and run the migrations
// including the custom migrations.
t.Logf("Creating new Postgres DB '%s' for testing "+
"migrations", dbName)
_, err := fixture.db.ExecContext(
context.Background(), "CREATE DATABASE "+dbName,
)
require.NoError(t, err)
cfg := fixture.GetConfig(dbName)
var db *PostgresStore
// Run the migration 3 times to test that the migrations
// are idempotent.
for i := 0; i < 3; i++ {
cfg.SkipMigrations = false
db, err = NewPostgresStore(cfg, test.migrations)
if test.expectedSuccess {
require.NoError(t, err)
} else {
require.Error(t, err)
// Also repoen the DB without migrations
// so we can read versions.
cfg.SkipMigrations = true
db, err = NewPostgresStore(cfg, nil)
require.NoError(t, err)
}
require.Equal(t,
test.expectedMigrationLog,
customMigrationLog,
)
// Create the migration executor to be able to
// query the current version.
driver, err := pgx_migrate.WithInstance(
db.DB, &pgx_migrate.Config{},
)
require.NoError(t, err)
require.Equal(
t, test.expectedSchemaVersion,
getSchemaVersion(t, driver, ""),
)
// Check the migraton version in the database.
version, err := db.GetDatabaseVersion(ctxb)
if test.expectedSchemaVersion != 0 {
require.NoError(t, err)
} else {
require.Equal(t, sql.ErrNoRows, err)
}
require.Equal(
t, test.expectedVersion, int(version),
)
}
})
}
}