Skip to content

Commit 51773fd

Browse files
authored
Unwrap SQLAlchemy IntegrityErrors (#18891)
* Unwrap SQLAlchemy errors * Add a test * Update tests accordingly
1 parent 4d45513 commit 51773fd

File tree

8 files changed

+59
-14
lines changed

8 files changed

+59
-14
lines changed

tests/unit/forklift/test_legacy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
from unittest import mock
1616

1717
import pretend
18+
import psycopg
1819
import pytest
1920

2021
from pypi_attestations import Attestation, Envelope, VerificationMaterial
2122
from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPTooManyRequests
2223
from sqlalchemy import and_, exists
23-
from sqlalchemy.exc import IntegrityError
2424
from sqlalchemy.orm import joinedload
2525
from trove_classifiers import classifiers
2626
from webob.multidict import MultiDict
@@ -4789,7 +4789,7 @@ def test_all_valid_classifiers_can_be_created(self, db_request):
47894789
@pytest.mark.parametrize("parent_classifier", ["private", "Private", "PrIvAtE"])
47904790
def test_private_classifiers_cannot_be_created(self, db_request, parent_classifier):
47914791
db_request.db.add(Classifier(classifier=f"{parent_classifier} :: Foo"))
4792-
with pytest.raises(IntegrityError):
4792+
with pytest.raises(psycopg.errors.CheckViolation):
47934793
db_request.db.commit()
47944794

47954795
def test_equivalent_version_one_release(self, pyramid_config, db_request):

tests/unit/ip_addresses/test_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# SPDX-License-Identifier: Apache-2.0
22

3+
import psycopg
34
import pytest
45

56
from sqlalchemy import sql
6-
from sqlalchemy.exc import IntegrityError
77

88
from warehouse.ip_addresses.models import BanReason
99

@@ -46,5 +46,5 @@ def test_invalid_transformed(self, db_request):
4646
],
4747
)
4848
def test_ban_data_constraint(self, db_request, kwargs):
49-
with pytest.raises(IntegrityError):
49+
with pytest.raises(psycopg.errors.CheckViolation):
5050
DBIpAddressFactory(**kwargs)

tests/unit/oidc/models/test_github.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# SPDX-License-Identifier: Apache-2.0
22

33
import pretend
4+
import psycopg
45
import pytest
5-
import sqlalchemy
66

77
from tests.common.db.oidc import GitHubPublisherFactory, PendingGitHubPublisherFactory
88
from warehouse.oidc import errors
@@ -676,7 +676,7 @@ def test_github_publisher_duplicates_cant_be_created(self, db_request):
676676
)
677677
db_request.db.add(publisher2)
678678

679-
with pytest.raises(sqlalchemy.exc.IntegrityError):
679+
with pytest.raises(psycopg.errors.UniqueViolation):
680680
db_request.db.commit()
681681

682682
@pytest.mark.parametrize(

tests/unit/oidc/models/test_gitlab.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# SPDX-License-Identifier: Apache-2.0
22

33
import pretend
4+
import psycopg
45
import pytest
5-
import sqlalchemy
66

77
from tests.common.db.oidc import GitLabPublisherFactory, PendingGitLabPublisherFactory
88
from warehouse.oidc import errors
@@ -710,7 +710,7 @@ def test_gitlab_publisher_duplicates_cant_be_created(self, db_request):
710710
)
711711
db_request.db.add(publisher2)
712712

713-
with pytest.raises(sqlalchemy.exc.IntegrityError):
713+
with pytest.raises(psycopg.errors.UniqueViolation):
714714
db_request.db.commit()
715715

