Skip to content

Commit

Permalink
func model page
Browse files Browse the repository at this point in the history
  • Loading branch information
niquola committed Aug 20, 2024
1 parent 8916cd7 commit cb0c140
Showing 1 changed file with 322 additions and 0 deletions.
322 changes: 322 additions & 0 deletions input/pagecontent/functional-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
This flattening transformation is described with a special resource type: ViewDefinition.
Although there are no universal flat views for most FHIR resources, we believe many useful use-case-specific views could exist.
ViewDefinitions are Canonical Resources.
ViewDefinitions can be published as part of Implementation Guides.
Complemented with standard ANSI SQL queries, they could be the foundation for interoperable analytics and reporting on FHIR.
This post will help you understand how ViewDefinition “works.”

ViewDefinition is an algorithm that describes the flattening transformation of FHIR resources, composed of combinations of few functions:

* `column({name:column_name,path: fhirpath},...)` - the main workhorse of transformation, this function will extract elements by fhirpath expressions and put the result into columns
* `where(fhirpath)` - function, which filters resources by fhirpath expression. For example, you may want to transform only specific profiles like blood pressure into a simple table
* `forEach(expr, transform)` - this function unnests collection elements into separate rows
* `select(rows1, rows2)` - this function cross-joins `rows1` and `rows2`, and is mostly used to join results of `forEach` with top-level columns
* `union(rows, rows)` - concatenates sets of rows. Main use case is combining rows from different branches of a resource (for example, `telecom` and `contact.telecom`).

A ViewDefinition is represented as a FHIR Resource ( JSON document) where the elements (keywords) correspond to the functions:

```js
{
"resourceType": "ViewDefinition",
"resource": "Patient",
// (0)
"where": [{filter: "active = true"}],
// (5)
"select": [
{
// (4)
"column": [
{"path": "getResourceKey()", "name": "id"},
{"path": "identifier.where(system='ssn')", "name": "ssn"},
]
},
{
// (3)
"unionAll": [
{
// (1)
"forEach": "telecom.where(system='phone')",
"column": [{"path": "value", "name": "phone"}]
},
{
// (2)
"forEach": "contact.telecom.where(system='phone')",
"column": [{"path": "value", "name": "phone"}]
}
]}
]
}
```

This view produce a table of patient contacts - row per telecom:

0. "where" filter only active patients
1. "forEach" unnest `Patient.telecom` and select phone
2. "forEach" unnest `Patient.contact.telecom` and select phone
3. "unionAll" concatinate results of 1 and 2
4. "column" statement extracts id and ssn
5. "select" statement cross-joins id and ssn with telecom phones

Here is example input and output for this ViewDefinition

```json
[
{
"resourceType": "Patient",
"id": "pt1",
"identifier": [{"system": "ssn", "value": "s1"}],
"telecom": [{"system": "phone", "value": "tt1"}],
"contact": [
{"telecom": [{"system": "phone", "value": "t12"}]},
{"telecom": [{"system": "phone", "value": "t13"}]}
]
},
{
"resourceType": "Patient",
"id": "pt2",
"identifier": [{"system": "ssn", "value": "s2"}],
"telecom": [{"system": "phone", "value": "t21"}],
"contact": [
{"telecom": [{"system": "phone", "value": "t22"}]},
{"telecom": [{"system": "phone", "value": "t23"}]}
]
}
]
```

### Result

| id | ssn | phone |
| -------- | -------- | -------- |
| pt1 | s1 | t11 |
| pt1 | s1 | t12 |
| pt1 | s1 | t13 |
| pt2 | s1 | t21 |
| pt2 | s1 | t22 |
| pt2 | s1 | t23 |





## FHIRPath subset

