package notificationstore import ( "context" "database/sql" "errors" "fmt" "time" "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" // isUniqueViolation reports whether err is a PostgreSQL unique-violation, // regardless of constraint name. func isUniqueViolation(err error) bool { var pgErr *pgconn.PgError if !errors.As(err, &pgErr) { return false } return pgErr.Code == pgUniqueViolationCode } // isNoRows reports whether err is sql.ErrNoRows. func isNoRows(err error) bool { return errors.Is(err, sql.ErrNoRows) } // nullableTime returns t.UTC() when non-zero, otherwise nil so the column // is bound as SQL NULL. The notification domain uses zero-valued time.Time // to express "absent" timestamps (no pointers), so the helper centralises // the boundary translation. func nullableTime(t time.Time) any { if t.IsZero() { return nil } return t.UTC() } // timeFromNullable copies an optional sql.NullTime read from PostgreSQL // into a domain time.Time, applying the global UTC normalisation rule. // Invalid (NULL) values become the zero time.Time. func timeFromNullable(value sql.NullTime) time.Time { if !value.Valid { return time.Time{} } return value.Time.UTC() } // 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 }