CapturedQuery.java

package li.selman.persistencetest.core;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import org.jspecify.annotations.Nullable;

/**
 * A single SQL statement execution captured at the JDBC layer, with everything needed to analyze, assert
 * on, or snapshot it.
 *
 * <p>Immutable and framework-agnostic: nothing here knows about Hibernate, Spring, or a specific JDBC
 * driver. {@code query-capture} is responsible for producing instances of this type from a real
 * {@link java.sql.Connection}.
 *
 * @param sequence 0-based order in which this statement was submitted to the driver, relative to other
 *     statements captured in the same session. Used to reconstruct an execution timeline.
 * @param timestamp wall-clock time execution started.
 * @param sql the exact SQL text passed to the driver, unmodified.
 * @param normalizedSql a normalized, deterministic rendering of {@code sql}; see
 *     {@code li.selman.persistencetest.core.normalize.SqlNormalizer}.
 * @param statementType the kind of statement this is.
 * @param tables tables (and views) referenced by the statement, lower-cased and sorted; empty if unknown
 *     (e.g. the normalizer couldn't parse this statement).
 * @param parameters bind parameters in position order; empty for statements with none.
 * @param duration wall-clock execution time, or {@code null} if execution never completed (e.g. the
 *     statement threw before the driver reported timing).
 * @param affectedRows rows affected as reported by the driver (JDBC update count), or {@code null} when not
 *     applicable (e.g. a {@code SELECT}) or not available.
 * @param exception the exception the statement failed with, or {@code null} if it succeeded.
 * @param threadName name of the thread that executed the statement.
 * @param connectionId an identifier stable for the lifetime of the underlying {@link java.sql.Connection},
 *     used to correlate statements executed on the same connection (e.g. within one transaction).
 */
public record CapturedQuery(
        long sequence,
        Instant timestamp,
        String sql,
        String normalizedSql,
        StatementType statementType,
        List<String> tables,
        List<BindParameter> parameters,
        @Nullable Duration duration,
        @Nullable Long affectedRows,
        @Nullable Throwable exception,
        String threadName,
        String connectionId) {

    @SuppressWarnings("Var") // reassigning a compact constructor's implicit parameter for a defensive copy
    // is the standard record idiom; it can't be annotated @Var since the underlying field is final.
    public CapturedQuery {
        if (sequence < 0) {
            throw new IllegalArgumentException("sequence must be >= 0, got " + sequence);
        }
        tables = List.copyOf(tables);
        parameters = List.copyOf(parameters);
    }

    /** Whether this statement failed. */
    public boolean isFailure() {
        return exception != null;
    }

    /** Whether this is a data-modifying statement ({@code INSERT}/{@code UPDATE}/{@code DELETE}). */
    public boolean isMutation() {
        return statementType == StatementType.INSERT
                || statementType == StatementType.UPDATE
                || statementType == StatementType.DELETE;
    }
}