// JPA persistence adapter — merged RDBMS base + PostgreSQL vendor module. // Owns JPA entities, Spring Data repositories, mappers, transaction/audit/lock/outbox port // 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). sourceSets { postgresqlIntegrationTest { java.setSrcDirs(['src/postgresqlIntegrationTest/java']) resources.setSrcDirs(['src/postgresqlIntegrationTest/resources']) compileClasspath += sourceSets.main.output runtimeClasspath += output + compileClasspath } } configurations { postgresqlIntegrationTestImplementation.extendsFrom testImplementation postgresqlIntegrationTestCompileOnly.extendsFrom testCompileOnly postgresqlIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly postgresqlIntegrationTestAnnotationProcessor.extendsFrom testAnnotationProcessor } ext.jpaPostgreSqlEvidenceImage = 'postgres:16-alpine' dependencies { implementation project(':application-core') implementation project(':shared-contract') implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // feature-distributed-lock-contract: Spring Integration JDBC LockRegistry backs the // multi-instance distributedLockProvider. Version managed by Spring Boot BOM. implementation 'org.springframework.integration:spring-integration-jdbc' // Vendor (PostgreSQL): Flyway migration API + PostgreSQL driver/dialect. Used only by the // .postgresql subpackage; the RDBMS base stays vendor-neutral (PERSISTENCE_RDBMS_STAYS_VENDOR_NEUTRAL). implementation 'org.springframework.boot:spring-boot-starter-flyway' runtimeOnly 'org.postgresql:postgresql' runtimeOnly 'org.flywaydb:flyway-database-postgresql' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-postgresql' postgresqlIntegrationTestImplementation 'org.testcontainers:testcontainers-junit-jupiter' postgresqlIntegrationTestRuntimeOnly 'org.postgresql:postgresql' } tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } def registerPostgreSqlReadinessTest = { String taskName, String testClass -> tasks.register(taskName, Test) { group = 'verification' description = "Runs the no-skip real PostgreSQL readiness scenario ${testClass}." testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath useJUnitPlatform() filter { includeTestsMatching testClass } failOnNoDiscoveredTests = true outputs.upToDateWhen { false } jvmArgs( '-Duser.timezone=UTC', "-Djpa.evidence.postgresql.image=${jpaPostgreSqlEvidenceImage}") } } def postgresqlLifecycleIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlLifecycleIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlLifecycleIntegrationTest') def postgresqlSecurityBaselineIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlSecurityBaselineIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlSecurityBaselineIntegrationTest') def postgresqlMigrationIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlMigrationIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlMigrationIntegrationTest') def postgresqlTransactionIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlTransactionIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlTransactionIntegrationTest') def postgresqlAggregateIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlAggregateIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlAggregateIntegrationTest') def postgresqlQueryIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlQueryIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlQueryIntegrationTest') def postgresqlIdempotencyIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlIdempotencyIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlIdempotencyIntegrationTest') def postgresqlOutboxStorageIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlOutboxStorageIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlOutboxStorageIntegrationTest') def postgresqlOutboxPollingIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlOutboxPollingIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlOutboxPollingIntegrationTest') def postgresqlInboxIntegrationTest = registerPostgreSqlReadinessTest( 'postgresqlInboxIntegrationTest', 'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlInboxIntegrationTest') def verifyJpaSqlConstructionSafety = tasks.register('verifyJpaSqlConstructionSafety') { group = 'verification' description = 'Rejects concatenated SQL construction and non-parameterized PostgreSQL timeout configuration.' File vendorSource = file('src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql') inputs.dir(vendorSource) doLast { List violations = [] vendorSource.eachFileRecurse { File source -> if (!source.name.endsWith('.java')) { return } String text = source.getText('UTF-8') def concatenatedSql = text =~ /(?s)(createNativeQuery|queryForObject|update)\s*\([^;]*"\s*\+/ if (concatenatedSql.find()) { violations << "${source}: concatenated SQL construction" } source.readLines().eachWithIndex { String line, int index -> if (line.contains("set_config('") && !line.contains('?')) { violations << "${source}:${index + 1}: set_config value is not parameterized" } } } if (!violations.isEmpty()) { throw new GradleException( "verifyJpaSqlConstructionSafety: ${violations.size()} violation(s):\n " + violations.join('\n ')) } logger.lifecycle( 'verifyJpaSqlConstructionSafety: OK — no concatenated SQL construction and all set_config values are parameterized.') } } def verifyJpaSecurityFixtures = tasks.register('verifyJpaSecurityFixtures') { group = 'verification' description = 'Verifies the no-skip PostgreSQL security fixture covers runtime-role namespace denial.' File fixture = file( 'src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlSecurityBaselineIntegrationTest.java') inputs.file(fixture) doLast { if (!fixture.isFile()) { throw new GradleException("verifyJpaSecurityFixtures: missing ${fixture}") } String text = fixture.getText('UTF-8') ['runtimeRoleCannotCreateInApplicationSchema', 'assertDockerAvailable', '42501'].each { String required -> if (!text.contains(required)) { throw new GradleException( "verifyJpaSecurityFixtures: ${fixture} is missing '${required}'") } } logger.lifecycle( 'verifyJpaSecurityFixtures: OK — no-skip Docker and runtime-role namespace denial fixtures are present.') } } postgresqlSecurityBaselineIntegrationTest.configure { dependsOn verifyJpaSqlConstructionSafety dependsOn verifyJpaSecurityFixtures dependsOn project(':adapter:inbound:web').tasks.named('jpaPersistenceRedactionContractTest') } apply from: rootProject.file('gradle/jpa-evidence.gradle')