Skip to content

Commit d4b170e

Browse files
committed
Remove PEP-249 exception mirror — use psycopg exceptions directly
Plain mirrored psycopg's entire PEP-249 exception hierarchy and used DatabaseErrorWrapper to catch every psycopg exception and re-raise it as a Plain equivalent. This was a Django pattern for multi-DB abstraction that served no purpose in a PostgreSQL-only framework. Now psycopg exceptions propagate unchanged. User code catches psycopg.IntegrityError, psycopg.errors.UniqueViolation, etc. directly, giving access to richer subclasses and diagnostic fields that the mirror obscured. DatabaseErrorWrapper is simplified to only track connection health (errors_occurred flag) without converting exception types.
1 parent 6acc8c1 commit d4b170e

21 files changed

Lines changed: 77 additions & 151 deletions

File tree

plain-cache/plain/cache/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from functools import cached_property
55
from typing import Any
66

7+
import psycopg
78
from opentelemetry import trace
89
from opentelemetry.semconv.attributes.db_attributes import (
910
DB_NAMESPACE,
@@ -12,7 +13,6 @@
1213
)
1314
from opentelemetry.trace import SpanKind
1415

15-
from plain.postgres import IntegrityError
1616
from plain.utils import timezone
1717

1818
tracer = trace.get_tracer("plain.cache")
@@ -137,7 +137,7 @@ def set(
137137
item, _ = self._model_class.query.update_or_create(
138138
key=self.key, defaults=defaults
139139
)
140-
except IntegrityError:
140+
except psycopg.IntegrityError:
141141
# Most likely a race condition in creating the item,
142142
# so trying again should do an update
143143
item, _ = self._model_class.query.update_or_create(

plain-flags/plain/flags/preflight.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from plain.postgres.db import OperationalError, ProgrammingError
1+
import psycopg
2+
23
from plain.preflight import PreflightCheck, PreflightResult, register_check
34
from plain.runtime import settings
45

@@ -25,7 +26,7 @@ def run(self) -> list[PreflightResult]:
2526

2627
try:
2728
flag_names = set(flag_names)
28-
except (ProgrammingError, OperationalError):
29+
except (psycopg.ProgrammingError, psycopg.OperationalError):
2930
# The table doesn't exist yet
3031
# (migrations probably haven't run yet),
3132
# so we can't check it.

plain-oauth/plain/oauth/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import datetime
44
from typing import TYPE_CHECKING, Any
55

6+
import psycopg
7+
68
from plain import postgres
79
from plain.auth import get_user_model
810
from plain.exceptions import ValidationError
911
from plain.postgres import transaction, types
10-
from plain.postgres.db import IntegrityError
1112
from plain.runtime import SettingsReference
1213
from plain.utils import timezone
1314

@@ -119,7 +120,7 @@ def get_or_create_user(
119120
**oauth_user.user_model_fields,
120121
)
121122
user.save()
122-
except (IntegrityError, ValidationError):
123+
except (psycopg.IntegrityError, ValidationError):
123124
raise OAuthUserAlreadyExistsError(
124125
provider_key=provider_key,
125126
user_model_fields=oauth_user.user_model_fields,

plain-oauth/plain/oauth/preflight.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from plain.postgres.db import OperationalError, ProgrammingError
1+
import psycopg
2+
23
from plain.preflight import PreflightCheck, PreflightResult, register_check
34

45

@@ -18,7 +19,7 @@ def run(self) -> list[PreflightResult]:
1819
keys_in_db = set(
1920
OAuthConnection.query.values_list("provider_key", flat=True).distinct()
2021
)
21-
except (OperationalError, ProgrammingError):
22+
except (psycopg.OperationalError, psycopg.ProgrammingError):
2223
# Check runs on plain migrate, and the table may not exist yet
2324
# or it may not be installed on the particular database intentionally
2425
return errors

plain-postgres/plain/postgres/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,14 +403,14 @@ with transaction.atomic():
403403

404404
### Read-only connections
405405

406-
Enforce read-only mode on the current database connection using `read_only()`. Any write (INSERT, UPDATE, DELETE, DDL) raises a [`ReadOnlyError`](./exceptions.py#ReadOnlyError):
406+
Enforce read-only mode on the current database connection using `read_only()`. Any write (INSERT, UPDATE, DELETE, DDL) raises `psycopg.errors.ReadOnlySqlTransaction`:
407407

408408
```python
409409
from plain.postgres.connections import read_only
410410

411411
with read_only():
412412
users = User.query.all() # reads work
413-
User.query.create(name="x") # raises ReadOnlyError
413+
User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
414414
```
415415

416416
This works with both autocommit queries and explicit `atomic()` blocks.

plain-postgres/plain/postgres/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# Imports that would create circular imports if sorted
77
from .base import Model
88
from .constraints import CheckConstraint, UniqueConstraint
9-
from .db import IntegrityError, get_connection
9+
from .db import get_connection
1010
from .deletion import CASCADE, DO_NOTHING, PROTECT, RESTRICT, SET, SET_DEFAULT, SET_NULL
1111
from .enums import IntegerChoices, TextChoices
1212
from .fields import (
@@ -111,7 +111,6 @@
111111
"ReverseManyToMany",
112112
# From db
113113
"get_connection",
114-
"IntegrityError",
115114
# From registry
116115
"register_model",
117116
"models_registry",

plain-postgres/plain/postgres/base.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
from plain.postgres.meta import Meta
1111
from plain.postgres.options import Options
1212

13+
import psycopg
14+
1315
import plain.runtime
1416
from plain.exceptions import NON_FIELD_ERRORS, ValidationError
1517
from plain.postgres import models_registry, transaction, types
1618
from plain.postgres.constants import LOOKUP_SEP
1719
from plain.postgres.constraints import CheckConstraint, UniqueConstraint
18-
from plain.postgres.db import (
19-
PLAIN_VERSION_PICKLE_KEY,
20-
DatabaseError,
21-
)
20+
from plain.postgres.db import PLAIN_VERSION_PICKLE_KEY
2221
from plain.postgres.deletion import Collector
2322
from plain.postgres.dialect import MAX_NAME_LENGTH
2423
from plain.postgres.exceptions import (
@@ -496,9 +495,11 @@ def _save_table(
496495
base_qs, id_val, values, update_fields, forced_update
497496
)
498497
if force_update and not updated:
499-
raise DatabaseError("Forced update did not affect any rows.")
498+
raise psycopg.DatabaseError("Forced update did not affect any rows.")
500499
if update_fields and not updated:
501-
raise DatabaseError("Save with update_fields did not affect any rows.")
500+
raise psycopg.DatabaseError(
501+
"Save with update_fields did not affect any rows."
502+
)
502503
if not updated:
503504
fields = meta.local_concrete_fields
504505
if not id_set:

plain-postgres/plain/postgres/cli/db.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
from collections import defaultdict
77

88
import click
9+
import psycopg
910

1011
from plain.cli import register_cli
1112

1213
from ..backups.cli import cli as backups_cli
13-
from ..db import OperationalError, get_connection
14+
from ..db import get_connection
1415
from ..dialect import quote_name
1516
from ..migrations.recorder import MIGRATION_TABLE_NAME
1617

@@ -126,7 +127,7 @@ def wait() -> None:
126127

127128
try:
128129
get_connection().ensure_connection()
129-
except OperationalError:
130+
except psycopg.OperationalError:
130131
waiting_for = True
131132

132133
if waiting_for:

plain-postgres/plain/postgres/db.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,7 @@
55
from plain import signals
66

77
from .connections import get_connection, has_connection
8-
from .exceptions import (
9-
DatabaseError,
10-
DatabaseErrorWrapper,
11-
DataError,
12-
Error,
13-
IntegrityError,
14-
InterfaceError,
15-
InternalError,
16-
NotSupportedError,
17-
OperationalError,
18-
ProgrammingError,
19-
ReadOnlyError,
20-
)
8+
from .exceptions import DatabaseErrorWrapper
219

2210
PLAIN_VERSION_PICKLE_KEY = "_plain_version"
2311

@@ -46,16 +34,6 @@ def close_old_connections(**kwargs: Any) -> None:
4634
"get_connection",
4735
"has_connection",
4836
"PLAIN_VERSION_PICKLE_KEY",
49-
"Error",
50-
"InterfaceError",
51-
"DatabaseError",
52-
"DataError",
53-
"OperationalError",
54-
"IntegrityError",
55-
"InternalError",
56-
"ProgrammingError",
57-
"NotSupportedError",
5837
"DatabaseErrorWrapper",
59-
"ReadOnlyError",
6038
"close_old_connections",
6139
]

plain-postgres/plain/postgres/deletion.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
from operator import attrgetter, or_
88
from typing import TYPE_CHECKING, Any
99

10+
import psycopg
11+
1012
from plain.postgres import (
1113
query_utils,
1214
transaction,
1315
)
14-
from plain.postgres.db import IntegrityError
1516
from plain.postgres.meta import Meta
1617
from plain.postgres.query import QuerySet
1718
from plain.postgres.sql import DeleteQuery, UpdateQuery
@@ -28,16 +29,16 @@
2829
_LAZY_ON_DELETE: set[Callable[..., Any]] = set()
2930

3031

31-
class ProtectedError(IntegrityError):
32+
class ProtectedError(psycopg.IntegrityError):
3233
def __init__(self, msg: str, protected_objects: Iterable[Any]) -> None:
3334
self.protected_objects = protected_objects
34-
super().__init__(msg, protected_objects)
35+
super().__init__(msg)
3536

3637

37-
class RestrictedError(IntegrityError):
38+
class RestrictedError(psycopg.IntegrityError):
3839
def __init__(self, msg: str, restricted_objects: Iterable[Any]) -> None:
3940
self.restricted_objects = restricted_objects
40-
super().__init__(msg, restricted_objects)
41+
super().__init__(msg)
4142

4243

4344
def CASCADE(collector: Collector, field: RelatedField, sub_objs: Any) -> None:

0 commit comments

Comments
 (0)