2929from pydantic import Field
3030from pydantic import ValidationError as PydanticValidationError
3131from sqlalchemy import cast , column , null , or_ , select
32- from sqlalchemy .orm import Query , Session
32+ from sqlalchemy .orm import Query , Session , selectinload
3333from sqlalchemy .sql .expression import nullslast
3434from starlette .responses import StreamingResponse
3535from starlette .status import (
8888from fides .api .schemas .external_https import PrivacyRequestResumeFormat
8989from fides .api .schemas .policy import ActionType , CurrentStep
9090from fides .api .schemas .privacy_request import (
91+ BULK_PRIVACY_REQUEST_BATCH_SIZE ,
9192 BulkPostPrivacyRequests ,
9293 BulkReviewResponse ,
9394 BulkSoftDeletePrivacyRequests ,
@@ -225,7 +226,9 @@ def create_privacy_request(
225226 privacy_request_service : PrivacyRequestService = Depends (
226227 get_privacy_request_service
227228 ),
228- data : Annotated [List [PrivacyRequestCreate ], Field (max_length = 50 )], # type: ignore
229+ data : Annotated [
230+ List [PrivacyRequestCreate ], Field (max_length = BULK_PRIVACY_REQUEST_BATCH_SIZE )
231+ ], # type: ignore
229232) -> BulkPostPrivacyRequests :
230233 """
231234 Given a list of privacy request data elements, create corresponding PrivacyRequest objects
@@ -251,7 +254,9 @@ def create_privacy_request_authenticated(
251254 verify_oauth_client ,
252255 scopes = [PRIVACY_REQUEST_CREATE ],
253256 ),
254- data : Annotated [List [PrivacyRequestCreate ], Field (max_length = 50 )], # type: ignore
257+ data : Annotated [
258+ List [PrivacyRequestCreate ], Field (max_length = BULK_PRIVACY_REQUEST_BATCH_SIZE )
259+ ], # type: ignore
255260) -> BulkPostPrivacyRequests :
256261 """
257262 Given a list of privacy request data elements, create corresponding PrivacyRequest objects
@@ -272,7 +277,10 @@ def privacy_request_csv_download(
272277 f = io .StringIO ()
273278 csv_file = csv .writer (f )
274279
275- privacy_request_ids : List [str ] = [r .id for r in privacy_request_query ]
280+ # Execute query once and convert to list to avoid multiple iterations
281+ privacy_requests : List [PrivacyRequest ] = privacy_request_query .all ()
282+ privacy_request_ids : List [str ] = [r .id for r in privacy_requests ]
283+
276284 denial_audit_log_query : Query = db .query (AuditLog ).filter (
277285 AuditLog .action == AuditLogAction .denied ,
278286 AuditLog .privacy_request_id .in_ (privacy_request_ids ),
@@ -281,7 +289,7 @@ def privacy_request_csv_download(
281289 r .privacy_request_id : r .message for r in denial_audit_log_query
282290 }
283291
284- identity_columns , custom_field_columns = get_variable_columns (privacy_request_query )
292+ identity_columns , custom_field_columns = get_variable_columns (privacy_requests )
285293
286294 csv_file .writerow (
287295 [
@@ -301,7 +309,7 @@ def privacy_request_csv_download(
301309 )
302310
303311 pr : PrivacyRequest
304- for pr in privacy_request_query :
312+ for pr in privacy_requests :
305313 denial_reason = (
306314 denial_audit_logs [pr .id ]
307315 if pr .status == PrivacyRequestStatus .denied and pr .id in denial_audit_logs
@@ -346,12 +354,12 @@ def privacy_request_csv_download(
346354
347355
348356def get_variable_columns (
349- privacy_request_query : Query ,
357+ privacy_requests : List [ PrivacyRequest ] ,
350358) -> tuple [List [str ], Dict [str , str ]]:
351359 identity_columns : Set [str ] = set ()
352360 custom_field_columns : Dict [str , str ] = {}
353361
354- for pr in privacy_request_query :
362+ for pr in privacy_requests :
355363 identity_columns .update (
356364 extract_identity_column_names (pr .get_persisted_identity ().model_dump ())
357365 )
@@ -848,10 +856,29 @@ def _shared_privacy_request_search(
848856
849857 if download_csv :
850858 _validate_result_size (query )
859+ # Eager load relationships needed for CSV export to avoid N+1 queries
860+ query = query .options (
861+ selectinload (PrivacyRequest .provided_identities ), # type: ignore[attr-defined]
862+ selectinload (PrivacyRequest .custom_fields ), # type: ignore[attr-defined]
863+ selectinload (PrivacyRequest .policy ).selectinload (Policy .rules ), # type: ignore[attr-defined]
864+ )
851865 # Returning here if download_csv param was specified
852866 logger .info ("Downloading privacy requests as csv" )
853867 return privacy_request_csv_download (db , query )
854868
869+ # Eager load relationships for regular list view to avoid N+1 queries
870+ if include_identities or include_custom_privacy_request_fields :
871+ eager_load_options = []
872+ if include_identities :
873+ eager_load_options .append (
874+ selectinload (PrivacyRequest .provided_identities ) # type: ignore[attr-defined]
875+ )
876+ if include_custom_privacy_request_fields :
877+ eager_load_options .append (
878+ selectinload (PrivacyRequest .custom_fields ) # type: ignore[attr-defined]
879+ )
880+ query = query .options (* eager_load_options )
881+
855882 # Conditionally embed execution log details in the response.
856883 if verbose :
857884 logger .info ("Finding execution and audit log details" )
@@ -1288,19 +1315,26 @@ def validate_manual_input(
12881315 ],
12891316)
12901317def bulk_restart_privacy_request_from_failure (
1291- privacy_request_ids : List [str ],
1318+ privacy_request_ids : Annotated [
1319+ List [str ], Field (max_length = BULK_PRIVACY_REQUEST_BATCH_SIZE )
1320+ ],
12921321 * ,
12931322 db : Session = Depends (deps .get_db ),
12941323) -> BulkPostPrivacyRequests :
1295- """Bulk restart a of privacy request from failure."""
1324+ """Bulk restart privacy requests from failure."""
12961325 succeeded : List [PrivacyRequestResponse ] = []
12971326 failed : List [Dict [str , Any ]] = []
1327+
1328+ # Fetch all privacy requests in one query to avoid N+1
1329+ privacy_requests_dict = {
1330+ pr .id : pr
1331+ for pr in PrivacyRequest .query_without_large_columns (db )
1332+ .filter (PrivacyRequest .id .in_ (privacy_request_ids ))
1333+ .all ()
1334+ }
1335+
12981336 for privacy_request_id in privacy_request_ids :
1299- privacy_request = (
1300- PrivacyRequest .query_without_large_columns (db )
1301- .filter (PrivacyRequest .id == privacy_request_id )
1302- .first ()
1303- )
1337+ privacy_request = privacy_requests_dict .get (privacy_request_id )
13041338
13051339 if not privacy_request :
13061340 failed .append (
@@ -2220,12 +2254,18 @@ def bulk_soft_delete_privacy_requests(
22202254 if client .id == CONFIG .security .oauth_root_client_id :
22212255 user_id = "root"
22222256
2223- for privacy_request_id in privacy_requests .request_ids :
2224- privacy_request = (
2225- PrivacyRequest .query_without_large_columns (db )
2226- .filter (PrivacyRequest .id == privacy_request_id )
2227- .first ()
2228- )
2257+ request_ids = privacy_requests .request_ids
2258+
2259+ # Fetch all privacy requests in one query to avoid N+1
2260+ privacy_requests_dict = {
2261+ pr .id : pr
2262+ for pr in PrivacyRequest .query_without_large_columns (db )
2263+ .filter (PrivacyRequest .id .in_ (request_ids ))
2264+ .all ()
2265+ }
2266+
2267+ for privacy_request_id in request_ids :
2268+ privacy_request = privacy_requests_dict .get (privacy_request_id )
22292269
22302270 if not privacy_request :
22312271 failed .append (
0 commit comments