CommitIndexProjectionResetMigrator.java
package com.taxonomy.shared.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
/**
* Removes a rebuildable commit-history projection before its first complete
* repository-tenant migration.
*
* <p>Historic commit-index rows do not contain trustworthy repository/workspace
* provenance. A partial migration must not retain some rows while deleting
* others, because persistent Hibernate Search documents for the deleted rows
* could survive. The complete target contract is detected from JDBC metadata;
* only while that contract is absent is the entire relational projection
* deleted. Once complete, subsequent starts are idempotent and preserve rows.</p>
*/
final class CommitIndexProjectionResetMigrator {
private static final Logger log = LoggerFactory.getLogger(
CommitIndexProjectionResetMigrator.class);
private static final String TABLE = "architecture_commit_index";
private static final Set<String> TARGET_UNIQUE_COLUMNS = Set.of(
"repository_id", "workspace_scope_key", "branch", "commit_id");
private final DataSource dataSource;
CommitIndexProjectionResetMigrator(DataSource dataSource) {
this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
}
void resetIfTargetContractIsIncomplete() {
try (Connection connection = dataSource.getConnection()) {
boolean previousAutoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
try {
TableRef table = findTable(connection, TABLE);
if (table == null || targetContractComplete(connection, table)) {
connection.commit();
return;
}
long rows = countRows(connection, table);
if (rows > 0) {
execute(connection, "DELETE FROM " + qualified(connection, table));
log.warn(
"Removed {} architecture commit-index projection row(s) before first complete tenant migration; authoritative JGit history remains intact",
rows);
}
connection.commit();
} catch (SQLException | RuntimeException error) {
rollbackQuietly(connection, error);
throw new IllegalStateException(
"Unable to reset incomplete commit-index projection safely",
error);
} finally {
connection.setAutoCommit(previousAutoCommit);
}
} catch (SQLException error) {
throw new IllegalStateException(
"Unable to open database for commit-index projection reset",
error);
}
}
private boolean targetContractComplete(
Connection connection,
TableRef table) throws SQLException {
for (String column : List.of(
"repository_id", "workspace_scope_key", "branch", "commit_id")) {
if (!columnExists(connection, table, column)
|| columnNullable(connection, table, column)) {
return false;
}
}
TableRef repositoryTable = findTable(connection, "system_repository");
if (repositoryTable == null
|| !hasImportedKey(
connection,
table,
"repository_id",
repositoryTable,
"repository_id")) {
return false;
}
return uniqueIndexes(connection, table).stream()
.anyMatch(index -> new LinkedHashSet<>(index.columns())
.equals(TARGET_UNIQUE_COLUMNS));
}
private boolean hasImportedKey(
Connection connection,
TableRef sourceTable,
String sourceColumn,
TableRef targetTable,
String targetColumn) throws SQLException {
try (ResultSet keys = connection.getMetaData().getImportedKeys(
sourceTable.catalog(), sourceTable.schema(), sourceTable.name())) {
while (keys.next()) {
if (sourceColumn.equalsIgnoreCase(keys.getString("FKCOLUMN_NAME"))
&& targetTable.name().equalsIgnoreCase(
keys.getString("PKTABLE_NAME"))
&& targetColumn.equalsIgnoreCase(
keys.getString("PKCOLUMN_NAME"))) {
return true;
}
}
}
return false;
}
private List<IndexDefinition> uniqueIndexes(
Connection connection,
TableRef table) throws SQLException {
Map<String, Map<Short, String>> byName = new LinkedHashMap<>();
try (ResultSet indexes = connection.getMetaData().getIndexInfo(
table.catalog(), table.schema(), table.name(), true, false)) {
while (indexes.next()) {
String name = indexes.getString("INDEX_NAME");
String column = indexes.getString("COLUMN_NAME");
short type = indexes.getShort("TYPE");
if (name == null || column == null
|| type == DatabaseMetaData.tableIndexStatistic) {
continue;
}
short ordinal = indexes.getShort("ORDINAL_POSITION");
byName.computeIfAbsent(name, ignored -> new TreeMap<>())
.put(ordinal, column.toLowerCase(Locale.ROOT));
}
}
List<IndexDefinition> result = new ArrayList<>();
byName.forEach((name, columns) -> result.add(
new IndexDefinition(name, List.copyOf(columns.values()))));
return result;
}
private boolean columnExists(
Connection connection,
TableRef table,
String logicalColumn) throws SQLException {
try (ResultSet columns = connection.getMetaData().getColumns(
table.catalog(), table.schema(), table.name(), "%")) {
while (columns.next()) {
String name = columns.getString("COLUMN_NAME");
if (name != null && name.equalsIgnoreCase(logicalColumn)) {
return true;
}
}
}
return false;
}
private boolean columnNullable(
Connection connection,
TableRef table,
String logicalColumn) throws SQLException {
try (ResultSet columns = connection.getMetaData().getColumns(
table.catalog(), table.schema(), table.name(), "%")) {
while (columns.next()) {
String name = columns.getString("COLUMN_NAME");
if (name != null && name.equalsIgnoreCase(logicalColumn)) {
return columns.getInt("NULLABLE")
!= DatabaseMetaData.columnNoNulls;
}
}
}
return true;
}
private TableRef findTable(
Connection connection,
String logicalName) throws SQLException {
String preferredSchema = safeSchema(connection);
List<TableRef> matches = new ArrayList<>();
try (ResultSet tables = connection.getMetaData().getTables(
connection.getCatalog(), null, "%", new String[] {"TABLE"})) {
while (tables.next()) {
String name = tables.getString("TABLE_NAME");
if (name != null && name.equalsIgnoreCase(logicalName)) {
matches.add(new TableRef(
tables.getString("TABLE_CAT"),
tables.getString("TABLE_SCHEM"),
name));
}
}
}
return matches.stream()
.sorted((left, right) -> Integer.compare(
preferredSchema != null
&& preferredSchema.equalsIgnoreCase(left.schema()) ? 0 : 1,
preferredSchema != null
&& preferredSchema.equalsIgnoreCase(right.schema()) ? 0 : 1))
.findFirst()
.orElse(null);
}
private long countRows(Connection connection, TableRef table) throws SQLException {
try (Statement statement = connection.createStatement();
ResultSet result = statement.executeQuery(
"SELECT COUNT(*) FROM " + qualified(connection, table))) {
result.next();
return result.getLong(1);
}
}
private void execute(Connection connection, String sql) throws SQLException {
try (Statement statement = connection.createStatement()) {
statement.execute(sql);
}
}
private String qualified(Connection connection, TableRef table) throws SQLException {
String tableName = quoted(connection, table.name());
return table.schema() == null || table.schema().isBlank()
? tableName
: quoted(connection, table.schema()) + "." + tableName;
}
private String quoted(Connection connection, String identifier) throws SQLException {
String quote = connection.getMetaData().getIdentifierQuoteString();
if (quote == null || quote.isBlank()) {
return identifier;
}
return quote + identifier.replace(quote, quote + quote) + quote;
}
private String safeSchema(Connection connection) {
try {
return connection.getSchema();
} catch (SQLException | AbstractMethodError ignored) {
return null;
}
}
private void rollbackQuietly(Connection connection, Throwable original) {
try {
connection.rollback();
} catch (SQLException rollbackFailure) {
original.addSuppressed(rollbackFailure);
}
}
private record TableRef(String catalog, String schema, String name) {
}
private record IndexDefinition(String name, List<String> columns) {
}
}