feat: use postgres
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
package userstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"galaxy/user/internal/domain/common"
|
||||
"galaxy/user/internal/ports"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
// pgUniqueViolationCode identifies the SQLSTATE returned by PostgreSQL when
|
||||
// a UNIQUE constraint is violated by INSERT or UPDATE.
|
||||
const pgUniqueViolationCode = "23505"
|
||||
|
||||
// classifyUniqueViolation maps a PostgreSQL unique-violation error to the
|
||||
// matching ports sentinel. constraint identifies which UNIQUE constraint name
|
||||
// the caller cares about so we can surface ports.ErrUserNameConflict for the
|
||||
// dedicated user-name index. Returns nil when err is not a unique violation
|
||||
// or does not match constraint.
|
||||
func classifyUniqueViolation(err error, constraint string, mapped error) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if !errors.As(err, &pgErr) || pgErr.Code != pgUniqueViolationCode {
|
||||
return nil
|
||||
}
|
||||
if constraint != "" && pgErr.ConstraintName != constraint {
|
||||
return nil
|
||||
}
|
||||
return mapped
|
||||
}
|
||||
|
||||
// isUniqueViolation reports whether err is a PostgreSQL unique-violation,
|
||||
// regardless of constraint name. Useful for "any conflict ⇒ ErrConflict"
|
||||
// translations on simple INSERT calls.
|
||||
func isUniqueViolation(err error) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
if !errors.As(err, &pgErr) {
|
||||
return false
|
||||
}
|
||||
return pgErr.Code == pgUniqueViolationCode
|
||||
}
|
||||
|
||||
// nullableString returns the trimmed string when s is non-empty, otherwise
|
||||
// reports a NULL stand-in usable in $-parameter lists. Empty strings are
|
||||
// stored as NULL so optional columns round-trip through nil.
|
||||
func nullableString(s string) any {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// nullableActorID converts an optional ActorID (the zero value indicates
|
||||
// "no caller supplied this field") to a NULL stand-in for SQL parameters.
|
||||
func nullableActorID(id common.ActorID) any {
|
||||
if id.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return id.String()
|
||||
}
|
||||
|
||||
// nullableActorType mirrors nullableActorID for ActorType.
|
||||
func nullableActorType(t common.ActorType) any {
|
||||
if t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return t.String()
|
||||
}
|
||||
|
||||
// nullableReasonCode mirrors nullableActorID for ReasonCode.
|
||||
func nullableReasonCode(code common.ReasonCode) any {
|
||||
if code.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return code.String()
|
||||
}
|
||||
|
||||
// nullableUserID mirrors nullableActorID for UserID.
|
||||
func nullableUserID(id common.UserID) any {
|
||||
if id.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return id.String()
|
||||
}
|
||||
|
||||
// nullableTime returns t.UTC() when non-nil, otherwise nil for NULL columns.
|
||||
func nullableTime(t *time.Time) any {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return t.UTC()
|
||||
}
|
||||
|
||||
// nullableCountry returns the upper-cased ISO 3166-1 alpha-2 string when set,
|
||||
// otherwise nil.
|
||||
func nullableCountry(code common.CountryCode) any {
|
||||
if code.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return code.String()
|
||||
}
|
||||
|
||||
// stringFromNullable trims an optional sql.NullString-like *string (read from
|
||||
// Postgres COLUMNAR_NULL) into an ActorID/ReasonCode/UserID-friendly string.
|
||||
func stringFromNullable(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
// timeFromNullable copies an optional *time.Time read from Postgres into a
|
||||
// new pointer normalised to UTC.
|
||||
func timeFromNullable(value *time.Time) *time.Time {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
utc := value.UTC()
|
||||
return &utc
|
||||
}
|
||||
|
||||
// mapNotFound translates sql.ErrNoRows into ports.ErrNotFound, leaving every
|
||||
// other error untouched.
|
||||
func mapNotFound(err error) error {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ports.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// withTimeout derives a child context bounded by timeout and prefixes context
|
||||
// errors with operation. Callers must always invoke the returned cancel.
|
||||
func withTimeout(ctx context.Context, operation string, timeout time.Duration) (context.Context, context.CancelFunc, error) {
|
||||
if ctx == nil {
|
||||
return nil, nil, fmt.Errorf("%s: nil context", operation)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", operation, err)
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return nil, nil, fmt.Errorf("%s: operation timeout must be positive", operation)
|
||||
}
|
||||
bounded, cancel := context.WithTimeout(ctx, timeout)
|
||||
return bounded, cancel, nil
|
||||
}
|
||||
Reference in New Issue
Block a user