Skip to content

Commit b85571b

Browse files
authored
Merge pull request #9647 from bhandras/sqldb-migration-base-version
sqldb: establish a base DB version even if it's not yet tracked
2 parents b6cf1bc + 83d4b7b commit b85571b

File tree

9 files changed

+436
-17
lines changed

9 files changed

+436
-17
lines changed

docs/release-notes/release-notes-0.19.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,9 @@ the on going rate we'll permit.
455455
flag](https://github.com/lightningnetwork/lnd/pull/9606/) for future
456456
compatibility.
457457

458+
* [Establish a base DB version even if it is not yet
459+
tracked](https://github.com/lightningnetwork/lnd/pull/9647).
460+
458461
## Code Health
459462

460463
* A code refactor that [moves all the graph related DB code out of the

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2
207207
// allows us to specify that as an option.
208208
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display
209209

210+
// Temporary replace until https://github.com/lightningnetwork/lnd/pull/9647 is
211+
// merged.
212+
replace github.com/lightningnetwork/lnd/sqldb => ./sqldb
213+
210214
// If you change this please also update docs/INSTALL.md and GO_VERSION in
211215
// Makefile (then run `make lint` to see where else it needs to be updated as
212216
// well).

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,6 @@ github.com/lightningnetwork/lnd/kvdb v1.4.12 h1:Y0WY5Tbjyjn6eCYh068qkWur5oFtioJl
464464
github.com/lightningnetwork/lnd/kvdb v1.4.12/go.mod h1:hx9buNcxsZpZwh8m1sjTQwy2SOeBoWWOZ3RnOQkMsxI=
465465
github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI=
466466
github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4=
467-
github.com/lightningnetwork/lnd/sqldb v1.0.7 h1:wQ4DdHY++uwxwth2CHL7s+duGqmMLaoIRBOQCa9HPTk=
468-
github.com/lightningnetwork/lnd/sqldb v1.0.7/go.mod h1:OG09zL/PHPaBJefp4HsPz2YLUJ+zIQHbpgCtLnOx8I4=
469467
github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
470468
github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA=
471469
github.com/lightningnetwork/lnd/tlv v1.3.0 h1:exS/KCPEgpOgviIttfiXAPaUqw2rHQrnUOpP7HPBPiY=

sqldb/migrations.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ type MigrationExecutor interface {
112112
// order. Migration details are stored in the global variable
113113
// migrationConfig.
114114
ExecuteMigrations(target MigrationTarget) error
115+
116+
// GetSchemaVersion returns the current schema version of the database.
117+
GetSchemaVersion() (int, bool, error)
118+
119+
// SetSchemaVersion sets the schema version of the database.
120+
//
121+
// NOTE: This alters the internal database schema tracker. USE WITH
122+
// CAUTION!!!
123+
SetSchemaVersion(version int, dirty bool) error
115124
}
116125

117126
var (
@@ -360,6 +369,46 @@ func ApplyMigrations(ctx context.Context, db *BaseDB,
360369
}
361370

362371
currentVersion = int(version)
372+
} else {
373+
// Since we don't have a version tracked by our own table yet,
374+
// we'll use the schema version reported by sqlc to determine
375+
// the current version.
376+
//
377+
// NOTE: This is safe because the first in-code migration was
378+
// introduced in version 7. This is only possible if the user
379+
// has a schema version <= 4.
380+
var dirty bool
381+
currentVersion, dirty, err = migrator.GetSchemaVersion()
382+
if err != nil {
383+
return err
384+
}
385+
386+
log.Infof("No database version found, using schema version %d "+
387+
"(dirty=%v) as base version", currentVersion, dirty)
388+
}
389+
390+
// Due to an a migration issue in v0.19.0-rc1 we may be at version 2 and
391+
// have a dirty schema due to failing migration 3. If this is indeed the
392+
// case, we need to reset the dirty flag to be able to apply the fixed
393+
// migration.
394+
// NOTE: this could be removed as soon as we drop v0.19.0-beta.
395+
if version == 2 {
396+
schemaVersion, dirty, err := migrator.GetSchemaVersion()
397+
if err != nil {
398+
return err
399+
}
400+
401+
if schemaVersion == 3 && dirty {
402+
log.Warnf("Schema version %d is dirty. This is "+
403+
"likely a consequence of a failed migration "+
404+
"in v0.19.0-rc1. Attempting to recover by "+
405+
"resetting the dirty flag", schemaVersion)
406+
407+
err = migrator.SetSchemaVersion(4, false)
408+
if err != nil {
409+
return err
410+
}
411+
}
363412
}
364413

365414
for _, migration := range migrations {

sqldb/migrations_test.go

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,294 @@ func TestCustomMigration(t *testing.T) {
452452
})
453453
}
454454
}
455+
456+
// TestSchemaMigrationIdempotency tests that the our schema migrations are
457+
// idempotent. This means that we can apply the migrations multiple times and
458+
// the schema version will always be the same.
459+
func TestSchemaMigrationIdempotency(t *testing.T) {
460+
dropMigrationTrackerEntries := func(t *testing.T, db *BaseDB) {
461+
_, err := db.Exec("DELETE FROM migration_tracker;")
462+
require.NoError(t, err)
463+
}
464+
465+
lastMigration := migrationConfig[len(migrationConfig)-1]
466+
467+
t.Run("SQLite", func(t *testing.T) {
468+
// First instantiate the database and run the migrations
469+
// including the custom migrations.
470+
t.Logf("Creating new SQLite DB for testing migrations")
471+
472+
dbFileName := filepath.Join(t.TempDir(), "tmp.db")
473+
var (
474+
db *SqliteStore
475+
err error
476+
)
477+
478+
// Run the migration 3 times to test that the migrations
479+
// are idempotent.
480+
for i := 0; i < 3; i++ {
481+
db, err = NewSqliteStore(&SqliteConfig{
482+
SkipMigrations: false,
483+
}, dbFileName)
484+
require.NoError(t, err)
485+
486+
dbToCleanup := db.DB
487+
t.Cleanup(func() {
488+
require.NoError(
489+
t, dbToCleanup.Close(),
490+
)
491+
})
492+
493+
ctxb := context.Background()
494+
require.NoError(
495+
t, db.ApplyAllMigrations(ctxb, GetMigrations()),
496+
)
497+
498+
version, dirty, err := db.GetSchemaVersion()
499+
require.NoError(t, err)
500+
501+
// Now reset the schema version to 0 and make sure that
502+
// we can apply the migrations again.
503+
require.Equal(t, lastMigration.SchemaVersion, version)
504+
require.False(t, dirty)
505+
506+
require.NoError(
507+
t, db.SetSchemaVersion(
508+
database.NilVersion, false,
509+
),
510+
)
511+
dropMigrationTrackerEntries(t, db.BaseDB)
512+
513+
// Make sure that we reset the schema version.
514+
version, dirty, err = db.GetSchemaVersion()
515+
require.NoError(t, err)
516+
require.Equal(t, -1, version)
517+
require.False(t, dirty)
518+
}
519+
})
520+
521+
t.Run("Postgres", func(t *testing.T) {
522+
// First create a temporary Postgres database to run
523+
// the migrations on.
524+
fixture := NewTestPgFixture(
525+
t, DefaultPostgresFixtureLifetime,
526+
)
527+
t.Cleanup(func() {
528+
fixture.TearDown(t)
529+
})
530+
531+
dbName := randomDBName(t)
532+
533+
// Next instantiate the database and run the migrations
534+
// including the custom migrations.
535+
t.Logf("Creating new Postgres DB '%s' for testing "+
536+
"migrations", dbName)
537+
538+
_, err := fixture.db.ExecContext(
539+
context.Background(), "CREATE DATABASE "+dbName,
540+
)
541+
require.NoError(t, err)
542+
543+
cfg := fixture.GetConfig(dbName)
544+
var db *PostgresStore
545+
546+
// Run the migration 3 times to test that the migrations
547+
// are idempotent.
548+
for i := 0; i < 3; i++ {
549+
cfg.SkipMigrations = false
550+
db, err = NewPostgresStore(cfg)
551+
require.NoError(t, err)
552+
553+
ctxb := context.Background()
554+
require.NoError(
555+
t, db.ApplyAllMigrations(ctxb, GetMigrations()),
556+
)
557+
558+
version, dirty, err := db.GetSchemaVersion()
559+
require.NoError(t, err)
560+
561+
// Now reset the schema version to 0 and make sure that
562+
// we can apply the migrations again.
563+
require.Equal(t, lastMigration.SchemaVersion, version)
564+
require.False(t, dirty)
565+
566+
require.NoError(
567+
t, db.SetSchemaVersion(
568+
database.NilVersion, false,
569+
),
570+
)
571+
dropMigrationTrackerEntries(t, db.BaseDB)
572+
573+
// Make sure that we reset the schema version.
574+
version, dirty, err = db.GetSchemaVersion()
575+
require.NoError(t, err)
576+
require.Equal(t, -1, version)
577+
require.False(t, dirty)
578+
}
579+
})
580+
}
581+
582+
// TestMigrationBug19RC1 tests a bug that was present in the migration code
583+
// at the v0.19.0-rc1 release.
584+
// The bug was fixed in: https://github.com/lightningnetwork/lnd/pull/9647
585+
// NOTE: This test may be removed once the final version of 0.19.0 is released.
586+
func TestMigrationSucceedsAfterDirtyStateMigrationFailure19RC1(t *testing.T) {
587+
// setMigrationTrackerVersion is a helper function that
588+
// updates the migration tracker table to a specific version that
589+
// simulates the conditions of the bug.
590+
setMigrationTrackerVersion := func(t *testing.T, db *BaseDB) {
591+
_, err := db.Exec(`
592+
DELETE FROM migration_tracker;
593+
INSERT INTO migration_tracker (version, migration_time)
594+
VALUES (2, CURRENT_TIMESTAMP);
595+
`)
596+
require.NoError(t, err)
597+
}
598+
599+
const (
600+
maxSchemaVersionBefore19RC1 = 4
601+
failingSchemaVersion = 3
602+
)
603+
604+
ctxb := context.Background()
605+
migrations := GetMigrations()
606+
migrations = migrations[:maxSchemaVersionBefore19RC1]
607+
lastMigration := migrations[len(migrations)-1]
608+
609+
// Make sure that the last migration is the one we expect.
610+
require.Equal(
611+
t, maxSchemaVersionBefore19RC1, lastMigration.SchemaVersion,
612+
)
613+
614+
t.Run("SQLite", func(t *testing.T) {
615+
// First instantiate the database and run the migrations
616+
// including the custom migrations.
617+
t.Logf("Creating new SQLite DB for testing migrations")
618+
619+
dbFileName := filepath.Join(t.TempDir(), "tmp.db")
620+
var (
621+
db *SqliteStore
622+
err error
623+
)
624+
625+
db, err = NewSqliteStore(&SqliteConfig{
626+
SkipMigrations: false,
627+
}, dbFileName)
628+
require.NoError(t, err)
629+
630+
dbToCleanup := db.DB
631+
t.Cleanup(func() {
632+
require.NoError(t, dbToCleanup.Close())
633+
})
634+
635+
require.NoError(t, db.ApplyAllMigrations(ctxb, migrations))
636+
637+
version, dirty, err := db.GetSchemaVersion()
638+
require.NoError(t, err)
639+
640+
// Now reset the schema version to 0 and make sure that
641+
// we can apply the migrations again.
642+
require.Equal(t, lastMigration.SchemaVersion, version)
643+
require.False(t, dirty)
644+
645+
// Set the schema version to the failing version and
646+
// make make the version dirty which essentially tells
647+
// golang-migrate that the migration failed.
648+
require.NoError(
649+
t, db.SetSchemaVersion(failingSchemaVersion, true),
650+
)
651+
652+
// Set the migration tracker to the failing version.
653+
setMigrationTrackerVersion(t, db.BaseDB)
654+
655+
// Close the DB so we can reopen it and apply the
656+
// migrations again.
657+
db.DB.Close()
658+
659+
db, err = NewSqliteStore(&SqliteConfig{
660+
SkipMigrations: false,
661+
}, dbFileName)
662+
require.NoError(t, err)
663+
664+
dbToCleanup2 := db.DB
665+
t.Cleanup(func() {
666+
require.NoError(t, dbToCleanup2.Close())
667+
})
668+
669+
require.NoError(t, db.ApplyAllMigrations(ctxb, migrations))
670+
671+
version, dirty, err = db.GetSchemaVersion()
672+
require.NoError(t, err)
673+
require.Equal(t, lastMigration.SchemaVersion, version)
674+
require.False(t, dirty)
675+
})
676+
677+
t.Run("Postgres", func(t *testing.T) {
678+
// First create a temporary Postgres database to run
679+
// the migrations on.
680+
fixture := NewTestPgFixture(
681+
t, DefaultPostgresFixtureLifetime,
682+
)
683+
t.Cleanup(func() {
684+
fixture.TearDown(t)
685+
})
686+
687+
dbName := randomDBName(t)
688+
689+
// Next instantiate the database and run the migrations
690+
// including the custom migrations.
691+
t.Logf("Creating new Postgres DB '%s' for testing "+
692+
"migrations", dbName)
693+
694+
_, err := fixture.db.ExecContext(
695+
context.Background(), "CREATE DATABASE "+dbName,
696+
)
697+
require.NoError(t, err)
698+
699+
cfg := fixture.GetConfig(dbName)
700+
var db *PostgresStore
701+
702+
cfg.SkipMigrations = false
703+
db, err = NewPostgresStore(cfg)
704+
require.NoError(t, err)
705+
706+
require.NoError(t, db.ApplyAllMigrations(ctxb, migrations))
707+
708+
version, dirty, err := db.GetSchemaVersion()
709+
require.NoError(t, err)
710+
711+
// Now reset the schema version to 0 and make sure that
712+
// we can apply the migrations again.
713+
require.Equal(t, lastMigration.SchemaVersion, version)
714+
require.False(t, dirty)
715+
716+
// Set the schema version to the failing version and
717+
// make make the version dirty which essentially tells
718+
// golang-migrate that the migration failed.
719+
require.NoError(
720+
t, db.SetSchemaVersion(failingSchemaVersion, true),
721+
)
722+
723+
// Set the migration tracker to the failing version.
724+
setMigrationTrackerVersion(t, db.BaseDB)
725+
726+
// Close the DB so we can reopen it and apply the
727+
// migrations again.
728+
db.DB.Close()
729+
730+
db, err = NewPostgresStore(cfg)
731+
require.NoError(t, err)
732+
733+
dbToCleanup2 := db.DB
734+
t.Cleanup(func() {
735+
require.NoError(t, dbToCleanup2.Close())
736+
})
737+
738+
require.NoError(t, db.ApplyAllMigrations(ctxb, migrations))
739+
740+
version, dirty, err = db.GetSchemaVersion()
741+
require.NoError(t, err)
742+
require.Equal(t, lastMigration.SchemaVersion, version)
743+
require.False(t, dirty)
744+
})
745+
}

0 commit comments

Comments
 (0)