Skip to content

Commit d2acacf

Browse files
committed
Add ScanLocation to pgtype.TimestampCodec
If ScanLocation is set, the timestamps will be assumed to be in the given location when scanning from the database. The Codec interface is now implemented by *pgtype.TimestampCodec instead of pgtype.TimestampCodec. This is technically a breaking change, but it is extremely unlikely that anyone is depending on this, and if there is downstream breakage it is trivial to fix. #1195 #1945
1 parent a22564d commit d2acacf

File tree

3 files changed

+62
-15
lines changed

3 files changed

+62
-15
lines changed

pgtype/pgtype_default.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func initDefaultMap() {
8181
defaultMap.RegisterType(&Type{Name: "text", OID: TextOID, Codec: TextCodec{}})
8282
defaultMap.RegisterType(&Type{Name: "tid", OID: TIDOID, Codec: TIDCodec{}})
8383
defaultMap.RegisterType(&Type{Name: "time", OID: TimeOID, Codec: TimeCodec{}})
84-
defaultMap.RegisterType(&Type{Name: "timestamp", OID: TimestampOID, Codec: TimestampCodec{}})
84+
defaultMap.RegisterType(&Type{Name: "timestamp", OID: TimestampOID, Codec: &TimestampCodec{}})
8585
defaultMap.RegisterType(&Type{Name: "timestamptz", OID: TimestamptzOID, Codec: &TimestamptzCodec{}})
8686
defaultMap.RegisterType(&Type{Name: "unknown", OID: UnknownOID, Codec: TextCodec{}})
8787
defaultMap.RegisterType(&Type{Name: "uuid", OID: UUIDOID, Codec: UUIDCodec{}})

pgtype/timestamp.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (ts *Timestamp) Scan(src any) error {
4646

4747
switch src := src.(type) {
4848
case string:
49-
return scanPlanTextTimestampToTimestampScanner{}.Scan([]byte(src), ts)
49+
return (&scanPlanTextTimestampToTimestampScanner{}).Scan([]byte(src), ts)
5050
case time.Time:
5151
*ts = Timestamp{Time: src, Valid: true}
5252
return nil
@@ -116,17 +116,21 @@ func (ts *Timestamp) UnmarshalJSON(b []byte) error {
116116
return nil
117117
}
118118

119-
type TimestampCodec struct{}
119+
type TimestampCodec struct {
120+
// ScanLocation is the location that the time is assumed to be in for scanning. This is different from
121+
// TimestamptzCodec.ScanLocation in that this setting does change the instant in time that the timestamp represents.
122+
ScanLocation *time.Location
123+
}
120124

121-
func (TimestampCodec) FormatSupported(format int16) bool {
125+
func (*TimestampCodec) FormatSupported(format int16) bool {
122126
return format == TextFormatCode || format == BinaryFormatCode
123127
}
124128

125-
func (TimestampCodec) PreferredFormat() int16 {
129+
func (*TimestampCodec) PreferredFormat() int16 {
126130
return BinaryFormatCode
127131
}
128132

129-
func (TimestampCodec) PlanEncode(m *Map, oid uint32, format int16, value any) EncodePlan {
133+
func (*TimestampCodec) PlanEncode(m *Map, oid uint32, format int16, value any) EncodePlan {
130134
if _, ok := value.(TimestampValuer); !ok {
131135
return nil
132136
}
@@ -220,27 +224,27 @@ func discardTimeZone(t time.Time) time.Time {
220224
return t
221225
}
222226

223-
func (TimestampCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan {
227+
func (c *TimestampCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan {
224228

225229
switch format {
226230
case BinaryFormatCode:
227231
switch target.(type) {
228232
case TimestampScanner:
229-
return scanPlanBinaryTimestampToTimestampScanner{}
233+
return &scanPlanBinaryTimestampToTimestampScanner{location: c.ScanLocation}
230234
}
231235
case TextFormatCode:
232236
switch target.(type) {
233237
case TimestampScanner:
234-
return scanPlanTextTimestampToTimestampScanner{}
238+
return &scanPlanTextTimestampToTimestampScanner{location: c.ScanLocation}
235239
}
236240
}
237241

238242
return nil
239243
}
240244

241-
type scanPlanBinaryTimestampToTimestampScanner struct{}
245+
type scanPlanBinaryTimestampToTimestampScanner struct{ location *time.Location }
242246

243-
func (scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) error {
247+
func (plan *scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) error {
244248
scanner := (dst).(TimestampScanner)
245249

246250
if src == nil {
@@ -264,15 +268,18 @@ func (scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) error
264268
microsecFromUnixEpochToY2K/1000000+microsecSinceY2K/1000000,
265269
(microsecFromUnixEpochToY2K%1000000*1000)+(microsecSinceY2K%1000000*1000),
266270
).UTC()
271+
if plan.location != nil {
272+
tim = time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), plan.location)
273+
}
267274
ts = Timestamp{Time: tim, Valid: true}
268275
}
269276

270277
return scanner.ScanTimestamp(ts)
271278
}
272279

273-
type scanPlanTextTimestampToTimestampScanner struct{}
280+
type scanPlanTextTimestampToTimestampScanner struct{ location *time.Location }
274281

275-
func (scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) error {
282+
func (plan *scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) error {
276283
scanner := (dst).(TimestampScanner)
277284

278285
if src == nil {
@@ -302,13 +309,17 @@ func (scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) error {
302309
tim = time.Date(year, tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), tim.Location())
303310
}
304311

312+
if plan.location != nil {
313+
tim = time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), plan.location)
314+
}
315+
305316
ts = Timestamp{Time: tim, Valid: true}
306317
}
307318

308319
return scanner.ScanTimestamp(ts)
309320
}
310321

311-
func (c TimestampCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
322+
func (c *TimestampCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
312323
if src == nil {
313324
return nil, nil
314325
}
@@ -326,7 +337,7 @@ func (c TimestampCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16,
326337
return ts.Time, nil
327338
}
328339

329-
func (c TimestampCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) {
340+
func (c *TimestampCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) {
330341
if src == nil {
331342
return nil, nil
332343
}

pgtype/timestamp_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,42 @@ func TestTimestampCodec(t *testing.T) {
3838
})
3939
}
4040

41+
func TestTimestampCodecWithScanLocationUTC(t *testing.T) {
42+
skipCockroachDB(t, "Server does not support infinite timestamps (see https://github.com/cockroachdb/cockroach/issues/41564)")
43+
44+
connTestRunner := defaultConnTestRunner
45+
connTestRunner.AfterConnect = func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
46+
conn.TypeMap().RegisterType(&pgtype.Type{
47+
Name: "timestamp",
48+
OID: pgtype.TimestampOID,
49+
Codec: &pgtype.TimestampCodec{ScanLocation: time.UTC},
50+
})
51+
}
52+
53+
pgxtest.RunValueRoundTripTests(context.Background(), t, connTestRunner, nil, "timestamp", []pgxtest.ValueRoundTripTest{
54+
// Have to use pgtype.Timestamp instead of time.Time as source because otherwise the simple and exec query exec
55+
// modes will encode the time for timestamptz. That is, they will convert it from local time zone.
56+
{pgtype.Timestamp{Time: time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local), Valid: true}, new(time.Time), isExpectedEq(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))},
57+
})
58+
}
59+
60+
func TestTimestampCodecWithScanLocationLocal(t *testing.T) {
61+
skipCockroachDB(t, "Server does not support infinite timestamps (see https://github.com/cockroachdb/cockroach/issues/41564)")
62+
63+
connTestRunner := defaultConnTestRunner
64+
connTestRunner.AfterConnect = func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
65+
conn.TypeMap().RegisterType(&pgtype.Type{
66+
Name: "timestamp",
67+
OID: pgtype.TimestampOID,
68+
Codec: &pgtype.TimestampCodec{ScanLocation: time.Local},
69+
})
70+
}
71+
72+
pgxtest.RunValueRoundTripTests(context.Background(), t, connTestRunner, nil, "timestamp", []pgxtest.ValueRoundTripTest{
73+
{time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), new(time.Time), isExpectedEq(time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local))},
74+
})
75+
}
76+
4177
// https://github.com/jackc/pgx/v4/pgtype/pull/128
4278
func TestTimestampTranscodeBigTimeBinary(t *testing.T) {
4379
defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {

0 commit comments

Comments
 (0)