Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 142 additions & 7 deletions pgtype/timestamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
)

const (
pgTimestampFormat = "2006-01-02 15:04:05.999999999"
jsonISO8601 = "2006-01-02T15:04:05.999999999"
pgTimestampFormat = "2006-01-02 15:04:05.999999999"
jsonISO8601 = "2006-01-02T15:04:05.999999999"
maxTimestampYear = 294276
maxTimestampBCYear = 4714
)

type TimestampScanner interface {
Expand Down Expand Up @@ -311,14 +313,16 @@ func (plan *scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) e
sbuf = sbuf[:len(sbuf)-3]
bc = true
}
tim, err := time.Parse(pgTimestampFormat, sbuf)
maxYear := int64(maxTimestampYear)
if bc {
maxYear = maxTimestampBCYear
}
tim, err := parseTimestampWithVariableYear(pgTimestampFormat, sbuf, false, maxYear, bc)
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())
if timestampOutOfRange(tim) {
return fmt.Errorf("timestamp out of range")
}

if plan.location != nil {
Expand All @@ -331,6 +335,137 @@ func (plan *scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) e
return scanner.ScanTimestamp(ts)
}

func parseTimestampWithVariableYear(layout, s string, preserveOffset bool, maxYear int64, bc bool) (time.Time, error) {
yearEnd, err := timestampYearEnd(s)
if err != nil {
return time.Time{}, err
}
if yearEnd == 4 && !bc {
if _, err := parseTimestampYear(s[:yearEnd], maxYear); err != nil {
return time.Time{}, err
}
tim, err := time.Parse(layout, s)
if err != nil {
return time.Time{}, err
}
if preserveOffset {
_, offset := tim.Zone()
if offset <= -16*60*60 || offset >= 16*60*60 {
return time.Time{}, fmt.Errorf("time zone displacement out of range")
}
}
return tim, nil
}

normalized, year, err := normalizeTimestampYear(s, maxYear, bc)
if err != nil {
return time.Time{}, err
}

tim, err := time.Parse(layout, normalized)
if err != nil {
return time.Time{}, err
}

loc := tim.Location()
if preserveOffset {
_, offset := tim.Zone()
if offset <= -16*60*60 || offset >= 16*60*60 {
return time.Time{}, fmt.Errorf("time zone displacement out of range")
}
loc = time.FixedZone("", offset)
}

if bc {
year = 1 - year
}

return time.Date(year, tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), loc), nil
}

func timestampOutOfRange(t time.Time) bool {
return t.Before(minTimestampTime().Add(-500*time.Nanosecond)) || !t.Before(maxTimestampTime().Add(500*time.Nanosecond))
}

func minTimestampTime() time.Time {
return time.Date(-4713, 11, 24, 0, 0, 0, 0, time.UTC)
}

func maxTimestampTime() time.Time {
return time.Date(maxTimestampYear, 12, 31, 23, 59, 59, 999999000, time.UTC)
}

func normalizeTimestampYear(s string, maxYear int64, bc bool) (string, int, error) {
yearEnd, err := timestampYearEnd(s)
if err != nil {
return "", 0, err
}

year64, err := parseTimestampYear(s[:yearEnd], maxYear)
if err != nil {
return "", 0, err
}
year := int(year64)

normalizedYear := "2001"
if isTimestampLeapYear(year, bc) {
normalizedYear = "2000"
}

return normalizedYear + s[yearEnd:], year, nil
}

func timestampYearEnd(s string) (int, error) {
yearEnd := -1
for i := 4; i < len(s); i++ {
if s[i] == '-' {
yearEnd = i
break
}
if s[i] < '0' || s[i] > '9' {
return 0, fmt.Errorf("invalid timestamp format")
}
}
if yearEnd == -1 {
return 0, fmt.Errorf("invalid timestamp format")
}
return yearEnd, nil
}

func parseTimestampYear(s string, maxYear int64) (int64, error) {
if len(s) == 0 {
return 0, fmt.Errorf("invalid timestamp format")
}

var n int64
for _, c := range []byte(s) {
if c < '0' || c > '9' {
return 0, fmt.Errorf("invalid timestamp format")
}
digit := int64(c - '0')
if n > (maxYear-digit)/10 {
return 0, fmt.Errorf("timestamp year out of range")
}
n = n*10 + digit
}
if n < 1 || n > maxYear {
return 0, fmt.Errorf("timestamp year out of range")
}

return n, nil
}

