You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Suppress empty parent objects when required children are absent (on_missing / required_slots) #264
Sparse-join misses now suppress the nested object (#217), but a parent whose only meaningful content is missing children is still emitted as a hollow shell. Example: a blood-pressure MeasurementObservationSet wrapping systolic + diastolic MeasurementObservations — if neither reading exists, the set still emits with valid identity/constant slots but observations: []. Emitting it asserts "a blood-pressure measurement happened" when nothing was measured (the same silent-shape hazard as #211).
This can't reuse #217's local rule ("object anchored to its populated_from table; no row → no object") because the parent's populated_from row does exist. Suppression here is non-local: it depends on the derived children, and must propagate upward.
Proposed direction:
Add an opt-in on_missing: control on a class_derivation (e.g. skip | none | empty), defaulting to current behavior — pruning a parent whose populated_from row exists should never be automatic.
Pair it with required_slots: (or similar) to define "empty": the parent is suppressed when its required slots resolve to absent. Needed because constants (observation_type, unit) and parent-derived identity slots (associated_participant) are never null, so "all slots None" is the wrong test — emptiness must be declared, not inferred.
Suppression cascades: a suppressed parent should let its container re-evaluate its own required_slots.
This issue addresses the next step in a progression of "silent-shape" correctness hazards in the sparse-join pipeline. It introduces upward-propagating suppression for parent objects whose meaningful content is entirely absent — a problem that #217's fix cannot solve because it works at the wrong scope.
Sparse implicit-join miss leaves a nested object as {value: None} instead of absent; also causes data-dependent ambiguity enforcement inconsistency. Prerequisite for this issue.
(This issue.) Even after #217's fix, a parent object is emitted as a hollow shell when all its derived children are suppressed.
The Problem
After #217 is resolved, a sparse-join miss correctly suppresses the nested object. But the parent — whose populated_from row genuinely exists — still emits with valid identity/constant slots and an empty child list (e.g. observations: []).
Concrete example (BDCHM blood-pressure transform): A MeasurementObservationSet wraps systolic and diastolic MeasurementObservations. If neither reading exists in the source data, the set still emits with associated_participant, observation_type, etc. all populated — but observations: []. This silently asserts "a blood-pressure measurement occurred" when nothing was measured.
Why #217's local rule can't be reused:#217's suppression is anchored to populated_from — no row in the joined table → no object. Here the parent's populated_from row does exist; suppression depends on the derived children, not the source row. This requires non-local, upward-propagating logic.
Proposed Spec Changes
Three cooperating mechanisms:
on_missing: on class_derivation — opt-in enum (skip | none | empty), defaulting to current behavior. A parent is never automatically pruned; the user must declare intent.
required_slots: on class_derivation — declares which slots define "non-emptiness." Necessary because constants (observation_type, unit) and identity slots (associated_participant) are never null; "all slots None" is the wrong test and would never trigger.
Cascading suppression — when a parent is suppressed, its own container re-evaluates itsrequired_slots, allowing emptiness to propagate up a multi-level hierarchy.
Key Design Considerations
Opt-in is essential. Auto-pruning parents based on child absence would break transforms that legitimately want a parent record even with no children.
required_slots: vs. inferring emptiness. The proposal explicitly acknowledges that slot-level inference doesn't work because constants always have values. Declared required slots give the user control.
Cascading propagation. A suppressed grandchild that causes a suppressed child should then allow the grandparent to re-evaluate — this needs careful ordering in ObjectTransformer (post-child-resolution pass before parent emission).
Interaction with on_missing: none vs. skip. The semantics of each value need to be pinned down: does none emit null in the parent list? Does skip omit entirely? Does empty permit an empty object through?
Test scaffolding. The BDCHM blood-pressure case is an ideal scaffold test: two child observations, both absent → parent suppressed; one present → parent emitted with partial observations list.
Activity
github-actions commentedon Jun 15, 2026
Summary
This issue addresses the next step in a progression of "silent-shape" correctness hazards in the sparse-join pipeline. It introduces upward-propagating suppression for parent objects whose meaningful content is entirely absent — a problem that #217's fix cannot solve because it works at the wrong scope.
Context Chain
populated_fromreferences a different table with nojoins:— the original "silent-shape" hazard. Motivated the vocabulary.{value: None}instead of absent; also causes data-dependent ambiguity enforcement inconsistency. Prerequisite for this issue.The Problem
After #217 is resolved, a sparse-join miss correctly suppresses the nested object. But the parent — whose
populated_fromrow genuinely exists — still emits with valid identity/constant slots and an empty child list (e.g.observations: []).Concrete example (BDCHM blood-pressure transform): A
MeasurementObservationSetwraps systolic and diastolicMeasurementObservations. If neither reading exists in the source data, the set still emits withassociated_participant,observation_type, etc. all populated — butobservations: []. This silently asserts "a blood-pressure measurement occurred" when nothing was measured.Why #217's local rule can't be reused: #217's suppression is anchored to
populated_from— no row in the joined table → no object. Here the parent'spopulated_fromrow does exist; suppression depends on the derived children, not the source row. This requires non-local, upward-propagating logic.Proposed Spec Changes
Three cooperating mechanisms:
on_missing:onclass_derivation— opt-in enum (skip | none | empty), defaulting to current behavior. A parent is never automatically pruned; the user must declare intent.required_slots:onclass_derivation— declares which slots define "non-emptiness." Necessary because constants (observation_type,unit) and identity slots (associated_participant) are never null; "all slots None" is the wrong test and would never trigger.Cascading suppression — when a parent is suppressed, its own container re-evaluates its
required_slots, allowing emptiness to propagate up a multi-level hierarchy.Key Design Considerations
required_slots:vs. inferring emptiness. The proposal explicitly acknowledges that slot-level inference doesn't work because constants always have values. Declared required slots give the user control.ObjectTransformer(post-child-resolution pass before parent emission).on_missing: nonevs.skip. The semantics of each value need to be pinned down: doesnoneemitnullin the parent list? Doesskipomit entirely? Doesemptypermit an empty object through?observationslist.Status & Path Forward
on_missing:vocabulary was already surfaced as an option in Sparse implicit-join semantics: nested object on no-match + data-dependent ambiguity enforcement #217's design discussion, making this a natural continuation of that design thread.