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>
279 lines
11 KiB
Plaintext
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
|