Add migration-5-to-7 module: Hibernate 5.6.15 vs 7.4.14 jar diff, compile checks, schema defaults and cheat-sheet snippets

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YPgGH6aMbSLANL3E2HHmS6
This commit is contained in:
2026-10-25 19:17:24 +00:00
co-authored by Claude Sonnet 5.5
parent 357ba84e5c
commit aea104f021
57 changed files with 5187 additions and 0 deletions
+1
View File
@@ -344,3 +344,4 @@ Modules with their own `pom.xml` that run plain Hibernate without the Spring Boo
| [`h8-preview`](h8-preview/) | Hibernate ORM 8 beta against Hibernate 7: what breaks | | [`h8-preview`](h8-preview/) | Hibernate ORM 8 beta against Hibernate 7: what breaks |
| [`inheritance`](inheritance/) | `SINGLE_TABLE` vs `JOINED` vs `TABLE_PER_CLASS` vs `@MappedSuperclass`: generated DDL, SQL, failures and timings (Hibernate `7.4.13.Final`) | | [`inheritance`](inheritance/) | `SINGLE_TABLE` vs `JOINED` vs `TABLE_PER_CLASS` vs `@MappedSuperclass`: generated DDL, SQL, failures and timings (Hibernate `7.4.13.Final`) |
| [`locking`](locking/) | Optimistic (`@Version`, retries) vs pessimistic (`PESSIMISTIC_WRITE`, `NOWAIT`, `SKIP LOCKED`) locking, lost updates and deadlocks on H2 (Hibernate `7.4.14.Final`) | | [`locking`](locking/) | Optimistic (`@Version`, retries) vs pessimistic (`PESSIMISTIC_WRITE`, `NOWAIT`, `SKIP LOCKED`) locking, lost updates and deadlocks on H2 (Hibernate `7.4.14.Final`) |
| [`migration-5-to-7`](migration-5-to-7/) | Hibernate `5.6.15.Final` vs `7.4.14.Final`: jar diff, Hibernate 5 code compiled against both, schema defaults, `hbm2ddl` validation of a Hibernate 5 schema, and cheat-sheet snippets |
+22
View File
@@ -0,0 +1,22 @@
# migration-5-to-7
Hibernate ORM **5.6.15.Final** (`org.hibernate:hibernate-core`, `javax.persistence`) against **7.4.14.Final**
(`org.hibernate.orm:hibernate-core`, `jakarta.persistence`), on in-memory and file H2 2.4.240. Companion to the
Hibernate 5 to 7 migration guide and cheat sheet on [ankurm.com](https://ankurm.com).
| path | what it is |
|---|---|
| `h5/` | the "before" side: the same probes on Hibernate 5.6.15 |
| `h7/` | the "after" side: the probes plus the cheat-sheet snippets (`CheatSheetTest`) on Hibernate 7.4.14 |
| `src/compat/` | Hibernate 5 style and Hibernate 7 style sources that are only compiled, never run |
| `scripts/api_diff.py` | jar diff of the two `hibernate-core` jars (`javax.persistence` read as `jakarta.persistence`) |
| `scripts/compile-old-apis.sh` | compiles `src/compat` against both versions |
| `scripts/schema_diff.py` | lines up the two `06-schema-*` files |
| `scripts/run-all.sh` | everything above; rewrites `output/` |
Needs JDK 17 or newer (built with 25), Maven 3.9, Python 3 and access to Maven Central. Hibernate 5.6.15 pulls Byte Buddy
1.12.18, which does not know a current JDK, so `h5/pom.xml` pins a newer Byte Buddy.
`output/02-removed-api.txt` lists exact signatures that disappeared (generics and `throws` ignored, internal and `spi`
packages left out). Parameter type changes such as `Serializable` to `Object` count as removed there, so treat it as a list to
check, not a list of deletions; the compile checks in `output/03` to `output/09` are the arbiter.
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ankurm</groupId>
<artifactId>migration-h5</artifactId>
<version>1.0.0</version>
<name>migration-h5</name>
<description>The "before" side: Hibernate ORM 5.6.15.Final with javax.persistence, run against in-memory H2</description>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hibernate.version>5.6.15.Final</hibernate.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Hibernate 5.6.15 pulls Byte Buddy 1.12.18, which does not know the JDK this runs on; a current one does -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.17.7</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<!-- note the group id: org.hibernate here, org.hibernate.orm from 6.0 on -->
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,68 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.Serializable;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import org.junit.jupiter.api.Test;
/** The same probes as the Hibernate 7 module, run on 5.6.15.Final. Output goes to ../output. */
class BaselineTest {
private static final String V = "5.6.15";
@Test
void schemaDefaults() {
try (Db db = new Db(Probe.class); Transcript t = new Transcript("06-schema-" + V)) {
t.line("Hibernate " + V + ", H2, @GeneratedValue with no strategy");
t.line("-- sequences (name, increment)");
List<String> seq = db.jdbc("select sequence_name, increment from information_schema.sequences order by 1");
seq.forEach(t::line);
t.line("-- columns of PROBE (name, type)");
List<String> cols = db.jdbc("select column_name, data_type from information_schema.columns where table_name = 'PROBE' order by column_name");
cols.forEach(t::line);
assertThat(seq).isNotEmpty();
assertThat(cols).hasSize(11);
}
}
@Test
void legacyQueries() {
try (Db db = new Db(Probe.class); Transcript t = new Transcript("07-legacy-queries-" + V)) {
t.line("Hibernate " + V);
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Serializable id = s.save(new Probe("alpha"));
t.line("session.save(...) returned: " + id + " (" + id.getClass().getSimpleName() + ")");
s.getTransaction().commit();
assertThat(id).isNotNull();
}
try (Session s = db.sessionFactory.openSession()) {
String ordinal = outcome(() -> s.createQuery("from Probe p where p.name = ?1").setParameter(1, "alpha").list());
t.line("HQL with ?1 : " + ordinal);
String bare = outcome(() -> s.createQuery("from Probe p where p.name = ?").setParameter(0, "alpha").list());
t.line("HQL with a bare ? : " + bare);
String enumInt = outcome(() -> s.createQuery("from Probe p where p.byOrdinal = 0").list());
t.line("HQL enum compared with 0 : " + enumInt);
String criteria = outcome(() -> s.createCriteria(Probe.class).add(Restrictions.eq("name", "alpha")).list());
t.line("session.createCriteria(...) : " + criteria);
assertThat(ordinal).startsWith("1 row");
assertThat(bare).startsWith("failed");
assertThat(enumInt).startsWith("0 row");
assertThat(criteria).startsWith("1 row");
}
}
}
static String outcome(java.util.function.Supplier<List<?>> query) {
try {
return query.get().size() + " row(s)";
} catch (RuntimeException e) {
String msg = String.valueOf(e.getMessage()).lines().findFirst().orElse("");
return "failed: " + e.getClass().getSimpleName() + ": " + (msg.length() > 140 ? msg.substring(0, 140) + "..." : msg);
}
}
}
@@ -0,0 +1,66 @@
package com.ankurm.migration;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/** Plain-Hibernate bootstrap against in-memory H2. */
final class Db implements AutoCloseable {
private static final AtomicInteger SEQ = new AtomicInteger();
final SessionFactory sessionFactory;
private final String url;
Db(Class<?>... entities) {
this.url = "jdbc:h2:mem:mig" + SEQ.incrementAndGet() + ";DB_CLOSE_DELAY=-1";
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.url", url)
.applySetting("hibernate.connection.username", "sa")
.applySetting("hibernate.connection.password", "")
.applySetting("hibernate.hbm2ddl.auto", "create-drop")
.build();
MetadataSources sources = new MetadataSources(registry);
for (Class<?> e : entities) {
sources.addAnnotatedClass(e);
}
this.sessionFactory = sources.buildMetadata().buildSessionFactory();
}
/** Raw JDBC against the same in-memory database; columns joined with a space. */
List<String> jdbc(String query) {
List<String> rows = new ArrayList<>();
try (Connection c = DriverManager.getConnection(url, "sa", "");
Statement st = c.createStatement();
ResultSet rs = st.executeQuery(query)) {
int n = rs.getMetaData().getColumnCount();
while (rs.next()) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= n; i++) {
if (i > 1) {
sb.append(' ');
}
sb.append(rs.getString(i));
}
rows.add(sb.toString());
}
} catch (SQLException e) {
throw new IllegalStateException(e);
}
return rows;
}
@Override
public void close() {
sessionFactory.close();
}
}
@@ -0,0 +1,39 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import java.nio.file.Files;
import java.nio.file.Path;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.junit.jupiter.api.Test;
/** Creates the schema Hibernate 5.6.15 generates, in an H2 file database that the Hibernate 7 module opens later. */
class LegacySchemaTest {
@Test
void createSchemaFile() throws Exception {
Path dir = Path.of("..", "target", "legacy-db");
Files.createDirectories(dir);
for (var f : dir.toFile().listFiles()) {
f.delete();
}
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.url", "jdbc:h2:file:" + dir.toAbsolutePath().normalize() + "/probe")
.applySetting("hibernate.connection.username", "sa")
.applySetting("hibernate.connection.password", "")
.applySetting("hibernate.hbm2ddl.auto", "create")
.build();
try (SessionFactory sf = new MetadataSources(registry).addAnnotatedClass(Probe.class).buildMetadata().buildSessionFactory();
Session s = sf.openSession()) {
s.beginTransaction();
s.persist(new Probe("legacy row"));
s.getTransaction().commit();
}
assertThat(Files.exists(dir.resolve("probe.mv.db"))).isTrue();
}
}
@@ -0,0 +1,46 @@
package com.ankurm.migration;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.UUID;
/** One field of each type whose default column mapping is worth comparing, and a generated id. */
@Entity
public class Probe {
@Id
@GeneratedValue
Long id;
String name;
boolean active;
Instant createdAt;
LocalDateTime occurredAt;
UUID uuid;
Duration span;
BigDecimal price;
byte[] payload;
@Enumerated(EnumType.ORDINAL)
Colour byOrdinal;
@Enumerated(EnumType.STRING)
Colour byName;
public enum Colour { RED, GREEN }
public Probe() {
}
Probe(String name) {
this.name = name;
}
}
@@ -0,0 +1,33 @@
package com.ankurm.migration;
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 them to output/NN-name.txt when closed. The tests assert what they print. */
final class Transcript implements AutoCloseable {
private final Path file;
private final List<String> lines = new ArrayList<>();
Transcript(String name) {
this.file = Path.of("..", "output", name + ".txt");
}
Transcript line(String s) {
lines.add(s);
return this;
}
@Override
public void close() {
try {
Files.createDirectories(file.getParent());
Files.write(file, lines);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ankurm</groupId>
<artifactId>migration-h7</artifactId>
<version>1.0.0</version>
<name>migration-h7</name>
<description>The "after" side: Hibernate ORM 7.4.14.Final with jakarta.persistence, run against in-memory H2</description>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hibernate.version>7.4.14.Final</hibernate.version>
</properties>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,8 @@
package com.ankurm.migration;
import jakarta.persistence.Embeddable;
/** A Java record used directly as an embeddable. */
@Embeddable
public record Address(String street, String city) {
}
@@ -0,0 +1,26 @@
package com.ankurm.migration;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.NaturalId;
@Entity
public class Author {
@Id
@GeneratedValue
Long id;
@NaturalId
String email;
String name;
public Author() {
}
Author(String email, String name) {
this.email = email;
this.name = name;
}
}
@@ -0,0 +1,29 @@
package com.ankurm.migration;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
@Entity
public class Book {
@Id
@GeneratedValue
Long id;
String title;
Integer pages;
@ManyToOne(fetch = FetchType.LAZY)
Author author;
public Book() {
}
Book(String title, Integer pages, Author author) {
this.title = title;
this.pages = pages;
this.author = author;
}
}
@@ -0,0 +1,217 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.junit.jupiter.api.Test;
/** Snippets for the cheat sheet. Each test prints what it did to ../output/1N-cheat-*.txt and asserts the same values. */
class CheatSheetTest {
@Test
void sessionApi() {
try (Db db = new Db(Author.class, Book.class); Transcript t = new Transcript("10-cheat-session-api")) {
Long id;
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Author a = new Author("[email protected]", "Ann");
s.persist(a);
id = a.id;
s.getTransaction().commit();
t.line("persist(a) -> id assigned on the object: " + id);
}
try (Session s = db.sessionFactory.openSession()) {
db.clear();
Author found = s.find(Author.class, id);
t.line("find(Author, id) -> " + found.name + " (" + db.sql.size() + " select)");
assertThat(found.name).isEqualTo("Ann");
assertThat(db.sql).hasSize(1);
}
try (Session s = db.sessionFactory.openSession()) {
db.clear();
Author ref = s.getReference(Author.class, id);
t.line("getReference(Author, id) -> initialized: " + Hibernate.isInitialized(ref) + " (" + db.sql.size() + " selects)");
assertThat(Hibernate.isInitialized(ref)).isFalse();
assertThat(db.sql).isEmpty();
}
try (Session s = db.sessionFactory.openSession()) {
Author detached = new Author("[email protected]", "Ann Smith");
detached.id = id;
s.beginTransaction();
Author managed = s.merge(detached);
t.line("merge(detached) -> returns a different instance: " + (managed != detached) + ", name now " + managed.name);
s.getTransaction().commit();
assertThat(managed).isNotSameAs(detached);
assertThat(managed.name).isEqualTo("Ann Smith");
}
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Author extra = new Author("[email protected]", "Bob");
s.persist(extra);
s.getTransaction().commit();
Long bobId = extra.id;
List<Author> many = s.findMultiple(Author.class, List.of(id, bobId, 999L));
t.line("findMultiple(ids 3) -> " + many.size() + " entries, missing id gives: " + many.get(2));
assertThat(many).hasSize(3);
assertThat(many.get(2)).isNull();
}
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Author a = s.find(Author.class, id);
s.remove(a);
s.getTransaction().commit();
t.line("remove(a) -> find afterwards: " + s.find(Author.class, id));
assertThat(s.find(Author.class, id)).isNull();
}
}
}
@Test
void hql() {
try (Db db = new Db(Author.class, Book.class); Transcript t = new Transcript("11-cheat-hql")) {
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Author ann = new Author("[email protected]", "Ann");
Author bob = new Author("[email protected]", "Bob");
s.persist(ann);
s.persist(bob);
s.persist(new Book("Alpha", 100, ann));
s.persist(new Book("Beta", 300, ann));
s.persist(new Book("Gamma", null, bob));
s.getTransaction().commit();
}
try (Session s = db.sessionFactory.openSession()) {
List<String> named = s.createSelectionQuery("select b.title from Book b where b.pages > :min order by b.title", String.class)
.setParameter("min", 150).getResultList();
t.line("named parameter : " + named);
assertThat(named).containsExactly("Beta");
List<String> ordinal = s.createSelectionQuery("select b.title from Book b where b.pages > ?1 order by b.title", String.class)
.setParameter(1, 50).getResultList();
t.line("?1 parameter : " + ordinal);
assertThat(ordinal).containsExactly("Alpha", "Beta");
List<String> page = s.createSelectionQuery("select b.title from Book b order by b.title limit 2 offset 1", String.class).getResultList();
t.line("limit 2 offset 1 in HQL : " + page);
assertThat(page).containsExactly("Beta", "Gamma");
List<String> nulls = s.createSelectionQuery("select b.title from Book b order by b.pages desc nulls last", String.class).getResultList();
t.line("order by ... nulls last : " + nulls);
assertThat(nulls).containsExactly("Beta", "Alpha", "Gamma");
Book none = s.createSelectionQuery("from Book b where b.title = 'Nope'", Book.class).getSingleResultOrNull();
t.line("getSingleResultOrNull() : " + none);
assertThat(none).isNull();
List<Book> fetched = s.createSelectionQuery("select b from Book b join fetch b.author where b.title = 'Alpha'", Book.class).getResultList();
t.line("join fetch : author initialized = " + Hibernate.isInitialized(fetched.get(0).author));
assertThat(Hibernate.isInitialized(fetched.get(0).author)).isTrue();
Long count = s.createSelectionQuery("select count(b) from Book b where b.author.name = :n", Long.class).setParameter("n", "Ann").getSingleResult();
t.line("count over an association : " + count);
assertThat(count).isEqualTo(2L);
}
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
int updated = s.createMutationQuery("update Book b set b.pages = b.pages + 1 where b.pages is not null").executeUpdate();
s.getTransaction().commit();
t.line("createMutationQuery update: " + updated + " rows");
assertThat(updated).isEqualTo(2);
}
}
}
@Test
void statelessSession() {
try (Db db = new Db(Author.class); Transcript t = new Transcript("12-cheat-stateless-session")) {
try (StatelessSession s = db.sessionFactory.openStatelessSession()) {
s.beginTransaction();
for (int i = 1; i <= 3; i++) {
s.insert(new Author("user" + i + "@example.com", "User " + i));
}
s.getTransaction().commit();
t.line("insert x3 (no persistence context) done");
}
try (StatelessSession s = db.sessionFactory.openStatelessSession()) {
Author a1 = s.get(Author.class, 1L);
Author a1again = s.get(Author.class, 1L);
t.line("get twice -> same instance: " + (a1 == a1again) + " (a stateless session has no first-level cache)");
assertThat(a1).isNotNull();
assertThat(a1).isNotSameAs(a1again);
t.line("rows in table: " + db.jdbc("select count(*) from Author"));
assertThat(db.jdbc("select count(*) from Author")).containsExactly("3");
}
}
}
@Test
void naturalId() {
try (Db db = new Db(Author.class); Transcript t = new Transcript("13-cheat-natural-id")) {
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
s.persist(new Author("[email protected]", "Ann"));
s.getTransaction().commit();
}
try (Session s = db.sessionFactory.openSession()) {
db.clear();
Author a = s.bySimpleNaturalId(Author.class).load("[email protected]");
t.line("bySimpleNaturalId(...).load -> " + a.name);
t.line("sql: " + db.sql.get(0));
assertThat(a.name).isEqualTo("Ann");
assertThat(db.sql.get(0)).contains("email");
}
}
}
@Test
void softDelete() {
try (Db db = new Db(Note.class); Transcript t = new Transcript("14-cheat-soft-delete")) {
Long id;
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Note n = new Note("remember");
s.persist(n);
id = n.id;
s.getTransaction().commit();
}
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
db.clear();
s.remove(s.find(Note.class, id));
s.getTransaction().commit();
t.line("remove(note) ran: " + db.sql.get(db.sql.size() - 1));
assertThat(db.sql.get(db.sql.size() - 1)).startsWith("update");
}
try (Session s = db.sessionFactory.openSession()) {
t.line("find afterwards : " + s.find(Note.class, id));
t.line("row still in the table: " + db.jdbc("select * from Note"));
assertThat(s.find(Note.class, id)).isNull();
assertThat(db.jdbc("select count(*) from Note")).containsExactly("1");
}
}
}
@Test
void recordEmbeddable() {
try (Db db = new Db(Shop.class); Transcript t = new Transcript("15-cheat-record-embeddable")) {
Long id;
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Shop shop = new Shop("Corner", new Address("1 Main St", "Pune"));
s.persist(shop);
id = shop.id;
s.getTransaction().commit();
}
try (Session s = db.sessionFactory.openSession()) {
Shop shop = s.find(Shop.class, id);
t.line("loaded: " + shop.address);
t.line("columns: " + db.jdbc("select column_name from information_schema.columns where table_name = 'SHOP' order by column_name"));
assertThat(shop.address).isEqualTo(new Address("1 Main St", "Pune"));
}
}
}
}
@@ -0,0 +1,76 @@
package com.ankurm.migration;
import org.hibernate.SessionFactory;
import org.hibernate.resource.jdbc.spi.StatementInspector;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/** Plain-Hibernate bootstrap against in-memory H2. */
final class Db implements AutoCloseable {
private static final AtomicInteger SEQ = new AtomicInteger();
final List<String> sql = java.util.Collections.synchronizedList(new ArrayList<>());
final SessionFactory sessionFactory;
private final String url;
Db(Class<?>... entities) {
this.url = "jdbc:h2:mem:mig" + SEQ.incrementAndGet() + ";DB_CLOSE_DELAY=-1";
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("jakarta.persistence.jdbc.url", url)
.applySetting("jakarta.persistence.jdbc.user", "sa")
.applySetting("jakarta.persistence.jdbc.password", "")
.applySetting("hibernate.hbm2ddl.auto", "create-drop")
.applySetting("hibernate.session_factory.statement_inspector", (StatementInspector) x -> {
sql.add(x);
return x;
})
.build();
MetadataSources sources = new MetadataSources(registry);
for (Class<?> e : entities) {
sources.addAnnotatedClass(e);
}
this.sessionFactory = sources.buildMetadata().buildSessionFactory();
}
void clear() {
sql.clear();
}
/** Raw JDBC against the same in-memory database; columns joined with a space. */
List<String> jdbc(String query) {
List<String> rows = new ArrayList<>();
try (Connection c = DriverManager.getConnection(url, "sa", "");
Statement st = c.createStatement();
ResultSet rs = st.executeQuery(query)) {
int n = rs.getMetaData().getColumnCount();
while (rs.next()) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= n; i++) {
if (i > 1) {
sb.append(' ');
}
sb.append(rs.getString(i));
}
rows.add(sb.toString());
}
} catch (SQLException e) {
throw new IllegalStateException(e);
}
return rows;
}
@Override
public void close() {
sessionFactory.close();
}
}
@@ -0,0 +1,64 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.hibernate.Session;
import org.junit.jupiter.api.Test;
/** The same probes as the Hibernate 5 module, run on 7.4.14.Final. Output goes to ../output. */
class MigrationProbeTest {
private static final String V = "7.4.14";
@Test
void schemaDefaults() {
try (Db db = new Db(Probe.class); Transcript t = new Transcript("06-schema-" + V)) {
t.line("Hibernate " + V + ", H2, @GeneratedValue with no strategy");
t.line("-- sequences (name, increment)");
List<String> seq = db.jdbc("select sequence_name, increment from information_schema.sequences order by 1");
seq.forEach(t::line);
t.line("-- columns of PROBE (name, type)");
List<String> cols = db.jdbc("select column_name, data_type from information_schema.columns where table_name = 'PROBE' order by column_name");
cols.forEach(t::line);
assertThat(seq).isNotEmpty();
assertThat(cols).hasSize(11);
}
}
@Test
void legacyQueries() {
try (Db db = new Db(Probe.class); Transcript t = new Transcript("07-legacy-queries-" + V)) {
t.line("Hibernate " + V);
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Probe p = new Probe("alpha");
s.persist(p);
t.line("session.persist(...) assigned the id: " + p.id + " (" + p.id.getClass().getSimpleName() + ")");
s.getTransaction().commit();
assertThat(p.id).isNotNull();
}
try (Session s = db.sessionFactory.openSession()) {
String ordinal = outcome(() -> s.createQuery("from Probe p where p.name = ?1").setParameter(1, "alpha").list());
t.line("HQL with ?1 : " + ordinal);
String bare = outcome(() -> s.createQuery("from Probe p where p.name = ?").setParameter(0, "alpha").list());
t.line("HQL with a bare ? : " + bare);
String enumInt = outcome(() -> s.createQuery("from Probe p where p.byOrdinal = 0").list());
t.line("HQL enum compared with 0 : " + enumInt);
assertThat(ordinal).startsWith("1 row");
assertThat(bare).startsWith("failed");
assertThat(enumInt).startsWith("0 row");
}
}
}
static String outcome(java.util.function.Supplier<List<?>> query) {
try {
return query.get().size() + " row(s)";
} catch (RuntimeException e) {
String msg = String.valueOf(e.getMessage()).lines().findFirst().orElse("");
return "failed: " + e.getClass().getSimpleName() + ": " + (msg.length() > 140 ? msg.substring(0, 140) + "..." : msg);
}
}
}
@@ -0,0 +1,23 @@
package com.ankurm.migration;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.SoftDelete;
@Entity
@SoftDelete
public class Note {
@Id
@GeneratedValue
Long id;
String text;
public Note() {
}
Note(String text) {
this.text = text;
}
}
@@ -0,0 +1,46 @@
package com.ankurm.migration;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.UUID;
/** One field of each type whose default column mapping is worth comparing, and a generated id. */
@Entity
public class Probe {
@Id
@GeneratedValue
Long id;
String name;
boolean active;
Instant createdAt;
LocalDateTime occurredAt;
UUID uuid;
Duration span;
BigDecimal price;
byte[] payload;
@Enumerated(EnumType.ORDINAL)
Colour byOrdinal;
@Enumerated(EnumType.STRING)
Colour byName;
public enum Colour { RED, GREEN }
public Probe() {
}
Probe(String name) {
this.name = name;
}
}
@@ -0,0 +1,26 @@
package com.ankurm.migration;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class Shop {
@Id
@GeneratedValue
Long id;
String name;
@Embedded
Address address;
public Shop() {
}
Shop(String name, Address address) {
this.name = name;
this.address = address;
}
}
@@ -0,0 +1,33 @@
package com.ankurm.migration;
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 them to output/NN-name.txt when closed. The tests assert what they print. */
final class Transcript implements AutoCloseable {
private final Path file;
private final List<String> lines = new ArrayList<>();
Transcript(String name) {
this.file = Path.of("..", "output", name + ".txt");
}
Transcript line(String s) {
lines.add(s);
return this;
}
@Override
public void close() {
try {
Files.createDirectories(file.getParent());
Files.write(file, lines);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,56 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.junit.jupiter.api.Test;
/** Opens the database the Hibernate 5.6.15 module created, with the same entity, and asks Hibernate 7 to validate it. */
class ValidateLegacySchemaTest {
@Test
void validateAndRunAgainstSchemaCreatedByHibernate5() throws Exception {
Path dir = Path.of("..", "target", "legacy-db");
assumeTrue(Files.exists(dir.resolve("probe.mv.db")), "run the h5 module first (scripts/run-all.sh does)");
String url = "jdbc:h2:file:" + dir.toAbsolutePath().normalize() + "/probe";
try (Transcript t = new Transcript("08-validate-against-5.6.15-schema-7.4.14")) {
t.line("Hibernate 7.4.14 opening the H2 file database written by Hibernate 5.6.15 (same Probe entity)");
String validate = open(url, "validate");
t.line("hbm2ddl.auto=validate: " + validate);
assertThat(validate).startsWith("failed");
String none = open(url, "none");
t.line("hbm2ddl.auto=none : " + none);
}
}
private static String open(String url, String mode) {
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("jakarta.persistence.jdbc.url", url)
.applySetting("jakarta.persistence.jdbc.user", "sa")
.applySetting("jakarta.persistence.jdbc.password", "")
.applySetting("hibernate.hbm2ddl.auto", mode)
.build();
try (SessionFactory sf = new MetadataSources(registry).addAnnotatedClass(Probe.class).buildMetadata().buildSessionFactory()) {
try (var s = sf.openSession()) {
Probe p = s.createSelectionQuery("from Probe", Probe.class).getResultList().get(0);
return "opened; loaded the row written by 5.6.15: name=" + p.name + ", uuid=" + p.uuid;
} catch (RuntimeException e) {
return "opened, but the first query failed: " + e.getClass().getSimpleName() + ": " + first(e);
}
} catch (RuntimeException e) {
return "failed: " + e.getClass().getSimpleName() + ": " + first(e);
}
}
private static String first(Throwable e) {
String m = String.valueOf(e.getMessage()).lines().findFirst().orElse("");
return m.length() > 220 ? m.substring(0, 220) + "..." : m;
}
}
@@ -0,0 +1,4 @@
5.x 7.x
coordinates org.hibernate:hibernate-core:5.6.15.Final org.hibernate.orm:hibernate-core:7.4.14.Final
class files Java 8 Java 17
persistence javax.persistence (JPA 2.2) jakarta.persistence (Jakarta Persistence 3.2)
@@ -0,0 +1,15 @@
== hibernate-core: public classes missing: 828, members removed from classes that remain: 2735, new classes: 5879
of the missing classes: 59 were deprecated in 5.6.15, 137 have a class of the same simple name somewhere in 7.4.14
packages with the most missing classes:
75 org.hibernate.dialect
74 org.hibernate.type
62 org.hibernate.type.descriptor.java
59 org.hibernate.criterion
53 org.hibernate.cfg
36 org.hibernate.type.descriptor.sql
35 org.hibernate.annotations
27 org.hibernate.dialect.function
25 org.hibernate.sql.ordering.antlr
23 org.hibernate.sql
23 org.hibernate.tool.hbm2ddl
22 org.hibernate.cfg.annotations
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
Hibernate 5.6.15, compiling OldHibernate.java
javac exit status: 0
@@ -0,0 +1,63 @@
Hibernate 7.4.14, compiling OldHibernate.java
src/compat/OldHibernate.java:4: error: cannot find symbol
import org.hibernate.Criteria;
^
symbol: class Criteria
location: package org.hibernate
src/compat/OldHibernate.java:8: error: package org.hibernate.criterion does not exist
import org.hibernate.criterion.Restrictions;
^
src/compat/OldHibernate.java:15: error: cannot find symbol
return s.save(o);
^
symbol: method save(Object)
location: variable s of type Session
src/compat/OldHibernate.java:19: error: cannot find symbol
s.update(o);
^
symbol: method update(Object)
location: variable s of type Session
src/compat/OldHibernate.java:23: error: cannot find symbol
s.saveOrUpdate(o);
^
symbol: method saveOrUpdate(Object)
location: variable s of type Session
src/compat/OldHibernate.java:27: error: cannot find symbol
s.delete(o);
^
symbol: method delete(Object)
location: variable s of type Session
src/compat/OldHibernate.java:31: error: incompatible types: void cannot be converted to Object
return s.load(Object.class, 1L);
^
src/compat/OldHibernate.java:35: error: cannot find symbol
Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
^
symbol: class Criteria
location: class OldHibernate
src/compat/OldHibernate.java:35: error: cannot find symbol
Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
^
symbol: method createCriteria(Class<Object>)
location: variable s of type Session
src/compat/OldHibernate.java:35: error: cannot find symbol
Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
^
symbol: variable Restrictions
location: class OldHibernate
src/compat/OldHibernate.java:40: error: cannot find symbol
return s.createSQLQuery("select 1").list();
^
symbol: method createSQLQuery(String)
location: variable s of type Session
src/compat/OldHibernate.java:44: error: incompatible types: FlushMode cannot be converted to FlushModeType
s.setFlushMode(FlushMode.AUTO);
^
src/compat/OldHibernate.java:48: error: cannot find symbol
return sf.getClassMetadata(Object.class);
^
symbol: method getClassMetadata(Class<Object>)
location: variable sf of type SessionFactory
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
13 errors
javac exit status: 1
@@ -0,0 +1,2 @@
Hibernate 5.6.15, compiling NewHibernate.java
javac exit status: 0
@@ -0,0 +1,6 @@
Hibernate 7.4.14, compiling NewHibernate.java
src/compat/NewHibernate.java:34: warning: [deprecation] createNativeQuery(String) in Session has been deprecated
return s.createNativeQuery("select 1").getResultList();
^
1 warning
javac exit status: 0
@@ -0,0 +1,2 @@
Hibernate 5.6.15, compiling OldAnnotations.java
javac exit status: 0
@@ -0,0 +1,27 @@
Hibernate 7.4.14, compiling OldAnnotations.java
src/compat/OldAnnotations.java:3: error: cannot find symbol
import org.hibernate.annotations.Where;
^
symbol: class Where
location: package org.hibernate.annotations
src/compat/OldAnnotations.java:7: error: cannot find symbol
@Where(clause = "deleted = false")
^
symbol: class Where
src/compat/OldAnnotations.java:10: error: cannot find symbol
@Type(type = "yes_no")
^
symbol: method type()
location: @interface Type
src/compat/OldAnnotations.java:10: error: annotation @Type is missing a default value for the element 'value'
@Type(type = "yes_no")
^
src/compat/OldAnnotations.java:13: warning: [removal] GenericGenerator in org.hibernate.annotations has been deprecated and marked for removal
@GenericGenerator(name = "g", strategy = "increment")
^
src/compat/OldAnnotations.java:13: warning: [removal] strategy() in GenericGenerator has been deprecated and marked for removal
@GenericGenerator(name = "g", strategy = "increment")
^
4 errors
2 warnings
javac exit status: 1
@@ -0,0 +1,32 @@
Hibernate 5.6.15, compiling NewAnnotations.java
src/compat/NewAnnotations.java:1: error: cannot find symbol
import org.hibernate.annotations.JdbcTypeCode;
^
symbol: class JdbcTypeCode
location: package org.hibernate.annotations
src/compat/NewAnnotations.java:2: error: cannot find symbol
import org.hibernate.annotations.SQLRestriction;
^
symbol: class SQLRestriction
location: package org.hibernate.annotations
src/compat/NewAnnotations.java:3: error: cannot find symbol
import org.hibernate.type.SqlTypes;
^
symbol: class SqlTypes
location: package org.hibernate.type
src/compat/NewAnnotations.java:6: error: cannot find symbol
@SQLRestriction("deleted = false")
^
symbol: class SQLRestriction
src/compat/NewAnnotations.java:9: error: cannot find symbol
@JdbcTypeCode(SqlTypes.CHAR)
^
symbol: class JdbcTypeCode
location: class NewAnnotations
src/compat/NewAnnotations.java:9: error: cannot find symbol
@JdbcTypeCode(SqlTypes.CHAR)
^
symbol: variable SqlTypes
location: class NewAnnotations
6 errors
javac exit status: 1
@@ -0,0 +1,2 @@
Hibernate 7.4.14, compiling NewAnnotations.java
javac exit status: 0
@@ -0,0 +1,15 @@
Hibernate 5.6.15, H2, @GeneratedValue with no strategy
-- sequences (name, increment)
HIBERNATE_SEQUENCE 1
-- columns of PROBE (name, type)
ACTIVE BOOLEAN
BYNAME CHARACTER VARYING
BYORDINAL INTEGER
CREATEDAT TIMESTAMP
ID BIGINT
NAME CHARACTER VARYING
OCCURREDAT TIMESTAMP
PAYLOAD BINARY VARYING
PRICE NUMERIC
SPAN BIGINT
UUID BINARY
@@ -0,0 +1,15 @@
Hibernate 7.4.14, H2, @GeneratedValue with no strategy
-- sequences (name, increment)
PROBE_SEQ 50
-- columns of PROBE (name, type)
ACTIVE BOOLEAN
BYNAME ENUM
BYORDINAL TINYINT
CREATEDAT TIMESTAMP WITH TIME ZONE
ID BIGINT
NAME CHARACTER VARYING
OCCURREDAT TIMESTAMP
PAYLOAD BINARY VARYING
PRICE NUMERIC
SPAN NUMERIC
UUID UUID
@@ -0,0 +1,15 @@
Hibernate 5.6.15 Hibernate 7.4.14
-- id sequences (name, increment)
HIBERNATE_SEQUENCE 1 PROBE_SEQ 50
-- column types in H2
ACTIVE BOOLEAN BOOLEAN (same)
BYNAME CHARACTER VARYING ENUM <-- differs
BYORDINAL INTEGER TINYINT <-- differs
CREATEDAT TIMESTAMP TIMESTAMP WITH TIME ZONE <-- differs
ID BIGINT BIGINT (same)
NAME CHARACTER VARYING CHARACTER VARYING (same)
OCCURREDAT TIMESTAMP TIMESTAMP (same)
PAYLOAD BINARY VARYING BINARY VARYING (same)
PRICE NUMERIC NUMERIC (same)
SPAN BIGINT NUMERIC <-- differs
UUID BINARY UUID <-- differs
@@ -0,0 +1,6 @@
Hibernate 5.6.15
session.save(...) returned: 1 (Long)
HQL with ?1 : 1 row(s)
HQL with a bare ? : failed: IllegalArgumentException: org.hibernate.QueryException: Legacy-style query parameters (`?`) are no longer supported; use JPA-style ordinal parameters (e.g., `?1`) ins...
HQL enum compared with 0 : 0 row(s)
session.createCriteria(...) : 1 row(s)
@@ -0,0 +1,5 @@
Hibernate 7.4.14
session.persist(...) assigned the id: 1 (Long)
HQL with ?1 : 1 row(s)
HQL with a bare ? : failed: IllegalArgumentException: org.hibernate.query.ParameterLabelException: Unlabeled ordinal parameter ('?' rather than ?1)
HQL enum compared with 0 : 0 row(s)
@@ -0,0 +1,3 @@
Hibernate 7.4.14 opening the H2 file database written by Hibernate 5.6.15 (same Probe entity)
hbm2ddl.auto=validate: failed: SchemaManagementException: Schema validation: wrong column type encountered in column [span] in table [Probe]; found [bigint (Types#BIGINT)], but expecting [numeric(21,0) (Types#NUMERIC)]
hbm2ddl.auto=none : opened; loaded the row written by 5.6.15: name=legacy row, uuid=null
@@ -0,0 +1,10 @@
Hibernate 5.6.15, compiling JakartaNamespace.java
src/compat/JakartaNamespace.java:1: error: package jakarta.persistence does not exist
import jakarta.persistence.Entity;
^
src/compat/JakartaNamespace.java:3: error: cannot find symbol
@Entity
^
symbol: class Entity
2 errors
javac exit status: 1
@@ -0,0 +1,2 @@
Hibernate 7.4.14, compiling JakartaNamespace.java
javac exit status: 0
@@ -0,0 +1,2 @@
Hibernate 5.6.15, compiling JavaxNamespace.java
javac exit status: 0
@@ -0,0 +1,10 @@
Hibernate 7.4.14, compiling JavaxNamespace.java
src/compat/JavaxNamespace.java:1: error: package javax.persistence does not exist
import javax.persistence.Entity;
^
src/compat/JavaxNamespace.java:3: error: cannot find symbol
@Entity
^
symbol: class Entity
2 errors
javac exit status: 1
@@ -0,0 +1,6 @@
persist(a) -> id assigned on the object: 1
find(Author, id) -> Ann (1 select)
getReference(Author, id) -> initialized: false (0 selects)
merge(detached) -> returns a different instance: true, name now Ann Smith
findMultiple(ids 3) -> 3 entries, missing id gives: null
remove(a) -> find afterwards: null
+8
View File
@@ -0,0 +1,8 @@
named parameter : [Beta]
?1 parameter : [Alpha, Beta]
limit 2 offset 1 in HQL : [Beta, Gamma]
order by ... nulls last : [Beta, Alpha, Gamma]
getSingleResultOrNull() : null
join fetch : author initialized = true
count over an association : 2
createMutationQuery update: 2 rows
@@ -0,0 +1,3 @@
insert x3 (no persistence context) done
get twice -> same instance: false (a stateless session has no first-level cache)
rows in table: [3]
@@ -0,0 +1,2 @@
bySimpleNaturalId(...).load -> Ann
sql: select a1_0.id,a1_0.email,a1_0.name from Author a1_0 where a1_0.email=?
@@ -0,0 +1,3 @@
remove(note) ran: update Note set deleted=true where id=? and deleted=false
find afterwards : null
row still in the table: [TRUE 1 remember]
@@ -0,0 +1,2 @@
loaded: Address[street=1 Main St, city=Pune]
columns: [CITY, ID, NAME, STREET]
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Compare the public API of hibernate-core 5.6.15.Final (org.hibernate) with 7.4.14.Final (org.hibernate.orm).
javax.persistence is read as jakarta.persistence, so the namespace move does not drown the real changes.
Writes output/01-versions-and-baseline.txt, output/02-removed-api.txt and output/02-removed-api-summary.txt.
Needs a JDK (javap) and access to Maven Central.
"""
import collections
import os
import pathlib
import re
import subprocess
import time
import urllib.error
import urllib.request
import zipfile
ROOT = pathlib.Path(__file__).resolve().parent.parent
JARS = ROOT / "target" / "jars"
OUT = ROOT / "output"
JAVAP = os.path.join(os.environ["JAVA_HOME"], "bin", "javap") if os.environ.get("JAVA_HOME") else "javap"
OLD = ("org/hibernate", "5.6.15.Final")
NEW = ("org/hibernate/orm", "7.4.14.Final")
def fetch(url, dest):
if dest.exists():
return
for _ in range(10):
try:
with urllib.request.urlopen(url) as r:
dest.write_bytes(r.read())
return
except urllib.error.HTTPError as e:
if e.code != 429:
raise
time.sleep(5)
raise RuntimeError("rate limited: " + url)
def jar(side):
group, v = side
dest = JARS / f"hibernate-core-{v}.jar"
fetch(f"https://repo1.maven.org/maven2/{group}/hibernate-core/{v}/hibernate-core-{v}.jar", dest)
return dest
def classes(path):
with zipfile.ZipFile(path) as z:
return sorted(n[:-6].replace("/", ".") for n in z.namelist()
if n.endswith(".class") and not n.endswith("module-info.class") and "META-INF" not in n)
def major(path):
with zipfile.ZipFile(path) as z:
name = next(n for n in z.namelist() if n.endswith(".class") and "META-INF" not in n and not n.endswith("module-info.class"))
return int.from_bytes(z.read(name)[6:8], "big")
def jakarta(text):
return text.replace("javax.persistence", "jakarta.persistence").replace("javax.transaction", "jakarta.transaction")
def norm(sig):
sig = jakarta(sig)
sig = re.sub(r"\s+throws\s+.*$", "", sig)
prev = None
while prev != sig:
prev = sig
sig = re.sub(r"<[^<>]*>", "", sig)
return " ".join(sig.split())
MEMBER = re.compile(r"^ ((?:public|protected)[^;]*);$")
def dump(path):
names = [c for c in classes(path) if ".internal." not in c and ".spi." not in c and not c.startswith("org.hibernate.sql.ast")
and not c.startswith("org.hibernate.sql.results") and not c.startswith("org.hibernate.query.sqm.tree")
and not c.startswith("org.hibernate.grammars")]
result = {}
for i in range(0, len(names), 150):
chunk = names[i:i + 150]
p = subprocess.run([JAVAP, "-v", "-protected", "-cp", str(path)] + chunk, capture_output=True, text=True)
for block in re.split(r"(?m)^Classfile ", p.stdout)[1:]:
lines = block.splitlines()
cls = jakarta(re.search(r"!/(.*)\.class", lines[0]).group(1).replace("/", "."))
public, seen_flags, in_pool, cur = False, False, False, None
members = {}
cls_dep = bool(re.search(r"^Deprecated: true", block, re.M))
for line in lines[1:]:
if not seen_flags and line.startswith(" flags:"):
public = "ACC_PUBLIC" in line
seen_flags = True
if line.startswith("Constant pool:"):
in_pool = True
continue
if in_pool:
if line.startswith("{"):
in_pool = False
continue
m = MEMBER.match(line)
if m:
cur = " ".join(m.group(1).split())
members[cur] = False
continue
if line.strip() == "Deprecated: true" and cur is not None:
members[cur] = True
if line.startswith("}"):
cur = None
result[cls] = (public, cls_dep, members)
return result
def main():
JARS.mkdir(parents=True, exist_ok=True)
OUT.mkdir(parents=True, exist_ok=True)
old, new = jar(OLD), jar(NEW)
mo, mn = major(old), major(new)
versions = [
f"{'':<12}{'5.x':<46}{'7.x'}",
f"{'coordinates':<12}{'org.hibernate:hibernate-core:5.6.15.Final':<46}org.hibernate.orm:hibernate-core:7.4.14.Final",
f"{'class files':<12}{'Java ' + str(mo - 44):<46}Java {mn - 44}",
f"{'persistence':<12}{'javax.persistence (JPA 2.2)':<46}jakarta.persistence (Jakarta Persistence 3.2)",
]
do, dn = dump(old), dump(new)
co = {jakarta(c) for c in classes(old)}
cn = {jakarta(c) for c in classes(new)}
simple_new = collections.defaultdict(list)
for c in cn:
simple_new[c.split(".")[-1]].append(c)
removed_cls, removed_mem = [], []
for c in sorted(co - cn):
if c in do and do[c][0]:
removed_cls.append((c, do[c][1], simple_new.get(c.split(".")[-1], [])))
for c in sorted(do):
if c in dn and do[c][0]:
new_norm = {norm(x) for x in dn[c][2]}
for sig, dep in do[c][2].items():
if norm(sig) not in new_norm:
removed_mem.append((c, jakarta(sig), dep))
lines = [f"== hibernate-core: public classes missing: {len(removed_cls)}, members removed from classes that remain: {len(removed_mem)}, "
f"new classes: {len(cn - co)}"]
for c, dep, same_name in removed_cls:
if same_name:
note = "same simple name exists in " + ", ".join(sorted(p.rsplit('.', 1)[0] for p in same_name)[:2])
else:
note = "no class with this name"
lines.append(f" class {c} [{'deprecated in 5.6' if dep else 'NOT deprecated in 5.6'}; {note}]")
for c, sig, dep in removed_mem:
lines.append(f" member {c}: {sig} [{'deprecated in 5.6' if dep else 'NOT deprecated in 5.6'}]")
(OUT / "01-versions-and-baseline.txt").write_text("\n".join(versions) + "\n")
(OUT / "02-removed-api.txt").write_text("\n".join(lines) + "\n")
# summary: counts per package, and how many of the missing classes have a namesake elsewhere
per_pkg = collections.Counter(c.rsplit(".", 1)[0] for c, _, _ in removed_cls)
dep_n = sum(1 for _, d, _ in removed_cls if d)
ren_n = sum(1 for _, _, s in removed_cls if s)
summary = [lines[0],
f" of the missing classes: {dep_n} were deprecated in 5.6.15, {ren_n} have a class of the same simple name somewhere in 7.4.14",
" packages with the most missing classes:"]
for pkg, n in per_pkg.most_common(12):
summary.append(f" {n:>4} {pkg}")
(OUT / "02-removed-api-summary.txt").write_text("\n".join(summary) + "\n")
print("\n".join(versions))
print("\n".join(summary))
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Compiles src/compat/*.java against Hibernate 5.6.15 (h5/) and 7.4.14 (h7/) and records javac's answer in output/.
set -u
cd "$(dirname "$0")/.."
mkdir -p target output
compile() { # version dir file outfile
local v=$1 dir=$2 f=$3 out=$4
rm -rf target/classes-$f-$v; mkdir -p target/classes-$f-$v
{
echo "Hibernate $v, compiling $f.java"
javac -Xlint:deprecation -proc:none -d target/classes-$f-$v -cp "$(cat target/cp-$v.txt)" src/compat/$f.java 2>&1 | grep -v "^Picked up"
echo "javac exit status: ${PIPESTATUS[0]}"
} >> $out
}
for pair in "h5:5.6.15" "h7:7.4.14"; do
dir=${pair%%:*}; v=${pair##*:}
(cd $dir && mvn -q -B dependency:build-classpath -Dmdep.outputFile=../target/cp-$v.txt >/dev/null) || { echo "classpath failed for $v"; exit 1; }
for spec in "OldHibernate:03-old-hibernate-apis" "NewHibernate:04-new-hibernate-apis" "OldAnnotations:05-old-annotations" "NewAnnotations:06-new-annotations" "JavaxNamespace:09-namespace-javax" "JakartaNamespace:09-namespace-jakarta"; do
f=${spec%%:*}; n=${spec##*:}
out=output/$n-$v.txt; : > $out
compile $v $dir $f $out
done
done
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Rewrites everything in output/. Needs JDK 17+, Maven 3.9, Python 3 and internet access to Maven Central.
set -e
cd "$(dirname "$0")/.."
python3 scripts/api_diff.py
(cd h5 && mvn -q -B clean test)
(cd h7 && mvn -q -B clean test)
python3 scripts/schema_diff.py
./scripts/compile-old-apis.sh
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Lines up output/06-schema-5.6.15.txt and output/06-schema-7.4.14.txt side by side into output/06-schema-diff.txt."""
import pathlib
OUT = pathlib.Path(__file__).resolve().parent.parent / "output"
def read(version):
seq, cols, section = {}, {}, None
for line in (OUT / f"06-schema-{version}.txt").read_text().splitlines():
if line.startswith("-- sequences"):
section = "seq"
elif line.startswith("-- columns"):
section = "col"
elif section == "seq":
name, inc = line.rsplit(" ", 1)
seq[name] = inc
elif section == "col":
name, typ = line.split(" ", 1)
cols[name] = typ
return seq, cols
s5, c5 = read("5.6.15")
s7, c7 = read("7.4.14")
rows = [f"{'':<14}{'Hibernate 5.6.15':<26}{'Hibernate 7.4.14':<26}", "-- id sequences (name, increment)"]
rows.append(f"{'':<14}{', '.join(f'{k} {v}' for k, v in s5.items()):<26}{', '.join(f'{k} {v}' for k, v in s7.items())}")
rows.append("-- column types in H2")
for name in sorted(set(c5) | set(c7)):
a, b = c5.get(name, "-"), c7.get(name, "-")
rows.append(f"{name:<14}{a:<26}{b:<26}{'(same)' if a == b else '<-- differs'}")
(OUT / "06-schema-diff.txt").write_text("\n".join(rows) + "\n")
print("\n".join(rows))
@@ -0,0 +1,5 @@
import jakarta.persistence.Entity;
@Entity
class JakartaNamespace {
}
@@ -0,0 +1,5 @@
import javax.persistence.Entity;
@Entity
class JavaxNamespace {
}
@@ -0,0 +1,11 @@
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.SQLRestriction;
import org.hibernate.type.SqlTypes;
/** The Hibernate 6 and 7 spelling of the same ideas. This does not compile on 5.6.15. */
@SQLRestriction("deleted = false")
class NewAnnotations {
@JdbcTypeCode(SqlTypes.CHAR)
boolean flag;
}
@@ -0,0 +1,40 @@
import java.util.List;
import org.hibernate.FlushMode;
import org.hibernate.Session;
/** The replacements for OldHibernate that exist on both 5.6.15 and 7.4.14, so they can be made before the upgrade. */
class NewHibernate {
void persist(Session s, Object o) {
s.persist(o);
}
Object merge(Session s, Object o) {
return s.merge(o);
}
void remove(Session s, Object o) {
s.remove(o);
}
Object find(Session s) {
return s.find(Object.class, 1L);
}
Object reference(Session s) {
return s.getReference(Object.class, 1L);
}
List<Object> query(Session s) {
return s.createQuery("from Object o where o.name = :n", Object.class).setParameter("n", "x").getResultList();
}
List<?> nativeQuery(Session s) {
return s.createNativeQuery("select 1").getResultList();
}
void flushMode(Session s) {
s.setHibernateFlushMode(FlushMode.AUTO);
}
}
@@ -0,0 +1,15 @@
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.Where;
/** Hibernate 5 annotation attributes. The annotations are applied to a plain class, so no persistence import is needed. */
@SuppressWarnings("deprecation")
@Where(clause = "deleted = false")
class OldAnnotations {
@Type(type = "yes_no")
boolean flag;
@GenericGenerator(name = "g", strategy = "increment")
Long id;
}
@@ -0,0 +1,50 @@
import java.io.Serializable;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
/** Hibernate 5 style calls that use no persistence annotations. Compiled by scripts/compile-old-apis.sh against 5.6.15 and 7.4.14. */
@SuppressWarnings({"deprecation", "rawtypes"})
class OldHibernate {
Serializable save(Session s, Object o) {
return s.save(o);
}
void update(Session s, Object o) {
s.update(o);
}
void saveOrUpdate(Session s, Object o) {
s.saveOrUpdate(o);
}
void delete(Session s, Object o) {
s.delete(o);
}
Object load(Session s) {
return s.load(Object.class, 1L);
}
List criteria(Session s) {
Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
return c.list();
}
List sql(Session s) {
return s.createSQLQuery("select 1").list();
}
void flushMode(Session s) {
s.setFlushMode(FlushMode.AUTO);
}
Object metadata(SessionFactory sf) {
return sf.getClassMetadata(Object.class);
}
}