Skip to main content

Terminology in SQL

The library can return the content of a FHIR value set or concept map as a Spark DataFrame, so that a SQL query can join to it like any other table. The DataFrames follow the relations defined by the SQL on FHIR Terminology in SQL page, so a query written against that specification runs unchanged once each relation is registered under the label the query uses.

The content is obtained from the configured terminology layer - a terminology server or the local terminology store - when the DataFrame is created, and held in the DataFrame. Every use of the DataFrame therefore sees the same content, however many times it is evaluated.

Value sets​

A value set is identified by its canonical URL, optionally followed by a |version suffix. The DataFrame has one row per member of the value set:

ColumnTypeNullContent
systemstringNoThe code system of the member
versionstringYesThe code system version used to determine membership
codestringNoThe member code
displaystringYesThe display text for the code
inactivebooleanYestrue where the code is inactive; null for most active codes

In this example, the conditions whose code belongs to a value set are selected with a semi-join, which tests membership without multiplying rows where the value set holds a code under more than one code system version.

from pathling import PathlingContext

pc = PathlingContext.create()
pc.spark.read.parquet("conditions.parquet").createOrReplaceTempView("conditions")
pc.value_set(
"http://example.org/ValueSet/cardiovascular-disease|2026"
).createOrReplaceTempView("cvd_codes")

pc.spark.sql("""
SELECT conditions.patient_id, conditions.code
FROM conditions
WHERE EXISTS (
SELECT 1
FROM cvd_codes
WHERE cvd_codes.system = conditions.system
AND cvd_codes.code = conditions.code
)
""").show()

An implicit value set, such as a SNOMED CT ECL expression, can be used in the same way. Build its URL with to_ecl_value_set (toEclValueSet in Java and Scala, tx_to_ecl_value_set in R), which encodes the expression. A raw ECL expression containing | characters would otherwise be read as a version suffix, because the canonical is split at its first |.

Concept maps​

A concept map is identified in the same way. The DataFrame has one row per mapping, plus one row for each source code that the map states has no mapping:

ColumnTypeNullContent
source_systemstringNoThe code system of the source code
source_versionstringYesThe source code system version the mapping was authored against
source_codestringNoThe source code
source_displaystringYesThe display text for the source code
target_systemstringYesThe code system the mapping translates into
target_versionstringYesThe target code system version
target_codestringYesThe target code; null where the map states there is no mapping
target_displaystringYesThe display text for the target code
relationshipstringYesA ConceptMapRelationship code; null where there is no mapping

The relationship is an R5 code. The equivalence of an R4 ConceptMap is converted to it, so that narrower becomes source-is-broader-than-target, for example. The |version suffix of a group's source and target is split into the version columns. Mappings that a flat table cannot represent, such as targets that carry dependsOn or product, cause the concept map to be rejected rather than partially returned. The group.unmapped default is not applied; write COALESCE(sct_to_icd10.target_code, conditions.code) to pass unmapped codes through.

A LEFT JOIN translates each code and keeps the codes that the map does not translate. The relationship test belongs in the ON clause, and admits null so that rows stating there is no mapping are kept:

pc.concept_map(
"http://example.org/ConceptMap/sct-to-icd10|2026"
).createOrReplaceTempView("sct_to_icd10")

pc.spark.sql("""
SELECT conditions.patient_id,
conditions.code,
sct_to_icd10.target_code,
sct_to_icd10.relationship
FROM conditions
LEFT JOIN sct_to_icd10
ON sct_to_icd10.source_system = conditions.system
AND sct_to_icd10.source_code = conditions.code
AND (sct_to_icd10.relationship IS NULL
OR sct_to_icd10.relationship <> 'not-related-to')
""").show()

Supplying a resource​

A ValueSet or ConceptMap that the terminology layer does not hold can be passed as FHIR R4 JSON instead:

PythonRJava and Scala
value_set_from_jsonpathling_value_set_from_jsonvalueSetFromJson
concept_map_from_jsonpathling_concept_map_from_jsonconceptMapFromJson

The resource must carry a url. Content that is not valid FHIR R4 is rejected rather than ignored, so an R5 ConceptMap, such as the worked example of the specification with its relationship and noMap elements, must be expressed with the R4 equivalence (unmatched in place of noMap) before it is supplied.

Where a supplied ValueSet carries an expansion, that expansion is the membership and the terminology layer is not consulted. An incomplete expansion, one that carries an offset or whose total exceeds the entries it lists, is rejected. A ValueSet that carries only a compose is expanded by the terminology layer. A supplied ConceptMap is converted directly.

with open("cardiovascular-disease.ValueSet.json") as f:
pc.value_set_from_json(f.read()).createOrReplaceTempView("cvd_codes")

Limits and failures​

The content of each DataFrame is held on the Spark driver, so its size is capped: 100,000 members for a value set and 100,000 mappings for a concept map. Each function takes an optional cap to raise or lower it (max_members and max_mappings in Python and R, a second argument in Java and Scala).

A value set or concept map that cannot be resolved is an error, never an empty DataFrame, so that a query never silently returns no rows because the terminology was missing. The same applies where the membership cannot be determined, where the version to use cannot be determined, where the concept map carries content that the table cannot represent, and where the content exceeds the cap. In Java and Scala the error is a TerminologyRelationException; in Python and R it is raised from that exception and carries its message, which names the value set or concept map and the reason.

The canonical URL and version resolved, the source, the member or mapping count, and for a value set the expansion identifier, timestamp and code system versions, are logged at INFO level by au.csiro.pathling.library.terminology.TerminologyRelations.