libpq: SQLSTATE-based error matching for query failures#7
Merged
adunstan merged 1 commit intoJun 17, 2026
Merged
Conversation
Extract the SQLSTATE from a failed result (PQresultErrorField, which bindings.py already declares) onto ResultData.sqlstate, carry it on QueryError, and add named QueryError subclasses (QueryCanceled, UniqueViolation, DeadlockDetected, ...) so a test can write `with pytest.raises(QueryCanceled):` instead of catching the generic QueryError and string-matching its message. query_safe raises the SQLSTATE-specific subclass via query_error_for(); every subclass remains catchable as QueryError / LibpqError, and an unmapped SQLSTATE still raises a plain QueryError.
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is one of a small set of capability differences my pytest port [1] has that pytap/v3 lacks; opening them as separate PRs so each is reviewable on its own and you can pick what's worth taking. No obligation — purely to make the deltas visible.
What this adds
Your
QueryError(andResultData) currently carry only the error message text, so a test can't assert on a specific error condition except by string-matching the (locale-dependent) message. This adds:PQresultErrorField(already declared in yourbindings.py, just unused) onto a newResultData.sqlstate, carried onQueryError.sqlstate/.sqlstate_class.QueryErrorsubclasses for the SQLSTATEs tests most often assert on —QueryCanceled,UniqueViolation,DeadlockDetected,SerializationFailure, etc. — dispatched byquery_error_for()inquery_safe.So a test reads:
instead of
pytest.raises(QueryError)followed by a message regex. Every subclass is still catchable asQueryError/LibpqError, and an unmapped SQLSTATE still raises a plainQueryError, so nothing existing breaks.Motivation / provenance
This revives the per-error-code idea from an earlier revision of Jelte's patchset (which you noted on -hackers had been dropped). SQLSTATE matching is the stable, locale-independent contract — message text changes with translations and across major versions, SQLSTATEs don't — so it's the right basis for negative-path assertions, of which the suite has many (constraint violations, lock timeouts, recovery-conflict cancellations).
The diff is deliberately small and in your idiom: +1 field on
ResultData, the subclasses + a dispatch table inerrors.py, and one call-site change inquery_safe.query_onevalis left raising plainQueryErrorsince it reads only the connection-level message (no per-result SQLSTATE); easy to extend later if wanted.[1] gburd/postgres#24