openehr-sqlite 0.3.0

openEHR persistence for SQLite: schema dialect and a complete embedded store
Documentation

openehr-sqlite

openEHR persistence for SQLite 3 — a complete embedded store.

Install

[dependencies]
openehr-sqlite = "0.2"
openehr-store = "0.2"
openehr = "0.2"

Requires Rust 1.90+ (edition 2024). SQLite itself is compiled in — no system library is needed, and none is used.

Conformance level: Verified

Unlike the five dialect-only crates, this one contains a working SqliteStore and runs the shared conformance suite against a real database in its own tests: every commit rule, every read, the archetype index, the append-only triggers, and DDL idempotence.

SQLite is the only one of the six engines that can be verified without provisioning a server, so it is the one where the shared logic actually gets exercised. See openehr-store/spec/conformance.md.

use openehr_sqlite::SqliteStore;
use openehr_store::{Store, conformance};

let mut store = SqliteStore::in_memory()?;
store.install()?;

let ehr = conformance::sample_ehr();
store.create_ehr(&ehr)?;
store.create_contribution(ehr.ehr_id(), &conformance::sample_contribution(uid, &[1]))?;
store.commit_composition(ehr.ehr_id(), &version, uid)?;

// The record as it stood at a moment in time.
let then = store.version_at_time(&container, &at)?;

Verified rather than Store because the suite runs in CI on every push against a bundled engine that cannot be absent, so the job cannot silently skip. It is the only crate at this level, being the only one with a Store.

The API

Operation Does
SqliteStore::in_memory() / ::open(path) open a store; from_connection adopts an existing rusqlite::Connection
install() create the schema, idempotently
create_ehr / get_ehr one health record
create_contribution one change set
commit_composition one version, with every commit rule enforced
get_version / latest_version read by identifier, or the head of a container
version_at_time the version current at an instant
all_versions a container's history, oldest first (H5.12)
find_compositions_by_archetype the query the index exists for

What it guarantees

The same commit rules as the library. A version belonging to another container, a duplicate, a missing predecessor, or a stale one are each refused and each distinguishable. The stale case is the one that matters: two clients both read version 2 and both write version 3, and openEHR's answer is a branch rather than a silent overwrite — so the store refuses and lets the caller decide.

Append-only, in the database. UPDATE and DELETE on the version table are refused by triggers, not by application convention. A guarantee that lives only in Rust ends the first time somebody opens the file with the sqlite3 CLI, and a test goes around the store to prove it.

Validation before writing. An invalid composition is refused before the transaction opens, so a refused commit leaves no container behind. A store that accepted one would make every later reader's validate() fail on data it cannot fix.

Foreign keys actually on. SQLite disables them per connection by default; this store enables them explicitly. Not doing so would accept a version pointing at a container that does not exist, silently, while the schema said otherwise.

SQLite specifics

Decision Why
TEXT for identifiers, with no lengths SQLite applies type affinity rather than enforcement, so a declared length would be documentation nothing checks.
TEXT for JSON There is no JSON type; the JSON1 extension operates on TEXT, so this is both the honest declaration and the working one.
INTEGER — Unix seconds — for derived instants SQLite has no date type. Storing the derived instant as text would make it sort identically to the authoritative column, collapsing the distinction the two columns exist to keep.
bundled SQLite The DDL and the JSON1 functions are version-dependent, so the engine is pinned rather than whatever the host ships.

Every instant is stored twice

openEHR times are ISO 8601 strings with deliberate partial precision: 2024-05 is a date known to the month and is not 2024-05-01. A native timestamp column silently completes it — fabricating a clinical fact — and normalises the lexical form.

So each time occupies …_text (authoritative, exact) and …_utc (derived, nullable, for ordering). The derived column is NULL whenever the instant is not established, and version_at_time skips those rows rather than guessing at a time zone — exactly as the library does.

What is not here

Not here Why
AQL execution Needs archetype path resolution; openehr parses and statically checks AQL and returns no rows (S1.6).
Archetype or template validation Validation is Reference-Model-level only (V9.9, lib:S1.4).
A tamper-evidence chain openehr has the primitives; this store does not use them and the schema has no digest columns (M3.16). Append-only is a weaker guarantee and must not be described as tamper evidence (PR12.11).
Read auditing Only writes are recorded (PR12.5).
Schema migration No migration mechanism and no applied-version metadata (O10.14).
Concurrency guarantees The commit rules and the unique index are in place, but nothing tests concurrent access (db:D-02).

Fuzzing

The dialect's identifier quoting is fuzzed by openehr-sqlite-fuzz, run in CI on every push.

Specification

Testing

cargo test

Runs the shared suite against an in-memory database, plus this crate's own tests for the trigger, the validation gate, lexical fidelity, and DDL idempotence — and tests/dialects.rs, which compares all six engines' DDL and fails if any two agree.

Licence

Any of these, at your option — MIT, Apache-2.0, BSD-3-Clause, GPL-2.0-only, or GPL-3.0-only. See LICENSE.md.