functions
Pure functions for EHR querying.
Module
Functions
apply_ehr_write_guard
def apply_ehr_write_guard( cache: CacheProtocol, task_hash: str, records: list[EHRDataRecord],) ‑> tuple[list[EHRDataRecord], list[EHRDataRecord]]:Split records into those safe to write and those that would destroy data.
ehr_data holds exactly one row per (task_hash, bitfount_patient_id) and
bulk_insert_ehr_data merges a fully-populated instance, so every write is
a destructive full-row replace: a None overwrites a good stored value, and
there is no history table and no versioning to recover it from. A patient
whose fetch failed this run therefore has to be kept away from the write,
not merely written carefully — which is what makes the fallback the rest of
this project relies on real, since reads are partition-wide and merge
never deletes.
A record is a failure record when its error is set. Both builders in this
module set it: on a negative record for a lookup that failed outright, and
on a built record whose every countable fetch group failed
(_every_supported_group_failed — which is narrower than "every group",
and narrower still than "every fetch"). This function does not re-derive
that verdict — error is not None is the discriminator.
This guard protects the total-failure case only. A fetch that retrieved some group still merges, so it can replace a stored row that held more (see the coverage-comparison note below). "A failed fetch cannot destroy stored data" is therefore not what this does.
error is None— always written. A successful fetch is this run's answer.errorset — written only when neither a stored row nor another record in this same batch already answers for the patient. With either, that is the better answer and this run's failure is dropped; with neither, the failure is worth recording, since the absence of a row cannot say whether the patient was ever queried.
The same-batch half matters because one patient can produce two records in
one run: processed_ids is keyed on the imaging query key, not on
BitfountPatientID, so a person reached under two imaging patient IDs is
fetched twice, and drop_ambiguous_patients leaves that case alone (the
EHR is not asserting they are different patients). If one fetch succeeded
and the other's every group failed, both records reached the write with the
same primary key and bulk_insert_ehr_data merges per record — so the one
written last decided the stored row, and a failure could erase a success
retrieved seconds earlier. The imaging route resolves this for lookup
failures already (built_patient_ids against failed_lookups); a group
failure inside a successful lookup never reached that check.
Arguments
cache: The run's cache, probed for stored rows.task_hash: The partition in scope. A row under anothertask_hashis not a fallback for this one.records: The records about to be written, afterdrop_ambiguous_patients.
Returns
A (writable, withheld) pair, each in input order: the records to
hand to bulk_insert_ehr_data, and the failure records dropped in
favour of a stored row or of this run's own successful one.
drop_ambiguous_patients
def drop_ambiguous_patients( records: list[EHRDataRecord],) ‑> tuple[list[EHRDataRecord], set[str]]:Drop records whose BitfountPatientID maps to more than one EHR patient.
ehr_data is keyed on (task_hash, bitfount_patient_id), and the
BitfountPatientID is derived from name + date of birth. Two distinct EHR
patient records therefore collapse onto one cache row — either one patient
duplicated in the EHR, or two different patients who share a name and date
of birth. Nothing available at this point tells those two cases apart, and
the session.merge in bulk_insert_ehr_data would silently attribute one
patient's codes and appointment history to the other, so the whole
BitfountPatientID is excluded instead.
This mirrors the name+DOB lookup paths, where an EHR search matching more
than one patient raises NonSpecificPatientError and the patient is
skipped rather than guessed at. Excluding a patient costs an enrolment;
guessing risks assessing one patient's eligibility against another
patient's clinical data.
Records that share a BitfountPatientID and an ehr_patient_id are one
EHR patient reached more than once (e.g. two imaging patient IDs for the
same person) and are left alone — the EHR itself is not asserting they are
different patients.
Arguments
records: Assembled records, before they are written to the cache.
Returns
A (kept, ambiguous) pair: the records for every unambiguous
BitfountPatientID in input order, and the set of BitfountPatientIDs
that were dropped. The caller deletes any row already stored under
an ambiguous ID — an earlier run may have written one before the
second EHR record was known, and that row cannot be trusted to
belong to either patient.