LegacyCoreSchemaPreflight.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.jgit.server.config;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Properties;
import com.google.gson.Gson;
import io.github.carstenartur.jgit.storage.hibernate.schema.LegacyCoreSchemaAdoption;
import io.github.carstenartur.jgit.storage.hibernate.schema.LegacyCoreSchemaAdoption.DuplicatePackIdentity;
import io.github.carstenartur.jgit.storage.hibernate.schema.LegacyCoreSchemaAdoption.LegacySchemaReport;
/**
* Runs the released Core legacy-schema validator without starting Hibernate or
* executing DDL.
*
* <p>The command uses the same database environment variables as the server and
* prints one JSON report to standard output. Any unsafe or unsupported schema
* causes a non-zero process exit through the validator exception.</p>
*/
public final class LegacyCoreSchemaPreflight {
private static final String CONNECTION_URL= "hibernate.connection.url"; //$NON-NLS-1$
private static final String CONNECTION_USER= "hibernate.connection.username"; //$NON-NLS-1$
private static final String CONNECTION_PASSWORD= "hibernate.connection.password"; //$NON-NLS-1$
private static final String CONNECTION_DRIVER= "hibernate.connection.driver_class"; //$NON-NLS-1$
private static final String POSTGRESQL_URL_PREFIX= "jdbc:postgresql:"; //$NON-NLS-1$
private static final String HSQLDB_URL_PREFIX= "jdbc:hsqldb:"; //$NON-NLS-1$
private static final String SQL_SERVER_URL_PREFIX= "jdbc:sqlserver:"; //$NON-NLS-1$
private static final String HSQLDB_IF_EXISTS= ";ifexists=true"; //$NON-NLS-1$
private LegacyCoreSchemaPreflight() {
}
/**
* Execute the read-only preflight from the server environment.
*
* @param args
* ignored
* @throws Exception
* if the driver cannot be loaded, the database cannot be read or
* the schema is unsafe to adopt
*/
public static void main(String[] args) throws Exception {
PreflightReport report= inspect(HibernateConfig.buildProperties());
System.out.println(toJson(report));
}
/**
* Open a JDBC connection from Hibernate properties and inspect the schema.
*
* @param properties
* database properties; no Hibernate bootstrap is performed
* @return validated report safe to use for the next adoption decision
* @throws ClassNotFoundException
* if an explicitly configured JDBC driver cannot be loaded
* @throws SQLException
* if the database connection fails
*/
static PreflightReport inspect(Properties properties)
throws ClassNotFoundException, SQLException {
Objects.requireNonNull(properties, "properties"); //$NON-NLS-1$
String url= requireProperty(properties, CONNECTION_URL);
requirePublishedLegacyAdoptionPath(url);
String driver= properties.getProperty(CONNECTION_DRIVER);
if (driver != null && !driver.isBlank()) {
Class.forName(driver);
}
String user= properties.getProperty(CONNECTION_USER, ""); //$NON-NLS-1$
String password= properties.getProperty(CONNECTION_PASSWORD, ""); //$NON-NLS-1$
try (Connection connection= DriverManager.getConnection(url, user, password)) {
connection.setReadOnly(true);
return inspect(connection);
}
}
/** Validate one existing JDBC connection without modifying its schema. */
static PreflightReport inspect(Connection connection) {
LegacySchemaReport report= LegacyCoreSchemaAdoption
.requireSafeToAdopt(connection);
Comparator<DuplicatePackIdentity> duplicateOrder= Comparator
.comparing(DuplicatePackIdentity::repositoryName)
.thenComparing(DuplicatePackIdentity::packName)
.thenComparing(DuplicatePackIdentity::packExtension)
.thenComparingLong(DuplicatePackIdentity::rowCount);
List<DuplicatePackIdentity> duplicatePackIdentities= report
.duplicatePackIdentities().stream().sorted(duplicateOrder).toList();
return new PreflightReport(report.columns().stream().sorted().toList(),
report.missingRequiredColumns().stream().sorted().toList(),
report.packRows(), report.incompletePackRows(),
duplicatePackIdentities, report.hasCommittedColumn(),
report.hasCommittedAtColumn(), report.requiresAdoption());
}
/** Serialize the bounded report without exposing connection credentials. */
static String toJson(PreflightReport report) {
return new Gson().toJson(report);
}
/** Require a JDBC family with a published pre-library adoption migration. */
static void requirePublishedLegacyAdoptionPath(String url) {
String normalizedUrl= Objects.requireNonNull(url, "url") //$NON-NLS-1$
.toLowerCase(Locale.ROOT);
if (!normalizedUrl.startsWith(POSTGRESQL_URL_PREFIX)
&& !normalizedUrl.startsWith(HSQLDB_URL_PREFIX)
&& !normalizedUrl.startsWith(SQL_SERVER_URL_PREFIX)) {
throw new IllegalArgumentException(
"No published pre-library legacy-adoption migration exists " //$NON-NLS-1$
+ "for JDBC family " + databaseFamily(normalizedUrl) //$NON-NLS-1$
+ ". Use PostgreSQL, HSQLDB or SQL Server, or add " //$NON-NLS-1$
+ "and release generic support upstream first."); //$NON-NLS-1$
}
if (normalizedUrl.startsWith(HSQLDB_URL_PREFIX)
&& !normalizedUrl.contains(HSQLDB_IF_EXISTS)) {
throw new IllegalArgumentException(
"HSQLDB legacy-schema preflight requires ;ifexists=true " //$NON-NLS-1$
+ "so a mistyped path cannot create a database."); //$NON-NLS-1$
}
}
private static String databaseFamily(String url) {
int separator= url.indexOf(':', "jdbc:".length()); //$NON-NLS-1$
return separator > 0 ? url.substring(0, separator + 1) : "unknown"; //$NON-NLS-1$
}
private static String requireProperty(Properties properties, String name) {
String value= properties.getProperty(name);
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(
"Missing required database property: " + name); //$NON-NLS-1$
}
return value;
}
/** Machine-readable result of the released read-only legacy-schema check. */
record PreflightReport(List<String> columns,
List<String> missingRequiredColumns, long packRows,
long incompletePackRows,
List<DuplicatePackIdentity> duplicatePackIdentities,
boolean hasCommittedColumn, boolean hasCommittedAtColumn,
boolean requiresAdoption) {
}
}