Skip to content

Commit 3c84710

Browse files
committed
pg: use native pgx store queries
Generate PostgreSQL queries for pgx/v5, then run normal reads through pgxpool and writes through pgx.Tx. Adapt generated query values and row tests while retaining database/sql solely for migrations and tagged test administration.
1 parent 816bc13 commit 3c84710

74 files changed

Lines changed: 1416 additions & 1822 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ require (
2525
github.com/jackc/pgx/v5 v5.10.0
2626
github.com/jessevdk/go-flags v1.6.1
2727
github.com/jrick/logrotate v1.1.2
28-
github.com/lib/pq v1.12.3
2928
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf
3029
github.com/lightninglabs/neutrino v0.18.0
3130
github.com/lightninglabs/neutrino/cache v1.1.4
@@ -78,6 +77,7 @@ require (
7877
github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee // indirect
7978
github.com/kkdai/bstream v1.0.0 // indirect
8079
github.com/klauspost/compress v1.18.5 // indirect
80+
github.com/lib/pq v1.12.3 // indirect
8181
github.com/lightningnetwork/lnd/clock v1.0.1 // indirect
8282
github.com/lightningnetwork/lnd/queue v1.0.1 // indirect
8383
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect

sqlc.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ sql:
1111
package: "sqlc"
1212

1313
# This is the driver package that sqlc will use in the generated code.
14-
sql_package: database/sql
14+
sql_package: pgx/v5
1515

1616
# Generate a `Querier` interface of all query methods. It's useful for
1717
# mocking in tests.
@@ -20,7 +20,7 @@ sql:
2020
# Export generated SQL statements so they're usable from other packages.
2121
emit_exported_queries: true
2222

23-
# Generate prepared statements for better performance with database/sql.
23+
# Generate prepared statements for better performance with pgx.
2424
emit_prepared_queries: true
2525

2626
- engine: "sqlite"

wallet/internal/db/itest/fixtures_pg_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
dberr "github.com/btcsuite/btcwallet/wallet/internal/db/err"
1414
"github.com/btcsuite/btcwallet/wallet/internal/sql/pg/sqlc"
1515
"github.com/jackc/pgx/v5/pgconn"
16+
"github.com/jackc/pgx/v5/pgtype"
1617
"github.com/stretchr/testify/require"
1718
)
1819

@@ -79,12 +80,12 @@ func CreateAccountWithNumber(t *testing.T, queries *sqlc.Queries,
7980
t.Context(), sqlc.CreateDerivedAccountParams{
8081
ScopeID: scopeID,
8182
AccountName: name,
82-
AccountNumber: sql.NullInt64{
83+
AccountNumber: pgtype.Int8{
8384
Int64: int64(accountNumber),
8485
Valid: true,
8586
},
8687
PublicKey: RandomBytes(32),
87-
MasterFingerprint: sql.NullInt64{},
88+
MasterFingerprint: pgtype.Int8{},
8889
},
8990
)
9091
require.NoError(t, err)

wallet/internal/db/itest/pg_test.go

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,61 @@ import (
2323
"github.com/btcsuite/btcd/wire/v2"
2424
"github.com/btcsuite/btcwallet/wallet/internal/db"
2525
"github.com/btcsuite/btcwallet/wallet/internal/db/pg"
26+
pgschema "github.com/btcsuite/btcwallet/wallet/internal/sql/pg"
2627
"github.com/btcsuite/btcwallet/wallet/internal/sql/pg/sqlc"
2728
"github.com/docker/go-connections/nat"
29+
"github.com/jackc/pgx/v5"
30+
"github.com/jackc/pgx/v5/pgtype"
31+
"github.com/jackc/pgx/v5/stdlib"
2832
"github.com/stretchr/testify/require"
2933
"github.com/testcontainers/testcontainers-go"
3034
"github.com/testcontainers/testcontainers-go/modules/postgres"
3135
"github.com/testcontainers/testcontainers-go/wait"
3236
)
3337

38+
// postgresTestStore owns integration-only adapters around a PostgreSQL Store.
39+
type postgresTestStore struct {
40+
*pg.Store
41+
42+
sqlDB *sql.DB
43+
}
44+
45+
var _ db.Store = (*postgresTestStore)(nil)
46+
47+
// DB returns the shared-pool database/sql adapter used by integration tests.
48+
func (s *postgresTestStore) DB() *sql.DB {
49+
return s.sqlDB
50+
}
51+
52+
// RollbackAllMigrations rolls back all PostgreSQL migrations.
53+
func (s *postgresTestStore) RollbackAllMigrations() error {
54+
return pgschema.RollbackMigrations(context.Background(), s.sqlDB)
55+
}
56+
57+
// ApplyAllMigrations reapplies all PostgreSQL migrations.
58+
func (s *postgresTestStore) ApplyAllMigrations() error {
59+
return pgschema.ApplyMigrations(context.Background(), s.sqlDB)
60+
}
61+
62+
// Close closes the integration adapter and native PostgreSQL Store.
63+
func (s *postgresTestStore) Close() error {
64+
err := s.sqlDB.Close()
65+
storeErr := s.Store.Close()
66+
67+
if err != nil {
68+
return fmt.Errorf("close integration database: %w", err)
69+
}
70+
71+
return storeErr
72+
}
73+
74+
// isPostgresTestStore reports whether store is the PostgreSQL test backend.
75+
func isPostgresTestStore(store any) bool {
76+
_, ok := store.(*postgresTestStore)
77+
78+
return ok
79+
}
80+
3481
const (
3582
// pgMaxIdentifierLen is the PostgreSQL maximum identifier length
3683
// (NAMEDATALEN - 1).
@@ -249,7 +296,7 @@ func sanitizedPgDBName(t *testing.T) string {
249296
// limit allows, exhausting the PostgreSQL connection pool. Avoid this by
250297
// creating NewTestStore inside each parallel subtest so its lifecycle is tied
251298
// to the subtest's parallel slot.
252-
func NewTestStore(t *testing.T) *pg.Store {
299+
func NewTestStore(t *testing.T) *postgresTestStore {
253300
t.Helper()
254301

255302
return NewTestStoreWithDerive(t, mockDeriveFunc())
@@ -258,7 +305,16 @@ func NewTestStore(t *testing.T) *pg.Store {
258305
// NewTestStoreWithDerive creates a new PostgreSQL database for testing with the
259306
// provided address derivation function.
260307
func NewTestStoreWithDerive(t *testing.T,
261-
deriveAddress db.AddressDerivationFunc) *pg.Store {
308+
deriveAddress db.AddressDerivationFunc) *postgresTestStore {
309+
310+
t.Helper()
311+
312+
return newTestStore(t, deriveAddress, 0)
313+
}
314+
315+
// newTestStore creates a PostgreSQL test store with an explicit pool limit.
316+
func newTestStore(t *testing.T, deriveAddress db.AddressDerivationFunc,
317+
maxConnections int) *postgresTestStore {
262318

263319
t.Helper()
264320
ctx := t.Context()
@@ -291,23 +347,52 @@ func NewTestStoreWithDerive(t *testing.T,
291347

292348
cfg := pg.Config{
293349
Dsn: testConnStr,
294-
MaxConnections: 0,
350+
MaxConnections: maxConnections,
295351
DeriveAddress: deriveAddress,
296352
}
297353

298354
store, err := pg.NewStore(t.Context(), cfg)
299355
require.NoError(t, err, "failed to create postgres store")
300356

357+
testStore := &postgresTestStore{
358+
Store: store,
359+
sqlDB: stdlib.OpenDBFromPool(store.Pool()),
360+
}
361+
301362
t.Cleanup(func() {
302-
_ = store.Close()
363+
_ = testStore.Close()
303364
})
304365

305-
return store
366+
return testStore
367+
}
368+
369+
// TestPostgresTestStoreSharesPool verifies the integration adapter releases
370+
// its connection back to the native pool when only one pool slot is available.
371+
func TestPostgresTestStoreSharesPool(t *testing.T) {
372+
t.Parallel()
373+
374+
store := newTestStore(t, mockDeriveFunc(), 1)
375+
sqlDB := store.DB()
376+
require.Same(t, sqlDB, store.DB())
377+
378+
ctx, cancel := context.WithTimeout(
379+
t.Context(), db.DefaultConnectionTimeout,
380+
)
381+
defer cancel()
382+
383+
require.NoError(t, sqlDB.PingContext(ctx))
384+
require.Zero(t, store.Pool().Stat().AcquiredConns())
385+
386+
wallets, err := store.Queries().ListWallets(
387+
ctx, sqlc.ListWalletsParams{PageLimit: 1},
388+
)
389+
require.NoError(t, err)
390+
require.Empty(t, wallets)
306391
}
307392

308393
// childSpendingTxIDs returns the direct child transaction IDs recorded for the
309394
// provided parent transaction hash.
310-
func childSpendingTxIDs(t *testing.T, store *pg.Store,
395+
func childSpendingTxIDs(t *testing.T, store *postgresTestStore,
311396
walletID uint32,
312397
txHash chainhash.Hash) []int64 {
313398

@@ -340,7 +425,7 @@ func childSpendingTxIDs(t *testing.T, store *pg.Store,
340425

341426
// txIDByHash returns the database row ID for the given wallet-scoped
342427
// transaction hash and reports whether the row exists.
343-
func txIDByHash(t *testing.T, store *pg.Store, walletID uint32,
428+
func txIDByHash(t *testing.T, store *postgresTestStore, walletID uint32,
344429
txHash chainhash.Hash) (int64, bool) {
345430

346431
t.Helper()
@@ -352,7 +437,7 @@ func txIDByHash(t *testing.T, store *pg.Store, walletID uint32,
352437
},
353438
)
354439
if err != nil {
355-
if errors.Is(err, sql.ErrNoRows) {
440+
if errors.Is(err, pgx.ErrNoRows) {
356441
return 0, false
357442
}
358443

@@ -364,7 +449,7 @@ func txIDByHash(t *testing.T, store *pg.Store, walletID uint32,
364449

365450
// setTxStatus rewrites one wallet-scoped transaction row to the provided
366451
// status using the internal status-update query.
367-
func setTxStatus(t *testing.T, store *pg.Store, walletID uint32,
452+
func setTxStatus(t *testing.T, store *postgresTestStore, walletID uint32,
368453
txHash chainhash.Hash, status db.TxStatus) {
369454

370455
t.Helper()
@@ -385,7 +470,7 @@ func setTxStatus(t *testing.T, store *pg.Store, walletID uint32,
385470

386471
// walletUtxoExists reports whether one wallet-scoped outpoint is currently
387472
// present in the UTXO set.
388-
func walletUtxoExists(t *testing.T, store *pg.Store,
473+
func walletUtxoExists(t *testing.T, store *postgresTestStore,
389474
walletID uint32,
390475
outPoint wire.OutPoint) bool {
391476

@@ -399,7 +484,7 @@ func walletUtxoExists(t *testing.T, store *pg.Store,
399484
},
400485
)
401486
if err != nil {
402-
if errors.Is(err, sql.ErrNoRows) {
487+
if errors.Is(err, pgx.ErrNoRows) {
403488
return false
404489
}
405490

@@ -411,7 +496,7 @@ func walletUtxoExists(t *testing.T, store *pg.Store,
411496

412497
// walletUtxoSpent reports whether one wallet-scoped outpoint exists and is
413498
// recorded as spent, i.e. its spend edge points at a spending transaction.
414-
func walletUtxoSpent(t *testing.T, store *pg.Store,
499+
func walletUtxoSpent(t *testing.T, store *postgresTestStore,
415500
walletID uint32,
416501
outPoint wire.OutPoint) bool {
417502

@@ -425,7 +510,7 @@ func walletUtxoSpent(t *testing.T, store *pg.Store,
425510
},
426511
)
427512
if err != nil {
428-
if errors.Is(err, sql.ErrNoRows) {
513+
if errors.Is(err, pgx.ErrNoRows) {
429514
return false
430515
}
431516

@@ -436,7 +521,7 @@ func walletUtxoSpent(t *testing.T, store *pg.Store,
436521
}
437522

438523
// clearUtxosSpentByTxID clears all UTXO spend edges claimed by one transaction.
439-
func clearUtxosSpentByTxID(t *testing.T, store *pg.Store,
524+
func clearUtxosSpentByTxID(t *testing.T, store *postgresTestStore,
440525
walletID uint32, txHash chainhash.Hash) {
441526

442527
t.Helper()
@@ -447,7 +532,7 @@ func clearUtxosSpentByTxID(t *testing.T, store *pg.Store,
447532
rows, err := store.Queries().ClearUtxosSpentByTxID(
448533
t.Context(), sqlc.ClearUtxosSpentByTxIDParams{
449534
WalletID: int64(walletID),
450-
SpentByTxID: sql.NullInt64{
535+
SpentByTxID: pgtype.Int8{
451536
Int64: txID,
452537
Valid: true,
453538
},

wallet/internal/db/itest/sqlite_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import (
1616
"github.com/stretchr/testify/require"
1717
)
1818

19+
// isPostgresTestStore reports whether store is the PostgreSQL test backend.
20+
func isPostgresTestStore(any) bool {
21+
return false
22+
}
23+
1924
// NewTestStore creates a new SQLite database for testing with migrations
2025
// applied. Each test gets its own temporary database file.
2126
func NewTestStore(t *testing.T) *sqlite.Store {

wallet/internal/db/itest/tx_corruption_pg_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"time"
88

99
"github.com/btcsuite/btcd/chainhash/v2"
10-
"github.com/btcsuite/btcwallet/wallet/internal/db/pg"
1110
"github.com/stretchr/testify/require"
1211
)
1312

@@ -19,7 +18,7 @@ import (
1918
// corruptTransactionStatus writes an invalid tx status after dropping the
2019
// validating constraints that normally reject it. The corruption itests use
2120
// this to verify that reads reject impossible tx states.
22-
func corruptTransactionStatus(t *testing.T, store *pg.Store,
21+
func corruptTransactionStatus(t *testing.T, store *postgresTestStore,
2322
walletID uint32, txHash chainhash.Hash, status int64) {
2423

2524
t.Helper()
@@ -55,7 +54,7 @@ func corruptTransactionStatus(t *testing.T, store *pg.Store,
5554
// corruptTransactionHash writes malformed tx-hash bytes after dropping the
5655
// fixed-length hash check. The corruption itests then verify that hash
5756
// decoding fails with the expected error path.
58-
func corruptTransactionHash(t *testing.T, store *pg.Store,
57+
func corruptTransactionHash(t *testing.T, store *postgresTestStore,
5958
walletID uint32, txHash chainhash.Hash, hash []byte) {
6059

6160
t.Helper()
@@ -82,7 +81,7 @@ func corruptTransactionHash(t *testing.T, store *pg.Store,
8281

8382
// forceRollbackBlockDeleteFailure installs a trigger that fails the rollback
8483
// block-deletion stage after sync-state rewind has run.
85-
func forceRollbackBlockDeleteFailure(t *testing.T, store *pg.Store) {
84+
func forceRollbackBlockDeleteFailure(t *testing.T, store *postgresTestStore) {
8685
t.Helper()
8786

8887
statements := []string{
@@ -104,7 +103,7 @@ func forceRollbackBlockDeleteFailure(t *testing.T, store *pg.Store) {
104103
// the non-negative height check and creating a matching block row. The
105104
// corruption itests use this to verify that reads reject impossible
106105
// confirmation metadata.
107-
func corruptTransactionBlockHeight(t *testing.T, store *pg.Store,
106+
func corruptTransactionBlockHeight(t *testing.T, store *postgresTestStore,
108107
walletID uint32, txHash chainhash.Hash, height int64) {
109108

110109
t.Helper()
@@ -144,7 +143,7 @@ func corruptTransactionBlockHeight(t *testing.T, store *pg.Store,
144143
// corruptUtxoOutputIndex writes an invalid output index after dropping the
145144
// non-negative output-index check. The corruption itests then verify that UTXO
146145
// decoding rejects the malformed persisted value.
147-
func corruptUtxoOutputIndex(t *testing.T, store *pg.Store,
146+
func corruptUtxoOutputIndex(t *testing.T, store *postgresTestStore,
148147
walletID uint32, txHash chainhash.Hash, oldIndex uint32, newIndex int64) {
149148

150149
t.Helper()
@@ -172,7 +171,7 @@ func corruptUtxoOutputIndex(t *testing.T, store *pg.Store,
172171
// corruptActiveLeaseLockID writes an invalid lease lock ID after dropping the
173172
// fixed-length lock-id check. The corruption itests use this to verify that
174173
// lease reads reject malformed lock identifiers.
175-
func corruptActiveLeaseLockID(t *testing.T, store *pg.Store,
174+
func corruptActiveLeaseLockID(t *testing.T, store *postgresTestStore,
176175
walletID uint32, txHash chainhash.Hash, outputIndex uint32, lockID []byte) {
177176

178177
t.Helper()

wallet/internal/db/itest/txstore_corruption_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/btcsuite/btcd/address/v2"
1111
"github.com/btcsuite/btcd/wire/v2"
1212
"github.com/btcsuite/btcwallet/wallet/internal/db"
13-
"github.com/btcsuite/btcwallet/wallet/internal/db/pg"
1413
"github.com/stretchr/testify/require"
1514
)
1615

@@ -22,7 +21,7 @@ func dropTableForCorruption(t *testing.T, store interface{ DB() *sql.DB },
2221
t.Helper()
2322

2423
stmt := "DROP TABLE " + table
25-
if _, ok := any(store).(*pg.Store); ok {
24+
if isPostgresTestStore(store) {
2625
stmt += " CASCADE"
2726
}
2827

0 commit comments

Comments
 (0)