ViewDefnitions use [minimal subset of FHIRPath](https://build.fhir.org/ig/FHIR/sql-on-fhir-v2/StructureDefinition-ViewDefinition.html#supported-fhirpath-functionality) to make implementation as simple as possible. As well spec introduces few special functions:
* `getResourceKey` - inderectly get resource id. Sometimes it could be complicated, that's why this layer of indirection!
* `getReferenceKey(resourceType)` - similar function to get id from reference



## Functions / Keywords

Let’s walk through every function in detail with examples

### column

The column function extracts elements into columns using FHIRPath expressions. The algorithm starts by receiving a list of {name, path} pairs. For each record in the given context, it evaluates the path expression to extract the desired elements. The resulting values are then added as columns to the output row.

```json
{
"column": [
{"name": "id", "path": "getResourceKey()"},
{"name": "bod", "path": "birthDate"},
{"name": "first_name", "path": "name.first().given.join(' ')"},
{"name": "last_name", "path": "name.first().family"},
{"name": "ssn", "path": "identifier.where(system='ssn').value.first()"},
{"name": "phone", "path": "telecom.where(system='phone').value.first()"},
]
}
```

Here is the naive js implementation:

```javascript
function column(cols, rows) {
return rows.map((row)=> {
return cols.reduce((res, col ) => {
res[col.name] = fhirpath(col.path, row)
return res
}, {})
})
}
```

### where

The where function retains only those records for which it FHIRPath expression returns true.

```json
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"where": [
{"filter": "meta.profile.where($this = 'myprofile').exists()"},
{"filter": "active = 'true'"}
]
}
```

Basic js implementation:

```javascript
function where(exprs, rows) {
return rows.filter((row)=> {
return exprs.every((expr)=>{
return fhirpath(expr, row) == true;
})
})
}
```


### forEach & forEachOrNull
The `forEach` function is intended to flattening nested collections by applying a transformation to each element. It consists of FHIRPath expression for collection to iterate and transformation to apply to each item. This function is akin to `flatMap` or `mapcat` in other programming languages.

```json
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [{
"forEach": "name",
"column": [
{"path": "given.join(' ')", "name": "first_name"},
{"path": "family", "name": "last_name"}
]
}]
}
```

There are two versions of this function: `forEach` and `forEachOrNul`. The primary difference is that `forEach` removes records where the FHIRPath expression returns no results, whereas `forEachOrNull` keeps an empty record in such cases.

```javascript
function forEach(path, expr, rows) {
return rows.flatMap((row)=> {
return fhirpath(expr, row).map((item)=>{
// evalKeyword will call column, select or other functions
return evalKeyword(expr, item)
})
})
}
```

### select

The select function is used in combination with forEach to cross-join parent elements (like `Patient.id`) with unnested collection elements like ( `Patient.name` ).
This function merges columns from each row set, resulting in a comprehensive combination of data from the input collections.

```json
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [
{
"column": [
{"path": "getResourceKey()", "name": "id"}
]
},
{
"forEach": "name",
"column": [
{"path": "given.join(' ')", "name": "first_name"},
{"path": "family", "name": "last_name"}
]
}
]
}
```

Naive implementation is:

```javascript
function select(rows1, rows2){
return rows1.flatMap((r1)=> {
return rows2.map((r2)=>{
// merge r1 and r2
return { ...r1, ...r2 }
})
})
}

select([{a: 1}, {a: 2}], [{b: 1}, {b: 2}])
//=>
[{a: 1, b: 1},
{a: 1, b: 2},
{a: 2, b: 1},
{a: 2, b: 2}]
```

### unionAll

The `unionAll` function combines rows from different branches of a resource tree by concatenating multiple record sets. This function essentially concatinates several collections of records into a single, unified collection, preserving all rows from the input sets.

```json
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [
{
"column": [
{"path": "getResourceKey()", "name": "id"}
]
},
{
"unionAll": [
{
"forEach": "telecom.where(system='phone')",
"column": [{"path": "value", "name": "phone"}]
},
{
"forEach": "contact.telecom.where(system='phone')",
"column": [{"path": "value", "name": "phone"}]
}
]}
]
}
```

Implementation is just a simple concatination:

```javascript
function unionAll(rowSets){
return rowSet.flatMap((rows)=> { return rows})
}

unionAll([1,2,3], [3,4,5])
//=>
[1,2,3,3,4,5]
```

In resource, different keywords could appear at the same level.
For example, `select`, `forEach` and `unionAll` in the same JSON node.
To interpret such nodes, you have to re-order keywords (functions) according to precedence (higher bubble up):

* forEach(OrNull)
* select
* unionAll
* column



```js
{
"forEach": FOREACH,
"column": [COLUMNS], // got into select
"unionALL": [UNIONS], // got into select
"select": [SELECTS]
}
//=>
{
"forEach": FOREACH
"select": [
{"column": [COLUMNS]},
{"unionAll": [UNIONS]},
SELECTS...
]
}
```

### Reference Implementation

To understand functional model in more details
take a look at [reference javascript implementation](https://github.com/FHIR/sql-on-fhir-v2/sof-js)
- it's just ~400 lines of code.

0 comments on commit cb0c140

Please sign in to comment.