716716
@pytest.mark.parametrize(

tests/unit/organizations/test_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import datetime
44

55
import pretend
6+
import psycopg
67
import pytest
78

89
from freezegun import freeze_time
910
from pyramid.authorization import Allow
1011
from pyramid.httpexceptions import HTTPPermanentRedirect
1112
from pyramid.location import lineage
12-
from sqlalchemy.exc import IntegrityError
1313

1414
from warehouse.authnz import Permissions
1515
from warehouse.organizations.models import (
@@ -724,7 +724,7 @@ def test_unique_constraint(self, db_session):
724724
)
725725

726726
# Attempt to create duplicate - should raise IntegrityError
727-
with pytest.raises(IntegrityError):
727+
with pytest.raises(psycopg.errors.UniqueViolation):
728728
DBOrganizationOIDCIssuerFactory.create(
729729
organization=organization,
730730
issuer_type=OIDCIssuerType.GitLab,

tests/unit/test_db.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import zope.sqlalchemy
1212

1313
from sqlalchemy import event
14-
from sqlalchemy.exc import OperationalError
14+
from sqlalchemy.exc import DBAPIError, OperationalError
1515

1616
from warehouse import db
1717
from warehouse.admin.flags import AdminFlag, AdminFlagValue
@@ -22,6 +22,7 @@
2222
_configure_alembic,
2323
_create_session,
2424
includeme,
25+
unwrap_dbapi_exceptions,
2526
)
2627

2728

@@ -241,3 +242,33 @@ class FakeRegistry(dict):
241242
)
242243
]
243244
assert config.registry["sqlalchemy.engine"] is engine
245+
246+
247+
def test_unwrap_dbapi_exceptions():
248+
original_exception = psycopg.OperationalError()
249+
sqlalchemy_exception = DBAPIError("foo", {}, original_exception)
250+
context = pretend.stub(
251+
sqlalchemy_exception=sqlalchemy_exception,
252+
original_exception=original_exception,
253+
)
254+
255+
with pytest.raises(psycopg.OperationalError) as e:
256+
unwrap_dbapi_exceptions(context)
257+
258+
assert e.value is original_exception
259+
260+
261+
def test_unwrap_dbapi_exceptions_no_op():
262+
# Not a DBAPIError
263+
context = pretend.stub(
264+
sqlalchemy_exception=OperationalError("foo", {}, None),
265+
original_exception=None,
266+
)
267+
unwrap_dbapi_exceptions(context)
268+
269+
# No original exception
270+
context = pretend.stub(
271+
sqlalchemy_exception=DBAPIError("foo", {}, None),
272+
original_exception=None,
273+
)
274+
unwrap_dbapi_exceptions(context)

warehouse/db.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pyramid.renderers import JSON
1717
from sqlalchemy import event, func, inspect
1818
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
19-
from sqlalchemy.exc import IntegrityError, OperationalError
19+
from sqlalchemy.exc import DBAPIError, IntegrityError, OperationalError
2020
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
2121

2222
from warehouse.metrics import IMetricsService
@@ -166,6 +166,19 @@ def cleanup(request):
166166
return session
167167

168168

169+
@event.listens_for(sqlalchemy.engine.Engine, "handle_error")
170+
def unwrap_dbapi_exceptions(context):
171+
"""
172+
Listens for SQLAlchemy errors and raises the original
173+
DBAPI (e.g., psycopg) exception instead.
174+
"""
175+
if (
176+
isinstance(context.sqlalchemy_exception, DBAPIError)
177+
and context.original_exception
178+
):
179+
raise context.original_exception from context.sqlalchemy_exception
180+
181+
169182
def includeme(config):
170183
# Add a directive to get an alembic configuration.
171184
config.add_directive("alembic_config", _configure_alembic)

warehouse/organizations/services.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import datetime
44

5+
from psycopg.errors import UniqueViolation
56
from sqlalchemy import delete, func, orm, select
6-
from sqlalchemy.exc import IntegrityError, NoResultFound
7+
from sqlalchemy.exc import NoResultFound
78
from zope.interface import implementer
89

910
from warehouse.accounts.models import TermsOfServiceEngagement, User
@@ -518,7 +519,7 @@ def rename_organization(self, organization_id, name):
518519
try:
519520
self.db.flush() # flush db now so organization.normalized_name available
520521
self.add_catalog_entry(organization_id)
521-
except IntegrityError:
522+
except UniqueViolation:
522523
raise ValueError(f'Organization name "{name}" has been used')
523524

524525
return organization

0 commit comments

Comments
 (0)