func isLeapYear(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}

func isTimestampLeapYear(year int, bc bool) bool {
if bc {
year = 1 - year
}
return isLeapYear(year)
}

func (c *TimestampCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
if src == nil {
return nil, nil
Expand Down
48 changes: 44 additions & 4 deletions pgtype/timestamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,53 @@ func TestTimestampTranscodeBigTimeBinary(t *testing.T) {
})
}

func TestTimestampCodecDecodeTextBigTime(t *testing.T) {
c := &pgtype.TimestampCodec{}

for _, tt := range []struct {
src string
want time.Time
}{
{src: `10000-01-02 03:04:05.123456`, want: time.Date(10000, 1, 2, 3, 4, 5, 123456000, time.UTC)},
{src: `00000000000010000-01-02 03:04:05.123456`, want: time.Date(10000, 1, 2, 3, 4, 5, 123456000, time.UTC)},
{src: `294276-12-31 23:59:59.999999`, want: time.Date(294276, 12, 31, 23, 59, 59, 999999000, time.UTC)},
{src: `294276-12-31 23:59:59.999999499`, want: time.Date(294276, 12, 31, 23, 59, 59, 999999499, time.UTC)},
{src: `4713-02-29 00:00:00 BC`, want: time.Date(-4712, 2, 29, 0, 0, 0, 0, time.UTC)},
{src: `4714-11-24 00:00:00 BC`, want: time.Date(-4713, 11, 24, 0, 0, 0, 0, time.UTC)},
{src: `4714-11-23 23:59:59.999999500 BC`, want: time.Date(-4713, 11, 23, 23, 59, 59, 999999500, time.UTC)},
} {
var ts pgtype.Timestamp
plan := c.PlanScan(nil, pgtype.TimestampOID, pgtype.TextFormatCode, &ts)

err := plan.Scan([]byte(tt.src), &ts)
require.NoError(t, err)
require.True(t, ts.Valid)
require.Equal(t, tt.want, ts.Time)
}
}

// https://github.com/jackc/pgtype/issues/74
func TestTimestampCodecDecodeTextInvalid(t *testing.T) {
c := &pgtype.TimestampCodec{}
var ts pgtype.Timestamp
plan := c.PlanScan(nil, pgtype.TimestampOID, pgtype.TextFormatCode, &ts)
err := plan.Scan([]byte(`eeeee`), &ts)
require.Error(t, err)

for _, src := range []string{
`eeeee`,
`0000-01-01 00:00:00`,
`10000-02-30 00:00:00`,
`10001-02-29 00:00:00`,
`294277-01-01 00:00:00`,
`294276-12-31 23:59:59.999999500`,
`4714-01-01 00:00:00 BC`,
`4714-11-23 23:59:59.999999499 BC`,
`4712-02-29 00:00:00 BC`,
`10000-01-01 00:00:00 BC`,
`9223372036854775808-01-01 00:00:00`,
} {
var ts pgtype.Timestamp
plan := c.PlanScan(nil, pgtype.TimestampOID, pgtype.TextFormatCode, &ts)
err := plan.Scan([]byte(src), &ts)
require.Error(t, err)
}
}

func TestTimestampMarshalJSON(t *testing.T) {
Expand Down
13 changes: 9 additions & 4 deletions pgtype/timestamptz.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,19 @@ func (plan *scanPlanTextTimestamptzToTimestamptzScanner) Scan(src []byte, dst an
format = pgTimestamptzHourFormat
}

tim, err := time.Parse(format, sbuf)
maxYear := int64(maxTimestampYear)
if bc {
maxYear = maxTimestampBCYear
} else {
maxYear++
}
tim, err := parseTimestampWithVariableYear(format, sbuf, true, maxYear, bc)
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())
if timestampOutOfRange(tim) {
return fmt.Errorf("timestamp out of range")
}

if plan.location != nil {
Expand Down
82 changes: 78 additions & 4 deletions pgtype/timestamptz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,87 @@ func TestTimestamptzTranscodeBigTimeBinary(t *testing.T) {
})
}

