33from fastapi import Depends , HTTPException , Request , Response
44from . import CRUDGenerator , NOT_FOUND , _utils
55from ._types import DEPENDENCIES , PAGINATION , PYDANTIC_SCHEMA as SCHEMA
6+ import re
7+ from typing import Tuple
68
79try :
810 from sqlalchemy .orm import Session
2022CALLABLE = Callable [..., Model ]
2123CALLABLE_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
2553def _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+
3162class 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