diff --git a/pgtype/pgtype.go b/pgtype/pgtype.go index 536a5fd8..6b96eff6 100644 --- a/pgtype/pgtype.go +++ b/pgtype/pgtype.go @@ -558,6 +558,23 @@ type scanPlanFail struct { } func (plan *scanPlanFail) Scan(src []byte, dst any) error { + // If src is NULL it might be possible to scan into dst even though it is the types are not compatible. While this + // may seem to be a contrived case it can occur when selecting NULL directly. PostgreSQL assigns it the type of text. + // It would be surprising to the caller to have to cast the NULL (e.g. `select null::int`). So try to figure out a + // compatible data type for dst and scan with that. + // + // See https://github.com/jackc/pgx/issues/1326 + if src == nil { + // As a horrible hack try all types to find anything that can scan into dst. + for oid := range plan.m.oidToType { + // using planScan instead of Scan or PlanScan to avoid polluting the planned scan cache. + plan := plan.m.planScan(oid, plan.formatCode, dst) + if _, ok := plan.(*scanPlanFail); !ok { + return plan.Scan(src, dst) + } + } + } + var format string switch plan.formatCode { case TextFormatCode: diff --git a/pgtype/pgtype_test.go b/pgtype/pgtype_test.go index 721872d6..1670db2c 100644 --- a/pgtype/pgtype_test.go +++ b/pgtype/pgtype_test.go @@ -329,6 +329,21 @@ func TestMapScanPointerToRenamedType(t *testing.T) { assert.Equal(t, "foo", string(*rs)) } +// https://github.com/jackc/pgx/issues/1326 +func TestMapScanNullToWrongType(t *testing.T) { + m := pgtype.NewMap() + + var n *int32 + err := m.Scan(pgtype.TextOID, pgx.TextFormatCode, nil, &n) + assert.NoError(t, err) + assert.Nil(t, n) + + var pn pgtype.Int4 + err = m.Scan(pgtype.TextOID, pgx.TextFormatCode, nil, &pn) + assert.NoError(t, err) + assert.False(t, pn.Valid) +} + func BenchmarkMapScanInt4IntoBinaryDecoder(b *testing.B) { m := pgtype.NewMap() src := []byte{0, 0, 0, 42}