Choosing an engine
Six ports, one specification. They differ in two files — map/src/ddl.rs and
the store crate — and in how much has been verified.
The short answer#
| If you want | Use | Because |
|---|---|---|
| Production today | fhir-postgresql |
the only port whose test suite substantiates its claims |
| Embedded, no server | fhir-sqlite |
one file, bundled engine, always-runnable tests |
| An existing MySQL/MariaDB estate | fhir-mysql, fhir-mariadb |
native stores and search, live CI gates |
| SQL Server | fhir-mssql |
native store and search, live-verified incl. upgrade (F-65); note the TLS advisory risk (F-67) |
| Oracle | fhir-oracle, cautiously |
native store and search, live-verified (F-68) — but no upgrade, no concurrency/redaction tests, and R4.5 snapshot reads are a confirmed open gap |
Status in detail#
| pg | sqlite | mysql | mariadb | mssql | oracle | |
|---|---|---|---|---|---|---|
| Conformance level | Reference | Store | Store | Store | Store | Store |
| Dialect annex exists and is real | • | • | • | • | • | • |
| Store implementation | • | • | • | • | • | • |
| Search | • | • | • | • | • | • |
| History, audit, chain | • | • | • | • | • | • |
| Transaction bundles | • | ~ | — | — | — | — |
| Conditional create/delete | • | • | — | — | — | — |
upgrade / backfill |
• | • | • | • | • | — |
chain_witness, re-sign |
• | — | — | — | — | — |
| Concurrency / redaction / audit tests | • | • | • | • | • | — |
| CI runs the right engine | ~ | ~ | ~ | ~ | ~ | — |
| Dialect annex describes the right engine | • | • | • | • | • | • |
(The CI row is ~ everywhere because per-family workflows are inert in the
monorepo — the workflow files provision the right engines but nothing runs
them until they are consolidated rootward, F-49.)
Full detail: conformance matrix.
The row that surprises people is the tests row. fhir-sqlite, fhir-mysql and
fhir-mariadb have working stores built on the same shared engine as
PostgreSQL's, and each now carries concurrency.rs, redaction.rs,
roundtrip_types.rs and upgrade.rs — 102 to 105 tests apiece, green against
their live engines (measured 2026-08-03).
What they still lack is a dedicated audit suite of PostgreSQL's depth, and
none has been run repeatedly enough to claim determinism (T11.15). This
paragraph said "none has a concurrency, redaction, or audit test" until
2026-08-03, which had stopped being true (F-63).
What each engine costs you#
The ColTy bindings, which is where the engines actually differ:
ColTy |
PostgreSQL | SQLite | MySQL | MariaDB | SQL Server |
|---|---|---|---|---|---|
Bool |
boolean |
INTEGER |
TINYINT(1) |
TINYINT(1) |
BIT |
Numeric |
numeric |
TEXT |
TEXT |
TEXT |
NVARCHAR(MAX) |
Text |
text |
TEXT |
TEXT |
TEXT |
NVARCHAR(MAX) |
TextC |
text COLLATE "C" |
TEXT COLLATE BINARY |
…utf8mb4_0900_bin |
…utf8mb4_nopad_bin |
NVARCHAR(450) …BIN2 |
Date |
date |
TEXT ISO |
DATE |
DATE |
DATE |
Timestamptz |
timestamptz |
TEXT ISO UTC |
DATETIME(6) |
DATETIME(6) |
DATETIME2(6) |
Jsonb |
jsonb |
TEXT |
LONGTEXT |
LONGTEXT |
NVARCHAR(MAX) |
ords |
smallint[] |
TEXT |
TEXT |
TEXT |
VARBINARY(255) |
PostgreSQL#
The reference. Everything is implemented and the test suite proves it:
concurrency.rs, audit.rs, redaction.rs, upgrade.rs, live.rs,
m2_semantics.rs, search_semantics.rs, bench.rs, against live PostgreSQL 18
in CI. Measured: 7,399 example resources round-trip losslessly, 94.8% of R5
search parameters compile, 6,146 resources/sec bulk load, 1.18 ms reads.
The only engine with a native array type, so ords is smallint[] and
PostgreSQL-only subscript idioms (ords[1] = 1) work here and nowhere else.
Costs: a server, and max_locks_per_transaction means installing 7,355 tables
needs a staged schema and a rename rather than one transaction.
The hash-chain pre-image used to be derived in SQL here, so a PostgreSQL chain
could not be verified by another port. That was F-07 and it is fixed:
canon.rs is shared and identical in all six, so a chain written by any port
verifies in any other (audit.md).
SQLite#
The embeddable one. No server, one file per FHIR version, engine bundled and pinned rather than whatever the host ships. Its tests need no environment variables and always run — which, as its own test header notes, means a green run there proves more than a green run in the inherited PostgreSQL suites.
Costs: transact_audited returns Unsupported deliberately (a compensating
unwind is not atomic, and refusing is better than pretending); no upgrade;
chain_witness and re-signing unimplemented; numeric range search works via
CAST(… AS REAL), which is correct but gives up the index. And the ords
subscript idiom the book teaches does not work on a TEXT column.
Concurrency is SQLite's, so WAL mode and a single writer.
MySQL and MariaDB#
Native stores with search, live CI gates on mysql:8.4 and mariadb:11.4.
Deliberately independent despite the shared ancestry (M14.0a–M14.0c) —
neither must read the other's schema, and neither holds back syntax the other
lacks.
The visible difference is the collation: utf8mb4_0900_bin on MySQL,
utf8mb4_nopad_bin on MariaDB. Both are their engine's spelling of the NO PAD
binary property M3.6b requires; utf8mb4_bin would be wrong, because it is
PAD SPACE and would make 'Smith' = 'Smith ' true.
Costs: no transact_audited, no conditional operations, no
checkpoint (upgrade/backfill_norm exist and are live-verified — F-15
closed here). DATETIME(6) rather than TIMESTAMP, because TIMESTAMP converts
on session time zone and its range ends in 2038.
SQL Server#
Store level since 2026-08-04 (F-65 — an earlier revision of this
section began "Not usable yet"). A real tiberius store with search,
live-verified against azure-sql-edge by 33 tests, 0 ignored, including
upgrade/backfill_norm (this port's upgrade is genuinely one
transaction — T-SQL DDL is transactional, M14.35). R4.5 snapshot reads
needed two live attempts: READ_COMMITTED_SNAPSHOT alone still tore;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT on a dedicated database is what
works. What to weigh before choosing it:
- The TLS advisory risk (
F-67): three unpatchedrustls-webpkiCVEs reach the shipping store crate, andnative-tlsfails the handshake — a standing risk awaiting an owner decision, soO10.7is!in the matrix. - Verified only against
azure-sql-edge, not full SQL Server (M14.31). - A token's
system/codeareNVARCHAR(MAX)and are dropped from their index, so those searches scan. The intended fix is a persisted computed column holding the leading 450 characters (P6.4a). - No
put_audited,transact_audited, or conditional operations.
Oracle#
Store level since 2026-08-04 (F-68 — an earlier revision of this
section said "Scaffold only, and nothing in it is Oracle"). ddl.rs is a
real Oracle emitter now (F-08 fixed): the full R5 schema — 9,636
statements — installs on 26ai with 0 invalid objects, and the store (the
oracle ODPI-C crate, synchronous, wrapped in spawn_blocking) runs its
CRUD/history/search/audit surface live, 7/7 tests, 0 ignored. The caveats
are sharper here than anywhere else:
R4.5snapshot reads are a confirmed open gap, not an unverified one:SET TRANSACTION READ ONLYfails withORA-01466on any session that has run DDL, sogetcurrently reads with no snapshot protection.- No
upgrade/backfill_norm, no concurrency or redaction tests, transport security undecided (M14.22), and no CI gate — the fake MySQL gate was removed rather than repointed (F-06), andscripts/db.shis the local gate.
Open Oracle questions that make this more than mechanical: no boolean type
before 23ai; VARCHAR2 capped at 4000 bytes with longer values becoming CLOB,
which cannot be indexed or compared like text; no IF NOT EXISTS anywhere, so
idempotence needs a PL/SQL block swallowing ORA-00955; SYS_CONTEXT in place of
a session GUC for the erasure flag; and whether an Oracle Database Free image
runs on arm64 at all, which decides whether live verification can be promised.
What does not vary#
Whichever you choose, these are identical by construction (X15.1):
- The relational map, shredding, and reconstruction — including lossless
round-trip (
R4.2). - The fold:
fold("Ærø") == "aero"on every engine, with no extension to install and no dependence on the engine's collation tables or Unicode version (X15.4). - Generated identifiers: every port budgets to 63 bytes, the tightest target, so
a name generated once is legal everywhere and two schemas are comparable
name-for-name (
X15.3). - The
ordsstored image, whatever type holds it (X15.5). - The canonical JSON the hash chain commits to (
X15.2) — with no exception since F-07 was fixed;canon.rsis identical in all six.
Migrating between engines#
There is no migration tool. The route is export from one and load into the
other, and it works because the logical content of a store is engine-
independent (X15.10): the same resource shredded by two ports produces the same
logical rows under the same identifiers.
What does not carry across is the hash chain. A chain verified under one port
should be verified there, before the export, and the destination starts a new
chain — reported as beginning where it begins, never backfilled (M3.16e).