Implements the Stable and Experimental JPA persistence platform designs against real PostgreSQL, adapted to this repository's fail-closed 19-leaf registry. The design models the platform as 25 Gradle projects. `src/settings.gradle` throws unless the registry holds exactly 19 leaves, so the plan's modules become packages inside `:adapter:outbound:persistence-jpa` (starter in `:app-bootstrap`, testkit in its own source set). The full mapping, the renames this repository's naming gate required, and every deliberate substitution are recorded in `docs/jpa/repository-adaptation.md`. Seven Docker-backed lanes replace the plan's seven JVM test suites. Each fails closed: a lane that discovers nothing, or a container that cannot start, is an error rather than a skip. Three defects the contracts found against a real server: - `CommitFailureClassifier` treated only SQLSTATE 40003, class 08, and transport breaks as completion-unknown. A backend terminated mid-commit reports 57P01, and the commit record may already be in the WAL — so a possibly-committed transaction could be re-run. 57P01/57P02/57P03 now classify as completion-unknown. - `SchemaTenantMigrationOrchestrator` recorded `MigrateResult`'s target version, which is empty for a tenant already current, reporting migrated tenants as unmigrated during a partial rollout. It now reads the applied version back from the tenant's schema history. - `JpaStreamExecutor` checked only the declared return type for reactive publishers, and `RegisteredPostgreSqlCopyLoader` passed the COPY timeout to `SET`, which is parsed before parameter binding. `JpaModuleBoundaryTest` enforces the plan's module map as package rules; `verifyCleanArchitectureDependencies` governs edges between leaves and cannot see these. Its first assertion is that the import is non-empty, because every rule under it is a `noClasses()` rule and would pass vacuously on an empty import. Verified: 128 container tests across all seven lanes, 1183 unit tests, `:adapter:outbound:persistence-jpa:check`, `:app-bootstrap:check`, `verifyCleanArchitectureDependencies`, `verifyOneTypePerFile`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1.5 KiB
ADR-JPA-004 — Flyway is the schema source of truth
- Status: Accepted
- Date: 2026-08-11
- Design: §31
Context
Hibernate can create and alter schema from the entity mapping. Flyway can apply versioned scripts. Both cannot own the schema.
Decision
Flyway owns every schema change. Hibernate validates and never mutates: ddl-auto is validate or
none, enforced at startup. The runtime database credential holds no DDL privilege, so the rule is
enforced by the server as well as by configuration.
Consequences
ddl-auto=update fails in a specific and expensive way: it adds but never drops or narrows, so the
result is a schema that is neither the previous one nor the one the mappings describe — produced
silently, by whichever instance started first, with no record of what it did.
Two credentials rather than one is what makes this more than a convention. A configuration rule can
be overridden by a property; a role without CREATE cannot be overridden by anything the
application does.
Validation fails closed and never repairs. repair rewrites the schema history to match the scripts
on disk, which resolves a checksum mismatch by deleting the evidence of which change is missing.
The cost is that a schema change requires a migration script and a deployment step. That is the intended cost: it makes schema change reviewable and reversible.
Enforcement
JpaDangerousConfigurationGuard; FlywaySchemaPolicy; FlywayValidationGate;
PostgreSqlRuntimeRoleVerifier; release gates flyway-validate and runtime-role-no-ddl.