db: skip SQL migration when no kvdb files exist

Avoid creating legacy bbolt database files when the kvdb-to-SQL
programmatic migration runs on a node that never had LiT kvdb state.

Note that we only skip the creating the database file if there is no
single bbolt database file. If for example there's only 1 existing
bbolt database file, we will still create the new bbolt database file
for the other 2 bbolt databases, as we for example need to pass the
accounts & session databases to the firewalldb migration, and we'd want
to avoid having the migrations become more complex by having to handle
the case where the other databases are missing.
This commit is contained in:
Viktor Torstensson 2026-06-11 23:04:37 +02:00
parent 27eba1e5c1
commit 059554474e
No known key found for this signature in database
GPG key ID: 961CC8259AE675D4
2 changed files with 188 additions and 0 deletions

View file

@ -6,12 +6,14 @@ import (
"encoding/binary"
"errors"
"fmt"
"path/filepath"
"time"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
"github.com/lightninglabs/lightning-terminal/accounts"
"github.com/lightninglabs/lightning-terminal/db/sqlcmig6"
"github.com/lightninglabs/lightning-terminal/db/tombstone"
"github.com/lightninglabs/lightning-terminal/firewalldb"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightningnetwork/lnd/clock"
@ -88,6 +90,40 @@ func kvdbToSqlProgrammaticMigration(ctx context.Context,
_ *sqldb.BaseDB, clock clock.Clock, q *sqlcmig6.Queries) error {
start := time.Now()
accountsActive, err := tombstone.KVDBFileExists(
filepath.Join(accountsDir, accounts.DBFilename),
)
if err != nil {
return fmt.Errorf("unable to inspect accounts kvdb: %w", err)
}
sessionsActive, err := tombstone.KVDBFileExists(
filepath.Join(networkDir, session.DBFilename),
)
if err != nil {
return fmt.Errorf("unable to inspect session kvdb: %w", err)
}
firewallActive, err := tombstone.KVDBFileExists(
filepath.Join(networkDir, firewalldb.DBFilename),
)
if err != nil {
return fmt.Errorf("unable to inspect rules kvdb: %w", err)
}
if !accountsActive && !sessionsActive && !firewallActive {
log.Infof("Skipping KVDB to SQL migration for all stores: " +
"no legacy database files exist")
return nil
}
if basicClient == nil {
return errors.New("lightning client is required for " +
"migration but was nil")
}
log.Infof("Starting KVDB to SQL migration for all stores")
accountStore, err := accounts.NewBoltStoreForMigration(

View file

@ -0,0 +1,152 @@
package migsets
import (
"context"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/lightninglabs/lightning-terminal/accounts"
"github.com/lightninglabs/lightning-terminal/db"
"github.com/lightninglabs/lightning-terminal/db/sqlcmig6"
"github.com/lightninglabs/lightning-terminal/firewalldb"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/sqldb/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
)
// TestKVDBToSQLProgrammaticMigrationSkipsMissingStores verifies that the kvdb
// to SQL migration does not create missing legacy kvdb files while scanning for
// stores to migrate.
func TestKVDBToSQLProgrammaticMigrationSkipsMissingStores(t *testing.T) {
t.Parallel()
accountsDir := t.TempDir()
networkDir := t.TempDir()
sqlStore := sqldb.NewTestSqliteDB(t, db.MakeTestMigrationSets())
queries := sqlcmig6.NewForType(
sqlStore.BaseDB, sqlStore.BackendType,
)
err := kvdbToSqlProgrammaticMigration(
context.Background(), nil, accountsDir, networkDir,
sqlStore.BaseDB, clock.NewDefaultClock(), queries,
)
require.NoError(t, err)
requireNoFile(t, filepath.Join(accountsDir, accounts.DBFilename))
requireNoFile(t, filepath.Join(networkDir, session.DBFilename))
requireNoFile(t, filepath.Join(networkDir, firewalldb.DBFilename))
}
// TestKVDBToSQLProgrammaticMigrationRunsWithOneBBoltDBFiles verifies that the
// migration still runs when only the accounts kvdb exists, creates the missing
// legacy files, and migrates the existing account data to SQL.
func TestKVDBToSQLProgrammaticMigrationRunsWithOneBBoltDBFiles(t *testing.T) {
t.Parallel()
ctx := context.Background()
testClock := clock.NewDefaultClock()
accountsDir := t.TempDir()
networkDir := t.TempDir()
accountStore, err := accounts.NewBoltStore(
accountsDir, accounts.DBFilename, testClock,
)
require.NoError(t, err)
_, err = accountStore.NewAccount(ctx, 1234, time.Time{}, "acct-1")
require.NoError(t, err)
require.NoError(t, accountStore.Close())
sqlStore := sqldb.NewTestSqliteDB(t, db.MakeTestMigrationSets())
queries := sqlcmig6.NewForType(
sqlStore.BaseDB, sqlStore.BackendType,
)
lndClient := newTestLightningClient(t)
err = kvdbToSqlProgrammaticMigration(
ctx, lndClient, accountsDir, networkDir,
sqlStore.BaseDB, testClock, queries,
)
require.NoError(t, err)
requireFileExists(t, filepath.Join(accountsDir, accounts.DBFilename))
requireFileExists(t, filepath.Join(networkDir, session.DBFilename))
requireFileExists(t, filepath.Join(networkDir, firewalldb.DBFilename))
dbAccounts, err := queries.ListAllAccounts(ctx)
require.NoError(t, err)
require.Len(t, dbAccounts, 1)
dbSessions, err := queries.ListSessions(ctx)
require.NoError(t, err)
require.Empty(t, dbSessions)
}
func requireNoFile(t *testing.T, path string) {
t.Helper()
_, err := os.Stat(path)
require.ErrorIs(t, err, os.ErrNotExist)
}
func requireFileExists(t *testing.T, path string) {
t.Helper()
_, err := os.Stat(path)
require.NoError(t, err)
}
func newTestLightningClient(t *testing.T) lnrpc.LightningClient {
t.Helper()
lis := bufconn.Listen(1024 * 1024)
server := grpc.NewServer()
lnrpc.RegisterLightningServer(server, &testLightningServer{})
go func() {
_ = server.Serve(lis)
}()
t.Cleanup(func() {
server.Stop()
require.NoError(t, lis.Close())
})
conn, err := grpc.DialContext(
context.Background(), "passthrough:///bufnet",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(
func(context.Context, string) (net.Conn, error) {
return lis.Dial()
},
),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, conn.Close())
})
return lnrpc.NewLightningClient(conn)
}
type testLightningServer struct {
lnrpc.UnimplementedLightningServer
}
func (t *testLightningServer) ListMacaroonIDs(context.Context,
*lnrpc.ListMacaroonIDsRequest) (*lnrpc.ListMacaroonIDsResponse, error) {
return &lnrpc.ListMacaroonIDsResponse{}, nil
}