Files
pgm/pgm.go

198 lines
5.1 KiB
Go
Raw Permalink Normal View History

// pgm
//
2025-10-18 14:43:42 +05:30
// A simple PG string query builder
//
// Author: Ankit Patial
2025-07-26 18:34:56 +05:30
package pgm
import (
2025-10-18 14:43:42 +05:30
"context"
"errors"
2025-11-16 11:37:02 +05:30
"fmt"
2025-10-18 14:43:42 +05:30
"log/slog"
2025-11-09 22:43:24 +05:30
"strings"
2025-10-18 14:43:42 +05:30
"sync/atomic"
2025-07-26 18:34:56 +05:30
"time"
2025-10-18 14:43:42 +05:30
"github.com/jackc/pgx/v5"
2025-07-26 18:34:56 +05:30
"github.com/jackc/pgx/v5/pgtype"
2025-10-18 14:43:42 +05:30
"github.com/jackc/pgx/v5/pgxpool"
2025-07-26 18:34:56 +05:30
)
2025-10-18 14:43:42 +05:30
var (
poolPGX atomic.Pointer[pgxpool.Pool]
ErrConnStringMissing = errors.New("connection string is empty")
)
2025-07-29 21:45:32 +05:30
2025-11-16 16:21:35 +05:30
// Common errors returned by pgm operations
var (
ErrInitTX = errors.New("failed to init db.tx")
ErrCommitTX = errors.New("failed to commit db.tx")
ErrNoRows = errors.New("no data found")
)
2025-08-11 22:37:25 +05:30
2025-11-16 16:21:35 +05:30
// Config holds the configuration for initializing the connection pool.
// All fields except ConnString are optional and will use pgx defaults if not set.
2025-10-18 14:43:42 +05:30
type Config struct {
ConnString string
MaxConns int32
MinConns int32
MaxConnLifetime time.Duration
MaxConnIdleTime time.Duration
2025-07-26 18:34:56 +05:30
}
2025-11-16 16:21:35 +05:30
// InitPool initializes the connection pool with the provided configuration.
// It validates the configuration and panics if invalid.
// This function should be called once at application startup.
//
// Example:
//
// pgm.InitPool(pgm.Config{
// ConnString: "postgres://user:pass@localhost/dbname",
// MaxConns: 100,
// MinConns: 5,
// })
2025-10-18 14:43:42 +05:30
func InitPool(conf Config) {
if conf.ConnString == "" {
panic(ErrConnStringMissing)
}
2025-11-16 11:37:02 +05:30
// Validate configuration
if conf.MaxConns > 0 && conf.MinConns > 0 && conf.MinConns > conf.MaxConns {
panic(fmt.Errorf("MinConns (%d) cannot be greater than MaxConns (%d)", conf.MinConns, conf.MaxConns))
}
if conf.MaxConns < 0 || conf.MinConns < 0 {
panic(errors.New("connection pool configuration cannot have negative values"))
}
2025-10-18 14:43:42 +05:30
cfg, err := pgxpool.ParseConfig(conf.ConnString)
if err != nil {
panic(err)
}
if conf.MaxConns > 0 {
cfg.MaxConns = conf.MaxConns // 100
}
if conf.MinConns > 0 {
2025-11-16 11:37:02 +05:30
cfg.MinConns = conf.MinConns // 5
2025-10-18 14:43:42 +05:30
}
if conf.MaxConnLifetime > 0 {
cfg.MaxConnLifetime = conf.MaxConnLifetime // time.Minute * 10
}
if conf.MaxConnIdleTime > 0 {
cfg.MaxConnIdleTime = conf.MaxConnIdleTime // time.Minute * 5
}
p, err := pgxpool.NewWithConfig(context.Background(), cfg)
if err != nil {
panic(err)
}
if err = p.Ping(context.Background()); err != nil {
panic(err)
}
poolPGX.Store(p)
2025-07-26 18:34:56 +05:30
}
2025-11-16 16:21:35 +05:30
// GetPool returns the initialized connection pool instance.
// It panics with a descriptive message if InitPool() has not been called.
// This is a fail-fast approach to catch programming errors early.
2025-10-18 14:43:42 +05:30
func GetPool() *pgxpool.Pool {
2025-11-16 16:21:35 +05:30
p := poolPGX.Load()
if p == nil {
panic("pgm: connection pool not initialized, call InitPool() first")
}
return p
2025-07-26 18:34:56 +05:30
}
2025-11-16 11:37:02 +05:30
// ClosePool closes the connection pool gracefully.
// Should be called during application shutdown.
func ClosePool() {
if p := poolPGX.Load(); p != nil {
p.Close()
poolPGX.Store(nil)
}
}
2025-11-16 16:21:35 +05:30
// BeginTx begins a new database transaction from the connection pool.
// Returns an error if the transaction cannot be started.
// Remember to commit or rollback the transaction when done.
//
// Example:
//
// tx, err := pgm.BeginTx(ctx)
// if err != nil {
// return err
// }
// defer tx.Rollback(ctx) // rollback on error
//
// // ... do work ...
//
// return tx.Commit(ctx)
2025-10-18 14:43:42 +05:30
func BeginTx(ctx context.Context) (pgx.Tx, error) {
tx, err := poolPGX.Load().Begin(ctx)
if err != nil {
2025-11-16 11:37:02 +05:30
slog.Error("failed to begin transaction", "error", err)
return nil, fmt.Errorf("failed to open db tx: %w", err)
2025-10-18 14:43:42 +05:30
}
2025-11-16 11:37:02 +05:30
return tx, nil
2025-07-26 18:34:56 +05:30
}
2025-11-16 16:21:35 +05:30
// IsNotFound checks if an error is a "no rows" error from pgx.
// Returns true if the error indicates no rows were found in a query result.
2025-10-18 14:43:42 +05:30
func IsNotFound(err error) bool {
return errors.Is(err, pgx.ErrNoRows)
2025-07-26 18:34:56 +05:30
}
2025-11-16 16:21:35 +05:30
// PgTime converts a Go time.Time to PostgreSQL timestamptz type.
// The time is stored as-is (preserves timezone information).
2025-10-18 14:43:42 +05:30
func PgTime(t time.Time) pgtype.Timestamptz {
return pgtype.Timestamptz{Time: t, Valid: true}
2025-07-26 18:34:56 +05:30
}
2025-11-16 16:21:35 +05:30
// PgTimeNow returns the current time as PostgreSQL timestamptz type.
2025-10-18 14:43:42 +05:30
func PgTimeNow() pgtype.Timestamptz {
return pgtype.Timestamptz{Time: time.Now(), Valid: true}
2025-07-26 18:34:56 +05:30
}
2025-11-09 22:43:24 +05:30
2025-11-16 16:21:35 +05:30
// TsAndQuery converts a text search query to use AND operator between terms.
// Example: "hello world" becomes "hello & world"
2025-11-09 22:43:24 +05:30
func TsAndQuery(q string) string {
return strings.Join(strings.Fields(q), " & ")
}
2025-11-16 16:21:35 +05:30
// TsPrefixAndQuery converts a text search query to use AND operator with prefix matching.
// Example: "hello world" becomes "hello:* & world:*"
2025-11-09 22:43:24 +05:30
func TsPrefixAndQuery(q string) string {
return strings.Join(fieldsWithSufix(q, ":*"), " & ")
}
2025-11-16 16:21:35 +05:30
// TsOrQuery converts a text search query to use OR operator between terms.
// Example: "hello world" becomes "hello | world"
2025-11-09 22:43:24 +05:30
func TsOrQuery(q string) string {
return strings.Join(strings.Fields(q), " | ")
}
2025-11-16 16:21:35 +05:30
// TsPrefixOrQuery converts a text search query to use OR operator with prefix matching.
// Example: "hello world" becomes "hello:* | world:*"
2025-11-09 22:43:24 +05:30
func TsPrefixOrQuery(q string) string {
return strings.Join(fieldsWithSufix(q, ":*"), " | ")
}
func fieldsWithSufix(v, sufix string) []string {
fields := strings.Fields(v)
prefixed := make([]string, len(fields))
for i, f := range fields {
prefixed[i] = f + sufix
}
return prefixed
}