Skip to main content

2 posts tagged with "PostgreSQL"

PostgreSQL

View All Tags

Tired of AQL to fetch openEHR data?

· 4 min read
Borut Jures
Author of ArcEHR

Traditionally, querying hierarchical and deeply nested openEHR Reference Model (RM) data requires using AQL (Archetype Query Language).

In ArcEHR, we map the openEHR Reference Model directly to a graph database using ArcadeDB. It also comes with a free and open-source data browser ArcadeDB Studio.

ArcadeDB Studio

Best of all? You can still query and manipulate this graph data using standard SQL through any PostgreSQL GUI client or library.

Here is a quick look at how INSERT and SELECT work under the hood.

tip

You can use your preferred PostgreSQL client to work with the ArcEHR database.

Developers can use their preferred PostgreSQL library to work with the ArcEHR database.

Inserting openEHR Data

To insert openEHR data, we use a simple SQL INSERT statement targeting the create_composition table. Behind the scenes, a database trigger automatically processes this payload, shredding the JSON and storing the composition directly into RM Document nodes and relationship edges.

INSERT INTO create_composition (ehr_id, content) VALUES ("ehr-1", {
"archetype_node_id": "openEHR-EHR-COMPOSITION.encounter.v1",
"name": {"value": "Vital Signs"},
"uid": {"_type": "OBJECT_VERSION_ID", "value": "8849182c::openEHRSys.example.com::1"},
"archetype_details": {
"archetype_id": {"value": "openEHR-EHR-COMPOSITION.encounter.v1"},
"template_id": {"value": "Example.v1::c7ec861c-c413-39ff-9965-a198ebf44747"},
"rm_version": "1.0.2"
},
"language": {"terminology_id": {"value": "ISO_639-1"}, "code_string": "en"},
"content": [{"_type": "COMPOSITION"}]
})

The schema of our create_composition table strictly mirrors the openEHR REST API, keeping integration painless.

FieldTypeRequiredDefaultAllowed values
ehr_idSTRINGMandatory
preferSTRINGOptionalreturn=minimalreturn=(representation|minimal|identifier)
acceptSTRINGOptionalapplication/jsonapplication/(json|xml)
content_typeSTRINGOptionalapplication/jsonapplication/(json|xml)
openehr_item_tagARRAYOptional-
openehr_version_item_tagARRAYOptional-
contentSTRINGMandatory

Querying openEHR Data (Zero Joins Needed)

Because this is a graph database, we use the database's native out() function to traverse edges. To find all blood pressure records for an EHR or Composition, we simply traverse the corresponding edge. No JOINs, no performance degradation as your dataset grows.

Scenario A: Simple SELECT with One Archetype

Let’s fetch the blood pressure data for a specific EHR:

SELECT $blood_pressure.$event AS blood_pressure
FROM EHR
LET $blood_pressure = out(`openEHR-EHR-OBSERVATION.blood_pressure.v2`)
WHERE $ehr = "ehr-1"

Result:

{
"blood_pressure": [[[
{"_type": "ELEMENT", "archetype_node_id": "at0004",
"name": {"_type": "DV_TEXT", "value": "Systolic"},
"value": {"_type": "DV_QUANTITY",
"magnitude": 140.5,
"units": "mm[Hg]"
}
},
{"_type": "ELEMENT", "archetype_node_id": "at0005",
"name": {"_type": "DV_TEXT", "value": "Diastolic"},
"value": {"_type": "DV_QUANTITY",
"magnitude": 80.1,
"units": "mm[Hg]"
}
}
]]]
}

Scenario B: Querying Multiple Archetypes with Filters

What if we want both blood pressure and pulse for a patient, filtered by specific clinical thresholds?

We can leverage the CONTAINS operator to traverse and filter nested collections in a single, readable query:

