Private
Public Access
1
0

feat(db): migrate from MySQL to PostgreSQL
All checks were successful
continuous-integration/drone/push Build is passing

Replace MySQL driver with pgx/v5 and pgxpool:
- Update sqlc to use postgresql engine
- Convert query.sql to PostgreSQL syntax ($1 params, CASE WHEN,
  ANY() arrays)
- Replace sql.DB with pgxpool.Pool throughout
- Change nullable types from sql.Null* to pgtype.*
- Update ID types from uint32 to int64 for PostgreSQL compatibility
- Delete MySQL-specific dynamic_connect.go
- Add opentelemetry.gowrap template for tracing
This commit is contained in:
2025-11-29 10:59:15 -08:00
parent 85d86bc837
commit c9481d12c6
22 changed files with 3293 additions and 1309 deletions

View File

@@ -2,7 +2,11 @@ package ntpdb
import (
"context"
"database/sql"
"errors"
"github.com/jackc/pgx/v5"
"go.ntppool.org/common/logger"
"go.opentelemetry.io/otel/trace"
)
type QuerierTx interface {
@@ -11,14 +15,17 @@ type QuerierTx interface {
Begin(ctx context.Context) (QuerierTx, error)
Commit(ctx context.Context) error
Rollback(ctx context.Context) error
// Conn returns the connection used by this transaction
Conn() *pgx.Conn
}
type Beginner interface {
Begin(context.Context) (sql.Tx, error)
Begin(context.Context) (pgx.Tx, error)
}
type Tx interface {
Begin(context.Context) (sql.Tx, error)
Begin(context.Context) (pgx.Tx, error)
Commit(ctx context.Context) error
Rollback(ctx context.Context) error
}
@@ -28,21 +35,33 @@ func (q *Queries) Begin(ctx context.Context) (QuerierTx, error) {
if err != nil {
return nil, err
}
return &Queries{db: &tx}, nil
return &Queries{db: tx}, nil
}
func (q *Queries) Commit(ctx context.Context) error {
tx, ok := q.db.(Tx)
if !ok {
return sql.ErrTxDone
// Commit called on Queries with dbpool, so treat as transaction already committed
return pgx.ErrTxClosed
}
return tx.Commit(ctx)
}
func (q *Queries) Conn() *pgx.Conn {
// pgx.Tx is an interface that has Conn() method
tx, ok := q.db.(pgx.Tx)
if !ok {
logger.Setup().Error("could not get connection from QuerierTx")
return nil
}
return tx.Conn()
}
func (q *Queries) Rollback(ctx context.Context) error {
tx, ok := q.db.(Tx)
if !ok {
return sql.ErrTxDone
// Rollback called on Queries with dbpool, so treat as transaction already committed
return pgx.ErrTxClosed
}
return tx.Rollback(ctx)
}
@@ -62,3 +81,41 @@ func (wq *WrappedQuerier) Begin(ctx context.Context) (QuerierTx, error) {
}
return NewWrappedQuerier(q), nil
}
func (wq *WrappedQuerier) Conn() *pgx.Conn {
return wq.QuerierTxWithTracing.Conn()
}
// LogRollback logs and performs a rollback if the transaction is still active
func LogRollback(ctx context.Context, tx QuerierTx) {
if !isInTransaction(tx) {
return
}
log := logger.FromContext(ctx)
log.WarnContext(ctx, "transaction rollback called on an active transaction")
// if caller ctx is done we still need rollback to happen
// so Rollback gets a fresh context with span copied over
rbCtx := context.Background()
if span := trace.SpanFromContext(ctx); span != nil {
rbCtx = trace.ContextWithSpan(rbCtx, span)
}
if err := tx.Rollback(rbCtx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.ErrorContext(ctx, "rollback failed", "err", err)
}
}
func isInTransaction(tx QuerierTx) bool {
if tx == nil {
return false
}
conn := tx.Conn()
if conn == nil {
return false
}
// 'I' means idle, so if it's not idle, we're in a transaction
return conn.PgConn().TxStatus() != 'I'
}