Skip to content

Commit 6ed36b1

Browse files
committed
Fix stdlib to return time.Time for PostgreSQL time columns
The `TimeOID` case was missing from the stdlib `Rows.Next()` switch, causing `time` columns to fall through to the `default` case. The `default` case scans into a `string`, which is not a valid `driver.Value` for temporal types. When `database/sql` attempts to assign the resulting `string` to a `*time.Time` scan destination, it fails with: ``` sql: Scan error on column index 0, name "t": unsupported Scan, storing driver.Value type string into type *time.Time ``` To fix this, a `TimeOID` case is added that scans into `pgtype.Time` and converts microseconds since midnight to `time.Time`, matching the existing conversion in `timeWrapper.ScanTime` (`pgtype/builtin_wrappers.go`).
1 parent 96b4dbd commit 6ed36b1

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

stdlib/sql.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ func (r *Rows) ColumnTypeScanType(index int) reflect.Type {
712712
return reflect.TypeFor[bool]()
713713
case pgtype.NumericOID:
714714
return reflect.TypeFor[float64]()
715-
case pgtype.DateOID, pgtype.TimestampOID, pgtype.TimestamptzOID:
715+
case pgtype.DateOID, pgtype.TimeOID, pgtype.TimestampOID, pgtype.TimestamptzOID:
716716
return reflect.TypeFor[time.Time]()
717717
case pgtype.ByteaOID:
718718
return reflect.TypeFor[[]byte]()
@@ -817,6 +817,38 @@ func (r *Rows) Next(dest []driver.Value) error {
817817
}
818818
return d, nil
819819
}
820+
case pgtype.TimeOID:
821+
var d pgtype.Time
822+
scanPlan := m.PlanScan(dataTypeOID, format, &d)
823+
r.valueFuncs[i] = func(src []byte) (driver.Value, error) {
824+
err := scanPlan.Scan(src, &d)
825+
if err != nil {
826+
return nil, err
827+
}
828+
if !d.Valid {
829+
return nil, nil
830+
}
831+
832+
// The microseconds-to-time.Time conversion here is duplicated from
833+
// timeWrapper.ScanTime in pgtype/builtin_wrappers.go. timeWrapper is
834+
// unexported, so we inline the conversion.
835+
836+
var maxRepresentableByTime int64 = 24*60*60*1000000 - 1
837+
if d.Microseconds > maxRepresentableByTime {
838+
return nil, fmt.Errorf("%d microseconds cannot be represented as time.Time", d.Microseconds)
839+
}
840+
841+
usec := d.Microseconds
842+
hours := usec / (60 * 60 * 1000000)
843+
usec -= hours * (60 * 60 * 1000000)
844+
minutes := usec / (60 * 1000000)
845+
usec -= minutes * (60 * 1000000)
846+
seconds := usec / 1000000
847+
usec -= seconds * 1000000
848+
ns := usec * 1000
849+
850+
return time.Date(2000, 1, 1, int(hours), int(minutes), int(seconds), int(ns), time.UTC), nil
851+
}
820852
case pgtype.TimestampOID:
821853
var d pgtype.Timestamp
822854
scanPlan := m.PlanScan(dataTypeOID, format, &d)

stdlib/sql_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,17 @@ func TestConnQueryRowUnknownType(t *testing.T) {
567567
})
568568
}
569569

570+
// https://github.com/jackc/pgx/issues/2508
571+
func TestConnQueryRowTimeScanIntoTimeTime(t *testing.T) {
572+
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
573+
var actual time.Time
574+
err := db.QueryRow("select '22:45:00'::time").Scan(&actual)
575+
require.NoError(t, err)
576+
expected := time.Date(2000, 1, 1, 22, 45, 0, 0, time.UTC)
577+
require.Equal(t, expected, actual)
578+
})
579+
}
580+
570581
func TestConnQueryJSONIntoByteSlice(t *testing.T) {
571582
testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
572583
_, err := db.Exec(`

0 commit comments

Comments
 (0)