SELECT $blood_pressure.$event AS blood_pressure, $pulse.$event AS pulse
FROM EHR
LET $blood_pressure = out(`openEHR-EHR-OBSERVATION.blood_pressure.v2`),
$pulse = out(`openEHR-EHR-OBSERVATION.pulse.v2`)
WHERE $ehr = "ehr-1"
AND $blood_pressure.$event CONTAINS (
archetype_node_id = "at0004" AND -- Systolic
value.magnitude > 140 AND value.units = "mm[Hg]")))
AND $pulse.$event CONTAINS (
archetype_node_id = "at0004" AND -- Rate
value.magnitude > 80 AND value.units = "/min")))

This returns a clean, structured JSON object containing both clinical data points simultaneously—without a single SQL JOIN:

{
"blood_pressure": [[[
{"_type": "ELEMENT", "archetype_node_id": "at0004",
"name": {"_type": "DV_TEXT", "value": "Systolic"},
"value": {"_type": "DV_QUANTITY",
"magnitude": 140.5,
"units": "mm[Hg]"
}
},
{"_type": "ELEMENT", "archetype_node_id": "at0005",
"name": {"_type": "DV_TEXT", "value": "Diastolic"},
"value": {"_type": "DV_QUANTITY",
"magnitude": 80.1,
"units": "mm[Hg]"
}
}
]]],
"pulse": [[[
{"_type": "ELEMENT", "archetype_node_id": "at0004",
"name": {"_type": "DV_TEXT", "value": "Rate"},
"value": {"_type": "DV_QUANTITY",
"magnitude": 83.7,
"units": "/min"
}
}
]]]
}

Developer Tooling

If you want to visualize your graph traversals, you can use the open-source ArcadeDB Studio browser to run and analyze your queries visually. Or, simply stick to your favorite PostgreSQL client and libraries — ArcEHR speaks PostgreSQL natively.

TypeScript PostgreSQL for openEHR data

· 5 min read
Borut Jures
Author of ArcEHR

One of the biggest hurdles in healthtech development is the sheer complexity of querying hierarchical clinical schemas. Usually, this means learning complex, niche query languages.

If you are using ArcEHR, you don't need any of that. You can query and insert graph-mapped openEHR data using the standard, vanilla pg library in TypeScript.

Because ArcEHR presents a PostgreSQL-compatible interface over ArcadeDB, your existing database drivers, parameterized queries, and connection pools work out of the box.

Here is how easily you can handle openEHR CRUD operations in TypeScript:

1. The Setup

No custom database clients required. Just import the standard pg driver, initialize your client, and connect.

import { Client } from 'pg'
const client = await new Client({
database: 'arcehr',
port: 5432,
user: 'root',
password: 'arcadedb'}).connect()

2. Inserting openEHR Data with Type Safety

When inserting openEHR data, you pass your structured JSON payload directly as a parameterized query. A trigger on the create_composition table handles the heavy lifting, automatically shredding the JSON into RM Document types and graph edges.

// Insert a new composition
const content = {
archetype_node_id: 'openEHR-EHR-COMPOSITION.encounter.v1',
name: {value: 'Vital Signs'},
uid: {_type: 'OBJECT_VERSION_ID', value: '8849182c::openEHRSys.example.com::1'},
archetype_details: {
archetype_id: {value: 'openEHR-EHR-COMPOSITION.encounter.v1'},
template_id: {value: 'Example.v1::c7ec861c-c413-39ff-9965-a198ebf44747'},
rm_version: '1.0.2',
},
language: {terminology_id: { value: 'ISO_639-1' }, code_string: 'en'},
content: [{ _type: 'OBSERVATION' }]
}
const insert = `INSERT INTO create_composition (ehr_id, content) VALUES ($1, $2)`
await client.query(insert, ['ehr-1', content])

The Response: You receive a clean response back, confirming the target EHR and returning the database record ID (@rid):

