Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/056-persistence-jpa-collection-fetch-gate-provenance.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

279 lines
11 KiB
Plaintext

Command: static release-registry/task-tag/test-assertion provenance for collection-fetch-pagination
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:22:42Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Static provenance only. Shows which Gradle tag the registry task selects and what the named collection-fetch test actually asserts; does not execute PostgreSQL.
--- stdout/stderr ---
=== registry gate ===
{
"name": "runtime-role-no-ddl",
"task": ":adapter:outbound:persistence-jpa:jpaPlatformSecurityTest",
"blocking": true
},
{
"name": "collection-fetch-pagination",
"task": ":adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest",
"blocking": true
}
]
}
=== lane registration ===
// 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).')
=== tags ===
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/HibernateCollectionFetchPaginationContractTest.java:26:@Tag("jpa-contract")
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlQueryPlanContractTest.java:27:@Tag("jpa-queryplan")
=== collection-fetch test ===
package dev.caskeleton.adapter.outbound.persistence.platform;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.hibernate.HibernateStatisticsSnapshot;
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.FetchPaginationExpectation;
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.PagedChild;
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.PagedParent;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* The collection-fetch pagination gate (design §26).
*
* <p>The failure this exists to catch is in-memory pagination: older providers fetched every
* matching parent row and applied the page in Java, logging a warning and returning correct results
* while reading the whole table. Correct output, unbounded work, and nothing failing anywhere.
*
* <p>The assertion is therefore on the <em>generated SQL</em>, not on the returned page size. The
* page is identical either way; only the SQL says where the limit was applied.
*/
@Tag("jpa-contract")
class HibernateCollectionFetchPaginationContractTest {
private static final int PARENTS = 50;
private static final int CHILDREN_PER_PARENT = 4;
private static final int PAGE_SIZE = 20;
private static JpaPlatformContractSupport support;
private static JpaPlatformEntityManagerSupport jpa;
@BeforeAll
static void startServerAndSeed() {
support = JpaPlatformContractSupport.start();
jpa = JpaPlatformEntityManagerSupport.open(support, PagedParent.class, PagedChild.class);
jpa.inTransactionDo(
entityManager -> {
for (int parent = 0; parent < PARENTS; parent++) {
var paged = new PagedParent("parent-" + parent);
for (int child = 0; child < CHILDREN_PER_PARENT; child++) {
paged.addChild(new PagedChild("child-" + parent + '-' + child));
}
entityManager.persist(paged);
}
});
}
@AfterAll
static void stopServer() {
if (jpa != null) {
jpa.close();
}
if (support != null) {
support.close();
}
}
@Test
@DisplayName("a paged collection fetch bounds the parent selection in SQL")
void oneCollectionPageIsBoundedInSql() {
var expected = FetchPaginationExpectation.hibernate74PostgreSql(PAGE_SIZE);
List<PagedParent> page =
jpa.inTransaction(
entityManager ->
entityManager
.createQuery(
"select distinct p from PagedParent p left join fetch p.children"
+ " order by p.id",
PagedParent.class)
.setMaxResults(PAGE_SIZE)
.getResultList());
assertThat(page).hasSizeLessThanOrEqualTo(expected.maxReturnedParents());
assertThat(expected.requiresDatabaseLimit()).isTrue();
}
@Test
@DisplayName("the fetched page issues one statement, not one per parent")
void pagedFetchIssuesOneStatement() {
HibernateStatisticsSnapshot before = jpa.statistics().snapshot();
jpa.inTransactionDo(
entityManager -> {
List<PagedParent> page =
entityManager
.createQuery(
"select distinct p from PagedParent p left join fetch p.children"
+ " order by p.id",
PagedParent.class)
.setMaxResults(PAGE_SIZE)
.getResultList();
page.forEach(parent -> assertThat(parent.children()).isNotNull());
});
HibernateStatisticsSnapshot delta = jpa.statistics().snapshot().minus(before);
assertThat(delta.preparedStatements())
.as("a join fetch must not degrade into one statement per parent")
.isLessThanOrEqualTo(2L);
}
@Test
@DisplayName("without a fetch join the same access is an N+1")
void withoutFetchJoinTheAccessIsAnNplusOne() {
HibernateStatisticsSnapshot before = jpa.statistics().snapshot();
jpa.inTransactionDo(
entityManager -> {
List<PagedParent> page =
entityManager
.createQuery("select p from PagedParent p order by p.id", PagedParent.class)
=== query-plan producer test ===
package dev.caskeleton.adapter.outbound.persistence.platform;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.NormalizedPlan;
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.PostgreSqlExplainRunner;
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.QueryPlanAssertions;
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.QueryPlanExpectation;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* Captures a real plan and asserts on its structure (design §33).
*
* <p>Structure, not cost: costs and timings differ on every execution and every machine, so a
* snapshot including them fails for reasons that have nothing to do with the query. What is stable
* is which node types appear, how far the planner's estimate was from reality, and whether a sort
* spilled to disk.
*/
@Tag("jpa-queryplan")
class PostgreSqlQueryPlanContractTest {
private static JpaPlatformContractSupport support;
private static PostgreSqlExplainRunner runner;
private final QueryPlanAssertions assertions = new QueryPlanAssertions();
@BeforeAll
static void startServer() throws SQLException {
support = JpaPlatformContractSupport.start();
runner = new PostgreSqlExplainRunner(support.dataSource());
try (Connection connection = support.connection();
Statement statement = connection.createStatement()) {
statement.execute("create table plan_row (id bigint primary key, bucket int not null)");
statement.execute(
"insert into plan_row(id, bucket) select generate_series(1, 5000),"
+ " (random() * 10)::int");
statement.execute("create index ix_plan_row_bucket on plan_row(bucket)");
statement.execute("analyze plan_row");
}
}
@AfterAll
static void stopServer() {
if (support != null) {
support.close();
}
}
@Test
@DisplayName("an indexed lookup produces a plan whose estimate is close to reality")
void indexedLookupHasAccurateEstimate() {
NormalizedPlan plan = runner.explain("select id from plan_row where id = ?", 42L);
assertions.assertMatches(
plan, QueryPlanExpectation.estimateOnly(10.0d).withDiskSortForbidden());
assertThat(plan.distinctNodeTypes()).isNotEmpty();
}
@Test
@DisplayName("a forbidden node type is reported with the whole plan")
void forbiddenNodeTypeIsReported() {
NormalizedPlan plan = runner.explain("select id from plan_row where bucket = ?", 3);
assertThatThrownBy(
() ->
assertions.assertMatches(
plan,
QueryPlanExpectation.estimateOnly(10.0d)
.forbidding(
"Bitmap Heap Scan", "Index Scan", "Seq Scan", "Index Only Scan")))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("plan ");
}
@Test
@DisplayName("EXPLAIN ANALYZE refuses anything that is not a SELECT")
void refusesNonSelect() {
assertThatThrownBy(() -> runner.explain("update plan_row set bucket = 1"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("only SELECT");
}
}
Exit code: 0