Measure Resolution¶
MeasureMember: the atomic result row¶
When a measure executes, it materialises MeasureMember rows:
@dataclass(frozen=True)
class MeasureMember:
person_id: int
measure_resolver: int
episode_id: Optional[int]
measure_date: Optional[date]
A MeasureMember is not just a boolean membership flag. It captures:
- who qualified
- under which resolver
- on what date
This preserved timing is what allows indicator windows, per-period reporting, and episode-aware logic to work later in the pipeline.
Canonical Combination Semantics¶
Measures compose child results using the canonical member shape. In the shipped config this is primarily through OR and AND.
OR: preserve all qualifying rows¶
OR logic unions child outputs without collapsing them.
Implementation shape:
- each child emits canonical rows
- rows are combined with
UNION ALL - all qualifying dates are preserved
Example:
- measure = ECOG 0 OR ECOG 1
If the same person qualifies on two different dates, both rows survive.

AND: resolver-aligned intersection¶
AND is not a simple person-level intersection. The same measure_resolver must satisfy all child criteria.
Implementation shape:
- child rows are aligned by
measure_resolver - the resulting qualification date is the latest child date
- rows with mismatched resolvers do not qualify together
This represents the earliest point at which all required conditions have become true for the same clinical context.
Example:
- measure = Stage III AND Radiotherapy
If Stage III qualifies under episode 10 and Radiotherapy qualifies under episode 10, the composite measure qualifies.
If the two child rows belong to different episodes, they do not intersect.

FIRST vs ANY¶
The engine compiles multiple SQL variants for measures and subqueries:
ANY: preserve all qualifying rowsFIRST: collapse to the earliest qualifying row per resolverUNDATED: keep qualifying membership without dates for intermediate set logic
Temporal Window: Event-to-Event Timing¶
A third measure kind handles cases where qualification depends on the relationship between two events — specifically, whether event B occurs within a defined window relative to event A.
This is expressed via MeasureTemporalWindow, a separate config table with a 1:1 relationship to Measure (enforced by primary key). A measure with a window_config row is a temporal window measure.
Anchor and candidate
The measure's own subquery_id defines the anchor event. A separate candidate_measure_id points to another measure whose events are evaluated relative to that anchor. The anchor is deduplicated to one row per resolver (earliest event) before the join.
Window bounds
window_min_days and window_max_days are optional. NULL means unbounded on that side. Negative values are valid — a candidate event occurring before the anchor can still qualify when it falls within the configured bounds. The emitted result remains the canonical MeasureMember row shape described above, so downstream comparison is based on the resolved measure_date selected by the window logic rather than on a separately emitted delta column.
Pick strategy (window_pick_strategy)
Controls which candidate event is retained per resolver after applying the window filter:
| Value | Behaviour |
|---|---|
any |
All qualifying candidate events are preserved |
earliest |
One row per resolver: earliest qualifying candidate date |
latest |
One row per resolver: latest qualifying candidate date |
closest |
One row per resolver: candidate date closest to anchor date |
Default when NULL: earliest.
Result date (result_date_source)
Controls which date is emitted as measure_date in the output:
| Value | Emitted date |
|---|---|
candidate |
Date of the candidate event |
anchor |
Date of the anchor event |
greatest |
Later of anchor or candidate |
least |
Earlier of anchor or candidate |
Default when NULL: candidate.
Resolver alignment (require_same_resolver)
When TRUE (default), the anchor and candidate are joined on both person_id and measure_resolver. When FALSE, the join is on person_id only.
SQL variants
sql_any() and sql_first() are supported. sql_undated() raises NotImplementedError — this surfaces as WARN (not FAIL) in is_executable() because the measure is still usable in the normal execution path.
Configuration
Temporal window measures are loaded via measure_temporal_window.csv in the config directory. The CSV is optional — existing deployments without it are unaffected.
Example — GP referral to treatment within 42 days:
Measure:
name: GP referral to first treatment or pall care <= 42d
combination: rule_or
subquery_id: <GP referral subquery>
MeasureTemporalWindow:
candidate_measure_id: <any treatment OR pall care composite>
window_min_days: NULL
window_max_days: 42
window_pick_strategy: earliest
result_date_source: candidate
require_same_resolver: TRUE
Nested Measures¶
Measures can be nested arbitrarily:
A AND (B OR C)
Evaluation proceeds bottom-up:
B OR Cresolves to all qualifying child rowsAresolves independentlyANDaligns rows onmeasure_resolver- the composite qualification date becomes the latest aligned child date
The canonical member shape is preserved at every level, which is why deeply nested logic can still feed report generation consistently.
