Skip to content

Commit d336bfd

Browse files
authored
Update sqlalchemy.py
1 parent e031eef commit d336bfd

File tree

1 file changed

+38
-10
lines changed

1 file changed

+38
-10
lines changed

pkgs/crouton/crouton/core/sqlalchemy.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from fastapi import Depends, HTTPException, Request, Response
44
from . import CRUDGenerator, NOT_FOUND, _utils
55
from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA
6+
import re
7+
from typing import Tuple
68

79
try:
810
from sqlalchemy.orm import Session
@@ -20,6 +22,32 @@
2022
CALLABLE = Callable[..., Model]
2123
CALLABLE_LIST = Callable[..., List[Model]]
2224

25+
# Regex covers PostgreSQL & SQLite duplicate-key messages.
26+
_DUPLICATE_RE = re.compile(r"Key \((?P<col>[^)]+)\)=\((?P<val>[^)]+)\) already exists", re.I)
27+
28+
29+
def _friendly_integrity_error(orig_exc: Exception) -> Tuple[int, str]:
30+
"""
31+
Map a low-level sqlalchemy.exc.IntegrityError.orig to
32+
(HTTP status-code, human-readable message).
33+
"""
34+
raw_msg = str(orig_exc)
35+
36+
# ----- UNIQUE / PRIMARY-KEY -----------------------------------------
37+
if getattr(orig_exc, "pgcode", None) in ("23505",) or "already exists" in raw_msg:
38+
match = _DUPLICATE_RE.search(raw_msg)
39+
if match:
40+
col, val = match["col"], match["val"]
41+
return 409, f"Duplicate value '{val}' for field '{col}'."
42+
return 409, "Duplicate key value violates a unique constraint."
43+
44+
# ----- FOREIGN-KEY ---------------------------------------------------
45+
if getattr(orig_exc, "pgcode", None) in ("23503",) or "foreign key constraint" in raw_msg:
46+
return 422, "Foreign-key constraint failed."
47+
48+
# ----- NOT-NULL / CHECK / other integrity ---------------------------
49+
return 422, raw_msg
50+
2351

2452
# Utility function for extracting query parameters
2553
def _extract_query_params(request: Request) -> Dict[str, Any]:
@@ -28,6 +56,9 @@ def _extract_query_params(request: Request) -> Dict[str, Any]:
2856

2957

3058

59+
60+
61+
3162
class SQLAlchemyCRUDRouter(CRUDGenerator[SCHEMA]):
3263
"""
3364
CRUD router built around SQLAlchemy ORM with robust error handling.
@@ -124,18 +155,16 @@ def _db_commit(self, db: Session) -> None:
124155
db.commit()
125156
except IntegrityError as exc:
126157
db.rollback()
127-
# Distinguish “duplicate” vs other relational failures
128-
raise HTTPException(
129-
status_code=409 if "unique" in str(exc.orig).lower() else 422,
130-
detail=str(exc.orig),
131-
) from exc
158+
status, detail = _friendly_integrity_error(exc.orig)
159+
raise HTTPException(status_code=status, detail=detail) from exc
132160
except SQLAlchemyError as exc:
133161
db.rollback()
134162
raise HTTPException(
135163
status_code=500,
136164
detail=f"Database error: {exc}",
137165
) from exc
138166

167+
139168
# ────────────────────────────
140169
# GET many
141170
# ────────────────────────────
@@ -186,15 +215,14 @@ def route(
186215
obj = self.db_model(**model.dict())
187216
except (TypeError, ValueError) as exc:
188217
raise HTTPException(400, f"Invalid payload: {exc}") from exc
189-
218+
190219
db.add(obj)
191-
self._db_commit(db)
192-
db.refresh(obj) # reflect DB-side defaults
193-
# FastAPI automatically sets 201 when returning Response after POST
220+
self._db_commit(db) # ← centralised commit & error handling
221+
db.refresh(obj)
194222
return obj
195-
196223
return route
197224

225+
198226
# ────────────────────────────
199227
# UPDATE
200228
# ────────────────────────────

0 commit comments

Comments
 (0)