mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-12-04 18:01:57 +01:00
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:
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user