pgx icon indicating copy to clipboard operation
pgx copied to clipboard

cannot scan NULL into *string

Open ork821 opened this issue 2 years ago • 8 comments

I have table with nullable values (email and avatar_url in my case) CREATE TABLE IF NOT EXISTS users ( id int NOT NULL PRIMARY KEY, email varchar(127), avatar_url varchar(255), created_at timestamp default now(), updated_at timestamp default now() );

I am trying to scan SELECT * from users into object like type PostSearchResult struct { Id int json:"id" CreatedAt pgtype.Timestamp json:"created_at" UpdatedAt pgtype.Timestamp json:"updated_at" AvatarUrl string json:"avatar_url" Email string json:"email" } When email or avatarUrl is null, then i got error cannot scan NULL into *string

Can I avoid this somehow? Or this is known bug?

ork821 avatar Jul 02 '23 15:07 ork821

Change AvatarUrl and Email to *string or pgtype.Text.

jackc avatar Jul 03 '23 23:07 jackc

@jackc why ScanType only return reflect.TypeOf("") instead of reflect.TypeOf(pgtype.Text)?

// ColumnTypeScanType returns the value type that can be used to scan types into.
func (r *Rows) ColumnTypeScanType(index int) reflect.Type {
	fd := r.rows.FieldDescriptions()[index]

	switch fd.DataTypeOID {
	case pgtype.Float8OID:
		return reflect.TypeOf(float64(0))
	case pgtype.Float4OID:
		return reflect.TypeOf(float32(0))
	case pgtype.Int8OID:
		return reflect.TypeOf(int64(0))
	case pgtype.Int4OID:
		return reflect.TypeOf(int32(0))
	case pgtype.Int2OID:
		return reflect.TypeOf(int16(0))
	case pgtype.BoolOID:
		return reflect.TypeOf(false)
	case pgtype.NumericOID:
		return reflect.TypeOf(float64(0))
	case pgtype.DateOID, pgtype.TimestampOID, pgtype.TimestamptzOID:
		return reflect.TypeOf(time.Time{})
	case pgtype.ByteaOID:
		return reflect.TypeOf([]byte(nil))
	default:
		return reflect.TypeOf("")
	}
}

quanqqqqq avatar Jul 26 '23 13:07 quanqqqqq

@quanqqqqq The example in the docs seems to show that the base type is used rather than a nullable wrapper: https://pkg.go.dev/database/sql/driver#RowsColumnTypeScanType. Notice that it says bigint should be reflect.TypeOf(int64(0)) and not reflect.TypeOf(sql.NullInt64).

Not really sure what the implications of changing that would be either so it seems safer to follow precedent.

jackc avatar Jul 26 '23 13:07 jackc

If you do not want to use a pointer type in your struct, you can use the coalesce function in your SQL select to set a default value if the column returns NULL https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-COALESCE-NVL-IFNULL IE:

select COALESCE(a_nullable_string_col, '') from table

ibejohn818 avatar Apr 15 '24 01:04 ibejohn818

@jackc While you have pointed out the correct resolution for the error, the error message being produced is moderately misleading:

cannot scan NULL into *string

In this case, the destination field on the struct has type string, but the error is describing it as *string, which makes it seem like the code would already be doing the correct thing.

As a test, I rigged up a small case to show that we're getting pointers to the destination type, rather than the type itself:

type testStruct struct {
	Test *int `db:"test"`
}
func getInvalid(ctx context.Context) (testStruct, error) {
	conn, err := pgx.Connect(ctx, connectionString)
	if err != nil {
		return testStruct{}, err
	}
	rows, err := conn.Query(ctx, "SELECT 'not an int' AS test")
	if err != nil {
		return testStruct{}, err
	}
	return pgx.CollectOneRow(rows, pgx.RowToStructByName[testStruct])
}


func Test_getInvalid(t *testing.T) {
	_, err := getInvalid(context.Background())
	assert.Equal(t,
		"can't scan into dest[0]: cannot scan text (OID 25) in text format into *int",
		err.Error())
})

// Expected: can't scan into dest[0]: cannot scan text (OID 25) in text format into *int
// Actual:   can't scan into dest[0]: cannot scan text (OID 25) in text format into **int

I just ran into this myself with the same kind of NULL issue: can't scan into dest[0]: cannot scan NULL into *time.Time I'm so familiar with this particular scenario that I initially missed the discrepancy, and only noticed when a less-familiar coworker was asking about how to resolve it

benjamincudi avatar Jul 23 '24 18:07 benjamincudi

@benjamincudi

The error makes more sense when it comes directly from a Scan call. It's very clear that a pointer is being passed in. But the Collect* and RowToStruct* functions hide the Scan.

I can see how that error can be confusing, but Scan can only return use the type it was given to build the error.

jackc avatar Jul 23 '24 22:07 jackc

Understanding that Scan can only know the type it was given, presumably something up the chain within the boundary of the pgx package is responsible for taking a pointer to the caller-provided field types, since it seems like setupStructScanTargets is the one taking the address: https://github.com/jackc/pgx/blob/a68e14fe5ad7caed8657816b9883ed418f3324ec/rows.go#L853

Given this, it seems like it may be possible to update the two internal call sites at least (positional and named scanners) with something along the lines of:

func (rs *positionalStructRowScanner) ScanRow(rows CollectableRow) error {
	typ := reflect.TypeOf(rs.ptrToStruct).Elem()
	// snipped remainder of method for brevity
	
	err := rows.Scan(scanTargets...)
	var sar ScanArgError
	if errors.As(err, &sar) {
		// This may not be the correct index, but my initial reading makes it seem like it is
		// *baseRows.Scan can likely provide the missing index or name for the error-causing field otherwise
		return fmt.Errorf("source field: %v, type %v: %w",
			typ.Field(sar.ColumnIndex).Name, typ.Field(sar.ColumnIndex).Type, err)
	}
	return err
}

Does this sound correct?

benjamincudi avatar Jul 25 '24 15:07 benjamincudi

I think that is the right place.

jackc avatar Aug 03 '24 00:08 jackc