mirror of
https://github.com/jackc/pgx.git
synced 2025-05-30 19:22:19 +00:00
Added ValueRoundTripTest to pgxtest Removed pgtype/testutil pgtype tests now run with all (applicable) query modes. This gives better coverage than before and revealed several bugs which are also fixed in this commit.
356 lines
7.9 KiB
Go
356 lines
7.9 KiB
Go
package pgtype
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/internal/pgio"
|
|
)
|
|
|
|
const pgTimestamptzHourFormat = "2006-01-02 15:04:05.999999999Z07"
|
|
const pgTimestamptzMinuteFormat = "2006-01-02 15:04:05.999999999Z07:00"
|
|
const pgTimestamptzSecondFormat = "2006-01-02 15:04:05.999999999Z07:00:00"
|
|
const microsecFromUnixEpochToY2K = 946684800 * 1000000
|
|
|
|
const (
|
|
negativeInfinityMicrosecondOffset = -9223372036854775808
|
|
infinityMicrosecondOffset = 9223372036854775807
|
|
)
|
|
|
|
type TimestamptzScanner interface {
|
|
ScanTimestamptz(v Timestamptz) error
|
|
}
|
|
|
|
type TimestamptzValuer interface {
|
|
TimestamptzValue() (Timestamptz, error)
|
|
}
|
|
|
|
// Timestamptz represents the PostgreSQL timestamptz type.
|
|
type Timestamptz struct {
|
|
Time time.Time
|
|
InfinityModifier InfinityModifier
|
|
Valid bool
|
|
}
|
|
|
|
func (tstz *Timestamptz) ScanTimestamptz(v Timestamptz) error {
|
|
*tstz = v
|
|
return nil
|
|
}
|
|
|
|
func (tstz Timestamptz) TimestamptzValue() (Timestamptz, error) {
|
|
return tstz, nil
|
|
}
|
|
|
|
// Scan implements the database/sql Scanner interface.
|
|
func (tstz *Timestamptz) Scan(src interface{}) error {
|
|
if src == nil {
|
|
*tstz = Timestamptz{}
|
|
return nil
|
|
}
|
|
|
|
switch src := src.(type) {
|
|
case string:
|
|
return scanPlanTextTimestamptzToTimestamptzScanner{}.Scan([]byte(src), tstz)
|
|
case time.Time:
|
|
*tstz = Timestamptz{Time: src, Valid: true}
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("cannot scan %T", src)
|
|
}
|
|
|
|
// Value implements the database/sql/driver Valuer interface.
|
|
func (tstz Timestamptz) Value() (driver.Value, error) {
|
|
if !tstz.Valid {
|
|
return nil, nil
|
|
}
|
|
|
|
if tstz.InfinityModifier != Finite {
|
|
return tstz.InfinityModifier.String(), nil
|
|
}
|
|
return tstz.Time, nil
|
|
}
|
|
|
|
func (tstz Timestamptz) MarshalJSON() ([]byte, error) {
|
|
if !tstz.Valid {
|
|
return []byte("null"), nil
|
|
}
|
|
|
|
var s string
|
|
|
|
switch tstz.InfinityModifier {
|
|
case Finite:
|
|
s = tstz.Time.Format(time.RFC3339Nano)
|
|
case Infinity:
|
|
s = "infinity"
|
|
case NegativeInfinity:
|
|
s = "-infinity"
|
|
}
|
|
|
|
return json.Marshal(s)
|
|
}
|
|
|
|
func (tstz *Timestamptz) UnmarshalJSON(b []byte) error {
|
|
var s *string
|
|
err := json.Unmarshal(b, &s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if s == nil {
|
|
*tstz = Timestamptz{}
|
|
return nil
|
|
}
|
|
|
|
switch *s {
|
|
case "infinity":
|
|
*tstz = Timestamptz{Valid: true, InfinityModifier: Infinity}
|
|
case "-infinity":
|
|
*tstz = Timestamptz{Valid: true, InfinityModifier: -Infinity}
|
|
default:
|
|
// PostgreSQL uses ISO 8601 for to_json function and casting from a string to timestamptz
|
|
tim, err := time.Parse(time.RFC3339Nano, *s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*tstz = Timestamptz{Time: tim, Valid: true}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type TimestamptzCodec struct{}
|
|
|
|
func (TimestamptzCodec) FormatSupported(format int16) bool {
|
|
return format == TextFormatCode || format == BinaryFormatCode
|
|
}
|
|
|
|
func (TimestamptzCodec) PreferredFormat() int16 {
|
|
return BinaryFormatCode
|
|
}
|
|
|
|
func (TimestamptzCodec) PlanEncode(m *Map, oid uint32, format int16, value interface{}) EncodePlan {
|
|
if _, ok := value.(TimestamptzValuer); !ok {
|
|
return nil
|
|
}
|
|
|
|
switch format {
|
|
case BinaryFormatCode:
|
|
return encodePlanTimestamptzCodecBinary{}
|
|
case TextFormatCode:
|
|
return encodePlanTimestamptzCodecText{}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type encodePlanTimestamptzCodecBinary struct{}
|
|
|
|
func (encodePlanTimestamptzCodecBinary) Encode(value interface{}, buf []byte) (newBuf []byte, err error) {
|
|
ts, err := value.(TimestamptzValuer).TimestamptzValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !ts.Valid {
|
|
return nil, nil
|
|
}
|
|
|
|
var microsecSinceY2K int64
|
|
switch ts.InfinityModifier {
|
|
case Finite:
|
|
microsecSinceUnixEpoch := ts.Time.Unix()*1000000 + int64(ts.Time.Nanosecond())/1000
|
|
microsecSinceY2K = microsecSinceUnixEpoch - microsecFromUnixEpochToY2K
|
|
case Infinity:
|
|
microsecSinceY2K = infinityMicrosecondOffset
|
|
case NegativeInfinity:
|
|
microsecSinceY2K = negativeInfinityMicrosecondOffset
|
|
}
|
|
|
|
buf = pgio.AppendInt64(buf, microsecSinceY2K)
|
|
|
|
return buf, nil
|
|
}
|
|
|
|
type encodePlanTimestamptzCodecText struct{}
|
|
|
|
func (encodePlanTimestamptzCodecText) Encode(value interface{}, buf []byte) (newBuf []byte, err error) {
|
|
ts, err := value.(TimestamptzValuer).TimestamptzValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !ts.Valid {
|
|
return nil, nil
|
|
}
|
|
|
|
var s string
|
|
|
|
switch ts.InfinityModifier {
|
|
case Finite:
|
|
|
|
t := ts.Time.UTC().Truncate(time.Microsecond)
|
|
|
|
// Year 0000 is 1 BC
|
|
bc := false
|
|
if year := t.Year(); year <= 0 {
|
|
year = -year + 1
|
|
t = time.Date(year, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC)
|
|
bc = true
|
|
}
|
|
|
|
s = t.Format(pgTimestamptzSecondFormat)
|
|
|
|
if bc {
|
|
s = s + " BC"
|
|
}
|
|
case Infinity:
|
|
s = "infinity"
|
|
case NegativeInfinity:
|
|
s = "-infinity"
|
|
}
|
|
|
|
buf = append(buf, s...)
|
|
|
|
return buf, nil
|
|
}
|
|
|
|
func (TimestamptzCodec) PlanScan(m *Map, oid uint32, format int16, target interface{}) ScanPlan {
|
|
|
|
switch format {
|
|
case BinaryFormatCode:
|
|
switch target.(type) {
|
|
case TimestamptzScanner:
|
|
return scanPlanBinaryTimestamptzToTimestamptzScanner{}
|
|
}
|
|
case TextFormatCode:
|
|
switch target.(type) {
|
|
case TimestamptzScanner:
|
|
return scanPlanTextTimestamptzToTimestamptzScanner{}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type scanPlanBinaryTimestamptzToTimestamptzScanner struct{}
|
|
|
|
func (scanPlanBinaryTimestamptzToTimestamptzScanner) Scan(src []byte, dst interface{}) error {
|
|
scanner := (dst).(TimestamptzScanner)
|
|
|
|
if src == nil {
|
|
return scanner.ScanTimestamptz(Timestamptz{})
|
|
}
|
|
|
|
if len(src) != 8 {
|
|
return fmt.Errorf("invalid length for timestamptz: %v", len(src))
|
|
}
|
|
|
|
var tstz Timestamptz
|
|
microsecSinceY2K := int64(binary.BigEndian.Uint64(src))
|
|
|
|
switch microsecSinceY2K {
|
|
case infinityMicrosecondOffset:
|
|
tstz = Timestamptz{Valid: true, InfinityModifier: Infinity}
|
|
case negativeInfinityMicrosecondOffset:
|
|
tstz = Timestamptz{Valid: true, InfinityModifier: -Infinity}
|
|
default:
|
|
tim := time.Unix(
|
|
microsecFromUnixEpochToY2K/1000000+microsecSinceY2K/1000000,
|
|
(microsecFromUnixEpochToY2K%1000000*1000)+(microsecSinceY2K%1000000*1000),
|
|
)
|
|
tstz = Timestamptz{Time: tim, Valid: true}
|
|
}
|
|
|
|
return scanner.ScanTimestamptz(tstz)
|
|
}
|
|
|
|
type scanPlanTextTimestamptzToTimestamptzScanner struct{}
|
|
|
|
func (scanPlanTextTimestamptzToTimestamptzScanner) Scan(src []byte, dst interface{}) error {
|
|
scanner := (dst).(TimestamptzScanner)
|
|
|
|
if src == nil {
|
|
return scanner.ScanTimestamptz(Timestamptz{})
|
|
}
|
|
|
|
var tstz Timestamptz
|
|
sbuf := string(src)
|
|
switch sbuf {
|
|
case "infinity":
|
|
tstz = Timestamptz{Valid: true, InfinityModifier: Infinity}
|
|
case "-infinity":
|
|
tstz = Timestamptz{Valid: true, InfinityModifier: -Infinity}
|
|
default:
|
|
bc := false
|
|
if strings.HasSuffix(sbuf, " BC") {
|
|
sbuf = sbuf[:len(sbuf)-3]
|
|
bc = true
|
|
}
|
|
|
|
var format string
|
|
if len(sbuf) >= 9 && (sbuf[len(sbuf)-9] == '-' || sbuf[len(sbuf)-9] == '+') {
|
|
format = pgTimestamptzSecondFormat
|
|
} else if len(sbuf) >= 6 && (sbuf[len(sbuf)-6] == '-' || sbuf[len(sbuf)-6] == '+') {
|
|
format = pgTimestamptzMinuteFormat
|
|
} else {
|
|
format = pgTimestamptzHourFormat
|
|
}
|
|
|
|
tim, err := time.Parse(format, sbuf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if bc {
|
|
year := -tim.Year() + 1
|
|
tim = time.Date(year, tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), tim.Location())
|
|
}
|
|
|
|
tstz = Timestamptz{Time: tim, Valid: true}
|
|
}
|
|
|
|
return scanner.ScanTimestamptz(tstz)
|
|
}
|
|
|
|
func (c TimestamptzCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
|
|
if src == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var tstz Timestamptz
|
|
err := codecScan(c, m, oid, format, src, &tstz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tstz.InfinityModifier != Finite {
|
|
return tstz.InfinityModifier.String(), nil
|
|
}
|
|
|
|
return tstz.Time, nil
|
|
}
|
|
|
|
func (c TimestamptzCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (interface{}, error) {
|
|
if src == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var tstz Timestamptz
|
|
err := codecScan(c, m, oid, format, src, &tstz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tstz.InfinityModifier != Finite {
|
|
return tstz.InfinityModifier, nil
|
|
}
|
|
|
|
return tstz.Time, nil
|
|
}
|