sqlc icon indicating copy to clipboard operation
sqlc copied to clipboard

Generate types for SQL constraint errors

Open maxhawkins opened this issue 5 years ago • 7 comments

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
}

maxhawkins avatar Oct 16 '20 19:10 maxhawkins

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
}

kyleconroy avatar Oct 19 '20 06:10 kyleconroy

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.

maxhawkins avatar Oct 19 '20 15:10 maxhawkins

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?

inconshreveable avatar Mar 09 '21 07:03 inconshreveable

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
}

maxhawkins avatar Mar 10 '21 17:03 maxhawkins

Any news?

lyubchev avatar Apr 24 '21 12:04 lyubchev

I would love to have such feature

aliml92 avatar Mar 17 '23 09:03 aliml92

pinging... a really good feature

efiShtain avatar Aug 24 '24 05:08 efiShtain