|
| 1 | +# SubjectSelectionQueryBuilder Utility Guide |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The `SubjectSelectionQueryBuilder` is a flexible utility for constructing SQL queries to retrieve screening subjects from the NHS BCSS Oracle database. It supports a comprehensive set of filters (subject selection criteria) based on: |
| 6 | + |
| 7 | +- Demographics and GP details |
| 8 | +- Screening status and due dates |
| 9 | +- Events and episodes |
| 10 | +- Kit usage and diagnostic activity |
| 11 | +- Appointments, tests, and CADS datasets |
| 12 | +- Lynch pathway logic and Notify message status |
| 13 | + |
| 14 | +For example: |
| 15 | +- NHS number |
| 16 | +- Subject age |
| 17 | +- Hub code |
| 18 | +- Screening centre code |
| 19 | +- GP practice linkage |
| 20 | +- Screening status |
| 21 | +- and many more, (including date-based and status-based filters). |
| 22 | + |
| 23 | +It also handles special cases such as `unchanged` values and supports modifiers like `NOT:` for negation. |
| 24 | + |
| 25 | +Queries are constructed dynamically based on `criteria`, `user`, and `subject` inputs. |
| 26 | + |
| 27 | +--- |
| 28 | + |
| 29 | +## How to Use |
| 30 | + |
| 31 | +### Import and Instantiate the Builder |
| 32 | + |
| 33 | +```python |
| 34 | +from utils.oracle.subject_selection_query_builder import SubjectSelectionQueryBuilder |
| 35 | + |
| 36 | +builder = SubjectSelectionQueryBuilder() |
| 37 | +``` |
| 38 | + |
| 39 | +### Build a subject selection query |
| 40 | + |
| 41 | +Using `build_subject_selection_query`: |
| 42 | + |
| 43 | +```python |
| 44 | +query, bind_vars = builder.build_subject_selection_query( |
| 45 | + criteria=criteria_dict, # Dict[str, str] of selection criteria |
| 46 | + user=test_user, # User object |
| 47 | + subject=test_subject, # Subject object (can be None) |
| 48 | + subjects_to_retrieve=100 # Optional limit |
| 49 | +) |
| 50 | +``` |
| 51 | +When you call `build_subject_selection_query(...)`, it returns a tuple containing: |
| 52 | + |
| 53 | +`query` — a complete SQL string with placeholders like :nhs_number, ready to be run against the database. |
| 54 | + |
| 55 | +`bind_vars` — a dictionary mapping those placeholders to their actual values, like {"nhs_number": "1234567890"}. |
| 56 | + |
| 57 | +This approach ensures injection-safe execution (defending against SQL injection attacks) and allows database engines to optimize and cache query plans for repeated execution. |
| 58 | + |
| 59 | +## Example Usage |
| 60 | + |
| 61 | +### Input |
| 62 | + |
| 63 | +```python |
| 64 | +criteria = { |
| 65 | + "NHS_NUMBER": "1234567890", |
| 66 | + "SCREENING_STATUS": "invited" |
| 67 | +} |
| 68 | + |
| 69 | +user = User(user_id=42, organisation=None) # Simulated user |
| 70 | +subject = Subject() # Optional; used for 'unchanged' logic |
| 71 | + |
| 72 | +builder = SubjectSelectionQueryBuilder() |
| 73 | +query, bind_vars = builder.build_subject_selection_query(criteria, user, subject) |
| 74 | +``` |
| 75 | + |
| 76 | +### Output |
| 77 | + |
| 78 | +#### Query |
| 79 | + |
| 80 | +```SQL |
| 81 | +SELECT ss.screening_subject_id, ... |
| 82 | +FROM screening_subject_t ss |
| 83 | +INNER JOIN sd_contact_t c ON c.nhs_number = ss.subject_nhs_number |
| 84 | +WHERE 1=1 |
| 85 | + AND c.nhs_number = :nhs_number |
| 86 | + AND ss.screening_status_id = 1001 |
| 87 | +FETCH FIRST 1 ROWS ONLY |
| 88 | +``` |
| 89 | +(Note: 1001 would be the resolved ID for "invited" in ScreeningStatusType.) |
| 90 | + |
| 91 | +#### bind_vars |
| 92 | + |
| 93 | +```python |
| 94 | +{ |
| 95 | + "nhs_number": "1234567890" |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +### What happens next? |
| 100 | + |
| 101 | +You can pass both values directly into your DB layer or test stub: |
| 102 | + |
| 103 | +```python |
| 104 | +cursor.execute(query, bind_vars) |
| 105 | +``` |
| 106 | + |
| 107 | +## Supported Inputs |
| 108 | + |
| 109 | +### 1. criteria (Dict[str, str]) |
| 110 | + |
| 111 | +This is the main filter configuration, where each entry represents one selection condition. |
| 112 | + |
| 113 | +The `key` is a string matching one of the `SubjectSelectionCriteriaKey` `values` (e.g. "SUBJECT_AGE", "SCREENING_STATUS", etc.) |
| 114 | + |
| 115 | +The `value` is the actual filter (e.g. "55", "> 60", "yes", "not:null", etc.) |
| 116 | + |
| 117 | +Example: |
| 118 | + |
| 119 | +```python |
| 120 | +{ |
| 121 | + "SUBJECT_HAS_EVENT_STATUS": "ES01", |
| 122 | + "SUBJECT_AGE": "> 60", |
| 123 | + "DATE_OF_DEATH": "null" |
| 124 | +} |
| 125 | +``` |
| 126 | +Each of those triggers a different clause in the generated SQL. |
| 127 | + |
| 128 | +### 2. user (User) |
| 129 | + |
| 130 | +This gives the builder context about who’s requesting the query, including their organisation and permissions. |
| 131 | + |
| 132 | +Some criteria (like "USER_HUB" or "USER_ORGANISATION") don’t refer to a fixed hub code, but instead dynamically map to the hub or screening centre of the user running the search. That’s where this comes into play. |
| 133 | + |
| 134 | +Example: |
| 135 | + |
| 136 | +```python |
| 137 | +"SUBJECT_HUB_CODE": "USER_HUB" |
| 138 | +``` |
| 139 | +This means “filter by the hub assigned to this user’s organisation,” not a fixed hub like ABC. |
| 140 | + |
| 141 | +### 3. subject (Subject) |
| 142 | +This is used when a filter wants to compare the current value in the database to an existing value on file—often represented by the "UNCHANGED" keyword. |
| 143 | + |
| 144 | +Example: |
| 145 | + |
| 146 | +```python |
| 147 | +"SCREENING_STATUS": "unchanged" |
| 148 | +``` |
| 149 | +That’s saying: “Only return subjects whose screening status has not changed compared to what’s currently recorded on the subject object.” |
| 150 | + |
| 151 | +Without a subject, "unchanged" logic isn’t possible and will raise a validation error. |
| 152 | + |
| 153 | +Together, these three inputs give the builder all it needs to translate human-friendly selection criteria into valid, safe, dynamic SQL. |
| 154 | + |
| 155 | +## Key Behavior Details |
| 156 | + |
| 157 | +Values like `yes`, `no`, `null`, `not null`, `unchanged` are normalized and interpreted internally. |
| 158 | + |
| 159 | +`NOT:` prefix in values flips logic where allowed (e.g. "NOT:ES01"). |
| 160 | + |
| 161 | +Most enums (like `YesNoType`, `ScreeningStatusType`, etc.) are resolved by description using .by_description() or .by_description_case_insensitive() calls. |
| 162 | + |
| 163 | +Joins to related datasets are added dynamically only when required (e.g. latest episode, diagnostic test joins). |
| 164 | + |
| 165 | +All dates are handled via Oracle `TRUNC(SYSDATE)` and `TO_DATE()` expressions to ensure consistent date logic. |
| 166 | + |
| 167 | + |
| 168 | +## Reference |
| 169 | + |
| 170 | +For a full list of supported `SubjectSelectionCriteriaKey` values and expected inputs, refer to the enumeration in: |
| 171 | + |
| 172 | +`classes/subject_selection_criteria_key.py` |
| 173 | + |
| 174 | +Or explore the full `SubjectSelectionQueryBuilder._dispatch_criteria_key()` method to review how each key is implemented. |
0 commit comments