func TestTimestamptzCodecDecodeTextBigTime(t *testing.T) {
c := &pgtype.TimestamptzCodec{ScanLocation: time.UTC}

for _, tt := range []struct {
src string
want time.Time
}{
{src: `10000-01-02 03:04:05.123456+00`, want: time.Date(10000, 1, 2, 3, 4, 5, 123456000, time.UTC)},
{src: `00000000000010000-01-02 03:04:05.123456+00`, want: time.Date(10000, 1, 2, 3, 4, 5, 123456000, time.UTC)},
{src: `294276-12-31 23:59:59.999999+00`, want: time.Date(294276, 12, 31, 23, 59, 59, 999999000, time.UTC)},
{src: `294276-12-31 23:59:59.999999499+00`, want: time.Date(294276, 12, 31, 23, 59, 59, 999999499, time.UTC)},
{src: `294276-12-31 23:59:59.999999+14`, want: time.Date(294276, 12, 31, 9, 59, 59, 999999000, time.UTC)},
{src: `294277-01-01 00:00:00+14`, want: time.Date(294276, 12, 31, 10, 0, 0, 0, time.UTC)},
{src: `294277-01-01 15:58:59.999999+15:59`, want: time.Date(294276, 12, 31, 23, 59, 59, 999999000, time.UTC)},
{src: `4713-02-29 00:00:00+00 BC`, want: time.Date(-4712, 2, 29, 0, 0, 0, 0, time.UTC)},
{src: `4714-11-24 00:00:00+00 BC`, want: time.Date(-4713, 11, 24, 0, 0, 0, 0, time.UTC)},
{src: `4714-11-23 10:00:00-14 BC`, want: time.Date(-4713, 11, 24, 0, 0, 0, 0, time.UTC)},
{src: `4714-11-23 09:59:59.999999500-14 BC`, want: time.Date(-4713, 11, 23, 23, 59, 59, 999999500, time.UTC)},
} {
var tstz pgtype.Timestamptz
plan := c.PlanScan(nil, pgtype.TimestamptzOID, pgtype.TextFormatCode, &tstz)

err := plan.Scan([]byte(tt.src), &tstz)
require.NoError(t, err)
require.True(t, tstz.Valid)
require.Equal(t, tt.want, tstz.Time)
}
}

func TestTimestamptzCodecDecodeTextBigTimePreservesOffset(t *testing.T) {
c := &pgtype.TimestamptzCodec{}

for _, tt := range []struct {
src string
want time.Time
}{
{src: `10000-01-02 03:04:05.123456+09`, want: time.Date(10000, 1, 2, 3, 4, 5, 123456000, time.FixedZone("", 9*60*60))},
} {
var tstz pgtype.Timestamptz
plan := c.PlanScan(nil, pgtype.TimestamptzOID, pgtype.TextFormatCode, &tstz)

err := plan.Scan([]byte(tt.src), &tstz)
require.NoError(t, err)
require.True(t, tstz.Valid)

_, offset := tstz.Time.Zone()
require.Equal(t, 9*60*60, offset)
require.Equal(t, tt.want, tstz.Time)
}
}

// https://github.com/jackc/pgtype/issues/74
func TestTimestamptzDecodeTextInvalid(t *testing.T) {
c := &pgtype.TimestamptzCodec{}
var tstz pgtype.Timestamptz
plan := c.PlanScan(nil, pgtype.TimestamptzOID, pgtype.TextFormatCode, &tstz)
err := plan.Scan([]byte(`eeeee`), &tstz)
require.Error(t, err)

for _, src := range []string{
`eeeee`,
`0000-01-01 00:00:00+00`,
`10000-02-30 00:00:00+00`,
`10001-02-29 00:00:00+00`,
`2024-01-01 00:00:00+16`,
`2024-01-01 00:00:00-16`,
`294277-01-01 00:00:00+00`,
`294277-01-01 15:59:00+15:59`,
`294276-12-31 23:59:59.999999500+00`,
`4714-01-01 00:00:00+00 BC`,
`4714-11-23 23:59:59.999999499+00 BC`,
`4714-11-23 09:59:59.999999499-14 BC`,
`4714-11-24 00:00:00+14 BC`,
`4712-02-29 00:00:00+00 BC`,
`10000-01-01 00:00:00+00 BC`,
`9223372036854775808-01-01 00:00:00+00`,
`10000-01-02 03:04:05.123456+16`,
`10000-01-02 03:04:05.123456-16`,
`294276-12-31 23:59:59.999999-14`,
} {
var tstz pgtype.Timestamptz
plan := c.PlanScan(nil, pgtype.TimestamptzOID, pgtype.TextFormatCode, &tstz)
err := plan.Scan([]byte(src), &tstz)
require.Error(t, err)
}
}

func TestTimestamptzMarshalJSON(t *testing.T) {
Expand Down