sql-on-fhir
Expert guidance for implementing SQL on FHIR v2 ViewDefinitions and operations to create portable, tabular projections of FHIR data. Use this skill when the user asks to create ViewDefinitions, flatte
Файлы5
Открыть в GitHubSKILL.md.claude/skills/sql-on-fhir/SKILL.md
GitHubSQL on FHIR
SQL on FHIR v2 defines portable, tabular projections of FHIR resources using FHIRPath expressions. ViewDefinitions transform hierarchical FHIR data into flat tables for analytics.
Core Concepts
A ViewDefinition projects exactly one FHIR resource type into rows and columns. It contains:
resource: The FHIR resource type (Patient, Observation, etc.)select: Column definitions and row iteration logicwhere: Optional filtering criteriaconstant: Reusable values for expressions
ViewDefinition Structure
Basic structure:
{
"resourceType": "ViewDefinition",
"resource": "Patient",
"status": "active",
"select": [
{
"column": [
{ "name": "id", "path": "id" },
{ "name": "gender", "path": "gender" },
{ "name": "birth_date", "path": "birthDate" }
]
}
]
}
For complete element reference, see references/view-definition-structure.md.
Column Definitions
Each column has:
name: Database-friendly identifier (pattern:^[A-Za-z][A-Za-z0-9_]*$)path: FHIRPath expression extracting the valuetype(optional): FHIR primitive type URIcollection(optional): Set true if column may contain arraysdescription(optional): Human-readable explanation
{
"column": [
{
"name": "family_name",
"path": "name.first().family",
"type": "string",
"description": "Patient's primary family name"
}
]
}
Row Iteration with forEach
Use forEach to create one row per element in a collection. Without forEach, one row per resource is created.
Example: One row per patient name:
{
"resource": "Patient",
"select": [
{ "column": [{ "name": "id", "path": "id" }] },
{
"forEach": "name",
"column": [
{ "name": "family", "path": "family" },
{ "name": "given", "path": "given.first()" }
]
}
]
}
forEachOrNull: Same as forEach but keeps a row with nulls when the collection is empty.
Nested forEach: Create cross-products by nesting:
{
"forEach": "contact",
"select": [
{
"column": [
{
"name": "contact_phone",
"path": "telecom.where(system='phone').value"
}
]
},
{
"forEach": "name.given",
"column": [{ "name": "given_name", "path": "$this" }]
}
]
}
Recursive Traversal with repeat
Use repeat to recursively traverse nested structures to any depth. This is essential for resources with arbitrary nesting like QuestionnaireResponse items.
Constraint: Only one of forEach, forEachOrNull, or repeat may be specified per select.
{
"resource": "QuestionnaireResponse",
"select": [
{ "column": [{ "name": "response_id", "path": "id" }] },
{
"repeat": ["item", "answer.item"],
"column": [
{ "name": "link_id", "path": "linkId" },
{
"name": "answer_text",
"path": "answer.value.ofType(string).first()"
}
]
}
]
}
The view runner:
- Starts at the resource root
- Evaluates each path in the
repeatarray - For each result, recursively applies the same paths
- Continues until no more matches exist
- Unions all results from all levels
This produces a flat table with all items regardless of nesting depth.
Filtering with where
Filter resources using FHIRPath expressions that must evaluate to true:
{
"resource": "Patient",
"where": [
{"path": "active = true"},
{"path": "name.exists()"}
],
"select": [...]
}
Multiple where clauses are ANDed together.
Constants
Define reusable values referenced via %name syntax:
{
"constant": [{ "name": "use_type", "valueString": "official" }],
"select": [
{
"forEach": "name.where(use = %use_type)",
"column": [{ "name": "official_name", "path": "family" }]
}
]
}
Supported constant types: valueString, valueInteger, valueBoolean, valueDecimal, valueDate, valueDateTime, valueCode.
Combining Data with unionAll
Combine multiple selection paths with matching column schemas:
{
"select": [
{ "column": [{ "name": "id", "path": "id" }] },
{
"unionAll": [
{
"forEach": "telecom",
"column": [
{ "name": "contact", "path": "value" },
{ "name": "system", "path": "system" }
]
},
{
"forEach": "contact.telecom",
"column": [
{ "name": "contact", "path": "value" },
{ "name": "system", "path": "system" }
]
}
]
}
]
}
FHIRPath Subset
ViewDefinitions use a minimal FHIRPath subset. Key functions:
| Function | Description |
|---|---|
first() |
First element of collection |
exists() |
True if collection has elements |
empty() |
True if collection is empty |
where(expr) |
Filter collection by condition |
ofType(type) |
Filter to specific FHIR type |
extension(url) |
Get extension by URL |
join(sep) |
Join collection into string |
getResourceKey() |
Resource ID (indirect access) |
getReferenceKey(type?) |
Extract ID from reference |
Path navigation:
- Dot notation:
name.family - Indexing:
name[0].family $this: Current context element
For complete FHIRPath reference, see references/fhirpath-subset.md.
Working with Extensions
Extract extension values using the extension() function:
{
"column": [
{
"name": "birth_sex",
"path": "extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex').value.ofType(code).first()"
}
]
}
Nested extensions:
{
"path": "extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-race').extension('ombCategory').value.ofType(Coding).code.first()"
}
Profiles
ShareableViewDefinition: For portable definitions. Requires:
url: Canonical identifiername: Database-safe identifierfhirVersion: Target FHIR version(s)- Column
type: Explicit types on all columns
TabularViewDefinition: For scalar/CSV output. Enforces:
- No
collection: truecolumns - Primitive types only (string, boolean, integer, etc.)
Design Constraints
ViewDefinitions intentionally exclude:
- Cross-resource joins (use SQL after flattening)
- Sorting, aggregation, limits (apply in analytics layer)
- Output format specification (runner determines format)
Operations
SQL on FHIR defines two operations for executing ViewDefinitions:
$viewdefinition-run
Synchronous execution returning results immediately.
GET [base]/ViewDefinition/[id]/$run?_format=csv
POST [base]/ViewDefinition/$run
Key parameters:
viewReferenceorviewResource: The ViewDefinition to execute_format: Output format (json,ndjson,csv,parquet)patient,group: Filter by patient/group_limit: Maximum rows
$export
Asynchronous bulk export for large datasets.
POST [base]/ViewDefinition/$export
Uses async pattern:
- POST with
Prefer: respond-async→ 202 Accepted + status URL - Poll status URL until complete
- Download results from output URLs
Key parameters:
view: One or more ViewDefinitions to export_format: Output formatpatient,group,_since: Filtering options
For complete operation details, parameters, and examples, see references/operations.md.
Examples
For comprehensive examples including Patient demographics, Condition flattening, blood pressure extraction, and complex unnesting patterns, see references/examples.md.