Skip to content

Multi-result mappings

A multi-result rule produces more than one output value per input row. The engine fans the source row into one CDM row per result — this is the standard pattern for "one source observation maps to several OMOP concepts" and similar 1-to-N transformations.

The [] and [N] suffixes

The suffix tells the parser the rule is multi-result.

Suffix Meaning
[] Variable number of results — produce as many rows as matches.
[N] Exactly N results expected. Fewer → row is skipped + logged.
UsagiMap("file.csv")[]    # variable
UsagiMap("file.csv")[3]   # exactly 3

LoopOver declares how multi-results combine

When several CDM fields all carry multi-result rules, the engine needs to know whether to zip them (i-th of one paired with i-th of another) or take the cartesian product. LoopOver on the CDM table is what declares this:

logic: LoopOver([:field_one, :field_two])           # zip
logic: LoopOver([:field_one], [:field_two])         # cartesian
logic: LoopOver([:field_one], [:field_two], [:f3])  # 3-way cartesian

Within a single bracketed group, all fields are paired by index — they must produce the same number of results at runtime. Across groups, every combination is generated.

The all= keyword pulls in fields that aren't multi-result but should still be carried into every output row:

logic: LoopOver(all=[:person_id], [:year_of_birth, :birth_datetime], [:month_of_birth])

:person_id is single-valued and rides along; the [:year_of_birth, :birth_datetime] group is zipped (i-th year paired with i-th datetime); the [:month_of_birth] group joins as a cartesian factor.

Worked example — multi-result UsagiMap with dependent ID generation

Source: intnum.csv with four integer columns. Goal: map them through a Usagi file (which can return multiple matches per input) and let the CDM generate person_id values that follow the multi-result of year_of_birth.

CDM person.year_of_birth:

logic: UsagiMap("B.csv")[]
logic: AssertNot(21)

The [] says "every match becomes a result". AssertNot(21) runs on each result — any result equal to 21 fails that path, leaving only the surviving years.

CDM person.person_id:

logic: AutoGenId([:year_of_birth, :month_of_birth])[]

AutoGenId takes a list of dependency fields. With [] it produces one new id per combined-result row — the engine sees that :year_of_birth is multi-result and re-dispatches AutoGenId accordingly.

CDM person.birth_datetime (computed from the multi-result year):

logic: GetCDMFieldValue(:year_of_birth)[] => :year
logic: Add(1900) => :year
logic: MergeDate([:year, "year"])

GetCDMFieldValue(:year_of_birth)[] reads the multi-result of another field on the same CDM table — this is the cleanest way to keep two fields' multi-results in lockstep.

CDM person (table-level):

logic: LoopOver(all=[:person_id], [:year_of_birth, :birth_datetime], [:month_of_birth])

person_id is generated once per output row; year_of_birth and birth_datetime are zipped (index-paired); month_of_birth factors in as a cartesian dimension.

Result-index dispatch (block syntax)

When a multi-result rule produces a fixed number of results and each result deserves a different downstream chain, the block syntax dispatches by index:

SomeRule()[3] => {
  Rule_A1()
  Rule_A2()
}, {
  Rule_B1()
}, {
  Rule_C1()
  Rule_C2()
  Rule_C3()
}

Result [0] enters block 1, [1] block 2, [2] block 3. With [] (variable count) the same block runs against every result.

Caveats

  • Empty results skip the row. Rule()[] that returns zero matches drops the source row — log it with an AssertNot or a default if you need visibility.
  • LoopOver is mandatory when a CDM table has any multi-result field, even if there is only one such field.
  • Spawn(:array) is the new-mode replacement when the multi-result is collected as an ARRAY value rather than fanned out immediately. See Arrays and New Mode.

See also

Source tests

tests/test062/ — multi-result UsagiMap, AutoGenId with dependencies, GetCDMFieldValue, the all= form of LoopOver. tests/test081/ — multi-result Use()[N] and AutoGenId()[N].