4. Data structures

Requirement prefix: R4 (openEHR's own name for the CLUSTER/ELEMENT package is Representation).

Where these invariants come from#

Read from the Release 1.0.2 publication (Data Structures Information Model, Rev 1.7.1, 5 Nov 2008, §5.2, §6.2), because the rendered RM page omits its class-definition tables. This section uses openEHR's own invariant names — Items_non_empty, Null_flavour_indicated, Period_consistency — so a reader can find each one in the class definition without a translation step (L10.5a).

Coverage#

  • R4.1 The crate MUST model ITEM_SINGLE, ITEM_LIST, ITEM_TABLE, ITEM_TREE, CLUSTER, ELEMENT, HISTORY, POINT_EVENT, and INTERVAL_EVENT.
  • R4.2 Every one of them MUST be a LOCATABLE (§5) and MUST therefore carry a runtime name and an archetype node id.

ELEMENT and the four ways to say nothing#

This is openEHR's most under-appreciated design decision, and the one a relational or FHIR-shaped pipeline most often destroys.

  • R4.3 An ELEMENT MUST have either a value or a null_flavour, never both and never neither (Null_flavour_indicated; Is_null_valid ties is_null to the absence of value).
  • R4.4 null_reason MUST be permitted only on an element with no value.
  • R4.5 null_flavour MUST be one of the four openEHR null flavours — 271|no information|, 253|unknown|, 272|masked|, 273|not applicable| — and the constructor MUST take a code rather than a free DV_CODED_TEXT, so that a fifth flavour cannot be invented.
  • R4.6 The four MUST remain distinguishable through construction, serialization, and validation. "No allergy history recorded" is 271|no information| and "no known allergies" is 273|not applicable|; software that treats them alike will eventually give a penicillin-allergic patient penicillin.
  • R4.7 272|masked| MUST be separately identifiable, because it is the only flavour that says a value exists. Consent filtering produces masked elements (§11), and a reader must be able to tell that from an unanswered question.

Containers#

  • R4.8 CLUSTER.items MUST be non-empty (Items_non_empty). An empty cluster is a heading with nothing under it; openEHR's way to say "not filled in" is a null ELEMENT, which carries a reason.
  • R4.9 ITEM_TABLE MUST expose row and column counts and cell access, and MUST report whether the table is regular rather than assuming it. Cell indices are zero-based, departing from openEHR's one-based element_at_cell_ij: this is a Rust API in which every other index is zero-based, and one crate with two conventions produces off-by-one errors that read as correct code.
  • R4.10 Every item structure MUST offer one traversal reaching every ELEMENT in document order, across all four shapes. De-identification and value extraction both need it, and writing it four times is how they diverge.

HISTORY and events#

  • R4.11 HISTORY MUST have events, or a summary, or both (Events_exists). A history with neither records that observations were made and supplies none of them.
  • R4.12 HISTORY.period MUST be positive when present, and is_periodic MUST be exactly "a period was declared" (Periodic_validity) — never inferred from event times, which would report a series as periodic because two nurses happened to chart on the hour.
  • R4.12a In a periodic history every event's offset from origin MUST be a whole multiple of period (Period_consistency). A series that declares a period its samples do not follow is not periodic, and software that resamples or graphs it on the strength of that declaration draws the wrong picture with nothing in the data looking wrong.
  • R4.12b EVENT.offset MUST be available, computed as openEHR defines it — time.diff(parent.origin). It is a derived value, so it is exposed on the history rather than the event: an EVENT here holds no back-pointer to its parent.
  • R4.12c Where an offset or a period is not a whole number of seconds — a calendar period of months or years, or an event whose time is not comparable with the origin — R4.12a MUST report not answerable rather than a verdict, for the reason given in R4.16.
  • R4.13 An event's time MUST NOT precede the history's origin, and validation MUST report it when it does (L10.6).
  • R4.14 INTERVAL_EVENT.time is the end of the interval it summarises. Documentation MUST say so wherever the attribute is exposed, because a reader that treats it as an instant places an eight-hour urine output at the moment the bag was measured.
  • R4.15 INTERVAL_EVENT.math_function MUST come from the openEHR event_math_function group.
  • R4.15a INTERVAL_EVENT.width MUST be non-negative. This is a declared narrowing: openEHR states no such invariant. What it buys is that interval_start_time (R4.16) cannot land after time, which would put an interval's start after its end. What it costs is that an instance carrying a negative width is refused rather than read.
  • R4.16 interval_start_time MUST be derivable where the arithmetic is exact, and MUST refuse where it is not: a width containing calendar months or years has no fixed length, because one month before 31 March is 28 February. Refusing is the correct answer (S1.12); approximating would place a clinical event on the wrong day, silently, in the direction nobody checks.
  • R4.17 EVENT.state MUST be modelled separately from EVENT.data. A blood pressure of 150/95 standing and the same value lying down are different findings.

View this page's source on GitHub — the crates are the source of truth; this site renders them.