2020 User ,
2121)
2222from ops_api .ops .services .ops_service import OpsService , ResourceNotFoundError , ValidationError
23- from ops_api .ops .utils .query_helpers import QueryHelper
2423
2524
2625@dataclass
@@ -227,12 +226,10 @@ def _get_research_projects_query(filters: ProjectFilters):
227226 Returns:
228227 SQLAlchemy select statement
229228 """
229+ # Base query with all necessary eager loading
230230 stmt = (
231231 select (ResearchProject )
232232 .distinct (ResearchProject .id )
233- .join (Agreement , isouter = True )
234- .join (BudgetLineItem , isouter = True )
235- .join (CAN , isouter = True )
236233 .options (
237234 selectinload (ResearchProject .agreements ).selectinload (Agreement .services_components ),
238235 selectinload (ResearchProject .agreements )
@@ -244,35 +241,63 @@ def _get_research_projects_query(filters: ProjectFilters):
244241 )
245242 )
246243
247- query_helper = QueryHelper (stmt )
244+ # For filtering, we use EXISTS subqueries to ensure each filter can match different
245+ # related records. This prevents the issue where a single joined row must satisfy
246+ # multiple conditions across different tables (e.g., one agreement for fiscal year
247+ # and a different agreement for agreement name).
248+ where_clauses = []
248249
249- # Apply portfolio filter (OR logic - match any portfolio)
250+ # Apply portfolio filter using EXISTS subquery
250251 if filters .portfolio_id :
251- query_helper .add_column_in_list (CAN .portfolio_id , filters .portfolio_id )
252+ portfolio_subquery = (
253+ select (1 )
254+ .select_from (Agreement )
255+ .join (BudgetLineItem )
256+ .join (CAN )
257+ .where (Agreement .project_id == ResearchProject .id )
258+ .where (CAN .portfolio_id .in_ (filters .portfolio_id ))
259+ .exists ()
260+ )
261+ where_clauses .append (portfolio_subquery )
252262
253- # Apply fiscal year filter (OR logic - match any fiscal year)
263+ # Apply fiscal year filter using EXISTS subquery
254264 if filters .fiscal_year :
255- if len (filters .fiscal_year ) == 1 :
256- fiscal_year = filters .fiscal_year [0 ]
257- query_helper .add_column_equals (BudgetLineItem .fiscal_year , fiscal_year )
258- else :
259- # Multiple fiscal years - use IN clause
260- query_helper .add_column_in_list (BudgetLineItem .fiscal_year , filters .fiscal_year )
265+ fy_subquery = (
266+ select (1 )
267+ .select_from (Agreement )
268+ .join (BudgetLineItem )
269+ .where (Agreement .project_id == ResearchProject .id )
270+ .where (BudgetLineItem .fiscal_year .in_ (filters .fiscal_year ))
271+ .exists ()
272+ )
273+ where_clauses .append (fy_subquery )
261274
262275 # Apply project search filter on project title (OR logic, exact match on title/short title)
263276 if filters .project_search :
264- query_helper .where_clauses .append (
265- or_ (Project .title .in_ (filters .project_search ), Project .short_title .in_ (filters .project_search ))
277+ where_clauses .append (
278+ or_ (
279+ ResearchProject .title .in_ (filters .project_search ),
280+ ResearchProject .short_title .in_ (filters .project_search ),
281+ )
266282 )
267283
268- # Apply agreement search filter on agreement name and nick_name (exact match - OR logic)
269- # Projects are returned if any agreement has name OR nick_name matching any search term
284+ # Apply agreement search filter using EXISTS subquery
270285 if filters .agreement_search :
271- query_helper .where_clauses .append (
272- or_ (Agreement .name .in_ (filters .agreement_search ), Agreement .nick_name .in_ (filters .agreement_search ))
286+ agreement_subquery = (
287+ select (1 )
288+ .select_from (Agreement )
289+ .where (Agreement .project_id == ResearchProject .id )
290+ .where (
291+ or_ (Agreement .name .in_ (filters .agreement_search ), Agreement .nick_name .in_ (filters .agreement_search ))
292+ )
293+ .exists ()
273294 )
295+ where_clauses .append (agreement_subquery )
296+
297+ # Apply all where clauses
298+ if where_clauses :
299+ stmt = stmt .where (* where_clauses )
274300
275- stmt = query_helper .get_stmt ()
276301 logger .debug (f"SQL: { stmt } " )
277302
278303 return stmt
@@ -287,55 +312,78 @@ def _get_administrative_and_support_projects_query(filters: ProjectFilters):
287312 Returns:
288313 SQLAlchemy select statement
289314 """
315+ # Base query with all necessary eager loading
290316 stmt = (
291317 select (AdministrativeAndSupportProject )
292318 .distinct (AdministrativeAndSupportProject .id )
293- .join (Agreement , isouter = True )
294- .join (BudgetLineItem , isouter = True )
295- .join (CAN , isouter = True )
296319 .options (
297- selectinload (ResearchProject .agreements ).selectinload (Agreement .services_components ),
298- selectinload (ResearchProject .agreements )
320+ selectinload (AdministrativeAndSupportProject .agreements ).selectinload (Agreement .services_components ),
321+ selectinload (AdministrativeAndSupportProject .agreements )
299322 .selectinload (Agreement .budget_line_items )
300323 .selectinload (BudgetLineItem .can ),
301- selectinload (ResearchProject .agreements ).selectinload (Agreement .special_topics ),
302- selectinload (ResearchProject .agreements ).selectinload (Agreement .research_methodologies ),
303- selectinload (ResearchProject .agreements ).selectinload (Agreement .team_members ),
324+ selectinload (AdministrativeAndSupportProject .agreements ).selectinload (Agreement .special_topics ),
325+ selectinload (AdministrativeAndSupportProject .agreements ).selectinload (Agreement .research_methodologies ),
326+ selectinload (AdministrativeAndSupportProject .agreements ).selectinload (Agreement .team_members ),
304327 )
305328 )
306329
307- query_helper = QueryHelper (stmt )
330+ # For filtering, we use EXISTS subqueries to ensure each filter can match different
331+ # related records. This prevents the issue where a single joined row must satisfy
332+ # multiple conditions across different tables (e.g., one agreement for fiscal year
333+ # and a different agreement for agreement name).
334+ where_clauses = []
308335
309- # Apply portfolio filter (OR logic - match any portfolio)
336+ # Apply portfolio filter using EXISTS subquery
310337 if filters .portfolio_id :
311- query_helper .add_column_in_list (CAN .portfolio_id , filters .portfolio_id )
338+ portfolio_subquery = (
339+ select (1 )
340+ .select_from (Agreement )
341+ .join (BudgetLineItem )
342+ .join (CAN )
343+ .where (Agreement .project_id == AdministrativeAndSupportProject .id )
344+ .where (CAN .portfolio_id .in_ (filters .portfolio_id ))
345+ .exists ()
346+ )
347+ where_clauses .append (portfolio_subquery )
312348
313- # Apply fiscal year filter (OR logic - match any fiscal year)
349+ # Apply fiscal year filter using EXISTS subquery
314350 if filters .fiscal_year :
315- if len (filters .fiscal_year ) == 1 :
316- fiscal_year = filters .fiscal_year [0 ]
317- query_helper .add_column_equals (BudgetLineItem .fiscal_year , fiscal_year )
318- else :
319- # Multiple fiscal years - use IN clause
320- query_helper .add_column_in_list (BudgetLineItem .fiscal_year , filters .fiscal_year )
351+ fy_subquery = (
352+ select (1 )
353+ .select_from (Agreement )
354+ .join (BudgetLineItem )
355+ .where (Agreement .project_id == AdministrativeAndSupportProject .id )
356+ .where (BudgetLineItem .fiscal_year .in_ (filters .fiscal_year ))
357+ .exists ()
358+ )
359+ where_clauses .append (fy_subquery )
321360
322- # Apply project search filter on project title (AND logic - must match all search terms )
361+ # Apply project search filter on project title (OR logic, exact match on title/short title )
323362 if filters .project_search :
324- query_helper . where_clauses .append (
363+ where_clauses .append (
325364 or_ (
326- Project .title .in_ (filters .project_search ),
327- Project .short_title .in_ (filters .project_search ),
365+ AdministrativeAndSupportProject .title .in_ (filters .project_search ),
366+ AdministrativeAndSupportProject .short_title .in_ (filters .project_search ),
328367 )
329368 )
330369
331- # Apply agreement search filter on agreement name and nick_name (exact match - OR logic)
332- # Projects are returned if any agreement has name OR nick_name matching any search term
370+ # Apply agreement search filter using EXISTS subquery
333371 if filters .agreement_search :
334- query_helper .where_clauses .append (
335- or_ (Agreement .name .in_ (filters .agreement_search ), Agreement .nick_name .in_ (filters .agreement_search ))
372+ agreement_subquery = (
373+ select (1 )
374+ .select_from (Agreement )
375+ .where (Agreement .project_id == AdministrativeAndSupportProject .id )
376+ .where (
377+ or_ (Agreement .name .in_ (filters .agreement_search ), Agreement .nick_name .in_ (filters .agreement_search ))
378+ )
379+ .exists ()
336380 )
381+ where_clauses .append (agreement_subquery )
382+
383+ # Apply all where clauses
384+ if where_clauses :
385+ stmt = stmt .where (* where_clauses )
337386
338- stmt = query_helper .get_stmt ()
339387 logger .debug (f"SQL: { stmt } " )
340388
341389 return stmt
@@ -626,24 +674,27 @@ def get_filter_options(self) -> dict[str, Any]:
626674 )
627675 project_types = sorted ([pt .name for pt in self .db_session .scalars (project_types_query ).all ()])
628676
629- # Step 6: Agreement names and nick_names - Query both and create a sorted list
677+ # Step 6: Agreement names and nick_names - Query both and create a sorted list of dicts
630678 agreement_names_query = (
631679 select (Agreement .id , Agreement .name , Agreement .nick_name )
632680 .join (Project , Agreement .project_id == Project .id )
633681 .where (Project .id .in_ (project_ids_subquery ))
634682 .where (Agreement .name .isnot (None ))
635683 )
636684
637- # Collect all names and nick_names into a single sorted list
638- # Don't need ids because the match query matches directly on name and nick_name.
639- agreement_values = set () # Use set to avoid duplicates
640- for _ , a_name , a_nick_name in self .db_session .execute (agreement_names_query ).all ():
641- if a_name :
642- agreement_values .add (a_name )
643- if a_nick_name :
644- agreement_values .add (a_nick_name )
645-
646- agreement_names = sorted (list (agreement_values ))
685+ # Collect all names and nick_names into a list of dicts with ids
686+ # Use a dict keyed by name to avoid duplicates while preserving id
687+ agreement_values_dict = {} # Key: name, Value: id
688+ for a_id , a_name , a_nick_name in self .db_session .execute (agreement_names_query ).all ():
689+ if a_name and a_name not in agreement_values_dict :
690+ agreement_values_dict [a_name ] = a_id
691+ if a_nick_name and a_nick_name not in agreement_values_dict :
692+ agreement_values_dict [a_nick_name ] = a_id
693+
694+ # Convert to list of dicts and sort by name
695+ agreement_names = sorted (
696+ [{"id" : a_id , "name" : name } for name , a_id in agreement_values_dict .items ()], key = lambda x : x ["name" ]
697+ )
647698
648699 # Build response
649700 filters = {
0 commit comments