Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .example_dotenv
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
ENGINE=sqlite://///your/database/url/goes/here/test.db
SOURCE_PATH=/where/do/you/keep/unzipped/athena_source/files/
SOURCE_PATH=/where/do/you/keep/unzipped/athena_source/files/
OMOP_CLINICAL_SCHEMA=omop
OMOP_HEALTH_SYSTEM_SCHEMA=omop
OMOP_HEALTH_ECONOMIC_SCHEMA=omop
OMOP_STRUCTURAL_SCHEMA=omop
OMOP_UNSTRUCTURED_SCHEMA=omop
OMOP_METADATA_SCHEMA=omop
OMOP_VOCABULARY_SCHEMA=vocabulary
OMOP_DERIVED_SCHEMA=results
124 changes: 124 additions & 0 deletions docs/api/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Configuration

OMOP Alchemy resolves table schemas at import time from a small set of environment
variables. This keeps the ORM static and easy to read while still allowing a
deployment to choose its schema layout once.

## Schema variables

![OMOP CDM v5.4 category map](https://ohdsi.github.io/CommonDataModel/images/cdm54.png)

The schema categories in OMOP Alchemy are based on the OMOP CDM v5.4 groupings
shown above. In practice, that means each ORM table is assigned to one of these
category buckets, and each bucket resolves to one schema variable.

OMOP Alchemy uses these category buckets as the source of truth:

- `clinical`
- `health_system`
- `health_economic`
- `structural`
- `unstructured`
- `metadata`
- `vocabulary`
- `derived`

Each table belongs to one of those categories, and therefore resolves its schema
from the corresponding `OMOP_*_SCHEMA` variable.

The table categories are grouped by default as follows:

- `OMOP_CLINICAL_SCHEMA=omop`
- `OMOP_HEALTH_SYSTEM_SCHEMA=omop`
- `OMOP_HEALTH_ECONOMIC_SCHEMA=omop`
- `OMOP_STRUCTURAL_SCHEMA=omop`
- `OMOP_UNSTRUCTURED_SCHEMA=omop`
- `OMOP_METADATA_SCHEMA=omop`
- `OMOP_VOCABULARY_SCHEMA=vocabulary`
- `OMOP_DERIVED_SCHEMA=results`

Variable-to-tables mapping:

- `OMOP_CLINICAL_SCHEMA`
- `person`
- `condition_occurrence`
- `death`
- `device_exposure`
- `drug_exposure`
- `measurement`
- `observation`
- `procedure_occurrence`
- `specimen`
- `OMOP_HEALTH_SYSTEM_SCHEMA`
- `care_site`
- `location`
- `provider`
- `visit_occurrence`
- `visit_detail`
- `OMOP_HEALTH_ECONOMIC_SCHEMA`
- `cost`
- `payer_plan_period`
- `OMOP_STRUCTURAL_SCHEMA`
- `episode`
- `episode_event`
- `fact_relationship`
- `OMOP_UNSTRUCTURED_SCHEMA`
- `note`
- `note_nlp`
- `OMOP_METADATA_SCHEMA`
- `cdm_source`
- `metadata`
- `OMOP_VOCABULARY_SCHEMA`
- `concept`
- `concept_ancestor`
- `concept_class`
- `concept_relationship`
- `concept_synonym`
- `domain`
- `drug_strength`
- `relationship`
- `source_to_concept_map`
- `vocabulary`
- `OMOP_DERIVED_SCHEMA`
- `cohort`
- `cohort_definition`
- `condition_era`
- `dose_era`
- `drug_era`
- `observation_period`

Set any of these to a different schema name to override the default. Set the value
to `none` or `null` to leave that category schema-less.

Operational implications when changing a category schema:

- Queries against that category will target the new schema because ORM table
objects carry the resolved schema.
- Maintenance commands will inspect/create/manage those tables in the same schema
unless `--db-schema` is explicitly provided as an override.
- Existing tables are not migrated automatically. If you change a schema variable,
move data/DDL separately or create the target tables before running workloads.

## Import order

Load environment variables before importing the CDM model package. The ORM classes
read their schema mixins during class construction.

For example:

```python
from omop_alchemy import load_environment

load_environment(".env")

from omop_alchemy.cdm.model.vocabulary import Concept

print(Concept.__table__.schema)
```

## Engine variables

The database engine is still resolved from the existing engine variables:

- `ENGINE_<SCHEMA>` when `engine_schema` is provided
- `ENGINE` as the fallback
4 changes: 4 additions & 0 deletions docs/getting-started/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Resolution order:
`engine_schema` selects the configured engine URL (`ENGINE_<SCHEMA>` or `ENGINE`).
`db_schema` selects the schema inside that database.

Schema-aware ORM tables are configured from environment variables before the model
package is imported. See [Configuration](../api/configuration.md) for the schema
env vars and defaults.

---

## Backend support at a glance
Expand Down
41 changes: 41 additions & 0 deletions omop_alchemy/cdm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from .base import (
CDMTableBase,
ClinicalSchemaMixin,
DerivedSchemaMixin,
HealthEconomicSchemaMixin,
HealthSystemSchemaMixin,
MetadataSchemaMixin,
StructuralSchemaMixin,
UnstructuredSchemaMixin,
VocabularySchemaMixin,
cdm_table,
merge_table_args,
omop_index,
omop_primary_key_index_name,
omop_table_options,
optional_concept_fk,
optional_int,
required_concept_fk,
required_int,
)

__all__ = [
"CDMTableBase",
"ClinicalSchemaMixin",
"DerivedSchemaMixin",
"HealthEconomicSchemaMixin",
"HealthSystemSchemaMixin",
"MetadataSchemaMixin",
"StructuralSchemaMixin",
"UnstructuredSchemaMixin",
"VocabularySchemaMixin",
"cdm_table",
"merge_table_args",
"omop_index",
"omop_primary_key_index_name",
"omop_table_options",
"optional_concept_fk",
"optional_int",
"required_concept_fk",
"required_int",
]
18 changes: 18 additions & 0 deletions omop_alchemy/cdm/base/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
from .cdm_table_base import CDMTableBase
from .decorators import cdm_table
from .schema_mixins import (
ClinicalSchemaMixin,
DerivedSchemaMixin,
HealthEconomicSchemaMixin,
HealthSystemSchemaMixin,
MetadataSchemaMixin,
StructuralSchemaMixin,
UnstructuredSchemaMixin,
VocabularySchemaMixin,
)
from .column_helpers import required_concept_fk, optional_concept_fk, optional_int, required_int
from .column_mixins import ValueMixin, ReferenceTable, DatedEvent, PersonScoped, HealthSystemContext, FactTable
from .indexing import merge_table_args, omop_index, omop_primary_key_index_name, omop_table_options
Expand All @@ -13,6 +23,14 @@
"ExpectedDomain",
"CDMTableBase",
"cdm_table",
"ClinicalSchemaMixin",
"DerivedSchemaMixin",
"HealthEconomicSchemaMixin",
"HealthSystemSchemaMixin",
"MetadataSchemaMixin",
"StructuralSchemaMixin",
"UnstructuredSchemaMixin",
"VocabularySchemaMixin",
"required_concept_fk",
"optional_concept_fk",
"optional_int",
Expand Down
14 changes: 14 additions & 0 deletions omop_alchemy/cdm/base/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@
T = TypeVar("T", bound=type)
MODEL_MODULE_PREFIX = "omop_alchemy.cdm.model."


def _infer_table_category(cls: type) -> str | None:
model_module = cls.__module__
if not model_module.startswith(MODEL_MODULE_PREFIX):
return None
suffix = model_module.removeprefix(MODEL_MODULE_PREFIX)
return suffix.split(".", 1)[0]


def _infer_table_schema(cls: type) -> str | None:
for base in cls.__mro__[1:]:
schema_name = getattr(base, "__omop_schema__", None)
if schema_name is not None:
return schema_name
return None

def cdm_table(cls: T) -> T:
"""
Mark a SQLAlchemy declarative class as a concrete OMOP CDM table.
Expand Down Expand Up @@ -38,4 +47,9 @@ def cdm_table(cls: T) -> T:
cls.__omop_is_cdm_table__ = True
cls.__omop_table_category__ = _infer_table_category(cls)

schema_name = _infer_table_schema(cls)
table = getattr(cls, "__table__", None)
if schema_name is not None and table is not None and table.schema is None:
table.schema = schema_name

return cls
48 changes: 48 additions & 0 deletions omop_alchemy/cdm/base/schema_mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

import os


def _schema_from_env(env_var: str, default_schema: str | None) -> str | None:
raw_value = os.getenv(env_var)
if raw_value is None:
return default_schema

normalized = raw_value.strip()
if not normalized:
return default_schema
if normalized.lower() in {"none", "null"}:
return None
return normalized


class ClinicalSchemaMixin:
__omop_schema__ = _schema_from_env("OMOP_CLINICAL_SCHEMA", "omop")


class DerivedSchemaMixin:
__omop_schema__ = _schema_from_env("OMOP_DERIVED_SCHEMA", "results")


class HealthEconomicSchemaMixin:
__omop_schema__ = _schema_from_env("OMOP_HEALTH_ECONOMIC_SCHEMA", "omop")


class HealthSystemSchemaMixin:
__omop_schema__ = _schema_from_env("OMOP_HEALTH_SYSTEM_SCHEMA", "omop")


class MetadataSchemaMixin:
__omop_schema__ = _schema_from_env("OMOP_METADATA_SCHEMA", "omop")


class StructuralSchemaMixin:
__omop_schema__ = _schema_from_env("OMOP_STRUCTURAL_SCHEMA", "omop")


class UnstructuredSchemaMixin:
__omop_schema__ = _schema_from_env("OMOP_UNSTRUCTURED_SCHEMA", "omop")


class VocabularySchemaMixin:
__omop_schema__ = _schema_from_env("OMOP_VOCABULARY_SCHEMA", "vocabulary")
2 changes: 2 additions & 0 deletions omop_alchemy/cdm/model/clinical/condition_occurrence.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ReferenceContext,
CDMTableBase,
cdm_table,
ClinicalSchemaMixin,
ModifierFieldConcepts,
ModifierTargetMixin,
merge_table_args,
Expand All @@ -23,6 +24,7 @@

@cdm_table
class Condition_Occurrence(
ClinicalSchemaMixin,
PersonScoped,
HealthSystemContext,
CDMTableBase,
Expand Down
3 changes: 2 additions & 1 deletion omop_alchemy/cdm/model/clinical/death.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from omop_alchemy.cdm.base import (
CDMTableBase,
cdm_table,
ClinicalSchemaMixin,
optional_concept_fk,
ReferenceContext,
DomainValidationMixin,
Expand All @@ -20,7 +21,7 @@
from ..clinical import Person, PersonView

@cdm_table
class Death(CDMTableBase, Base):
class Death(ClinicalSchemaMixin, CDMTableBase, Base):
__tablename__ = "death"
__table_args__ = merge_table_args(
omop_table_options(cluster_on=omop_primary_key_index_name("death")),
Expand Down
2 changes: 2 additions & 0 deletions omop_alchemy/cdm/model/clinical/device_exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
FactTable,
CDMTableBase,
cdm_table,
ClinicalSchemaMixin,
required_concept_fk,
optional_concept_fk,
optional_int,
Expand All @@ -20,6 +21,7 @@

@cdm_table
class Device_Exposure(
ClinicalSchemaMixin,
PersonScoped,
CDMTableBase,
FactTable,
Expand Down
2 changes: 2 additions & 0 deletions omop_alchemy/cdm/model/clinical/drug_exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ReferenceContext,
CDMTableBase,
cdm_table,
ClinicalSchemaMixin,
required_concept_fk,
optional_concept_fk,
optional_int,
Expand All @@ -24,6 +25,7 @@

@cdm_table
class Drug_Exposure(
ClinicalSchemaMixin,
PersonScoped,
CDMTableBase,
FactTable,
Expand Down
3 changes: 2 additions & 1 deletion omop_alchemy/cdm/model/clinical/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
from omop_alchemy.cdm.base import (
CDMTableBase,
cdm_table,
ClinicalSchemaMixin,
ValueMixin,
merge_table_args,
omop_index,
)

@cdm_table
class Measurement(Base, CDMTableBase, ValueMixin):
class Measurement(ClinicalSchemaMixin, Base, CDMTableBase, ValueMixin):
__tablename__ = "measurement"
__table_args__ = merge_table_args(
ValueMixin.__table_args__,
Expand Down
Loading