openehr-sqlite
openEHR persistence for SQLite 3 — a complete embedded store.
Install
[]
= "0.2"
= "0.2"
= "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 SqliteStore;
use ;
let mut store = in_memory?;
store.install?;
let ehr = sample_ehr;
store.create_ehr?;
store.create_contribution?;
store.commit_composition?;
// The record as it stood at a moment in time.
let then = store.version_at_time?;
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
spec/databases/— storage model, commit rules, conformance ladderspec/databases/conformance-matrix.md— what is verified today, including the two store requirements that are notspec/audit.md— known gaps
Testing
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.