diff --git a/stdlib/sql.go b/stdlib/sql.go index a37d58c41..5cdd04b9d 100644 --- a/stdlib/sql.go +++ b/stdlib/sql.go @@ -712,7 +712,7 @@ func (r *Rows) ColumnTypeScanType(index int) reflect.Type { return reflect.TypeFor[bool]() case pgtype.NumericOID: return reflect.TypeFor[float64]() - case pgtype.DateOID, pgtype.TimestampOID, pgtype.TimestamptzOID: + case pgtype.DateOID, pgtype.TimeOID, pgtype.TimestampOID, pgtype.TimestamptzOID: return reflect.TypeFor[time.Time]() case pgtype.ByteaOID: return reflect.TypeFor[[]byte]() @@ -817,6 +817,38 @@ func (r *Rows) Next(dest []driver.Value) error { } return d, nil } + case pgtype.TimeOID: + var d pgtype.Time + scanPlan := m.PlanScan(dataTypeOID, format, &d) + r.valueFuncs[i] = func(src []byte) (driver.Value, error) { + err := scanPlan.Scan(src, &d) + if err != nil { + return nil, err + } + if !d.Valid { + return nil, nil + } + + // The microseconds-to-time.Time conversion here is duplicated from + // timeWrapper.ScanTime in pgtype/builtin_wrappers.go. timeWrapper is + // unexported, so we inline the conversion. + + var maxRepresentableByTime int64 = 24*60*60*1000000 - 1 + if d.Microseconds > maxRepresentableByTime { + return nil, fmt.Errorf("%d microseconds cannot be represented as time.Time", d.Microseconds) + } + + usec := d.Microseconds + hours := usec / (60 * 60 * 1000000) + usec -= hours * (60 * 60 * 1000000) + minutes := usec / (60 * 1000000) + usec -= minutes * (60 * 1000000) + seconds := usec / 1000000 + usec -= seconds * 1000000 + ns := usec * 1000 + + return time.Date(2000, 1, 1, int(hours), int(minutes), int(seconds), int(ns), time.UTC), nil + } case pgtype.TimestampOID: var d pgtype.Timestamp scanPlan := m.PlanScan(dataTypeOID, format, &d) diff --git a/stdlib/sql_test.go b/stdlib/sql_test.go index e8ca78771..8145a3cb4 100644 --- a/stdlib/sql_test.go +++ b/stdlib/sql_test.go @@ -567,6 +567,17 @@ func TestConnQueryRowUnknownType(t *testing.T) { }) } +// https://github.com/jackc/pgx/issues/2508 +func TestConnQueryRowTimeScanIntoTimeTime(t *testing.T) { + testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) { + var actual time.Time + err := db.QueryRow("select '22:45:00'::time").Scan(&actual) + require.NoError(t, err) + expected := time.Date(2000, 1, 1, 22, 45, 0, 0, time.UTC) + require.Equal(t, expected, actual) + }) +} + func TestConnQueryJSONIntoByteSlice(t *testing.T) { testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) { _, err := db.Exec(`