Skip to content

Suppress empty parent objects when required children are absent (on_missing / required_slots) #264

Open

Description

@amc-corey-cox

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.

Notes:

Origin: design discussion while fixing #217; real-world case from a BDCHM blood-pressure transform.

Activity

  1. github-actions commented on Jun 15, 2026

    @github-actions

    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

    Issue Status Summary
    #211 ✅ Closed (2026-06-12) Silent nulls when a nested populated_from references a different table with no joins: — the original "silent-shape" hazard. Motivated the vocabulary.
    #217 🔴 Open (v0.5.3 milestone) 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.
    #264 🔴 Open (v0.6.0 target) (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:

    1. 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.

    2. 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.

    3. 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

    • 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.

    Status & Path Forward

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions