145 lines
8.5 KiB
Markdown
145 lines
8.5 KiB
Markdown
# 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.
|
|
|
|
Being a rendering used to be a claim rather than a mechanism: the tables below were still typed by
|
|
hand, so a major demoted in the registry stayed Stable here and kept its full release job.
|
|
`JpaReleaseRenderingTest` now compares the database table, the gate table and `jpa-release.yml`'s
|
|
matrix and promotion lists to the registry, and `verifyJpaReleaseGateTasks` resolves every gate's
|
|
task against the real Gradle task graph. Edit the registry; these tables follow, or the build fails.
|
|
|
|
Two renderings stayed outside that comparison until they were added to it. `jpa-nightly.yml` runs
|
|
its own matrix and nothing checked it, so a demotion corrected the release lane and left the nightly
|
|
lane certifying the major. And an Experimental major's "compatibility lane only" named no file: the
|
|
lane existed, but the registry, this document and the release workflow could each be read end to end
|
|
without establishing that, so a reader looking for it concluded there was none. An Experimental major
|
|
now has to be recorded as the target of a lane in `.github/workflows`, and a Stable lane may not run
|
|
it.
|
|
|
|
## 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 | [`jpa-next-postgresql19.yml`](../../.github/workflows/jpa-next-postgresql19.yml) — `NOT_EXECUTABLE`: no `postgres:19-alpine` is published, so no container of that major has been started; 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 |
|
|
| Technical auditing — `audit/AuditableEntity` | Stable (canonical) |
|
|
| Technical auditing — `auditing/AuditMetadata` | Candidate, not composed |
|
|
| 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 |
|
|
|
|
### The two audit mechanisms
|
|
|
|
`audit/AuditableEntity` is the canonical one: `created_*`/`updated_*`, a 256-character actor,
|
|
stamped explicitly by the repository adapter. It is what the sample entities extend and what the
|
|
migrations were written for.
|
|
|
|
`auditing/AuditMetadata` is a second, complete mechanism with different column names
|
|
(`modified_*`), a different actor length (64) and a different capture lifecycle (Spring Data
|
|
listeners). Nothing embeds it and nothing composes `JpaAuditingConfiguration`, which is why it is
|
|
listed as a candidate rather than as a capability: promoting it means choosing between reshaping it
|
|
to the canonical columns and writing a forward migration for the new ones, and that choice has not
|
|
been made. Until it is, an entity picks one mechanism or none — enforced on the production graph by
|
|
`JpaAuditMechanismRule.entitiesUseExactlyOneAuditMechanism`.
|
|
|
|
Neither mechanism reaches a bulk or native update. Both stamp on an ordinary save — one in the
|
|
adapter, one on a managed entity's lifecycle — so a statement that goes straight to the database
|
|
leaves the audit columns showing the previous save. A bulk update of an audited entity must
|
|
therefore set the audit column in the statement, which
|
|
`JpaAuditMechanismRule.bulkUpdatesOfAuditedEntitiesStampAudit` checks over the production graph.
|
|
|
|
## 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.
|