Skip to main content

openehr_postgresql/
lib.rs

1//! openEHR persistence for **`PostgreSQL` 18**.
2//!
3//! This crate supplies one [`Dialect`]. Everything else — the storage model,
4//! the projection from openEHR objects onto rows, the commit rules, the
5//! conformance suite — lives in [`openehr_store`], which all five engine crates
6//! share. A dialect owns four things and no more: type spellings, identifier
7//! quoting, placeholder style, and how the engine enforces append-only.
8//!
9//! # Conformance level: **Schema**
10//!
11//! This crate emits DDL, and that DDL has been executed against
12//! **`PostgreSQL` 18**: five tables and seven indexes created, the script
13//! re-applied as a no-op, foreign keys enforced, and both append-only tables
14//! observed refusing `UPDATE` and `DELETE` with a row present and unchanged
15//! afterwards. `openehr-store/scripts/verify-schema.sh postgresql` reproduces
16//! it from a fresh container.
17//!
18//! It does **not** contain a store: there is no driver dependency, no
19//! connection handling, and no implementation of [`openehr_store::Store`].
20//!
21//! That is stated plainly because the sibling FHIR monorepo in this repository
22//! carries an audit finding (**F-01**) for six READMEs that claimed a working
23//! store, a CLI, and 7,399 round-tripped resources in ports where none of it
24//! existed. See `spec/conformance.md` for what each level means.
25//!
26//! ```
27//! use openehr_postgresql::PostgresqlDialect;
28//! use openehr_store::{ColTy, Dialect, ddl_script};
29//!
30//! let sql = ddl_script(&PostgresqlDialect);
31//! assert!(sql.contains("CREATE TABLE IF NOT EXISTS \"openehr_version\""));
32//! // JSON is native, and the derived instant column is a real timestamp.
33//! assert_eq!(PostgresqlDialect.col_sql(ColTy::Json), "jsonb");
34//! assert_eq!(PostgresqlDialect.col_sql(ColTy::InstantUtc), "timestamptz");
35//! // …while the authoritative one is text, so the lexical form survives.
36//! assert_eq!(PostgresqlDialect.col_sql(ColTy::Instant), "text");
37//! ```
38
39use openehr_store::schema::Table;
40use openehr_store::{ColTy, Dialect, Placeholder};
41
42/// The `PostgreSQL` dialect.
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub struct PostgresqlDialect;
45
46impl Dialect for PostgresqlDialect {
47    fn name(&self) -> &'static str {
48        "PostgreSQL"
49    }
50
51    fn col_sql(&self, ty: ColTy) -> String {
52        match ty {
53            // `text`, not `varchar(n)`. PostgreSQL stores both identically and
54            // `varchar(n)` only adds a length check that would reject a long
55            // but legal `ARCHETYPE_ID` — and rejecting conformant data is a
56            // worse failure than storing a few extra bytes.
57            ColTy::Id(_) | ColTy::Text(_) | ColTy::LongText | ColTy::Instant => "text",
58            // `jsonb`, not `json`: the store never needs to reproduce the
59            // document's byte form from the column, because the canonical form
60            // is regenerated from the parsed object (`J9.12`). What `jsonb`
61            // buys is containment and path indexes.
62            ColTy::Json => "jsonb",
63            ColTy::InstantUtc => "timestamptz",
64            ColTy::Int => "bigint",
65            ColTy::Bool => "boolean",
66        }
67        .to_owned()
68    }
69
70    fn quote(&self, identifier: &str) -> String {
71        format!("\"{}\"", identifier.replace('"', "\"\""))
72    }
73
74    fn placeholder(&self) -> Placeholder {
75        Placeholder::Dollar
76    }
77
78    fn append_only_sql(&self, table: &Table) -> Vec<String> {
79        // Enforced in the database, not only in the application. A guarantee
80        // that lives in application code ends the first time somebody opens
81        // psql, and openEHR's whole change-control model rests on this one
82        // (`V8.10`).
83        let name = self.quote(table.name);
84        let function = format!("openehr_refuse_mutation_{}", table.name);
85        vec![
86            format!(
87                "CREATE OR REPLACE FUNCTION {}() RETURNS trigger AS $$\n\
88                 BEGIN\n  \
89                 RAISE EXCEPTION '{} is append-only (openEHR V8.10)';\n\
90                 END;\n$$ LANGUAGE plpgsql",
91                self.quote(&function),
92                table.name
93            ),
94            format!(
95                "CREATE OR REPLACE TRIGGER {} BEFORE UPDATE OR DELETE ON {} \
96                 FOR EACH ROW EXECUTE FUNCTION {}()",
97                self.quote(&format!("trg_append_only_{}", table.name)),
98                name,
99                self.quote(&function)
100            ),
101        ]
102    }
103}