Skip to content

Commit f387c63

Browse files
authored
Merge pull request #1826 from ViktorT-11/2025-09-check-dirty-database-version
tapdb: error on dirty database versions
2 parents edae5a6 + cfc6812 commit f387c63

File tree

3 files changed

+101
-1
lines changed

3 files changed

+101
-1
lines changed

docs/release-notes/release-notes-0.7.0.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@
5353
requiring a fully initialized AuxSweeper, allowing sweep operations to proceed
5454
during server startup.
5555

56+
- [Ensure that tapd won't start if the latest db migration errors and sets the
57+
db to a dirty state for sqlite database
58+
backends](https://github.com/lightninglabs/taproot-assets/pull/1826).
59+
If the latest migration errors and sets the db to a dirty state, startup of
60+
tapd will now be prevented for all database backend types. Previously, this
61+
safeguard did not work correctly for SQLite backends if the most recent
62+
migration failed. Tapd could then still start even though the database was
63+
dirty. This issue has been resolved, and the behavior is now consistent across
64+
all database backend types.
65+
5666
# New Features
5767

5868
## Functional Enhancements

tapdb/migrations.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,19 @@ func applyMigrations(fs fs.FS, driver database.Driver, path, dbName string,
166166
return err
167167
}
168168

169-
migrationVersion, _, _ := sqlMigrate.Version()
169+
migrationVersion, dirty, err := sqlMigrate.Version()
170+
if err != nil && !errors.Is(err, migrate.ErrNilVersion) {
171+
return fmt.Errorf("unable to determine current migration "+
172+
"version: %w", err)
173+
}
174+
175+
// If the migration version is dirty, we should not proceed with further
176+
// migrations, as this indicates that a previous migration did not
177+
// complete successfully and requires manual intervention.
178+
if dirty {
179+
return fmt.Errorf("database is in a dirty state at version "+
180+
"%v, manual intervention required", migrationVersion)
181+
}
170182

171183
// As the down migrations may end up *dropping* data, we want to
172184
// prevent that without explicit accounting.

tapdb/migrations_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tapdb
33
import (
44
"context"
55
"encoding/hex"
6+
"errors"
67
"fmt"
78
"os"
89
"path/filepath"
@@ -694,3 +695,80 @@ func TestMigration37(t *testing.T) {
694695

695696
require.Len(t, burns, 5)
696697
}
698+
699+
// TestDirtySqliteVersion tests that if a migration fails and leaves an Sqlite
700+
// database backend in a dirty state, any attempts of re-executing migrations on
701+
// the db (i.e. restart tapd), will fail with an error indicating that the
702+
// database is in a dirty state. This is regardless of whether the failing
703+
// migration is the latest migration or an intermediate migration.
704+
func TestDirtySqliteVersion(t *testing.T) {
705+
var (
706+
err error
707+
testError = errors.New("test error")
708+
709+
// testPostMigrationChecks1 is a map that will trigger a
710+
// migration callback for migration 2 which always returns an
711+
// error. This is used to simulate an intermediate migration
712+
// that fails and leaves the db in a dirty state.
713+
testPostMigrationChecks1 = map[uint]postMigrationCheck{
714+
2: func(ctx context.Context, q sqlc.Querier) error {
715+
return testError
716+
},
717+
}
718+
719+
// testPostMigrationChecks2 is a map that will trigger a
720+
// migration callback for the latest migration which always
721+
// returns an error. This is used to simulate that the latest
722+
// migration fails and leaves the db in a dirty state.
723+
testPostMigrationChecks2 = map[uint]postMigrationCheck{
724+
LatestMigrationVersion: func(ctx context.Context,
725+
q sqlc.Querier) error {
726+
727+
return testError
728+
},
729+
}
730+
)
731+
732+
// First, we'll test that intermediate migration that fails and leaves
733+
// the db in a dirty state is detected on subsequent migration attempts.
734+
// Note that this test only targets Sqlite database backends, and
735+
// therefore we create a new Sqlite test db for this.
736+
db1 := NewTestSqliteDBWithVersion(t, 1)
737+
738+
// As intend that the failing migration version should be an
739+
// intermediate migration, we use the testPostMigrationChecks1 when
740+
// executing the migration. Note that we use the `backupAndMigrate` func
741+
// as the MigrationTarget, to simulate what is used in production for
742+
// Sqlite database backends.
743+
err = db1.ExecuteMigrations(db1.backupAndMigrate, WithPostStepCallbacks(
744+
makePostStepCallbacks(db1, testPostMigrationChecks1),
745+
))
746+
require.ErrorIs(t, err, testError)
747+
748+
// If we now attempt to execute migrations again, it should fail with an
749+
// error indicating that the db is in a dirty state.
750+
err = db1.ExecuteMigrations(db1.backupAndMigrate, WithPostStepCallbacks(
751+
makePostStepCallbacks(db1, testPostMigrationChecks1),
752+
))
753+
require.ErrorContains(t, err, "database is in a dirty state")
754+
755+
// Next, we'll test that if the **latest** migration fails and leaves
756+
// the db in a dirty state, that this is also detected on subsequent
757+
// migration attempts.
758+
db2 := NewTestSqliteDBWithVersion(t, 1)
759+
760+
// As we intend that the failing migration version should be the latest
761+
// migration, we now use the testPostMigrationChecks2 when executing
762+
// the migration.
763+
err = db2.ExecuteMigrations(db2.backupAndMigrate, WithPostStepCallbacks(
764+
makePostStepCallbacks(db2, testPostMigrationChecks2),
765+
))
766+
require.ErrorIs(t, err, testError)
767+
768+
// If we now attempt to execute migrations again, it should fail with an
769+
// error indicating that the db is in a dirty state.
770+
err = db2.ExecuteMigrations(db2.backupAndMigrate, WithPostStepCallbacks(
771+
makePostStepCallbacks(db2, testPostMigrationChecks2),
772+
))
773+
require.ErrorContains(t, err, "database is in a dirty state")
774+
}

0 commit comments

Comments
 (0)