lightning-terminal/db/postgres.go
ffranr a0e63124c0
multi: reformat long lines for readability
- Replace occurrences of `// nolint:lll` with `// nolint:ll` across
  files for consistency.
- Reformat multiline strings, comments, and function parameters to
  improve clarity and adhere to style guidelines.
- Add `// nolint:ll` comments where necessary to prevent linter
  warnings.
2025-12-09 16:12:03 +00:00

207 lines
6.4 KiB
Go

package db
import (
"database/sql"
"fmt"
"testing"
"time"
postgres_migrate "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/lightninglabs/lightning-terminal/db/sqlc"
"github.com/stretchr/testify/require"
)
const (
dsnTemplate = "postgres://%v:%v@%v:%d/%v?sslmode=%v"
// defaultMaxIdleConns is the number of permitted idle connections.
defaultMaxIdleConns = 6
// defaultConnMaxIdleTime is the amount of time a connection can be
// idle before it is closed.
defaultConnMaxIdleTime = 5 * time.Minute
)
var (
// DefaultPostgresFixtureLifetime is the default maximum time a Postgres
// test fixture is being kept alive. After that time the docker
// container will be terminated forcefully, even if the tests aren't
// fully executed yet. So this time needs to be chosen correctly to be
// longer than the longest expected individual test run time.
DefaultPostgresFixtureLifetime = 60 * time.Minute
// postgresSchemaReplacements is a map of schema strings that need to be
// replaced for postgres. This is needed because we write the schemas
// to work with sqlite primarily, and postgres has some differences.
postgresSchemaReplacements = map[string]string{
"BLOB": "BYTEA",
"INTEGER PRIMARY KEY": "BIGSERIAL PRIMARY KEY",
"TIMESTAMP": "TIMESTAMP WITHOUT TIME ZONE",
"UNHEX": "DECODE",
}
)
// PostgresConfig holds the postgres database configuration.
//
// nolint:ll
type PostgresConfig struct {
SkipMigrations bool `long:"skipmigrations" description:"Skip applying migrations on startup."`
Host string `long:"host" description:"Database server hostname."`
Port int `long:"port" description:"Database server port."`
User string `long:"user" description:"Database user."`
Password string `long:"password" description:"Database user's password."`
DBName string `long:"dbname" description:"Database name to use."`
MaxOpenConnections int `long:"maxconnections" description:"Max open connections to keep alive to the database server."`
MaxIdleConnections int `long:"maxidleconnections" description:"Max number of idle connections to keep in the connection pool."`
ConnMaxLifetime time.Duration `long:"connmaxlifetime" description:"Max amount of time a connection can be reused for before it is closed. Valid time units are {s, m, h}."`
ConnMaxIdleTime time.Duration `long:"connmaxidletime" description:"Max amount of time a connection can be idle for before it is closed. Valid time units are {s, m, h}."`
RequireSSL bool `long:"requiressl" description:"Whether to require using SSL (mode: require) when connecting to the server."`
}
// DSN returns the dns to connect to the database.
func (s *PostgresConfig) DSN(hidePassword bool) string {
var sslMode = "disable"
if s.RequireSSL {
sslMode = "require"
}
password := s.Password
if hidePassword {
// Placeholder used for logging the DSN safely.
password = "****"
}
return fmt.Sprintf(dsnTemplate, s.User, password, s.Host, s.Port,
s.DBName, sslMode)
}
// PostgresStore is a database store implementation that uses a Postgres
// backend.
type PostgresStore struct {
cfg *PostgresConfig
*BaseDB
}
// NewPostgresStore creates a new store that is backed by a Postgres database
// backend.
func NewPostgresStore(cfg *PostgresConfig) (*PostgresStore, error) {
log.Infof("Using SQL database '%s'", cfg.DSN(true))
rawDb, err := sql.Open("pgx", cfg.DSN(false))
if err != nil {
return nil, err
}
maxConns := defaultMaxConns
if cfg.MaxOpenConnections > 0 {
maxConns = cfg.MaxOpenConnections
}
maxIdleConns := defaultMaxIdleConns
if cfg.MaxIdleConnections > 0 {
maxIdleConns = cfg.MaxIdleConnections
}
connMaxLifetime := defaultConnMaxLifetime
if cfg.ConnMaxLifetime > 0 {
connMaxLifetime = cfg.ConnMaxLifetime
}
connMaxIdleTime := defaultConnMaxIdleTime
if cfg.ConnMaxIdleTime > 0 {
connMaxIdleTime = cfg.ConnMaxIdleTime
}
rawDb.SetMaxOpenConns(maxConns)
rawDb.SetMaxIdleConns(maxIdleConns)
rawDb.SetConnMaxLifetime(connMaxLifetime)
rawDb.SetConnMaxIdleTime(connMaxIdleTime)
queries := sqlc.NewPostgres(rawDb)
s := &PostgresStore{
cfg: cfg,
BaseDB: &BaseDB{
DB: rawDb,
Queries: queries,
},
}
// Now that the database is open, populate the database with our set of
// schemas based on our embedded in-memory file system.
if !cfg.SkipMigrations {
if err := s.ExecuteMigrations(TargetLatest); err != nil {
return nil, fmt.Errorf("error executing migrations: "+
"%w", err)
}
}
return s, nil
}
// ExecuteMigrations runs migrations for the Postgres database, depending on the
// target given, either all migrations or up to a given version.
func (s *PostgresStore) ExecuteMigrations(target MigrationTarget,
optFuncs ...MigrateOpt) error {
opts := defaultMigrateOptions()
for _, optFunc := range optFuncs {
optFunc(opts)
}
driver, err := postgres_migrate.WithInstance(
s.DB, &postgres_migrate.Config{},
)
if err != nil {
return fmt.Errorf("error creating postgres migration: %w", err)
}
postgresFS := newReplacerFS(sqlSchemas, postgresSchemaReplacements)
return applyMigrations(
postgresFS, driver, "sqlc/migrations", s.cfg.DBName, target,
opts,
)
}
// NewTestPostgresDB is a helper function that creates a Postgres database for
// testing.
func NewTestPostgresDB(t *testing.T) *PostgresStore {
t.Helper()
t.Logf("Creating new Postgres DB for testing")
sqlFixture := NewTestPgFixture(t, DefaultPostgresFixtureLifetime, true)
store, err := NewPostgresStore(sqlFixture.GetConfig())
require.NoError(t, err)
t.Cleanup(func() {
sqlFixture.TearDown(t)
})
return store
}
// NewTestPostgresDBWithVersion is a helper function that creates a Postgres
// database for testing and migrates it to the given version.
func NewTestPostgresDBWithVersion(t *testing.T, version uint) *PostgresStore {
t.Helper()
t.Logf("Creating new Postgres DB for testing, migrating to version %d",
version)
sqlFixture := NewTestPgFixture(t, DefaultPostgresFixtureLifetime, true)
storeCfg := sqlFixture.GetConfig()
storeCfg.SkipMigrations = true
store, err := NewPostgresStore(storeCfg)
require.NoError(t, err)
err = store.ExecuteMigrations(TargetVersion(version))
require.NoError(t, err)
t.Cleanup(func() {
sqlFixture.TearDown(t)
})
return store
}