proposal: database/sql: Support for generics with NullTypes Interface
To support handling of generics, add an interface that includes all the null types
type NullTypes interface { NullString | NullInt64 | NullTime | NullBool ... }
what does the interface enable? why can't you define the interface in the code that's using it?
This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — rsc for the proposal review group
It's still unclear what this would enable us to do.
The only reason for adding this is because the sql package defines all the null types. As it's generic code in the software using the SQL package, it would be better if the interface is defined in the SQL package because if the SQL package adds another type in the future then the generic will not work unless the interface defined in the software using the SQL package updates their interface.
So if someone doesn't want to support all the types in their generic code then they define their own interface. But if, as I have, the written code works with all types then it would be nice, if a future release of the sql package added the new type to a published interface instead of the current risk that something breaks.
@timdadd I don't understand how you would use the interface. Do you have concrete code you can share that would make use of this interface?
@rsc I have to unmarshall json messages that have an unknown structure but follow the sql.null format because they were marshalled from SQL structs. Being SQL null they are of the format xxx/valid. I want to save writing code for each SQL Null type, so instead I build the functions using a single generic function.
Here is my generic function that builds 3 real functions
// interfaceToType takes a json Interface and unmarshalls to the SQL type given
func interfaceToType[V sql.NullString | sql.NullInt64 | sql.NullTime](m interface{}, t V) (res V, err error) {
var buf []byte
if buf, err = json.Marshal(m); err != nil {
return res, logErrF("couldn't marshall the map %w:%v", m, err)
}
logf("interfaceToType", string(buf))
if err = json.Unmarshal(buf, &res); err != nil {
return res, logErrF("couldn't unmarshall the map %s:%v", buf, err)
}
logf("interfaceToType", res)
return
}
The calling code is like this:
if an.DisplayName, err = interfaceToType(attrNameMap["display_name"], an.DisplayName); err != nil {
return
}
If an interface exists then I can use instead of [] and then if you add a new type then my code just needs recompilation.
Here's a bit of JSON I have to unmarshall, but I can't use a struct because the JSON structure is inconsistent:
"attr_type": "T",
"attr_type_format_id": {
"Int64": 0,
"Valid": false
},
"format_value": "",
"import": false,
"optional": false,
"normalise": true,
"display_name": {
"String": "",
"Valid": true
},
Of course you might have a much better way of doing all this, if so then please let me know.
I don't see anything in interfaceToType that is specific to SQL. It seems like a shorter function would be:
func unmarshal[T any](m interface{}) (res T, err error) {
var buf []byte
if buf, err = json.Marshal(m); err != nil {
return res, logErrF("couldn't marshall the map %w:%v", m, err)
}
logf("interfaceToType", string(buf))
if err = json.Unmarshal(buf, &res); err != nil {
return res, logErrF("couldn't unmarshall the map %s:%v", buf, err)
}
logf("interfaceToType", res)
return
}
and then used as
if an.DisplayName, err = unmarshal[sql.NullString](attrNameMap["display_name"]); err != nil {
return
}
This wouldn't need the null interface constraint at all.
Based on the discussion above, this proposal seems like a likely decline. — rsc for the proposal review group
No change in consensus, so declined. — rsc for the proposal review group