Generate types for SQL constraint errors
Most of my Postgres handlers have an error handling block that converts Postgres constraint errors to domain error types. It looks something like this:
if e, ok := err.(*pq.Error); ok {
switch e.Constraint {
case "invite_codes_user_id_fkey":
return api.ErrUserNotFound
case "invite_codes_user_id_key":
return api.ErrInvitedAlready
}
return err
} else if err == sql.ErrNoRows {
return api.ErrInviteInvalid
} else if err != nil {
return err
}
If I alter my schema (for instance rename the user_id column) I need to remember to change the error handling code to match.
It would be convenient if sqlc generated error types for common constraint errors and returned those generated error types instead of the raw *pq.Error. That way I'd get a type error if the constraint no longer exists.
switch err.(type) {
case dbgen.ErrInviteCodesUserIDFKey:
return api.ErrUserNotFound
case dbgen.ErrInviteCodesUserIDKey:
return api.ErrInvitedAlready
case sql.ErrNoRows:
return api.ErrInviteInvalid
default:
return err
}
This feature was actually something I proposed long ago in #130. The reason I didn't build it out was that no one at the time wanted it. Before implementing this, I think we'd need to take another pass at the proposed code. I don't think we want to change the type of error that is returned from the methods. While Go 1.13 introduced new APIs around error handling, changing the return types of errors would be a large breaking change.
Instead, what if we generated methods like os.IsNotExist? Your example would then look like:
switch {
case dbgen.IsInviteCodesUserIDFKeyErr(err):
return api.ErrUserNotFound
case dbgen.IsInviteCodesUserIDKeyErr(err):
return api.ErrInvitedAlready
case err == sql.ErrNoRows:
return api.ErrInviteInvalid
default:
return err
}
Your proposal looks good to me!
I like this better than the approach suggested in #130. It would work well with other error-handing code that uses errors.Is, and still provide type safety.
I've run into this enough that I'd really like this. We have lots of code that looks like this and is very fragile because of it:
switch {
case dberrs.IsPostgresConstraint(err, "ssh_user_cert_ssh_ca_id_fkey"):
return nil, someSpecialUserFacingError()
case err != nil:
return nil, err
}
If this feature were available I'd convert all constraints to use type safe equivalents instead and eliminate a whole class of bugs immediately.
I'd like to propose a slightly more general purpose API that is similar to the one we use at ngrok which looks like:
Example Usage:
switch {
// this can be sqlc.IsConstraintError in a future world with a general purpose sqlc package
case dbgren.IsConstraintError(err, dbgen.InviteCodesUserIDFkeyConstraint):
return nil, someSpecialUserFacingError()
case err != nil:
return nil, err
}
The generated code would look like the following:
type Constraint string
const (
InviteCodesUserIDConstraint Constraint = "invite_codes_user_id_fkey"
)
func IsConstraint(err error, constraints ...Constraint) bool {
}
Note the variadic argument which allows you to check against multiple constraints in a single call. Thoughts?
I personally like the dbgen.IsInviteCodesUserIDFKeyErr approach better than dbgen.IsConstraintError(err, dbgen.InviteCodesUserIDFkeyConstraint) since it's less typing for the typical case.
If you wanted to match multiple constraints with that technique you could use a case statement with multiple predicates:
switch {
case dbgen.IsInviteCodesUserIDFKeyErr(err), dbgen.IsInviteCodesUserIDKeyErr(err):
return api.ErrUserNotFound
default:
return err
}
Any news?
I would love to have such feature
pinging... a really good feature