# JPA Persistence Platform — Support Matrix **This document is a rendering. The machine-readable source is [`src/config/jpa/release-registry.json`](../../src/config/jpa/release-registry.json).** `JpaReleaseManifest` used to parse this file with regular expressions: every `PostgreSQL NN` it mentioned became a supported version, whatever table or sentence produced the match. An Experimental major joined the Stable list, a version named once in prose counted as supported, and demoting a major changed nothing so long as the string survived somewhere in the document. Now the registry declares a support level per major as a field, each gate names the Gradle task that produces its evidence, and this document describes what the registry says. ## Database | Database | Support | Evidence | |---|---|---| | PostgreSQL 16 | Stable | full contract suite, release lane (own matrix job) | | PostgreSQL 17 | Stable | full contract suite, release lane (own matrix job) | | PostgreSQL 18 | Stable | full contract suite, release lane (own matrix job) | | PostgreSQL 19 | Experimental | compatibility lane only; promotion requires an ADR | | H2 | Local convenience | **never** evidence of PostgreSQL behaviour | Each major gets its **own release job**, because for a while it did not. The release lane passed `-Pjpa.matrix.versions=16,17,18` to a `JpaPlatformContractSupport.start()` that used `selectedVersions().get(0)`, so the whole integration suite ran against PostgreSQL 16 and this table recorded 17 and 18 as fully covered on the strength of a three-assertion smoke test. `start()` now refuses a multi-version selection outright, `jpa-release.yml` fans out to one job per major, and a promotion job requires all three majors' evidence to carry the same commit SHA — so a removed major removes the release, not the evidence for it. **Provider baseline.** The gates run against the Hibernate version the Spring Boot BOM resolves — **7.1.8.Final** — which the registry records as `stable-tested-baseline`. This document previously called 7.4 the Stable baseline and the pagination gate was named `hibernate-7.4-fetch-pagination`, so every run of that gate produced evidence labelled with a provider it had never executed against. 7.4 is recorded as `compatibility-target`; it becomes the baseline when a full lane has actually run on it. H2 is not a second production target. It reports different SQLSTATEs for the same violation, no JSONB operators, no range types, and no concurrent index builds. A green H2 run is evidence that the code compiles and runs, and nothing more. `SKIP LOCKED` needs its own sentence, because two documents said different things about it. The module's `CLAUDE.md` records a measurement: H2 2.4.240 accepts `FOR UPDATE SKIP LOCKED` and does genuinely skip locked rows, which is why the outbox claim SQL is identical on both vendors. This document previously said H2 has no such guarantee. Both are right about different questions, and the distinction is the point: **observed behaviour in the version we measured is not a production guarantee, and it is never PostgreSQL contract evidence.** The measurement is why the claim SQL needs no vendor branch; the absence of a guarantee is why every concurrency contract still runs against a real PostgreSQL. ## Specification and provider | Component | Stable | Experimental | |---|---|---| | Jakarta Persistence | 3.2 | 4.0 (lane) | | Hibernate ORM | 7.4 declared baseline | 8 (lane) | | Spring Boot | repository BOM | — | The Hibernate row needs a note. The design declares 7.4 as the Stable provider; this repository's Spring Boot BOM resolves 7.1.x. `HibernateProviderPolicy` holds both — the declared baseline as a constant, the resolved version read from Hibernate itself — and `driftsFromDeclaredBaseline()` makes the difference visible instead of asserting a constant against itself. See [repository-adaptation.md](repository-adaptation.md) §4. ## Capability support levels | Capability | Level | |---|---| | Full-transaction retry | Stable | | Commit completion evidence | Stable | | Keyset pagination | Stable | | JDBC batch | Stable | | Flyway schema gate | Stable | | Runtime role verification | Stable | | Observability | Stable | | PostgreSQL native write (`ON CONFLICT`/`RETURNING`) | Advanced | | PostgreSQL work claim (`SKIP LOCKED`) | Advanced | | PostgreSQL JSONB | Advanced | | PostgreSQL array and range | Advanced | | Bulk DML | Advanced | | Hibernate `StatelessSession` | Advanced | | PostgreSQL `COPY` | Admin (J4) | | Hibernate second-level cache | Advanced | | Hibernate Envers | Advanced | | Multi-tenancy (column, RLS, schema, database) | Experimental | | Consistency-aware read replica | Experimental | ## Release gates Each row is a way the platform could pass its tests and still be wrong in production. | Gate | Kind | What it prevents | |---|---|---| | `postgresql-contract` | gate | a release whose only database evidence came from H2 | | `completion-unknown-no-retry` | gate | automatically re-running a write that may already have committed | | `osiv-disabled` | gate | lazy loading from the view layer, one query per rendered row | | `flyway-validate` | gate | Hibernate mutating a deployed schema, or running against one it was not built for | | `runtime-role-no-ddl` | gate | the application's own credential being able to alter or drop schema objects | | `collection-fetch-pagination` | gate | a paged collection fetch silently reading the whole table and paginating in memory | ## Explicitly unsupported - Reactive JPA. JPA is a blocking specification; a reactive facade over it moves the blocking call onto an event loop rather than removing it. - Hibernate as the production schema writer. `ddl-auto` never mutates a deployed schema. - A platform-owned generic CRUD repository. Domains own their repositories (design §10.1). - Automatic reconciliation of a completion-unknown transaction. The platform records; the domain resolves.