feat(jpa): implement the JPA relational persistence platform
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b5aee50e3
commit
0e61f86eb5
@@ -3,6 +3,15 @@
|
||||
// implementations, and the vendor-neutral SPI interfaces (OutboxClaimRepository /
|
||||
// SqlStateErrorMapping). The PostgreSQL driver, flyway-database-postgresql dialect, and vendor
|
||||
// Flyway migrations live only under the .postgresql subpackage (ArchUnit keeps the base neutral).
|
||||
// The JPA relational persistence platform (docs/superpowers/specs/2026-08-11-jpa-persistence-
|
||||
// platform-design.md) models itself as 18 Stable library modules. This repository's fail-closed
|
||||
// 19-leaf registry outranks that layout, so those modules are packages here and
|
||||
// JpaModuleBoundaryTest enforces the design's module dependency table. The full mapping is in
|
||||
// docs/jpa/repository-adaptation.md.
|
||||
//
|
||||
// The testkit is its own source set rather than part of `test` because more than one lane consumes
|
||||
// it and because a source set whose dependencies are declared only on the test configurations gives
|
||||
// the design's "no production module depends on the testkit" guarantee without a new Gradle project.
|
||||
sourceSets {
|
||||
postgresqlIntegrationTest {
|
||||
java.setSrcDirs(['src/postgresqlIntegrationTest/java'])
|
||||
@@ -10,6 +19,16 @@ sourceSets {
|
||||
compileClasspath += sourceSets.main.output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
testkit {
|
||||
java.srcDir 'src/testkit/java'
|
||||
compileClasspath += sourceSets.main.output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
jpaPlatformPerformanceTest {
|
||||
java.srcDir 'src/jpaPlatformPerformanceTest/java'
|
||||
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
@@ -17,6 +36,20 @@ configurations {
|
||||
postgresqlIntegrationTestCompileOnly.extendsFrom testCompileOnly
|
||||
postgresqlIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
postgresqlIntegrationTestAnnotationProcessor.extendsFrom testAnnotationProcessor
|
||||
testkitImplementation.extendsFrom testImplementation
|
||||
testkitRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
jpaPlatformPerformanceTestImplementation.extendsFrom testImplementation
|
||||
jpaPlatformPerformanceTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
}
|
||||
|
||||
// Every test lane compiles and runs against the testkit.
|
||||
sourceSets.test {
|
||||
compileClasspath += sourceSets.testkit.output
|
||||
runtimeClasspath += sourceSets.testkit.output
|
||||
}
|
||||
sourceSets.postgresqlIntegrationTest {
|
||||
compileClasspath += sourceSets.testkit.output
|
||||
runtimeClasspath += sourceSets.testkit.output
|
||||
}
|
||||
|
||||
ext.jpaPostgreSqlEvidenceImage = 'postgres:16-alpine'
|
||||
@@ -43,9 +76,41 @@ dependencies {
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
// JPA platform observability (design §37). Micrometer's observation API already arrives with
|
||||
// Spring; the meter registry does not, and the platform's transaction/query/retry metrics need
|
||||
// it. Version managed by the Spring Boot BOM.
|
||||
implementation 'io.micrometer:micrometer-core'
|
||||
|
||||
// Querydsl and Envers are Advanced opt-ins (design §4.2): the platform implements their
|
||||
// contracts, but the Stable runtime classpath must not carry either. compileOnly keeps them off
|
||||
// every deployment while still compiling the support classes; a deployment that opts in adds the
|
||||
// artifact itself, and the guards refuse the capability when the classes are absent.
|
||||
compileOnly 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
|
||||
compileOnly 'org.hibernate.orm:hibernate-envers'
|
||||
|
||||
testImplementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
|
||||
testImplementation 'org.hibernate.orm:hibernate-envers'
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||
|
||||
postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-postgresql'
|
||||
postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-junit-jupiter'
|
||||
postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-toxiproxy'
|
||||
postgresqlIntegrationTestRuntimeOnly 'org.postgresql:postgresql'
|
||||
|
||||
testkitImplementation 'org.testcontainers:testcontainers'
|
||||
testkitImplementation 'org.testcontainers:testcontainers-postgresql'
|
||||
testkitImplementation 'org.testcontainers:testcontainers-junit-jupiter'
|
||||
testkitImplementation 'org.testcontainers:testcontainers-toxiproxy'
|
||||
testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||
testkitRuntimeOnly 'org.postgresql:postgresql'
|
||||
|
||||
// The performance lane starts its own servers: pool saturation is only observable against a
|
||||
// real database, because the thing being measured is what happens when every connection to it
|
||||
// is already held.
|
||||
jpaPlatformPerformanceTestImplementation 'org.testcontainers:testcontainers'
|
||||
jpaPlatformPerformanceTestImplementation 'org.testcontainers:testcontainers-postgresql'
|
||||
jpaPlatformPerformanceTestImplementation 'org.testcontainers:testcontainers-junit-jupiter'
|
||||
jpaPlatformPerformanceTestRuntimeOnly 'org.postgresql:postgresql'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
|
||||
@@ -169,4 +234,80 @@ postgresqlSecurityBaselineIntegrationTest.configure {
|
||||
dependsOn project(':adapter:inbound:web').tasks.named('jpaPersistenceRedactionContractTest')
|
||||
}
|
||||
|
||||
// JPA platform lanes (design §40-§41). Each maps one of the plan's JVM test suites onto this
|
||||
// leaf's existing Docker-backed source set; the mapping is recorded in
|
||||
// docs/jpa/repository-adaptation.md §3.
|
||||
//
|
||||
// Every lane fails closed. `failOnNoDiscoveredTests` matters more here than usual: a selected lane
|
||||
// that discovers nothing reports success, and a contract suite that silently stopped running is
|
||||
// indistinguishable from one that passes.
|
||||
Closure<Void> registerJpaPlatformLane = { String taskName, String tag, String description ->
|
||||
tasks.register(taskName, Test) {
|
||||
group = 'verification'
|
||||
it.description = description
|
||||
testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs
|
||||
classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath
|
||||
useJUnitPlatform {
|
||||
includeTags tag
|
||||
}
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
jvmArgs('-Duser.timezone=UTC')
|
||||
// The Stable matrix selection. An unknown or empty value is an error in
|
||||
// PostgreSqlVersion.parseSelection rather than an empty run.
|
||||
systemProperty 'jpa.matrix.versions',
|
||||
(project.findProperty('jpa.matrix.versions') ?: '16').toString()
|
||||
}
|
||||
}
|
||||
|
||||
def jpaPlatformContractTest = registerJpaPlatformLane(
|
||||
'jpaPlatformContractTest',
|
||||
'jpa-contract',
|
||||
'Runs the JPA platform contract suite against real PostgreSQL (design §40).')
|
||||
def jpaPlatformMigrationTest = registerJpaPlatformLane(
|
||||
'jpaPlatformMigrationTest',
|
||||
'jpa-migration',
|
||||
'Runs the Flyway upgrade snapshot scenarios (design §31).')
|
||||
def jpaPlatformFailureTest = registerJpaPlatformLane(
|
||||
'jpaPlatformFailureTest',
|
||||
'jpa-failure',
|
||||
'Reproduces deadlock, serialization, and commit-ambiguity failures (design §39).')
|
||||
def jpaPlatformQueryPlanTest = registerJpaPlatformLane(
|
||||
'jpaPlatformQueryPlanTest',
|
||||
'jpa-queryplan',
|
||||
'Asserts query plan structure and planner estimate error (design §33).')
|
||||
def jpaPlatformSecurityTest = registerJpaPlatformLane(
|
||||
'jpaPlatformSecurityTest',
|
||||
'jpa-security',
|
||||
'Verifies runtime role privileges and search_path safety (design §36).')
|
||||
|
||||
// Machine-dependent bounds live in their own source set and never gate an ordinary build: attaching
|
||||
// them to `check` would make a laptop's `check` fail for reasons that are not about the code.
|
||||
def jpaPlatformPerformanceTest = tasks.register('jpaPlatformPerformanceTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Certifies Hikari pool and REQUIRES_NEW connection pressure (design §38).'
|
||||
testClassesDirs = sourceSets.jpaPlatformPerformanceTest.output.classesDirs
|
||||
classpath = sourceSets.jpaPlatformPerformanceTest.runtimeClasspath
|
||||
useJUnitPlatform()
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
jvmArgs('-Duser.timezone=UTC')
|
||||
systemProperty 'performance.assertions.enabled',
|
||||
(project.findProperty('performance.assertions.enabled') ?: 'false').toString()
|
||||
}
|
||||
|
||||
// The JPA release gate (design §41). Aggregates every lane whose absence would let one of the
|
||||
// documented gates in docs/jpa/support-matrix.md pass unverified.
|
||||
tasks.register('jpaPlatformReleaseGate') {
|
||||
group = 'verification'
|
||||
description = 'Runs every JPA platform lane required for a release (design §41).'
|
||||
dependsOn tasks.named('test')
|
||||
dependsOn jpaPlatformContractTest
|
||||
dependsOn jpaPlatformMigrationTest
|
||||
dependsOn jpaPlatformFailureTest
|
||||
dependsOn jpaPlatformQueryPlanTest
|
||||
dependsOn jpaPlatformSecurityTest
|
||||
dependsOn jpaPlatformPerformanceTest
|
||||
}
|
||||
|
||||
apply from: rootProject.file('gradle/jpa-evidence.gradle')
|
||||
|
||||
Reference in New Issue
Block a user