// Package storage opens the PostgreSQL pool, applies embedded migrations and // provides small helpers shared by repositories. All SQL lives in the domain // packages; this package knows nothing about minds or events. package storage import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" ) // Open connects to PostgreSQL and verifies the connection. func Open(ctx context.Context, url string) (*pgxpool.Pool, error) { cfg, err := pgxpool.ParseConfig(url) if err != nil { return nil, fmt.Errorf("storage: parse url: %w", err) } if cfg.MaxConns < 8 { cfg.MaxConns = 8 } cfg.MaxConnLifetime = time.Hour cfg.HealthCheckPeriod = 30 * time.Second if cfg.ConnConfig.RuntimeParams == nil { cfg.ConnConfig.RuntimeParams = map[string]string{} } cfg.ConnConfig.RuntimeParams["application_name"] = "noema" // A statement timeout bounds runaway queries from any subsystem (T18). if _, set := cfg.ConnConfig.RuntimeParams["statement_timeout"]; !set { cfg.ConnConfig.RuntimeParams["statement_timeout"] = "30000" } pool, err := pgxpool.NewWithConfig(ctx, cfg) if err != nil { return nil, fmt.Errorf("storage: connect: %w", err) } pingCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if err := pool.Ping(pingCtx); err != nil { pool.Close() return nil, fmt.Errorf("storage: ping: %w", err) } return pool, nil } // ErrNotFound is returned by repositories when a row does not exist. var ErrNotFound = errors.New("not found") // ErrConflict is returned when a uniqueness constraint is violated. var ErrConflict = errors.New("conflict") // Translate maps driver errors to storage sentinels so callers can use errors.Is. func Translate(err error) error { if err == nil { return nil } if errors.Is(err, pgx.ErrNoRows) { return ErrNotFound } var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { return fmt.Errorf("%w: %s", ErrConflict, pgErr.ConstraintName) } return err } // Tx runs fn in a transaction, committing on nil error and rolling back otherwise. func Tx(ctx context.Context, pool *pgxpool.Pool, fn func(tx pgx.Tx) error) error { tx, err := pool.Begin(ctx) if err != nil { return fmt.Errorf("begin: %w", err) } defer tx.Rollback(ctx) //nolint:errcheck // rollback after commit is a no-op if err := fn(tx); err != nil { return err } return tx.Commit(ctx) } // Querier is implemented by both *pgxpool.Pool and pgx.Tx so repositories can // run inside or outside a transaction. type Querier interface { Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row }