Skip to content

Investigate how to apply FHIR-35915 Semantics to the CQL Evaluation Engine #3915

Description

@alexanderkiel

Context

CQL intervals may have null boundaries, and the meaning depends on whether the boundary is closed or open:

  • Closed (Interval[null, 5]) — the boundary is the minimum/maximum value of the point type. Unambiguous, and every part of the spec agrees.
  • Open (Interval(null, 5]) — the boundary is unknown. This is where the spec has historically been inconsistent.

FHIR-35915 ("Clarify interval computation semantics with open null boundaries", Technical Correction, resolved Persuasive against CQL 1.5) settles it: an open null boundary is an uncertainty over a bounded range, not a plain null. The normalization given in the ticket:

If interval.low  is null then interval.low  = Uncertainty(minimumValue, interval.high)
If interval.high is null then interval.high = Uncertainty(interval.low,  maximumValue)

The bound matters. In Interval(null, 7] the unknown low is not arbitrary — it is at most 7, because the interval must be valid. Many comparisons are therefore determinate even though a boundary is unknown, and returning null for them is wrong.

The ticket was accepted on the grounds that the JavaScript reference implementation (cql-execution) already behaves this way.

The problem

The correction was applied to Chapter 5 (Language Semantics) only. Other parts of the spec still describe plain null propagation:

  • Appendix B — CQL Reference: Start returns null when the low boundary is open and the low value is null; Overlaps, In, Includes, Meets, Before/After are all defined as "using the semantics described in the Start and End operators".
  • ELM schema documentation (expression.xsd, Interval): states that computations involving an open null boundary will result in null.

An implementation written against Appendix B alone — which is the natural reading, since that's where the operator semantics live — will produce null where the accepted semantics require true or false.

Worked examples

Cases where the two readings diverge (X overlaps Y):

Expression Appendix B FHIR-35915
Interval(null, 7] overlaps Interval[5, 10] null true
Interval[7, null) overlaps Interval[5, 10] null true
Interval(null, 5] meets after Interval[11, null) null false

Cases where null is genuinely correct under both readings — the uncertainty range really does straddle the comparison point:

Expression Result
Interval(null, 7] overlaps Interval[1, 6] null

(If the low boundary were 6, X is the unit interval {7} and there is no overlap; if it were 2, there is. Genuinely unknown.)

Cases that were already correct under both readings, worth keeping as regression guards:

Expression Result
Interval(null, 3] overlaps Interval[5, 10] false
Interval[12, null) overlaps Interval[5, 10] false

Test expressions from the ticket itself:

define TestIntervalNull1: end of (Interval[1, 10] intersect Interval[5, null)) <= 10  // true
define TestIntervalNull2: end of (Interval[1, 10] intersect Interval[5, null)) >= 5   // true
define TestIntervalNull3: end of (Interval[1, 10] intersect Interval[5, null)) > 10   // false
define TestIntervalNull4: end of (Interval[1, 10] intersect Interval[5, null)) < 5    // false

Investigation tasks

  • Determine what Blaze currently returns for the divergent cases above. Add them as tests first, marked with expected values per FHIR-35915, so the gap is visible.
  • Check whether the CQL conformance suite (cqframework/cql-tests) actually carries TestMeetsAfterNull with the corrected false and the four TestIntervalNull* cases. A Persuasive resolution means accepted, not necessarily that the tests were regenerated — if they're absent, the suite won't catch this either way and we need our own coverage.
  • Compare against cql-execution (JS, cited in the ticket as already conforming) and the Java engine in cqframework/clinical_quality_language, which may not have been updated. Knowing where each stands tells us what conformance actually looks like in practice.
  • Decide on the boundary representation. The cheapest route is a boundary value that is either a point or a [lo, hi] range, with the comparison operators returning true/false when the range does not straddle and null when it does. Every interval operator then falls out without special-casing.
  • Check whether Blaze already has an uncertainty representation for date/time imprecision (duration between on partial dates produces uncertainties by the same Chapter 5 rules). If so, reuse it rather than introducing a second mechanism.
  • Clarify what start of / end of should return to CQL-land when the boundary is uncertain. Chapter 5 says uncertainty is implicitly converted and does not surface in the syntax, so this needs a defined answer rather than leaking an internal type.
  • Scope check: which operators are affected beyond the comparison set? At minimum intersect, union, except, width of, duration/difference of, collapse, expand.

Follow-up

Consider filing a separate HL7 JIRA ticket noting that Appendix B's Start/End definitions and the ELM schema documentation were not updated per FHIR-35915. Narrow, non-substantive, and it removes the trap for the next implementer.

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions