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

Cannot detect unique constraint violation in a backend-agnostic way #360

Closed
jooert opened this Issue Jun 23, 2016 · 7 comments

Comments

Projects
None yet
3 participants
@jooert

jooert commented Jun 23, 2016

When trying to insert a new row with an already existing primary key, Diesel returns the generic DatabaseError(String). Therefore it is impossible to tell this case apart from any other database error that might occur without parsing the (backend-specific) error message.

(This came up while implementing ruma/ruma#77.)

@sgrif

This comment has been minimized.

Member

sgrif commented Jun 27, 2016

Agreed that we should include more information about the error when possible. I need to think through the API though, as I'm not sure I want to make QueryResult generic over the backend.

@sgrif

This comment has been minimized.

Member

sgrif commented Jun 27, 2016

I guess rather than try to implement an exhaustive enum that is backend specific, a more useful API would be to identify the "common" errors that are likely to be handled programmatically, and abstract over them.

sgrif added a commit that referenced this issue Jun 27, 2016

Don't leak PGResult pointers when queries failed
Noticed this memory leak while exploring #360. We're never freeing the
result in the error case. Ideally I'd like to have a test for this, but
I'm not entirely sure if we actually can.

@sgrif sgrif added this to the 0.7 milestone Jun 27, 2016

@jooert

This comment has been minimized.

jooert commented Jun 27, 2016

Sounds great!

sgrif added a commit that referenced this issue Jun 27, 2016

Provide richer information from `DatabaseError`
This is part of the restructuring required to give a reasonable solution
to #360. In the case of PG, we hold onto the result pointer, and expose
all of the most commonly relevant fields that can be returned from
`PQresultErrorField`. Of the fields we've chosen to expose, only the
primary message is guaranteed to be present.

As per libpq's documentation, no assumptions can be made about when any
other fields will or will not be present, so they unconditionally return
an option, instead of exposing a separate API for different error types
where we expect certain fields to always be present.

This works out with SQLite pretty well as well, since SQLite doesn't
actually give any additional information beyond the error code and
"extended error code". I think it might be possible for us to improve
the error message by using `sqlite3_errmsg` instead of `sqlite3_errstr`,
but that needs further exploration. Either way, short of parsing the
error message, we won't be able to populate any of the other fields so
we continue to return a string.

This restructuring has the side effect of encoding the fix for the leak
resolved by #361 in the type system, which should help to ensure that
it causes no further problems in the future.
@sgrif

This comment has been minimized.

Member

sgrif commented Jun 27, 2016

Anyone have opinions on

enum Error {
    DatabaseError(Box<DatabaseErrorInformation>),
    UniqueConstraintViolation(Box<DatabaseErrorInformation>),
    ForeignKeyViolation(Box<DatabaseErrorInformation>),
    InvalidTransactionState(Box<DatabaseErrorInformation>),
    // Rest of error variants, which is not related to the database specifically
}

vs.

enum Error {
    DatabaseError(DatabaseErrorKind, Box<DatabaseErrorInformation>),
    // Rest of error variants, which is not related to the database specifically
}

enum DatabaseErrorKind {
    UniqueConstraintViolation,
    ForeignKeyViolation,
    InvalidTransactionState,
    #[doc(hidden)]
    __Unknown, // Match against _ instead, more variants may be added in the future
}

vs.

enum Error {
    DatabaseError(DatabaseErrorKind, Box<DatabaseErrorInformation>),
    // Rest of error variants, which is not related to the database specifically
}

enum DatabaseErrorKind {
    ConstraintViolation(ConstraintKind),
    InvalidTransactionState,
    #[doc(hidden)]
    __Unknown, // Match against _ instead, more variants may be added in the future
}

enum ConstraintKind {
    Integrity,
    Restrict,
    NotNull,
    ForeignKey,
    Unique,
    Check,
    Exclusion,
}

So the difference between matching: UniqueConstraintViolation(error), DatabaseError(UniqueConstraintViolation, error), or DatabaseError(ConstraintViolation(Unique), error). The first one is definitely more concise, but the second one feels better structured internally. The third one is quite verbose (especially when you include the additional imports), but it is the most flexible in that it would allow users to for example handle any constraint violation without caring what kind.

/cc @diesel-rs/contributors

@killercup

This comment has been minimized.

Member

killercup commented Jun 27, 2016

I would probably go with the third as it's the most specific representation: You can easily match against a very specific error as well as an error class if you know it's a common one that you want to deal with explicitly.

Let me fiddle a bit:

fn save_user(&self, &new_user) {
    let saving = insert(&new_user).into(users::table).execute(self.connection);

    if let Err(Error::DatabaseError(DatabaseErrorKind::ConstraintViolation(ConstraintKind::Unique))) = saving {
        //? Is there a way to easily know which field(s) the constraint failed for?
        let suggestion = format!("{}1", new_user.name);
        return self.abort_with(MyError::NameTaken(suggestion));
    } else if let Err(Error::DeserializationError(_)) = saving {
        return self.abort_with(MyError::ShitsOnFire);
    }

    let result = try(saving); // No custom message for the rest
    ...
}

This looks a bit verbose, but is really specific. Maybe you could implement some convenience methods on Error?

(Back in the old days, I once parsed the string representation of a MongoDB error because the JS driver only gave me access to that. Not that great of an afternoon.)

@sgrif

This comment has been minimized.

Member

sgrif commented Jun 27, 2016

I'm starting to lean more towards 2 actually. This isn't meant to be an exhaustive list of all the possible errors, but to aid the common cases that are recovered from programatically. If we need exhaustiveness, I would probably want to expose a method with the raw error code instead. I do like separating the kind out into its own enum though to make it easy to handle "the query failed"

@sgrif

This comment has been minimized.

Member

sgrif commented Jun 27, 2016

    //? Is there a way to easily know which field(s) the constraint failed for?

You can get the table name and the constraint name on PG, but it won't tell you the column separately or anything like that (makes sense, it could be a composite index). On SQLite there is no way to get the specific constraint that failed.

EDIT: Also you can't pattern match on those specifics (other than with a guard). I really wanted to try to make that happen, but there's no way without adding a lifetime to the error enum, since it'd need to be an &'a str, and it'd be ugly to pattern match on anyway since the variant would have to contain the result, etc. Only way around either one is to use a String, and you can't pattern match that.

sgrif added a commit that referenced this issue Jul 2, 2016

Provide the ability to handle unique constraint violations
This lays the groundwork for a more robust error handling story, but to
start we are only handling `UniqueViolation` specifically. SQLite
separates primary key violations from unique constraints, but I've opted
to group them together.

Due to how PG handles errors, we will only know one of the constraints
that was violated (if more than one was violated), and you can only get
the constraint name, not the column name (in the single column case).
SQLite provides none of this information, and the error message would
have to be parsed to determine which constraint failed.

Ultimately I think all of this is fine, as you rarely are expecting more
than exactly one constraint to fail in a given query.

Fixes #360

sgrif added a commit that referenced this issue Jul 2, 2016

Provide the ability to handle unique constraint violations
This lays the groundwork for a more robust error handling story, but to
start we are only handling `UniqueViolation` specifically. SQLite
separates primary key violations from unique constraints, but I've opted
to group them together.

Due to how PG handles errors, we will only know one of the constraints
that was violated (if more than one was violated), and you can only get
the constraint name, not the column name (in the single column case).
SQLite provides none of this information, and the error message would
have to be parsed to determine which constraint failed.

Ultimately I think all of this is fine, as you rarely are expecting more
than exactly one constraint to fail in a given query.

Fixes #360

sgrif added a commit that referenced this issue Jul 2, 2016

Provide the ability to handle unique constraint violations
This lays the groundwork for a more robust error handling story, but to
start we are only handling `UniqueViolation` specifically. SQLite
separates primary key violations from unique constraints, but I've opted
to group them together.

Due to how PG handles errors, we will only know one of the constraints
that was violated (if more than one was violated), and you can only get
the constraint name, not the column name (in the single column case).
SQLite provides none of this information, and the error message would
have to be parsed to determine which constraint failed.

Ultimately I think all of this is fine, as you rarely are expecting more
than exactly one constraint to fail in a given query.

Fixes #360

@sgrif sgrif closed this in #366 Jul 2, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment