Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8fbc1ca
generic event projection contracts
gkennos Aug 28, 2026
2ca6199
core projections and predicates
gkennos Aug 28, 2026
53f4d76
attachment and mapping
gkennos Aug 28, 2026
4b2c93f
materialised view centralisation
gkennos Aug 30, 2026
8d4dc54
cleanup and consolidation of public episode contracts
gkennos Aug 31, 2026
401dcb1
added figures to documentation
gkennos Aug 31, 2026
39e6e2d
fixing timeline resolvers
gkennos Aug 31, 2026
053ae6f
metadata cleanup for projections
gkennos Aug 31, 2026
81cf007
metadata cleanup for projections
gkennos Aug 31, 2026
49a94b0
mat view cleanup
gkennos Aug 31, 2026
bb87b06
removed funky tests
gkennos Sep 2, 2026
b61f50e
pulling in mv-compliant orm-loader version
gkennos Sep 2, 2026
6c3eb95
pulling in mv-compliant orm-loader version
gkennos Sep 2, 2026
6b6e5e9
move condition modifiers to toolkit
gkennos Sep 7, 2026
d0f7d76
modifiers cleanup
gkennos Sep 7, 2026
d03f417
modifier projections cleanup
gkennos Sep 7, 2026
b1d7ee7
bumping deps
gkennos Sep 7, 2026
2582113
minor code comments
gkennos Sep 7, 2026
7645a50
updated postgres test coverage for modifier and stage selection
gkennos Sep 7, 2026
88ebaf0
export ATTACHMENT_METHOD
gkennos Sep 7, 2026
618c543
Refactor shared toolkit SQL helpers and contracts, update projections…
gkennos Sep 9, 2026
0225247
docs updates
gkennos Sep 9, 2026
b8ea5b4
docs updates
gkennos Sep 9, 2026
163b226
review findings
gkennos Sep 16, 2026
db8d23f
deprecation marker
gkennos Sep 16, 2026
0913aa6
minor test fixture updates
gkennos Sep 16, 2026
6d626d5
outstanding review comments
gkennos Sep 17, 2026
d9a48d9
removed unnecessary internal branch compatibility affordances
gkennos Sep 17, 2026
fc25375
minor comment update
gkennos Sep 17, 2026
6f66f70
fiiiine
gkennos Sep 17, 2026
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: 10 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ uv run pytest -q
uv run ruff check .
```

## Ownership boundaries

Before adding general database or ORM infrastructure, check whether it belongs in a lower-level dependency:

- [`orm-loader`](https://australiancancerdatanetwork.github.io/orm-loader/) owns domain-independent loading, serialization, and materialized-view lifecycle mechanics.
- `omop-alchemy` owns OMOP table models, clinical semantics, and OMOP-specific selectables and row grains.
- Consuming applications own view registries, dependency and rebuild policy, and deployment orchestration.

Do not add materialized-view DDL, lifecycle helpers, or orchestration to `omop_alchemy`.

## Opening a pull request

1. Apply **exactly one** label before merging:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ notebooks/
.dockerignore
docker/
tests/test_meds*
site/
site/
_design/
3 changes: 2 additions & 1 deletion .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ layers =
omop_alchemy.toolkit.analytics
omop_alchemy.toolkit.episodes
omop_alchemy.toolkit.core
omop_alchemy.cdm
omop_alchemy.toolkit._utils
omop_alchemy.cdm
49 changes: 12 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
# OMOP Alchemy

**OMOP Alchemy** provides a canonical, typed, SQLAlchemy-first representation of the
[OHDSI OMOP Common Data Model (CDM)](https://ohdsi.github.io/CommonDataModel/).
**OMOP Alchemy** provides a canonical, typed, SQLAlchemy-first representation of the [OHDSI OMOP Common Data Model (CDM)](https://ohdsi.github.io/CommonDataModel/).

It is designed to support **research-ready analytics, validation, and exploration**
of OMOP data using modern Python tooling, without imposing ETL conventions or
execution-time side effects.
It is designed to support fluency for **research-ready analytics, validation, and exploration** of OMOP data using modern Python tooling, without imposing ETL conventions or execution-time side effects.

---

Expand All @@ -16,41 +13,21 @@ OMOP Alchemy is intentionally:
- **Declarative**
Defines tables, columns, relationships, and constraints

- **SQLAlchemy-native**
Built for SQLAlchemy 2.x ORM usage

- **Safe to import anywhere**
No implicit engine creation, no global state, no environment assumptions.

- **Typed and inspectable**
Models are fully typed and introspectable for validation, tooling, and IDE support.

- **Backend-agnostic**
Designed to work across PostgreSQL, SQLite, and other SQLAlchemy-supported databases.

---

## What this package does *not* do

OMOP Alchemy deliberately avoids:

- Enforcing ETL conventions or data pipelines
- Auto-creating databases or loading vocabularies
- Imposing analytics frameworks or dashboards
- Making assumptions about deployment environments

These concerns are intentionally left to downstream tooling.
Layered abstractions for adding in new supported backend behaviours.

---

## Core features

- SQLAlchemy ORM models for OMOP CDM tables
- Explicit foreign key and relationship definitions
- Read-only *View* classes for safe navigation and analytics
- Lightweight mapper versions to provide simple model validation against CDM for use in ETL loops without side-effects or performance hit that can come from relationship instantiation within the runtime
- Read-only *View* classes for safe navigation and analytics that include complex multi-table objects such as conditions with their modifiers, episodes with their events
- Domain validation helpers for OMOP concept integrity
- CSV loading utilities for controlled ingestion and testing
- Lightweight schema and model validation against CDM specs

---

Expand All @@ -60,18 +37,18 @@ These concerns are intentionally left to downstream tooling.
from omop_alchemy.cdm.model.vocabulary.concept import ConceptView

concept = session.get(ConceptView, 320128) # Lung cancer
concept.domain.domain_id # "Condition"
concept.vocabulary.vocabulary_id # "SNOMED"
concept.is_standard # True
concept.domain.domain_id # "Condition"
concept.vocabulary.vocabulary_id # "SNOMED"
concept.is_standard # True
```

---

## Status

This project is currently beta.
The core API under `cdm/` should be considered stable as of the 1.x release.

The API is stabilising, but some modules may change as real-world use cases expand. Feedback and issues are welcome.
The toolkit API is experimental and carries no compatibility guarantees. Feedback and issues are welcome.

### Some additional background

Expand All @@ -82,9 +59,7 @@ This work builds on earlier research and tooling presented at the 2023 OHDSI APA

## Configuration

OMOP Alchemy reads all database connection and schema settings from
[oa-configurator](https://github.com/AustralianCancerDataNetwork/oa-configurator).
No `.env` files or `ENGINE` environment variables are needed.
OMOP Alchemy reads all database connection and schema settings from [oa-configurator](https://github.com/AustralianCancerDataNetwork/oa-configurator). No `.env` files or `ENGINE` environment variables are needed.

Run once after installation:

Expand All @@ -93,4 +68,4 @@ omop-config init
omop-config configure omop_alchemy
```

See [Configuration](docs/getting-started/configuration.md) for full details.
See [Configuration](docs/getting-started/configuration.md) for full details.
64 changes: 20 additions & 44 deletions docs/advanced/fulltext.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# PostgreSQL Full-Text Search

OMOP Alchemy includes an **optional** PostgreSQL full-text search integration for
selected vocabulary text fields.
OMOP Alchemy includes an **optional** PostgreSQL full-text search integration for selected vocabulary text fields.

This feature is deliberately bolt-on:

Expand Down Expand Up @@ -32,19 +31,12 @@ backend.concept_synonym_name_tsvector_expression()

These helpers return the best available expression for the configured environment:

- if the optional sidecar `tsvector` columns are registered in metadata, they return the
stored column
- otherwise they fall back to an inline computed PostgreSQL expression using
`to_tsvector(...)`
- if the optional sidecar `tsvector` columns are registered in metadata, they return the stored column
- otherwise they fall back to an inline computed PostgreSQL expression using `to_tsvector(...)`

### Example (PostgreSQL Documentation)

A tsvector value is a sorted list of distinct lexemes, which are words that have been normalized to merge different variants of the same word.
Sorting and duplicate-elimination are done automatically during input


A `tsvector` value is a sorted list of distinct lexemes (normalized word forms).
Sorting and duplicate elimination are applied automatically during input.
A `tsvector` value is a sorted list of distinct lexemes (normalised word forms). Sorting and duplicate elimination are applied automatically during input.

```sql
SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector;
Expand All @@ -63,8 +55,7 @@ omop-alchemy fulltext install
omop-alchemy fulltext populate
```

If your running Python process should use the stored sidecar columns through ORM
metadata, register them once at startup:
If your running Python process should use the stored sidecar columns through ORM metadata, register them once at startup:

```python
from omop_alchemy.backends import resolve_backend
Expand All @@ -73,8 +64,7 @@ backend = resolve_backend(engine)
backend.register_fulltext_metadata()
```

That is enough to activate the feature. The rest of this page explains when to use it
and how to operate it safely.
That is enough to activate the feature. The rest of this page explains when to use it and how to operate it safely.

## When To Use It

Expand All @@ -101,12 +91,9 @@ Full-text search is useful, but it also introduces operational tradeoffs:
- explicit backfill / refresh work
- PostgreSQL-specific behavior

Many users only need occasional text matching and are perfectly fine with inline search
expressions. Others want fast repeated full-text lookups across large vocabularies and
are happy to manage the extra schema objects.
Many users only need occasional text matching and are perfectly fine with inline search expressions. Others want fast repeated full-text lookups across large vocabularies and are happy to manage the extra schema objects.

OMOP Alchemy therefore treats full-text sidecars as an **optional PostgreSQL
enhancement**, not as part of the core required OMOP schema.
OMOP Alchemy therefore treats full-text sidecars as an **optional PostgreSQL enhancement**, not as part of the core required OMOP schema.

---

Expand Down Expand Up @@ -134,8 +121,7 @@ with Session(engine) as session:
)
```

In practice you will often want the PostgreSQL full-text match operator rather than
equality:
In practice you will often want the PostgreSQL full-text match operator rather than equality:

```python
vector = backend.concept_name_tsvector_expression()
Expand All @@ -144,15 +130,13 @@ query = sa.func.plainto_tsquery("english", "edoxaban")
stmt = sa.select(Concept).where(vector.op("@@")(query))
```

This mode is simple and portable at the library level, but PostgreSQL must compute the
vector expression at query time unless the planner can otherwise optimize it.
This mode is simple and portable at the library level, but PostgreSQL must compute the vector expression at query time unless the planner can otherwise optimize it.

### 2. Stored Sidecar Mode

This mode adds real `tsvector` columns to the database and optionally GIN indexes.

Once installed and registered, the helper functions point at the stored columns instead
of recomputing vectors inline.
Once installed and registered, the helper functions point at the stored columns instead of recomputing vectors inline.

This is the mode you want when:

Expand Down Expand Up @@ -195,17 +179,15 @@ omop-alchemy fulltext drop

## Important Behavior

The current implementation uses **ordinary nullable sidecar `tsvector` columns**, not
generated columns and not trigger-managed columns.
The current implementation uses **ordinary nullable sidecar `tsvector` columns**, not generated columns and not trigger-managed columns.

That means:

- `install` creates the columns and optional GIN indexes
- `populate` backfills or refreshes the values
- future data changes are **not** reflected automatically until you repopulate

This is a deliberate choice because it keeps the feature explicit and easier to manage
alongside bulk vocabulary loads.
This is a deliberate choice because it keeps the feature explicit and easier to manage alongside bulk vocabulary loads.

---

Expand Down Expand Up @@ -245,8 +227,7 @@ The same idea applies to `backend.concept_synonym_name_tsvector_expression()`.

## Metadata Registration

If your process will use the stored sidecar columns directly, register them into the ORM
metadata:
If your process will use the stored sidecar columns directly, register them into the ORM metadata:

```python
from omop_alchemy.backends import resolve_backend
Expand All @@ -255,15 +236,13 @@ backend = resolve_backend(engine)
backend.register_fulltext_metadata()
```

If you later remove the columns from the database in the same process and want query
helpers to fall back cleanly again:
If you later remove the columns from the database in the same process and want query helpers to fall back cleanly again:

```python
backend.unregister_fulltext_metadata()
```

This only affects SQLAlchemy metadata in the current Python process. It does not alter
the database by itself.
This only affects SQLAlchemy metadata in the current Python process. It does not alter the database by itself.

---

Expand All @@ -275,16 +254,13 @@ This feature is PostgreSQL-specific in its database form because it relies on:
- PostgreSQL full-text query functions such as `to_tsvector` and `plainto_tsquery`
- optional GIN indexes

The helper expressions can still be imported safely, but the sidecar install / populate /
drop lifecycle is only meaningful on PostgreSQL.
The helper expressions can still be imported safely, but the sidecar install / populate / drop lifecycle is only meaningful on PostgreSQL.

---

## Operational Gotchas
## Operational Notes:

- treat the sidecar columns as **derived search state**, not source-of-truth data
- if you bulk-load new vocabulary rows, rerun `omop-alchemy fulltext populate`
- if you use `reconcile-schema`, the sidecar columns and indexes are intentional
database additions outside the core OMOP schema
- GIN indexes can be expensive to build on large vocabularies, so plan that as a real
maintenance operation rather than a trivial toggle
- if you use `reconcile-schema`, the sidecar columns and indexes are intentional database additions outside the core OMOP schema
- GIN indexes can be expensive to build on large vocabularies, so plan that as a real maintenance operation rather than a trivial toggle
Loading
Loading