feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
@@ -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
}