Summary
A schema’s mapping tells Linkr where a database keeps its patients, stays, notes, concepts, events and drugs. Each of them is a relation: a query that returns a fixed list of columns, its contract. A relation is filled in through a form when one table is enough, or written in SQL when it takes joins — a check then verifies that it honours its contract.
Relations, not tables
Linkr’s pages never query person or patients directly. They query relations with fixed names — linkr_patient, linkr_visit, linkr_event_measurements… — that always return the same columns, whatever the database’s model: patient_id, birth_date, gender for patients; visit_id, start_datetime, end_datetime for hospitalizations. That list of columns, with their types, is the relation’s contract.
Your tables
person
death
The database’s model, whatever it is.
mapping →
linkr_patient
patient_id · birth_date · gender · death_datetime…
Always the same columns: the contract.
→
Linkr
Patient data, cohorts, statistics, catalog…
Reads the relations only.
The mapping is how each relation is produced from your tables. An OMOP database and a MIMIC database produce the same relations by different routes — and the same dashboard works on both.
The Mapping tab
On a schema’s page, the Mapping tab opens on the Source view, the one with the forms; the Diagram view places the relations on the table diagram. It is read-only until you click Edit, top right; Save — or Cmd/Ctrl + S — commits.
The sub-tabs sort the relations by clinical subject, each with its colour, the same as in the diagram. All shows them one after the other.
linkr_patientpersonLEFT JOIN death d ON d.person_id = p.person_idpatient_id*person_idbirth_datebirth_datetimebirth_yearⓘyear_of_birthgender_source_valuegender_source_valuedeath_datetimed.death_datetimeRaw values of gender_source_value meaning male and female.
MF—linkr_visitvisit_occurrencevisit_id*visit_occurrence_idpatient_id*person_idstart_datetime*visit_start_datetimeend_datetimevisit_end_datetimevisit_typevisit_source_valuecare_site_idcare_site_idUnit stays
Not mapped.
Clinical notes
Not mapped.
linkr_concept_omopconceptconcept_id*concept_idconcept_name*concept_nameconcept_codeconcept_codeterminology_idvocabulary_idcategorydomain_idsubcategoryconcept_class_idlinkr_event_measurementsmeasurementpatient_id*person_idconcept_id*measurement_concept_idstart_datetime*measurement_datetimevisit_idvisit_occurrence_idvisit_detail_idvisit_detail_idsource_concept_idmeasurement_source_concept_idvalue_numbervalue_as_numberunitunit_source_valueomoplinkr_event_diagnosescondition_occurrencepatient_id*person_idconcept_id*condition_concept_idstart_datetime*condition_start_datetimevisit_idvisit_occurrence_idend_datetimecondition_end_datetimeomoplinkr_drug_drugsdrug_exposurepatient_id*person_idconcept_id*drug_concept_idstart_datetime*drug_exposure_start_datetimevisit_idvisit_occurrence_idend_datetimedrug_exposure_end_datetimequantityquantityrouteroute_source_valuedose_source_valuedose_unit_source_valueomopadministration| Sub-tab | Its relations | What depends on it |
|---|---|---|
| Patient | Patients, and its gender values | Everything. With no patient relation, the Patient data page refuses to render. |
| Hospitalization | Hospitalizations and Unit stays | Stay-based cohort criteria, lengths of stay, the admission timeline, the catalog’s services. |
| Notes | Clinical notes | The Notes widget and text-based cohort criteria. |
| Concepts | One or more dictionaries; the first is the default | Showing names instead of codes, everywhere. |
| Events | As many relations as needed — measurements, diagnoses, procedures… | The Concepts page, cohort criteria, the patient timeline. |
| Drugs | As many relations as needed, each with its Kind | The same uses as events, plus doses, rates and durations. |
Patients, Hospitalizations, Unit stays and Clinical notes are single. Dictionaries, events and drugs are added in edit mode — Add dictionary, Add event relation, Add drug relation — and named: their Label, or their Key for a dictionary, gives the relation its name (linkr_event_measurements). A block’s bin removes it, after a confirmation — except the Patients block, which cannot be removed.
Filling a relation through the form
The form fits whenever one table carries the relation on its own. An empty block offers Map from a table; a filled block has three parts.
- Table — the source schema and table. Both fields suggest the tables the DDL knows, without ever forcing the value: a half-described model stays editable.
- Filter — a SQL condition on that table’s columns, to keep only part of it:
category = 'Vital signs'. This is what lets you draw several event relations from one table. - Columns — one row per contract column. Each is filled by a Column of the table, or by a Constant — a fixed value, like
'ICU'for a stay type. Required columns carry an asterisk; a column left empty comes back empty (NULL).
Outside edit mode, only the filled columns show. Some carry an ⓘ explaining what Linkr derives from them: birth_year is the fallback when birth_date is not mapped, and gender is computed on its own from gender_source_value.
Gender values
The Patients block also asks for the Gender values: the raw value of gender_source_value meaning male, the one meaning female, and optionally the one meaning unknown — M and F in OMOP, 1 and 2 elsewhere. Linkr fills gender with them, which the statistics and the sex criteria use.
An event relation’s dictionary
Every event or drug relation states which Dictionary turns its codes into names: Default — the first dictionary of the Concepts sub-tab —, a specific dictionary, or None (concepts named inline).
“None” means the concept column already holds the name
That is the case of a MIMIC drug field carrying “Vancomycin” rather than a numeric identifier: the column is then used as the name, and no join is made.
Getting this wrong produces no error: the join happens anyway, between a drug name and an identifier, and returns nothing. It is the first thing to check when a Concepts page stays empty.
A dictionary also accepts, beyond its contract, extra columns — Add extra column —, which take the extra_ prefix.
Drugs
Drugs have their own sub-tab because they have their own columns. Each relation has a Kind — Administrations, what was given, or Prescriptions, what was prescribed — and its contract adds the dose columns to the event ones: quantity, amount, rate, concentration and duration, each with its unit, and is_continuous for a continuous infusion.
The event columns — value, unit — are derived from the dose columns when you do not map them. A drug relation therefore works wherever an event relation does.
Writing a relation in SQL
The form reads one table only. Everything else is written in SQL: a date of death kept in another table, a code that is only found by joining terminology and code, or an entity-attribute-value warehouse — where a single measurement is spread over several rows that have to be put back together.
Each block’s SQL button — or Write SQL on an empty block — opens the relation’s window. It opens on the SQL generated from the form; as soon as you change it, it is marked Modified, and Reset goes back to the generated SQL. The query must be a single SELECT that names its columns exactly as the contract does (AS patient_id).
SQL — linkr_event_measurements
Generated from the form, or written by hand (Cmd+S saves). The contract on the right lists the columns to return.
Three tabs:
- SQL — the editor, and the Contract on the right: the columns to return, the required ones in bold, each with its expected type. Cmd/Ctrl + S saves, and runs the check again.
- Contract check — on the chosen database, Check describes what the query returns, without reading the data. Every contract column is filled, required, missing, of the wrong type, or not returned (NULL); columns outside the contract are ignored. A column of the wrong type is fixed with a
CASTin the query. - Preview — Run shows the first 100 rows the relation returns.
A schema has no data: the check runs on a database
The check and the preview run on a database that uses this schema, picked in the window. If there is none yet, the window says so: install one from this schema — or create it with New from schema, see Databases.
Once the SQL is saved, the block reads Defined in SQL, followed by the columns the last check found filled. Those columns are recorded with the query: that is how the rest of the app knows, for instance, whether an age criterion or a value in the timeline is available. Until a check has run, the block says so.
The SQL window saves straight away, without going through Edit, as long as you may edit the schema.
The form and the SQL do not add up
A relation defined in SQL ignores its form. Changing the form of such a relation regenerates the SQL and discards your edit: Linkr asks first, with Discard the custom SQL?
One last warning from the check is worth knowing: a window function over the whole relation (OVER without PARTITION BY) stops Linkr from filtering by patient upstream, and every patient page then scans the whole table. Use a source key as the id instead, or partition the window by patient.
One schema, several databases
A database copies the schema at the moment it is attached — the DDL as well as the mapping — and records which schema, and which version, its copy comes from. It does not point at it.
The schema
OMOP CDM 5.4 — version 1.3.0
Installed once in the workspace.
copy
”ICU” database
Copy of version 1.2.0
OverriddenMeasurements: redefined in SQL for this database
”MIMIC-IV demo” database
Copy of version 1.3.0
That is what makes a database robust: it keeps working if the schema is edited, deleted, or was never installed on the instance it lands on.
And it is what lets it have its own quirks. A database can override a relation of its copy: redefine it for itself only — for instance because its measurements are stored differently —, while the schema and the other databases stay untouched. In the database’s Mapping tab, such a relation carries the Overridden badge, and the SQL window checks and previews on the database directly. See A database’s mapping.
Editing a schema does not update existing databases
The correction does not propagate on its own. To apply it to a database, open that database’s Mapping tab: when the schema has changed since the copy, an Update from preset button replaces the copy with the current version, keeping the database’s overrides. Choosing a different schema in Edit, on the other hand, starts over: the copy is made again and the overrides are dropped.
Symmetrically, deleting a schema breaks no database — each one keeps its copy. Its card simply shows “Schema (not installed)” and stops being a link.
A few limits
- A column that does not exist raises no error. A typo in a column name does not break the relation: that column simply comes back empty. The contract check or the preview reveals it.
- A preset’s joins stay read-only in the form. A relation installed with a join — OMOP’s date of death, read from the
deathtable — shows its join greyed out: the table, the filter and the plain columns stay editable, the join itself is changed in SQL. - The check needs a database. With no database installed from the schema, a SQL relation saves but cannot be verified.
Going further
- Schemas — what a schema is, and its two halves.
- Getting and exploring a schema — the catalog, importing, the DDL and the diagram.
- A database’s mapping — overriding a relation, and following the schema’s updates.
- Concepts — what the dictionaries make possible.
- Concept mapping — aligning your local codes to a standard terminology.