UnconditionalMutationAudit.java
package io.github.databaseaudits.audit.runtime;
import java.util.List;
import java.util.Set;
import io.github.databaseaudits.capture.SqlCapturingStatementInspector;
import lombok.AllArgsConstructor;
/**
* No {@code UPDATE} or {@code DELETE} without a {@code WHERE} clause may be
* executed.
*
* <p>
* An unconditional mutation rewrites or wipes an entire table — a destroyed
* fixture in a test, a data-loss incident in production, usually an accidental
* derived-delete or a {@code @Modifying} {@code @Query} missing its predicate.
* Detection is a literal token scan for a leading {@code UPDATE}/{@code DELETE}
* with no {@code WHERE}; the word "where" inside a string literal could in
* theory mask one, but that is negligible with bind parameters. Reads
* {@link SqlCapturingStatementInspector} (run after the workload); throws on an
* empty capture rather than reporting nothing vacuously. Pass a deliberate
* full-table statement (normalized text) as {@code excludedStatements}.
*
* <p>
* Fix: add a {@code WHERE} clause, or exclude a deliberate full-table
* statement.
*/
@AllArgsConstructor
public class UnconditionalMutationAudit {
private final SqlCapturingStatementInspector sqlCapturer;
/**
* Returns every captured {@code UPDATE}/{@code DELETE} with no
* {@code WHERE} clause (normalized), except the excluded statements; an
* empty list when none.
*
* @param excludedStatements
* The normalized statement texts to skip.
* @throws IllegalStateException
* If nothing was captured, so the audit
* would otherwise report nothing
* vacuously.
*/
public List<String> audit(final Set<String> excludedStatements) {
final Set<String> capturedSql = sqlCapturer.capturedSql();
if (capturedSql.isEmpty()) {
throw new IllegalStateException(
SqlCapturingStatementInspector.EMPTY_CAPTURE_MESSAGE);
}
return capturedSql.stream().map(sqlCapturer::normalize)
.filter(this::isUnconditionalMutation)
.filter(sql -> !excludedStatements.contains(sql.toLowerCase()))
.distinct().sorted().toList();
}
private boolean isUnconditionalMutation(final String normalizedSql) {
final String upper = normalizedSql.toUpperCase();
return (upper.startsWith("UPDATE ") || upper.startsWith("DELETE "))
&& !upper.contains(" WHERE ");
}
}