{
ehr_id: 'ehr-1',
content: '{"archetype_node_id":"openEHR-EHR-COMPOSITION.encounter.v1","name":{"value":"Vital Signs"},"uid":{"_type":"OBJECT_VERSION_ID","value":"8849182c::openEHRSys.example.com::1"},"archetype_details":{"archetype_id":{"value":"openEHR-EHR-COMPOSITION.encounter.v1"},"template_id":{"value":"Example.v1::c7ec861c-c413-39ff-9965-a198ebf44747"},"rm_version":"1.0.2"},"language":{"terminology_id":{"value":"ISO_639-1"},"code_string":"en"},"content":[{"_type":"COMPOSITION"}]}',
prefer: 'return=minimal',
content_type: null,
accept: null,
'@rid': '#72:1'
}

3. Querying openEHR Data (Zero-Join Graph Traversal)

Because ArcEHR stores this data as a graph under the hood, we can traverse nested clinical paths in a single query without performance-killing relational JOINs.

Scenario A: Fetching Blood Pressure

Lets pull all blood pressure records for a specific EHR. Note how we use standard $1 placeholders to prevent SQL injection:

const select = `SELECT $blood_pressure.$event AS blood_pressure
FROM EHR
LET $blood_pressure = out("openEHR-EHR-OBSERVATION.blood_pressure.v2")
WHERE $ehr = $1`
await client.query(select, ['ehr-1'])

Resulting JSON: You get back clean, structured openEHR Reference Model JSON elements directly in your application:

{
blood_pressure: [[[
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0004","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Systolic"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":746.5,"units":"mm[Hg]"}},
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0005","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Diastolic"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":526.1,"units":"mm[Hg]"}}
]],[[
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0004","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Systolic"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":746.5,"units":"mm[Hg]"}},
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0005","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Diastolic"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":526.1,"units":"mm[Hg]"}}
]]]
}

Scenario B: Multi-Archetype Queries with Path Filtering

What if you need to fetch both blood pressure and pulse, but only return records where the patient's vitals exceed specific safety thresholds?

We use the CONTAINS operator to query inside the nested clinical arrays:

const select = `SELECT $blood_pressure.$event AS blood_pressure, $pulse.$event AS pulse
FROM EHR
LET $blood_pressure = out("openEHR-EHR-OBSERVATION.blood_pressure.v2"),
$pulse = out("openEHR-EHR-OBSERVATION.pulse.v2")
WHERE $ehr = $1
AND $blood_pressure.$event CONTAINS (
archetype_node_id = "at0004" AND -- Systolic
value.magnitude > 140 AND value.units = "mm[Hg]")
AND $pulse.$event CONTAINS (
archetype_node_id = "at0004" AND -- Rate
value.magnitude > 80 AND value.units = "/min")`
await client.query(select, ['ehr-1'])

Result:

{
blood_pressure: [[[
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0004","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Systolic"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":746.5,"units":"mm[Hg]"}},
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0005","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Diastolic"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":526.1,"units":"mm[Hg]"}}
]],[[
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0004","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Systolic"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":746.5,"units":"mm[Hg]"}},
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0005","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Diastolic"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":526.1,"units":"mm[Hg]"}}
]]],
pulse: [[[
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0004","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Rate"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":636.7,"units":"/min"}}
]],[[
{"@type":"ELEMENT","_type":"ELEMENT","archetype_node_id":"at0004","name":{"@type":"DV_TEXT","_type":"DV_TEXT","value":"Rate"},"value":{"@type":"DV_QUANTITY","_type":"DV_QUANTITY","magnitude":636.7,"units":"/min"}}
]]]
}

No database schema migrations, no nested relational mapping code, and no performance loss. Just standard PostgreSQL queries executing lightning-fast graph traversals in Node.js.

The Takeaway

When building modern clinical applications, you shouldn't have to compromise between the rich standards of openEHR and developer-friendly tooling. ArcEHR lets you use the standard Node/TypeScript ecosystem you already know and love to query complex health data seamlessly.