Skip to content

Commit 85cdb4b

Browse files
accounts: add migration code from kvdb to SQL
This commit introduces the migration logic for transitioning the accounts store from kvdb to SQL. Note that as of this commit, the migration is not yet triggered by any production code, i.e. only tests execute the migration logic.
1 parent 07d69ee commit 85cdb4b

File tree

3 files changed

+638
-2
lines changed

3 files changed

+638
-2
lines changed

accounts/kv_sql_migration_test.go

+348
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
package accounts
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"testing"
8+
"time"
9+
10+
"github.com/lightninglabs/lightning-terminal/db"
11+
"github.com/lightningnetwork/lnd/clock"
12+
"github.com/lightningnetwork/lnd/fn"
13+
"github.com/lightningnetwork/lnd/lnrpc"
14+
"github.com/lightningnetwork/lnd/lntypes"
15+
"github.com/lightningnetwork/lnd/sqldb"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
// TestAccountStoreMigration tests the migration of account store from a bolt
20+
// backed to a SQL database. Note that this test does not attempt to be a
21+
// complete migration test.
22+
func TestAccountStoreMigration(t *testing.T) {
23+
t.Parallel()
24+
25+
ctx := context.Background()
26+
27+
// When using build tags that creates a kvdb store for NewTestDB, we
28+
// skip this test as it is only applicable for postgres and sqlite tags.
29+
store := NewTestDB(t, clock.NewTestClock(time.Now()))
30+
if _, ok := store.(*BoltStore); ok {
31+
t.Skipf("Skipping account store migration test for kvdb build")
32+
}
33+
34+
makeSQLDB := func(t *testing.T, clock *clock.TestClock) (*SQLStore,
35+
*db.TransactionExecutor[SQLQueries]) {
36+
37+
testDBStore := NewTestDB(t, clock)
38+
store, ok := testDBStore.(*SQLStore)
39+
require.True(t, ok)
40+
41+
baseDB := store.BaseDB
42+
43+
genericExecutor := db.NewTransactionExecutor(
44+
baseDB, func(tx *sql.Tx) SQLQueries {
45+
return baseDB.WithTx(tx)
46+
},
47+
)
48+
49+
return store, genericExecutor
50+
}
51+
52+
migrationTest := func(t *testing.T, kvStore *BoltStore,
53+
clock *clock.TestClock, expectLastIndex bool) {
54+
55+
sqlStore, txEx := makeSQLDB(t, clock)
56+
57+
var opts sqldb.MigrationTxOptions
58+
err := txEx.ExecTx(
59+
ctx, &opts, func(tx SQLQueries) error {
60+
return MigrateAccountStoreToSQL(
61+
ctx, kvStore, tx,
62+
)
63+
},
64+
)
65+
require.NoError(t, err)
66+
67+
// MigrateAccountStoreToSQL will check if the inserted accounts
68+
// and indices equals the migrated ones, but as a sanity check
69+
// we'll also fetch the accounts and indices from the store and
70+
// compare them to the original.
71+
// First we compare the migrated accounts to the original ones.
72+
kvAccounts, err := kvStore.Accounts(ctx)
73+
require.NoError(t, err)
74+
numAccounts := len(kvAccounts)
75+
76+
sqlAccounts, err := sqlStore.Accounts(ctx)
77+
require.NoError(t, err)
78+
require.Equal(t, numAccounts, len(sqlAccounts))
79+
80+
for i := 0; i < numAccounts; i++ {
81+
assertEqualAccounts(t, kvAccounts[i], sqlAccounts[i])
82+
}
83+
84+
// After that we compare the migrated indices.
85+
kvAddIndex, kvSettleIndex, err := kvStore.LastIndexes(ctx)
86+
if errors.Is(err, ErrNoInvoiceIndexKnown) {
87+
// If the db doesn't have any indices, we can't compare
88+
// them.
89+
require.False(t, expectLastIndex)
90+
return
91+
} else {
92+
require.NoError(t, err)
93+
require.True(t, expectLastIndex)
94+
}
95+
96+
sqlAddIndex, sqlSettleIndex, err := sqlStore.LastIndexes(ctx)
97+
require.NoError(t, err)
98+
99+
require.Equal(t, kvAddIndex, sqlAddIndex)
100+
require.Equal(t, kvSettleIndex, sqlSettleIndex)
101+
}
102+
103+
tests := []struct {
104+
name string
105+
expectLastIndex bool
106+
populateDB func(t *testing.T, kvStore *BoltStore)
107+
}{
108+
{
109+
"empty",
110+
false,
111+
// Don't populate the DB.
112+
func(t *testing.T, kvStore *BoltStore) {},
113+
},
114+
{
115+
"account no expiry",
116+
false,
117+
func(t *testing.T, kvStore *BoltStore) {
118+
// Create an account that does not expire.
119+
acct1, err := kvStore.NewAccount(
120+
ctx, 0, time.Time{}, "foo",
121+
)
122+
require.NoError(t, err)
123+
require.False(t, acct1.HasExpired())
124+
},
125+
},
126+
{
127+
"account with expiry",
128+
false,
129+
func(t *testing.T, kvStore *BoltStore) {
130+
// Create an account that does expire.
131+
acct1, err := kvStore.NewAccount(
132+
ctx, 0, time.Now().Add(time.Hour),
133+
"foo",
134+
)
135+
require.NoError(t, err)
136+
require.False(t, acct1.HasExpired())
137+
},
138+
},
139+
{
140+
"account with updated expiry",
141+
false,
142+
func(t *testing.T, kvStore *BoltStore) {
143+
// Create an account that does expire.
144+
acct1, err := kvStore.NewAccount(
145+
ctx, 0, time.Now().Add(time.Hour),
146+
"foo",
147+
)
148+
require.NoError(t, err)
149+
require.False(t, acct1.HasExpired())
150+
151+
err = kvStore.UpdateAccountBalanceAndExpiry(
152+
ctx, acct1.ID, fn.None[int64](),
153+
fn.Some(time.Now().Add(time.Minute)),
154+
)
155+
require.NoError(t, err)
156+
},
157+
},
158+
{
159+
"account with balance",
160+
false,
161+
func(t *testing.T, kvStore *BoltStore) {
162+
// Create an account with balance
163+
acct1, err := kvStore.NewAccount(
164+
ctx, 100000, time.Time{}, "foo",
165+
)
166+
require.NoError(t, err)
167+
require.False(t, acct1.HasExpired())
168+
},
169+
},
170+
{
171+
"account with updated balance",
172+
false,
173+
func(t *testing.T, kvStore *BoltStore) {
174+
// Create an account with balance
175+
acct1, err := kvStore.NewAccount(
176+
ctx, 100000, time.Time{}, "foo",
177+
)
178+
require.NoError(t, err)
179+
require.False(t, acct1.HasExpired())
180+
181+
err = kvStore.CreditAccount(
182+
ctx, acct1.ID, 10000,
183+
)
184+
require.NoError(t, err)
185+
186+
err = kvStore.DebitAccount(
187+
ctx, acct1.ID, 20000,
188+
)
189+
require.NoError(t, err)
190+
},
191+
},
192+
{
193+
"account with invoices",
194+
true,
195+
func(t *testing.T, kvStore *BoltStore) {
196+
// Create an account with balance
197+
acct1, err := kvStore.NewAccount(
198+
ctx, 0, time.Time{}, "foo",
199+
)
200+
require.NoError(t, err)
201+
require.False(t, acct1.HasExpired())
202+
203+
hash1 := lntypes.Hash{1, 2, 3, 4}
204+
err = kvStore.AddAccountInvoice(
205+
ctx, acct1.ID, hash1,
206+
)
207+
require.NoError(t, err)
208+
209+
err = kvStore.StoreLastIndexes(ctx, 1, 0)
210+
require.NoError(t, err)
211+
212+
hash2 := lntypes.Hash{1, 2, 3, 4, 5}
213+
err = kvStore.AddAccountInvoice(
214+
ctx, acct1.ID, hash2,
215+
)
216+
require.NoError(t, err)
217+
218+
err = kvStore.StoreLastIndexes(ctx, 2, 1)
219+
require.NoError(t, err)
220+
},
221+
},
222+
{
223+
"account with payments",
224+
false,
225+
func(t *testing.T, kvStore *BoltStore) {
226+
// Create an account with balance
227+
acct1, err := kvStore.NewAccount(
228+
ctx, 0, time.Time{}, "foo",
229+
)
230+
require.NoError(t, err)
231+
require.False(t, acct1.HasExpired())
232+
233+
hash1 := lntypes.Hash{1, 1, 1, 1}
234+
known, err := kvStore.UpsertAccountPayment(
235+
ctx, acct1.ID, hash1, 100,
236+
lnrpc.Payment_UNKNOWN,
237+
)
238+
require.NoError(t, err)
239+
require.False(t, known)
240+
241+
hash2 := lntypes.Hash{2, 2, 2, 2}
242+
known, err = kvStore.UpsertAccountPayment(
243+
ctx, acct1.ID, hash2, 200,
244+
lnrpc.Payment_IN_FLIGHT,
245+
)
246+
require.NoError(t, err)
247+
require.False(t, known)
248+
249+
hash3 := lntypes.Hash{3, 3, 3, 3}
250+
known, err = kvStore.UpsertAccountPayment(
251+
ctx, acct1.ID, hash3, 200,
252+
lnrpc.Payment_SUCCEEDED,
253+
)
254+
require.NoError(t, err)
255+
require.False(t, known)
256+
257+
hash4 := lntypes.Hash{4, 4, 4, 4}
258+
known, err = kvStore.UpsertAccountPayment(
259+
ctx, acct1.ID, hash4, 200,
260+
lnrpc.Payment_FAILED,
261+
)
262+
require.NoError(t, err)
263+
require.False(t, known)
264+
},
265+
},
266+
{
267+
"multiple accounts",
268+
true,
269+
func(t *testing.T, kvStore *BoltStore) {
270+
// Create two accounts with balance and that
271+
// expires.
272+
acct1, err := kvStore.NewAccount(
273+
ctx, 100000, time.Now().Add(time.Hour),
274+
"foo",
275+
)
276+
require.NoError(t, err)
277+
require.False(t, acct1.HasExpired())
278+
279+
acct2, err := kvStore.NewAccount(
280+
ctx, 200000, time.Now().Add(time.Hour),
281+
"bar",
282+
)
283+
require.NoError(t, err)
284+
require.False(t, acct2.HasExpired())
285+
286+
// Create invoices for both accounts.
287+
hash1 := lntypes.Hash{1, 1, 1, 1}
288+
err = kvStore.AddAccountInvoice(
289+
ctx, acct1.ID, hash1,
290+
)
291+
require.NoError(t, err)
292+
293+
err = kvStore.StoreLastIndexes(ctx, 1, 0)
294+
require.NoError(t, err)
295+
296+
hash2 := lntypes.Hash{2, 2, 2, 2}
297+
err = kvStore.AddAccountInvoice(
298+
ctx, acct2.ID, hash2,
299+
)
300+
require.NoError(t, err)
301+
302+
err = kvStore.StoreLastIndexes(ctx, 2, 0)
303+
require.NoError(t, err)
304+
305+
// Create payments for both accounts.
306+
hash3 := lntypes.Hash{3, 3, 3, 3}
307+
known, err := kvStore.UpsertAccountPayment(
308+
ctx, acct1.ID, hash3, 100,
309+
lnrpc.Payment_SUCCEEDED,
310+
)
311+
require.NoError(t, err)
312+
require.False(t, known)
313+
314+
hash4 := lntypes.Hash{4, 4, 4, 4}
315+
known, err = kvStore.UpsertAccountPayment(
316+
ctx, acct2.ID, hash4, 200,
317+
lnrpc.Payment_IN_FLIGHT,
318+
)
319+
require.NoError(t, err)
320+
require.False(t, known)
321+
},
322+
},
323+
}
324+
325+
for _, test := range tests {
326+
tc := test
327+
328+
t.Run(tc.name, func(t *testing.T) {
329+
t.Parallel()
330+
331+
var kvStore *BoltStore
332+
testClock := clock.NewTestClock(time.Now())
333+
334+
kvStore, err := NewBoltStore(
335+
t.TempDir(), DBFilename, testClock,
336+
)
337+
require.NoError(t, err)
338+
339+
tc.populateDB(t, kvStore)
340+
341+
t.Cleanup(func() {
342+
require.NoError(t, kvStore.db.Close())
343+
})
344+
345+
migrationTest(t, kvStore, testClock, tc.expectLastIndex)
346+
})
347+
}
348+
}

0 commit comments

Comments
 (0)