Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: database/sql: add a common method to unwrap value of sql.Null* types #63633

Open
dolmen opened this issue Oct 19, 2023 · 7 comments
Open
Labels
Milestone

Comments

@dolmen
Copy link
Contributor

dolmen commented Oct 19, 2023

Proposal

Add an interface to database/sql implemented by sql.Null* types to unwrap the NULL or the value:

// NullUnwrap allows to unwrap sql.Null* types.
type NullUnwrap interface {
	UnwrapNull() (value any, valid bool)
}

(names to be debatted)

Extend sql.Null* types to implement it:

func (n Null[T]) UnwrapNull() (value any, valid bool) {                                                                                                                                                     
	if !n.Valid {
		return nil, false
	}
	return n.V, true
}

Why?

There is currently no builtin generic way to unwrap the value of types sql.Null* (sql.NullBool,sql.NullByte, sql.NullInt16, sql.NullInt32sql.NullInt64, sql.NullFloat64, sql.NullString, sql.NullTime).

But at least the list of types was known, so it was possible to use a type switch.

With the addition of sql.Null[T] (#60370) for Go 1.22 the type switch solution will not be enough.

The only ways to unwrap an sql.Null[T] are just hacks using either the driver.Valuer interface or reflect:

(full example: https://go.dev/play/p/DnStyHLekX1?v=gotip)

func UnwrapNullable(val driver.Valuer) (value any, valid bool) {
	v := reflect.ValueOf(val)
	if v.Field(1).Bool() {
		return v.Field(0).Interface(), true
	}
	return nil, false
}

func UnwrapNullable2(val driver.Valuer) (value any, valid bool) {
	value, err := val.Value()
	if err != nil {
		panic(err)
	}
	if value != nil {
		return value, true
	}
	return nil, false
}

A common method available on sql.Null* types would allow to have an efficient unwrapping and be type safe as unwrapping could be done after an interface check.

That interface could also help to distinguish types that are able to represent a NULL value (types that never return valid == false should not implement it).

@gopherbot gopherbot added this to the Proposal milestone Oct 19, 2023
@seankhliao
Copy link
Member

cc @kardianos @bradfitz

@earthboundkid
Copy link
Contributor

earthboundkid commented Oct 20, 2023

ISTM, it should be called "wrap" not "unwrap" because you are wrapping it in the any type interface.

Also, I think you should return a zero T, not nil interface in the invalid case, so callers can still use a type switch to figure out what type it was supposed to be.

// Wrap the value of n in an interface for use in dynamic type switches.
// Even if n is invalid, it will still return a value that is of concrete type T.
func (n Null[T]) Wrap() (value any, valid bool) {                                                                                                                                                     
	if !n.Valid {
		var zero T
		return zero, false
	}
	return n.V, true
}

@doggedOwl
Copy link

doggedOwl commented Oct 20, 2023

Also, I think you should return a zero T, not nil interface

I agree. In that case wouldn't it more appropriate to for the return type to be (value T, valid bool) instead of (value any, valid bool) ?

@earthboundkid
Copy link
Contributor

earthboundkid commented Oct 20, 2023

I agree. In that case wouldn't it more appropriate to for the return type to be (value T, valid bool) instead of (value any, valid bool) ?

I thought that at first too, but no because you want to be able to do

type NullWrap interface {
	Wrap() (value any, valid bool)
}

if nw, ok := val.(NullWrap); ok {
    rval, ok := nw.Wrap()
    switch rval.(type) {
    // etc.
    }
}

If the interface were generic, you couldn't do a runtime assert of nw, ok := val.(NullWrap); ok.

I don't think NullWrap needs to be defined in the sql package though. Let callers who need it define it on their end.

@dolmen
Copy link
Contributor Author

dolmen commented Oct 22, 2023

I don't think NullWrap needs to be defined in the sql package though. Let callers who need it define it on their end.

If the interface is not declared, it will not be documented. Having documentation for it is part of the proposal.

@dolmen
Copy link
Contributor Author

dolmen commented Oct 22, 2023

ISTM, it should be called "wrap" not "unwrap" because you are wrapping it in the any type interface.

I called it Unwrap because it takes one value as input and returns 2 values: one any and one bool.

I think that the NullWrap name would be unsuited for that reason.

@narqo
Copy link
Contributor

narqo commented Jan 2, 2024

To help understand the motivation behind the proposal, could you give some practical examples of a use-case or a problem, where one wanted to unpack some Null*Like value into a (any, bool) pair?

The current version mentions:

[before Go 1.22] the list of types was known, so it was possible to use a type switch

Note that a user could always define their own nullable types, e.g. NullIPAddr to use as a Scanner/Valuer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Incoming
Development

No branches or pull requests

6 participants