-
Notifications
You must be signed in to change notification settings - Fork 18k
database/sql: panic inserting a nil pointer when underlying type implements driver.Valuer #8415
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
Comments
Even attempting to do this would require package reflect in database/sql, which is a step backward. If you want to use *MyType with this package, then put the Valuer method on *MyType. |
Well... Also, here is another related problem I've run into: When I implement So my current workaround is to only use standard types in database code, and avoid any uses of |
Yes, I think @rsc missed something here. The database/sql/driver package already relies on the reflect pkacage. If the value implements
Reopening in case somebody wants to implement this, with a test. |
@ianlancetaylor I think you may have it backwards in your snippet. It seems like you want the code to check if a pointer to the var takes the Value interface, then try to get that pointer. I believe all the other combos of implemented on a pointer or not / value a pointer or not work with the existing code. I got this working, with tests, on my machine: ptrType := reflect.PtrTo(reflect.TypeOf(v))
if ptrType.Implements(reflect.TypeOf((*Valuer)(nil)).Elem()) {
uPtr := unsafe.Pointer(&v)
tPtr := reflect.NewAt(ptrType, uPtr)
return defaultConverter{}.ConvertValue(tPtr.Elem().Interface())
} I had to use unsafe to create the pointer because it wouldn't let let me do something like: ptr, ok := (interface{}(&val)).(Valuer) However, I am a go noobie so I may be missing something. What do you think? |
@ConnorFoody OK, that is a different perspective. You are suggesting that people should always write a pointer method, and the database/sql package should invent a pointer if necessary. |
@ianlancetaylor Thanks for getting back to me! Sorry, I didn't properly read the issue. I agree with what you said earlier. The call on the nil value happens in a loop that builds and returns an array so I think the proper solution is to add the value into the array. I changed the code to check if the value is nil between the cast to a if svi, ok := arg.(driver.Valuer); ok {
svk := reflect.TypeOf(svi).Kind()
canCheckNil := svk == reflect.Interface || svk == reflect.Ptr
if canCheckNil && reflect.ValueOf(arg).IsNil() {
arg = svi // new code
//arg = nil <-- can't do this
} else {
// do current approach
sv, err := svi.Value() For whatever reason it wouldn't take a nil value. It seems like it is indented behaviour for it to not take a nil value, but it sort of seems odd that it would take a typed nil but not a straight up nil. |
@ianlancetaylor Sorry for the bump, would appreciate your help in getting this through. The test looks like this : var ptr *nonPtrImpl
ptr = nil
...
stmt.Exec(ptr, ...) == _, "" // used to panic And the fix (line for context) looks like this: if svi, ok := arg.(driver.Valuer); ok {
svk := reflect.TypeOf(svi).Kind()
canCheckNil := svk == reflect.Interface || svk == reflect.Ptr
if canCheckNil && reflect.ValueOf(arg).IsNil() {
arg = svi // new code
} else {
...
arg = svi.Value() // where panic was happening
}
// convert arg All the tests passed with all.bash. Does this look ok to you? May I submit for review? |
First I should say that I really don't know much about the database/sql package. Your suggested change does not seem quite right to me. Your code seems to go back to the case I was discussing when I reopened the issue above. You don't need to check for whether If the pointer is The only interesting case is a pointer that is |
We ran into the same issue. In function ConvertValue (sql/driver/types.go) could we not implement the same check for nil pointers that is used on standard types (line 238) on types that implement the Valuer interface? Something like:
To be clear, this would always insert NULL for a nil pointer, regardless of how the underlying type's Value method handles nil pointers, in cases where the Value method is defined on a pointer. We could check if the method is implemented on a pointer and then call it, however, I can't imagine any situation where you would not want to insert a dbase NULL for a nil pointer. |
I just want to share a temporary solution to this problem. My database uses a nullable UUID for a column. I'm using github.com/twinj/uuid. I've created a wrapper called NullableUUID and instead of using // NullableUUID wrapper to fix nullable UUID. See https://github.com/golang/go/issues/8415
type NullableUUID uuid.Uuid
// Value implements Sql/Value so it can be converted to DB value
func (u *NullableUUID) Value() (driver.Value, error) {
if u == nil || len(*u) == 0 {
return nil, nil
}
return u.MarshalText()
}
// MarshalText helps convert to value for JSON
func (u *NullableUUID) MarshalText() ([]byte, error) {
if u == nil || len(*u) == 0 {
return nil, nil
}
return uuid.Uuid(*u).MarshalText()
}
// UnmarshalText helps convert to value for JSON
func (u *NullableUUID) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
parsed, err := uuid.Parse(string(data))
if err != nil {
return err
}
*u = NullableUUID(parsed)
return nil
}
type opportunity struct {
ID *int `json:"id,omitempty" db:"OpportunityId,omitempty"`
GlobalOpportunityID *NullableUUID `json:"globalOpportunityId,string,omitempty" db:"GlobalOpportunityId,omitempty"`
GlobalPartnerID *NullableUUID `json:"globalPartnerId,string,omitempty" db:"GlobalPartnerId,omitempty"`
} |
Does @220kts have the right solution? |
The following code would work to fix this issue: https://go-review.googlesource.com/30814 . I'm uncertain if this is the correct fix (feedback welcome). While it will resolve this issue, the issue may also be resolved by documenting that the driver.Valuer interface should not contain nil, but just return a nil value. |
Your fix looks good to me. IMO documenting that the driver.Valuer interface should not contain nil is problematic on the client side. Thanks. |
@220kts Could you explain with some examples why that is problematic for a consumer of the |
We use the Valuer and Scanner interfaces frequently for JSON data from the internet that we insert/retrieve to a json or jsonb column in Postgres but where we want to decode the JSON representation to a Go type first (ie. not use a []byte). We may want to decode the JSON to Go types to do something with the data or just validate that the specific JSON representation sent to the dbase has the properties we need. For any such type, there may be instances where the data is optional (database column accepts NULL) and other instances where the data is required (database column has a not null constraint). In a non-trivial application, the most elegant and robust client implementation using those types is to use pointers for optional data and values for required data, this corresponds logically to the dbase structure and ensures the output to the internet corresponds to the data stored in the dbase. However, doing so requires implementing the Value() method on a value, which currently breaks when the Value() method is called on a nil pointer. If my client has to enforce that the driver.Valuer interface is not nil then the client would have to invent an entirely new scheme to send null out to the internet on output, since I'm now working with not-nil variables where my underlying data in the dbase is actually NULL. The attached is an abbreviated example of a large-ish project around building codes for HVAC systems. We need to have design conditions (climate data) for each home in our dbase but there are optional design conditions for each HVAC system that would locally overwrite the project's design conditions (say you wanted your inlaw apartment heated to 75F instead of 70F for the rest of the house). Let me know if any questions or issues. The issue is exactly as @ianlancetaylor stated previously:
|
The other option for the HVAC example is to do the following:
Do you see a problem with using the above design? Rather than passing in a null *designConditions, you would pass in a designConditions{NULL: true}. I'm not suggesting you alter a schema design to not include nulls, I'm just suggesting there might be a more appropriate way to design the custom Valuer type. Would that work for you? Would it be an issue in your code? |
@kardianos I understand we can implement a workaround like that (for example, the pq NullTime type works in a similar fashion) but it's not ideal: if I add the NULL property you are suggesting then I have to write custom JSON Marshal and Unmarshal functions or my JSON output to the client will be some type of empty JSON object instead of a JavaScript null. This can be done of course but it adds unnecessary code and tends to obfuscate your application's logic, ie. when a piece of data is NULL in the datastore the most logical thing is to have corresponding nil in the application. Perhaps I'm missing something but what is the argument not to make this fix? Thanks |
@kardianos It's also worth noting that the current implementation is different between basic types and types that implement the Valuer interface and inconsistent with existing documentation:
Currently, for a nil pointer on a basic type, ConvertValue returns nil, for a nil pointer on a type that implements the Valuer interface, it calls the Value() method if implemented on a pointer or breaks if the Value() method is implemented on a value. It doesn't seem very elegant to me to change the documentation to state that the client can send nil pointers on basic types but not on types that implement the Valuer interface? On the other hand the code you wrote for the proposed fix brings the behavior inline with what is currently documented. |
A nil valued interface is always problematic and is generally avoided. I also have small concerns of the time cost it will add for this check for an edge case that can be avoided by construction the type more idiomatically. I'm going to remove the DO NOT SUMBIT label and send to @bradfitz . I'm not super keen on it, but it does fit into the general framework |
CL https://golang.org/cl/30814 mentions this issue. |
@220kts
Key change is Value is now a pointer to designConditions, not value, and it checks if m is nil before marshal. This issue should be closed. This isn't an issue with |
I disagree but at the end of the day I can live with it. My main concerns are this:
For what it's worth, the person consuming the database/sql package is likely an average application programmer like myself and they - for better or worse - expect it to "just work". With all due respect, if @bradfitz says "that bug is confusing" you can be sure it is confusing for the average programmer consuming the database/sql package. Our workaround has been to implement the Value() function with a pointer receiver, remove the not NULL constraints on all corresponding columns and (obviously) use pointers for all our client side variables. That's the only way we can be reasonably sure not to cause a conflict at the dbase since our client side applications are of significant enough size that we could never guarantee none of our pointers will be nil. |
Why is column's nullability in the database schema a factor in this discussion? If the column is NOT NULL and you send a NULL on accident, the database will return an error. That's good, no? It helps you find your mistakes. When I say this bug is confusing, it's because it's long, bounces around different topics, and doesn't have a concise summary anywhere. I admit I haven't read the whole thread. @kardianos, care to summarize? Maybe there's something to do here with better error messages at least? |
I don't know how columns nullability got involved, but the reason I followed this discussion is represented in the OP. Given the following code
When uuid.UUID is a Valuer the database/sql codebase tries to call I guess what I'm saying is that the database/sql package should not rely on the result of I understand that someone may want to cover these problems with their codebase, but isn't there a better way to handle that? If I wanted to substitute |
@bradfitz In my experience column's nullability typically determines whether we use a value or a pointer on the application's side. That's not a hard requirement but it makes logical sense and helps to ensure that our application's output corresponds to the data in the datastore. In my opinion the essence of this bug/feature is simply that: it allows the client to use a mix of values and pointers for types that implement the Valuer interface. |
@bradfitz The original code sample submitted gives a good summary. The main issue is consistency of handling builtin types and Valuer types. |
@bradfitz The posters are creating a MyType that implements the driver.Valuer interface. They have an existing practice in their applications of representing nullable columns as *(type) and not null columns as (type). The issue they are running into is that when they have a *MyType driver.Valuer it fails because the implementation Of course the above doesn't mesh with how Go operates, as it makes a situation where the valuer type is of type MyType so the Valuer interface is not nil but points to nothing and the Valuer method cannot be used as it isn't called in this case. I see two solutions that doesn't involve modifying
I understand both of these solutions would alter how you handle NULL values in your existing programs, or at least how you would prefer to handle them. As a personal opinion, I think it is a mistake to construe Go's nil with the database NULL concept. There is a great talk about how the nil value is still useful and is distinct from database NULL you may have seen https://speakerdeck.com/campoy/understanding-nil https://www.youtube.com/watch?v=ynoY2xz-F8s . |
I agree with @a13xb the package converts a nil string pointer to database NULL and I expected it to do the same for non builtins. |
Okay, that's a pretty good argument. @kardianos, how about https://golang.org/cl/31259 ? |
CL https://golang.org/cl/31259 mentions this issue. |
Was this merged in 1.8? I didn't see it in the release notes. |
Yes. If you click on the hash 0ce1d79 above, that page will show, just under the commit message text, the tags that contain this commit. go1.8 is there. The release notes do not list every fixed bug. |
Thank you very much |
Attachments:
The text was updated successfully, but these errors were encountered: