Add h8-preview module: hibernate-demo suite against Hibernate 8.0.0.Beta3 and Jakarta Persistence 4.0.0-M7

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Gth99spB1CzQmBFofXmU4Z
This commit is contained in:
Claude
2026-09-28 08:42:17 +00:00
parent 8568c0ce6c
commit 833f3e1c31
35 changed files with 769 additions and 0 deletions
@@ -0,0 +1,65 @@
package com.ankurm.h8;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.resource.jdbc.spi.StatementInspector;
/** Plain Hibernate bootstrap on an in-memory H2, no Spring, no persistence.xml. */
final class Db {
/** Every SQL string Hibernate prepares, in order. */
static final List<String> SQL = new ArrayList<>();
public static class Recorder implements StatementInspector {
@Override
public String inspect(String sql) {
SQL.add(sql);
return sql;
}
}
private Db() {
}
static SessionFactory open(Class<?>... entities) {
return open(java.util.Map.of(), entities);
}
static SessionFactory open(java.util.Map<String, String> extra, Class<?>... entities) {
StandardServiceRegistryBuilder b = new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.url", "jdbc:h2:mem:" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1")
.applySetting("hibernate.connection.username", "sa")
.applySetting("hibernate.hbm2ddl.auto", "create-drop")
.applySetting("hibernate.session_factory.statement_inspector", Recorder.class.getName());
extra.forEach(b::applySetting);
StandardServiceRegistry registry = b.build();
MetadataSources sources = new MetadataSources(registry);
for (Class<?> e : entities) {
sources.addAnnotatedClass(e);
}
return sources.buildMetadata().buildSessionFactory();
}
static Throwable root(Throwable t) {
while (t.getCause() != null && t.getCause() != t) {
t = t.getCause();
}
return t;
}
static String describe(Throwable t) {
StringBuilder sb = new StringBuilder(t.getClass().getName());
Throwable c = t.getCause();
while (c != null && c != t) {
sb.append(" <- ").append(c.getClass().getName());
t = c;
c = c.getCause();
}
return sb.toString();
}
}
@@ -0,0 +1,57 @@
package com.ankurm.h8;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.function.Supplier;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.Test;
/** Same code on both versions; the transcript records what each throws or returns. */
class ExceptionContractsTest {
private static String attempt(Supplier<Object> action) {
try {
Object r = action.get();
return "returned " + r;
}
catch (RuntimeException ex) {
return "threw " + Db.describe(ex);
}
}
@Test
void recordsExceptionContracts() {
Transcript t = new Transcript("01-exception-contracts");
try (SessionFactory sf = Db.open(Author.class)) {
// 1. execute a query whose named parameter was never bound
t.line("unbound parameter : " + attempt(() -> sf.fromSession(s -> s
.createQuery("select a from Author a where a.name = :name", Author.class).getResultList().size())));
// 2. bind a null
t.line("null parameter : " + attempt(() -> sf.fromSession(s -> s
.createQuery("select a from Author a where a.name = :name", Author.class)
.setParameter("name", (String) null).getResultList().size())));
// 3. bind a String to a Long
t.line("type mismatch : " + attempt(() -> sf.fromSession(s -> s
.createQuery("select a from Author a where a.id = :id", Author.class)
.setParameter("id", "not-a-number").getResultList().size())));
// 4. unique constraint violation on flush
t.line("unique violation : " + attempt(() -> sf.fromTransaction(s -> {
s.persist(new Author("A", "[email protected]"));
s.persist(new Author("B", "[email protected]"));
s.flush();
return "no exception";
})));
// 5. a parameter name that is not in the query
t.line("unknown parameter : " + attempt(() -> sf.fromSession(s -> s
.createQuery("select a from Author a where a.name = :name", Author.class)
.setParameter("nope", "x").getResultList().size())));
// 6. getSingleResult on no rows
List<Author> none = sf.fromSession(s -> s.createNamedQuery("Author.byName", Author.class)
.setParameter("name", "nobody").getResultList());
t.line("named query, no row: " + none.size() + " rows");
}
t.write();
assertThat(Transcript.LABEL).isNotBlank();
}
}
@@ -0,0 +1,41 @@
package com.ankurm.h8;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.Map;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.Test;
/** Persist p1, c1, p2, c2 in that order and record the order in which Hibernate PREPARES its INSERT statements. */
class FlushOrderTest {
private static List<String> run(Map<String, String> settings) {
Db.SQL.clear();
try (SessionFactory sf = Db.open(settings, Parent.class, Child.class)) {
Db.SQL.clear();
sf.inTransaction(s -> {
Parent p1 = new Parent("p1");
Parent p2 = new Parent("p2");
s.persist(p1);
s.persist(new Child("c1", p1));
s.persist(p2);
s.persist(new Child("c2", p2));
});
return Db.SQL.stream().filter(q -> q.startsWith("insert")).map(q -> q.split(" ")[2]).toList();
}
}
@Test
void recordsInsertOrder() {
Transcript t = new Transcript("02-flush-order");
List<String> byDefault = run(Map.of());
t.line("default : " + byDefault);
List<String> legacy = run(Map.of("hibernate.flush.queue.type", "legacy"));
t.line("hibernate.flush.queue.type=legacy: " + legacy);
List<String> graph = run(Map.of("hibernate.flush.queue.type", "graph"));
t.line("hibernate.flush.queue.type=graph : " + graph);
t.write();
assertThat(byDefault).isNotEmpty();
}
}
@@ -0,0 +1,26 @@
package com.ankurm.h8;
import static org.assertj.core.api.Assertions.assertThat;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.Test;
/** A JPA @NamedQuery whose text is an UPDATE: accepted or rejected at start-up? */
class MutatingNamedQueryTest {
@Test
void recordsStartupOutcome() {
Transcript t = new Transcript("03-mutating-named-query");
String outcome;
try (SessionFactory sf = Db.open(MutatingNamedQueryEntity.class)) {
int rows = sf.fromTransaction(s -> s.createNamedQuery("Mutating.rename").setParameter("name", "x").executeUpdate());
outcome = "session factory built; createNamedQuery(..).executeUpdate() returned " + rows;
}
catch (RuntimeException ex) {
outcome = "threw " + Db.describe(ex) + " :: " + String.valueOf(Db.root(ex).getMessage()).lines().findFirst().orElse("");
}
t.line(outcome);
t.write();
assertThat(outcome).isNotBlank();
}
}
@@ -0,0 +1,37 @@
package com.ankurm.h8;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/** Collects lines and writes output/NN-name-<label>.txt (label = h8 or h7). */
final class Transcript {
static final String LABEL = System.getProperty("h.label", "h8");
static final String VERSION = System.getProperty("h.version", "?");
private final List<String> lines = new ArrayList<>();
private final Path file;
Transcript(String name) {
this.file = Path.of("output", name + "-" + LABEL + ".txt");
line("# " + name + " (Hibernate ORM " + VERSION + ", JDK " + Runtime.version().feature() + ")");
}
Transcript line(String s) {
lines.add(s);
return this;
}
void write() {
try {
Files.createDirectories(file.getParent());
Files.write(file, lines);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}