DefaultPrepAndExpectedTestCase.java

/*
 *
 * The DbUnit Database Testing Framework
 * Copyright (C)2002-2008, DbUnit.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
package org.dbunit;

import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.dbunit.assertion.DefaultFailureHandler;
import org.dbunit.assertion.FailureHandler;
import org.dbunit.assertion.comparer.value.ValueComparer;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConfigOverrides;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.IResultSetTableFactory;
import org.dbunit.database.connection.AutoCommitOffWarning;
import org.dbunit.database.connection.ConnectionOwnership;
import org.dbunit.database.connection.LeftOpenConnectionWarning;
import org.dbunit.database.connection.TestScopedConnection;
import org.dbunit.database.rowcount.RowCountCheck;
import org.dbunit.database.rowcount.RowCountChecker;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.ColumnFilterTable;
import org.dbunit.dataset.Columns;
import org.dbunit.dataset.CompositeDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.FilteredTableMetaData;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ITableMetaData;
import org.dbunit.dataset.SortedTable;
import org.dbunit.dataset.filter.DefaultColumnFilter;
import org.dbunit.operation.DatabaseOperation;
import org.dbunit.util.TableFormatter;
import org.dbunit.util.fileloader.DataFileLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test case base class supporting prep data and expected data. Prep data is the
 * data needed for the test to run. Expected data is the data needed to compare
 * if the test ran successfully.
 * <p>
 * configureTest(), setupData(), verifyData(), and cleanupData() share one
 * {@link org.dbunit.database.IDatabaseConnection} for a test's lifecycle,
 * acquired lazily on first use and closed once by cleanupData(), instead of
 * each acquiring (and often closing) its own. Calling any of those methods
 * without an eventual cleanupData() call - e.g. testing them individually
 * rather than through preTest()/postTest() - leaves that connection open.
 * <p>
 * If databaseTester is configured with a
 * {@link org.dbunit.database.CachingConnectionProvider} shared across test
 * methods, set {@link #setCloseConnectionAfterTest(boolean)} to false so
 * cleanupData() does not close a connection other tests still expect to
 * reuse; the provider's owner is then responsible for closing it once,
 * itself, when the whole run finishes. Leaving the connection open, which a
 * no-op listener on the databaseTester does too, requires a databaseTester
 * that hands out the same connection on repeated calls, as one over a
 * CachingConnectionProvider or over one fixed connection does. With one that
 * hands out a new connection each time, such as a
 * {@link JdbcDatabaseTester} without a CachingConnectionProvider or a
 * {@link DataSourceDatabaseTester} over a connection pool, nothing closes the
 * connection left open. An instance used for one test leaves one open after
 * it. An instance reused across tests keeps one for its next test and leaves
 * another open each time a step fails, because a failing step makes this
 * class forget the connection so that the next step acquires another.
 * This class logs a warning when it detects a {@link JdbcDatabaseTester}
 * without a CachingConnectionProvider in that situation.
 * <p>
 * The databaseTester's own {@link IOperationListener} is told about the
 * connection each of setupData()'s and cleanupData()'s operations retrieves,
 * through {@link IOperationListener#connectionRetrieved}, so a listener that
 * configures every connection runs here as it does when the tester drives
 * onSetup() and onTearDown() itself. It is never told the operations finished,
 * as this class decides when the shared connection closes.
 * <p>
 * The connection must be in autocommit mode. The setup and teardown
 * operations run here (CLEAN_INSERT, DELETE_ALL, ...) do not manage a
 * transaction of their own, and {@link org.dbunit.operation.TransactionOperation}
 * refuses a connection whose autocommit is already off, so on a
 * non-autocommit connection the prep and teardown writes are never
 * committed: invisible to any other connection, and - with
 * {@link #setCloseConnectionAfterTest(boolean)} false - held as locks on a
 * connection the database's idle-in-transaction timeout may terminate
 * mid-run. This class logs a warning when it detects such a connection.
 * <p>
 * The {@code verifyData()} method hands assertion failures to
 * {@link org.dbunit.assertion.DefaultFailureHandler} by default, which throws
 * on the first mismatch found. Set {@link #setFailureHandler(FailureHandler)}
 * to, for example, a {@link org.dbunit.assertion.DiffCollectingFailureHandler}
 * to collect every {@link org.dbunit.assertion.Difference} instead; that is
 * not the default since most tests want to keep failing fast.
 * <p>
 * Not thread-safe. An instance holds the state of one test at a time: its
 * datasets, verify table definitions, shared connection and row count
 * baseline. Use one instance per concurrently running test, for example a
 * Spring prototype bean, or turn off parallel execution for tests that share
 * an instance, such as one in a static {@code @DbUnitTestCase} field.
 * Otherwise concurrent tests overwrite each other's datasets and close each
 * other's connection.
 *
 * @see "org.dbunit.DefaultPrepAndExpectedTestCaseDiIT, a composition-based (DI) usage example in the test sources"
 * @see "org.dbunit.DefaultPrepAndExpectedTestCaseExtIT, an inheritance-based usage example in the test sources"
 *
 * @author Jeff Jensen jeffjensen AT users.sourceforge.net
 * @author Last changed by: $Author$
 * @version $Revision$ $Date$
 * @since 2.4.8
 */
public class DefaultPrepAndExpectedTestCase extends DBTestCase
        implements PrepAndExpectedTestCase
{
    private static final Logger log = LoggerFactory.getLogger(DefaultPrepAndExpectedTestCase.class);

    private static final String DATABASE_TESTER_IS_NULL_MSG =
            "databaseTester is null; must configure or set it first";

    /** Message prefix used for wrapped test failures. */
    public static final String TEST_ERROR_MSG = "DbUnit test error.";

    private IDatabaseTester databaseTester;
    private DataFileLoader dataFileLoader;

    /**
     * Whether lookupFeatureValue() and cleanupData() close the connection
     * they are done with; false when databaseTester shares a
     * CachingConnectionProvider across test methods and this instance must
     * not close a connection other tests still expect to reuse.
     *
     * @since 3.4.0
     */
    private boolean closeConnectionAfterTest = true;

    // per test data
    private IDataSet prepDataSet = new DefaultDataSet();
    private IDataSet expectedDataSet = new DefaultDataSet();
    private VerifyTableDefinition[] verifyTableDefs = {};

    /**
     * Connection shared by setupData()/verifyData()/cleanupData() for one
     * test's lifecycle instead of each acquiring (and often closing) its own:
     * acquired lazily on first use from {@link #getConnection()}, closed once
     * by cleanupData() unless {@link #closeConnectionAfterTest} is false or
     * the tester's or this instance's operation listener is the no-op listener,
     * and re-acquired if the pool or server closed it between reused test
     * methods.
     *
     * @since 3.6.0
     */
    private final TestScopedConnection reusableConnectionHolder = newReusableConnectionHolder();

    /**
     * Builds {@link #reusableConnectionHolder}. Its {@link ConnectionOwnership}
     * reads {@link #closeConnectionAfterTest} and {@link #getOwnershipListener()}
     * fresh at release time; its third input - whether the borrowing lifecycle
     * ran - is always {@code true} here, since this class only ever releases the
     * connection from its own cleanupData(), which runs only after setupData()
     * already acquired it. Since 3.6.0 a listener that is
     * (or wraps) {@link IOperationListener#NO_OP_OPERATION_LISTENER} keeps
     * cleanupData() from closing the connection even when
     * {@link #closeConnectionAfterTest} is true - the established signal that the
     * connection is owned elsewhere, now honored on this path too and not only
     * by the annotation runtime. On each connection it acquires, it logs the warnings of
     * {@link AutoCommitOffWarning} and {@link LeftOpenConnectionWarning}, the latter reading the
     * databaseTester when it runs, as the databaseTester can change between tests.
     *
     * @return The holder.
     */
    private TestScopedConnection newReusableConnectionHolder()
    {
        final ConnectionOwnership ownership = new ConnectionOwnership(
                () -> closeConnectionAfterTest, this::getOwnershipListener, () -> true);
        final LeftOpenConnectionWarning leftOpenConnectionWarning =
                new LeftOpenConnectionWarning(ownership, () -> databaseTester);
        return new TestScopedConnection(this::acquireSharedConnection, ownership,
                new AutoCommitOffWarning().andThen(leftOpenConnectionWarning));
    }

    /**
     * Acquires the connection {@link #reusableConnectionHolder} shares: the databaseTester's
     * connection with {@link #setUpDatabaseConfig(DatabaseConfig)} applied to it, as
     * {@link #getConnection()} does. That method cannot release a connection whose configuration
     * fails to apply, for example because a {@code databaseConfigProperties} value is invalid, so
     * one the databaseTester hands out afresh on each call would be referenced by nothing and
     * leak. This releases it, honoring who owns it, before the failure propagates.
     *
     * @return The connection, configured.
     * @throws Exception If the databaseTester cannot supply a connection, or applying the
     *             configuration fails.
     */
    private IDatabaseConnection acquireSharedConnection() throws Exception
    {
        requireDatabaseTester();

        final IDatabaseConnection connection = databaseTester.getConnection();
        try
        {
            setUpDatabaseConfig(connection.getConfig());
        } catch (final Throwable t)
        {
            reusableConnectionHolder.adopt(connection);
            closeReusableConnectionSuppressing(t);
            throw t;
        }
        return connection;
    }

    /**
     * Returns the listener {@link ConnectionOwnership} consults to decide whether this
     * instance may close the shared connection. The no-op listener is the signal that a
     * connection is managed elsewhere, and a caller sets it on whichever it controls: the
     * tester it hands this instance, or - for a subclass - this instance's own
     * {@link #getOperationListener()}. Either one being (or wrapping) the no-op listener
     * therefore wins; otherwise this instance's own listener is returned.
     *
     * @return The listener that signals the connection is managed elsewhere, if either does;
     *         otherwise this instance's own listener.
     */
    private IOperationListener getOwnershipListener()
    {
        final IOperationListener ownListener = getOperationListener();
        if (databaseTester == null)
        {
            return ownListener;
        }

        final IOperationListener testerListener = databaseTester.getOperationListener();
        if (ConnectionPreservingOperationListener.unwrapsToNoOp(testerListener))
        {
            return testerListener;
        }
        return ownListener;
    }

    /**
     * isCaseSensitiveTableNames as resolved by configureTest(), cached so
     * cleanupData() does not need a second connection just to re-read this
     * same DatabaseConfig feature flag.
     *
     * @since 3.4.0
     */
    private Boolean cachedIsCaseSensitiveTableNames;

    /**
     * Manages the row count check baseline used by preTest() and cleanupData() to detect a
     * table the test left dirty - either one it should have cleaned up and did not, or a
     * reference table it wrongly cleaned.
     *
     * @since 3.6.0
     */
    private final RowCountChecker rowCountChecker = new RowCountChecker();

    private ExpectedDataSetAndVerifyTableDefinitionVerifier expectedDataSetAndVerifyTableDefinitionVerifier =
            new DefaultExpectedDataSetAndVerifyTableDefinitionVerifier();

    /**
     * FailureHandler for verifyData()'s assertion failures. Null (the
     * default) leaves compareData() using
     * {@link org.dbunit.Assertion#assertWithValueComparer(ITable, ITable, Column[], ValueComparer, Map)}'s
     * own {@link org.dbunit.assertion.DefaultFailureHandler}, configured with
     * the additionalColumnInfo computed by {@link #makeAdditionalColumnInfo};
     * that default is intentional so most tests keep failing fast on the
     * first mismatch. Set this, for example to a
     * {@link org.dbunit.assertion.DiffCollectingFailureHandler}, only when a
     * test needs to collect every {@link org.dbunit.assertion.Difference}
     * instead.
     *
     * @since 3.5.0
     */
    private FailureHandler failureHandler;

    /**
     * DatabaseConfig property name/value pairs applied to the connection shared by
     * setupData(), verifyData() and cleanupData(), every time {@link #getConnection()}
     * resolves it - see {@link #setUpDatabaseConfig(DatabaseConfig)}. Null (the default)
     * applies none.
     *
     * @since 3.6.0
     */
    private Properties databaseConfigProperties;

    /**
     * Applies {@link #databaseConfigProperties} to a connection's {@link DatabaseConfig} and
     * puts back what they replaced when this test's lifecycle ends, so a connection kept past
     * the test - {@link #closeConnectionAfterTest} false, or the no-op listener - carries none
     * of this test's values into whatever uses it next.
     *
     * @since 3.6.0
     */
    private DatabaseConfigOverrides databaseConfigOverrides =
            new DatabaseConfigOverrides(new Properties());

    final TableFormatter tableFormatter = new TableFormatter();

    /** Create new instance. */
    public DefaultPrepAndExpectedTestCase()
    {
    }

    /**
     * Create new instance with specified dataFileLoader and databaseTester.
     *
     * @param dataFileLoader
     *            Load to use for loading the data files.
     * @param databaseTester
     *            Tester to use for database manipulation.
     */
    public DefaultPrepAndExpectedTestCase(final DataFileLoader dataFileLoader,
            final IDatabaseTester databaseTester)
    {
        this.dataFileLoader = dataFileLoader;
        this.databaseTester = databaseTester;
    }

    /**
     * Create new instance with specified dataFileLoader and databaseTester.
     *
     * @param dataFileLoader
     *            Load to use for loading the data files.
     * @param databaseTester
     *            Tester to use for database manipulation.
     * @param closeConnectionAfterTest
     *            Whether or not to close the database connection after each test.
     *
     * @since 3.4.0
     */
    public DefaultPrepAndExpectedTestCase(final DataFileLoader dataFileLoader,
            final IDatabaseTester databaseTester,
            final boolean closeConnectionAfterTest)
    {
        this.dataFileLoader = dataFileLoader;
        this.databaseTester = databaseTester;
        this.closeConnectionAfterTest = closeConnectionAfterTest;
    }

    /**
     * Create new instance with specified test case name.
     *
     * @param name
     *            The test case name.
     */
    public DefaultPrepAndExpectedTestCase(final String name)
    {
        super(name);
    }

    /**
     * {@inheritDoc} This implementation returns the databaseTester set by the
     * test.
     */
    @Override
    public IDatabaseTester newDatabaseTester() throws Exception
    {
        // questionable, but there is not a "setter" for any parent...
        return databaseTester;
    }

    /**
     * {@inheritDoc} Returns the prep dataset.
     */
    @Override
    public IDataSet getDataSet() throws Exception
    {
        return prepDataSet;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Executes against the connection shared with setupData(), verifyData()
     * and cleanupData() for this test's lifecycle rather than a fresh one,
     * and leaves it open; cleanupData() closes it. See #800.
     * <p>
     * A null argument means none. When this fails, for example because a data
     * file is missing or invalid, this instance is left with no prep data,
     * expected data or verify table definitions, so a cleanupData() that
     * follows cleans nothing left over from an earlier test. The shared
     * connection it acquired is released too; one the caller had already
     * resolved, for example by injecting it into a test method parameter, is
     * left open for the caller.
     */
    @Override
    public void configureTest(
            final VerifyTableDefinition[] verifyTableDefinitions,
            final String[] prepDataFiles, final String[] expectedDataFiles)
            throws Exception
    {
        requireDatabaseTester();
        log.debug("configureTest: saving instance variables");

        final boolean acquiredConnectionHere =
                reusableConnectionHolder.peekConnection() == null;
        try
        {
            final boolean isCaseSensitiveTableNames = lookupFeatureValue(
                    DatabaseConfig.FEATURE_CASE_SENSITIVE_TABLE_NAMES);
            log.debug("configureTest: using case sensitive table names={}",
                    isCaseSensitiveTableNames);

            final IDataSet newPrepDataSet = makeCompositeDataSet(prepDataFiles,
                    "prep", isCaseSensitiveTableNames);
            final IDataSet newExpectedDataSet = makeCompositeDataSet(
                    expectedDataFiles, "expected", isCaseSensitiveTableNames);

            this.cachedIsCaseSensitiveTableNames = isCaseSensitiveTableNames;
            this.prepDataSet = newPrepDataSet;
            this.expectedDataSet = newExpectedDataSet;
            this.verifyTableDefs = verifyTableDefinitions == null
                    ? new VerifyTableDefinition[0]
                    : verifyTableDefinitions;
        } catch (final Throwable t)
        {
            clearPerTestData();
            if (acquiredConnectionHere)
            {
                closeReusableConnectionSuppressing(t);
            }
            throw t;
        }
    }

    /**
     * Empties the per-test data, so that a cleanupData() following a failed
     * configureTest() runs against nothing rather than against a mix of the
     * new prep data and the previous test's expected data.
     */
    private void clearPerTestData()
    {
        this.prepDataSet = new DefaultDataSet();
        this.expectedDataSet = new DefaultDataSet();
        this.verifyTableDefs = new VerifyTableDefinition[0];
    }

    /**
     * {@inheritDoc} Applies {@link #databaseConfigProperties}, when set - the composition-based
     * equivalent of overriding this method, for a caller that cannot subclass to do so directly
     * (e.g. {@code org.dbunit.annotation}'s {@code @DbUnitProperty}). The values replaced are
     * put back when this test's lifecycle ends - see {@link #cleanupData()}.
     *
     * <p>The lifecycle retrieves its connection several times, and each retrieval calls this
     * method. The values are applied once per {@link DatabaseConfig} until they are put back, and
     * a later call does nothing, so a property set on the {@link DatabaseConfig} directly in
     * between is not overwritten. The exception is a database tester with an operation listener
     * of its own, which may change any of the values at each set up and tear down operation: the
     * values are applied again directly after it, so they still win.
     *
     * <p><strong>Note:</strong> a subclass overriding this method must call
     * {@code super.setUpDatabaseConfig(config)} to keep
     * {@link #setDatabaseConfigProperties(java.util.Properties)} - and therefore
     * {@code @DbUnitProperty} on the annotation-driven path - working.
     *
     * @throws IllegalStateException If a property value is invalid for its target
     *             {@link DatabaseConfig} entry.
     */
    @Override
    protected void setUpDatabaseConfig(final DatabaseConfig config)
    {
        if (databaseConfigOverrides.hasAppliedTo(config))
        {
            return;
        }

        applyDatabaseConfigProperties(config);
    }

    /**
     * Applies {@link #databaseConfigProperties} to a connection's {@link DatabaseConfig},
     * whether or not they are already in force, remembering what they replace.
     *
     * @param config The configuration to apply them to.
     * @throws IllegalStateException If a property value is invalid for its target
     *             {@link DatabaseConfig} entry.
     */
    private void applyDatabaseConfigProperties(final DatabaseConfig config)
    {
        try
        {
            databaseConfigOverrides.applyTo(config);
        } catch (final DatabaseUnitException e)
        {
            throw new IllegalStateException("Failed to apply a databaseConfigProperties value.",
                    e);
        }
    }

    private boolean lookupFeatureValue(final String featureName)
            throws Exception
    {
        final boolean acquiredConnectionHere = reusableConnectionHolder.peekConnection() == null;
        try
        {
            final IDatabaseConnection reusableConnection =
                    getReusableConnection();
            final DatabaseConfig config = reusableConnection.getConfig();
            return config.getFeature(featureName);
        } catch (final Exception e)
        {
            if (acquiredConnectionHere)
            {
                closeReusableConnectionSuppressing(e);
            }
            throw e;
        }
    }

    /**
     * Return the connection shared by lookupFeatureValue(), setupData(),
     * verifyData() and cleanupData() for the current test's lifecycle,
     * acquiring it on first use instead of a fresh connection at each step.
     * <p>
     * When {@link #closeConnectionAfterTest} is false this connection is kept
     * across test methods, where the connection pool or the database server can
     * close it between tests - a pool max-lifetime or reap, a bounced
     * application context, a
     * {@link org.dbunit.database.CachingConnectionProvider#close()}. A closed
     * one is discarded before it is handed back, so the next call re-acquires
     * from databaseTester - letting a
     * {@link org.dbunit.database.CachingConnectionProvider} behind it supply a
     * live replacement - rather than this instance reusing a connection every
     * later lifecycle step would only fail on.
     *
     * @return The shared connection.
     * @throws Exception On dbUnit errors.
     * @since 3.4.0
     */
    @Override
    public IDatabaseConnection getReusableConnection() throws Exception
    {
        final IDatabaseConnection connection = reusableConnectionHolder.getConnection();
        applyDatabaseConfigPropertiesToKeptConnection(connection);
        return connection;
    }

    /**
     * Applies {@link #databaseConfigProperties} to a connection kept from an earlier test, whose
     * values {@link #restoreDatabaseConfig()} put back at the end of it. A newly acquired
     * connection already has them, from {@link #setUpDatabaseConfig(DatabaseConfig)}; a kept one
     * is handed back without that running again, and the first reads of its
     * {@link DatabaseConfig} - the case sensitivity {@link #configureTest} resolves, the row
     * count check baseline - come before {@link #setupData()} retrieves it through the listener.
     *
     * @param connection The connection about to be handed out; may be null.
     */
    private void applyDatabaseConfigPropertiesToKeptConnection(
            final IDatabaseConnection connection)
    {
        if (connection == null || databaseConfigOverrides.isEmpty())
        {
            return;
        }

        final DatabaseConfig config = connection.getConfig();
        if (databaseConfigOverrides.hasAppliedTo(config))
        {
            return;
        }
        setUpDatabaseConfig(config);
    }

    /**
     * Puts back the {@link DatabaseConfig} values {@link #databaseConfigProperties} replaced, at
     * the end of this test's lifecycle or when the properties change.
     */
    private void restoreDatabaseConfig()
    {
        databaseConfigOverrides.restore();
    }

    /**
     * Release the connection shared by lookupFeatureValue(), setupData(),
     * verifyData() and cleanupData(), if one was acquired: closes it and
     * forgets it when {@link #closeConnectionAfterTest} is true (the default)
     * and neither the tester's nor this instance's operation listener is the
     * no-op listener; otherwise
     * leaves it open for its real owner - a
     * {@link org.dbunit.database.CachingConnectionProvider}, an
     * externally-supplied fixed connection - and keeps it memoized so a later
     * lifecycle step reuses it rather than orphaning another. A kept connection
     * the pool or server has since closed is dropped and re-acquired on the
     * next {@link #getReusableConnection()}.
     *
     * @throws Exception On close errors.
     * @since 3.4.0
     */
    private void closeReusableConnection() throws Exception
    {
        try
        {
            reusableConnectionHolder.release();
        } finally
        {
            restoreDatabaseConfig();
        }
    }

    /**
     * Close the reusable connection, attaching any close failure to the given
     * primary throwable via {@link Throwable#addSuppressed(Throwable)} rather
     * than letting it replace and hide the primary. Mirrors the exception
     * safety of {@link #runTest} and {@code DatabaseTestCase.tearDown(Throwable)}.
     * <p>
     * Only ever called from a lifecycle step that has already failed, so it also
     * forgets the shared connection even when {@link #closeConnectionAfterTest}
     * is false and {@link #closeReusableConnection()} therefore left it open: a
     * step that just threw may have broken it, so the next
     * {@link #getReusableConnection()} re-acquires rather than reusing it. Any
     * {@link org.dbunit.database.CachingConnectionProvider} behind
     * {@code databaseTester} still owns closing it.
     *
     * @param primary
     *            The exception already in flight to attach a close failure
     *            to.
     * @since 3.4.0
     */
    private void closeReusableConnectionSuppressing(final Throwable primary)
    {
        reusableConnectionHolder.releaseSuppressing(primary);
        restoreDatabaseConfig();
    }

    /**
     * Make a {@link ReusableConnectionDatabaseTester} configured with the
     * given dataset and databaseTester's setUpOperation and
     * tearDownOperation, for setupData() or cleanupData() to run
     * {@link IDatabaseTester#onSetup()} or {@link IDatabaseTester#onTearDown()}
     * on, respectively. Setting both operations regardless of which one the
     * caller uses is safe: {@link AbstractDatabaseTester#onSetup()} only
     * reads setUpOperation and {@link AbstractDatabaseTester#onTearDown()}
     * only reads tearDownOperation, so the other is simply never consulted.
     *
     * @param dataSet
     *            The dataset to run the operation against.
     * @return The configured tester.
     * @throws Exception On dbUnit errors.
     * @since 3.4.0
     */
    private IDatabaseTester makeReusableConnectionDatabaseTester(
            final IDataSet dataSet) throws Exception
    {
        final IDatabaseTester reusableTester =
                new ReusableConnectionDatabaseTester(
                        this::getReusableConnection);
        reusableTester.setSetUpOperation(getSetUpOperation());
        reusableTester.setTearDownOperation(getTearDownOperation());
        reusableTester.setDataSet(dataSet);
        // This instance, not the listener, owns the shared connection's lifecycle (see
        // getReusableConnection()/closeReusableConnection()), so a blanket
        // ConnectionPreservingOperationListener keeps operationSetUpFinished/
        // operationTearDownFinished from closing it while still forwarding connectionRetrieved
        // so a user-defined listener runs its connection-configuration logic.
        reusableTester.setOperationListener(newConnectionRetrievedListener());
        return reusableTester;
    }

    /**
     * Makes the listener for {@link #makeReusableConnectionDatabaseTester(IDataSet)}: a blanket
     * {@link ConnectionPreservingOperationListener} over this instance's own listener, so only
     * connectionRetrieved() gets through and nothing is closed behind this instance's back.
     * When the databaseTester has a listener of its own, a caller's way to configure every
     * connection, it is told about the connection first, as it is when the tester drives
     * onSetup()/onTearDown() itself, and this instance's own listener, which applies
     * {@link #setUpDatabaseConfig(DatabaseConfig)}, second, so the values this instance applies
     * win. As that call does nothing for a connection the values are already applied to, they are
     * applied again directly after the databaseTester's listener, which may have changed any of
     * them. A databaseTester listener that is this instance's own, or the no-op listener, is not
     * told again.
     *
     * @return The listener for the shared connection's tester.
     */
    private IOperationListener newConnectionRetrievedListener()
    {
        final IOperationListener ownListener = getOperationListener();
        final IOperationListener testersListener = getTestersOwnListener();
        if (testersListener == null || testersListener == ownListener
                || testersListener == IOperationListener.NO_OP_OPERATION_LISTENER)
        {
            return new ConnectionPreservingOperationListener(ownListener);
        }
        return new TestersListenerFirstOperationListener(testersListener,
                connection -> applyDatabaseConfigProperties(connection.getConfig()),
                ownListener);
    }

    /**
     * Returns the databaseTester's listener beneath any layers of
     * {@link ConnectionPreservingOperationListener}, including the one the annotation runtime
     * installs, which applies the annotation values and captures the row count baseline, both
     * of which this instance does itself.
     *
     * @return The databaseTester's own listener, or null if it has none.
     */
    private IOperationListener getTestersOwnListener()
    {
        if (databaseTester == null)
        {
            return null;
        }

        IOperationListener listener = databaseTester.getOperationListener();
        while (listener instanceof ConnectionPreservingOperationListener)
        {
            listener = ((ConnectionPreservingOperationListener) listener).getDelegate();
        }
        return listener;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Captures the row count check baseline, if enabled, before setting up the prep data - see
     * {@link RowCountCheck#capture(IDatabaseConnection)}.
     */
    @Override
    public void preTest() throws Exception
    {
        captureRowCountBaseline();
        setupData();
    }

    /**
     * Capture the row count check baseline, using the connection shared with the rest of this
     * test's lifecycle. A no-op that leaves no baseline captured when the check is disabled.
     *
     * @throws Exception On dbUnit errors.
     * @since 3.6.0
     */
    private void captureRowCountBaseline() throws Exception
    {
        final boolean acquiredConnectionHere = reusableConnectionHolder.peekConnection() == null;
        try
        {
            rowCountChecker.capture(getReusableConnection());
        } catch (final Exception e)
        {
            if (acquiredConnectionHere)
            {
                closeReusableConnectionSuppressing(e);
            }
            throw e;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void preTest(final VerifyTableDefinition[] tables,
            final String[] prepDataFiles, final String[] expectedDataFiles)
            throws Exception
    {
        configureTest(tables, prepDataFiles, expectedDataFiles);
        preTest();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object runTest(final VerifyTableDefinition[] verifyTables,
            final String[] prepDataFiles, final String[] expectedDataFiles,
            final PrepAndExpectedTestCaseSteps testSteps) throws Exception
    {
        final Object result;

        try
        {
            preTest(verifyTables, prepDataFiles, expectedDataFiles);
            log.info("runTest: running test steps");
            result = runTestSteps(testSteps);
        } catch (final Throwable e)
        {
            log.error(TEST_ERROR_MSG, e);
            // don't verify table data when test execution has errors as:
            // * a verify data failure masks the test error exception
            // * tables in unknown state and therefore probably not accurate
            try
            {
                postTest(false);
            } catch (final Throwable cleanupFailure)
            {
                // never let a cleanup failure replace and hide the real test
                // failure; keep e as the thrown exception, cleanupFailure alongside
                e.addSuppressed(cleanupFailure);
            }
            throw e;
        }

        postTest();

        return result;
    }

    /**
     * Run the provided test steps. Override as necessary for custom logic.
     *
     * @param testSteps
     *            The test steps to run.
     * @return the user-defined object returned by the test steps.
     * @throws Exception if the test steps fail.
     */
    protected Object runTestSteps(final PrepAndExpectedTestCaseSteps testSteps)
            throws Exception
    {
        return testSteps.run();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void postTest() throws Exception
    {
        postTest(true);
    }

    /**
     * {@inheritDoc}
     * <p>
     * When {@code verifyData} is false - the test steps already failed - discards any
     * captured row count check baseline, so cleanupData() skips that check too: the database
     * is in an unknown state, so a count difference would be noise around the real failure,
     * not a finding worth its own report. A verification failure discards it the same way,
     * so cleanupData() does not throw a row count difference in its place: the comparison
     * failure names what differs and is the one to fix first, while a cleanup failure still
     * shadows it as before.
     */
    @Override
    public void postTest(final boolean verifyData) throws Exception
    {
        Throwable verifyFailure = null;
        try
        {
            if (verifyData)
            {
                verifyData();
            } else
            {
                rowCountChecker.discardBaseline();
            }
        } catch (final Throwable t)
        {
            verifyFailure = t;
            rowCountChecker.discardBaseline();
            throw t;
        } finally
        {
            // it is deliberate to have cleanup exceptions shadow verify
            // failures so user knows db is probably in unknown state (for
            // those not using an in-memory db or transaction rollback),
            // otherwise would mask probable cause of subsequent test
            // failures; the verify failure rides along as suppressed on the
            // cleanup exception so it is not lost entirely
            try
            {
                cleanupData();
            } catch (final Throwable cleanupFailure)
            {
                if (verifyFailure != null)
                {
                    cleanupFailure.addSuppressed(verifyFailure);
                }
                throw cleanupFailure;
            }
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Runs the tear down operation against the connection shared with
     * setupData() and verifyData() for this test's lifecycle, then closes it.
     * See #800.
     */
    @Override
    public void cleanupData() throws Exception
    {
        requireDatabaseTester();
        try
        {
            final boolean isCaseSensitiveTableNames;
            if (cachedIsCaseSensitiveTableNames == null)
            {
                isCaseSensitiveTableNames = lookupFeatureValue(
                        DatabaseConfig.FEATURE_CASE_SENSITIVE_TABLE_NAMES);
            } else
            {
                isCaseSensitiveTableNames = cachedIsCaseSensitiveTableNames;
            }
            log.debug("cleanupData: using case sensitive table names={}",
                    isCaseSensitiveTableNames);

            final IDataSet[] dataSets = {prepDataSet, expectedDataSet};
            final IDataSet dataset = new CompositeDataSet(dataSets, true,
                    isCaseSensitiveTableNames);
            final String[] tableNames = dataset.getTableNames();
            final int count = tableNames.length;
            log.info("cleanupData: about to clean up {} tables={}", count,
                    tableNames);

            final IDatabaseTester reusableTester =
                    makeReusableConnectionDatabaseTester(dataset);
            reusableTester.onTearDown();
            log.debug("cleanupData: Clean up done");
            verifyRowCountUnchanged();
            closeReusableConnection();
        } catch (final Exception e)
        {
            log.error("cleanupData: Exception:", e);
            closeReusableConnectionSuppressing(e);
            throw e;
        }
    }

    /**
     * Verify the row count check baseline, if one was captured, using the connection shared
     * with the rest of this test's lifecycle. A no-op when no baseline was captured - the
     * check is disabled, capture never ran, or {@link #postTest(boolean)} discarded it
     * because the test steps already failed - which does not acquire a connection just to
     * find that out.
     *
     * @throws Exception On dbUnit errors, including {@link org.dbunit.database.rowcount.UnexpectedRowCountException}
     *             when a table's row count no longer matches the baseline.
     * @since 3.6.0
     */
    private void verifyRowCountUnchanged() throws Exception
    {
        if (rowCountChecker.hasBaseline())
        {
            rowCountChecker.verify(getReusableConnection());
        }
    }

    /**
     * Legacy JUnit-3-era set-up hook. Not invoked automatically under JUnit 5;
     * kept for subclasses that drive the lifecycle manually. Runs
     * {@link #setupData()} on the connection shared with the rest of this
     * test's lifecycle, leaving the databaseTester's dataset and operation
     * listener as the caller set them.
     */
    @Override
    protected void setUp() throws Exception
    {
        setupData();
    }

    /**
     * Legacy JUnit-3-era tear-down hook. Not invoked automatically under JUnit 5;
     * kept for subclasses that drive the lifecycle manually. Runs
     * {@link #cleanupData()}, which tears down the prep and expected tables once
     * and releases the shared connection. Calling it after a full
     * {@link #runTest} or {@link #postTest} cycle cleans up a second time, as
     * both already ran cleanupData().
     */
    @Override
    protected void tearDown() throws Exception
    {
        cleanupData();
    }

    /**
     * Legacy JUnit-3-era tear-down hook for a test whose failure is already in
     * flight. Runs {@link #cleanupData()} the same way as {@link #tearDown()},
     * and throws {@code testFailure} afterward, with any clean up failure
     * attached to it as suppressed rather than replacing it.
     *
     * @param testFailure
     *            The throwable already in flight from the test body, or
     *            <code>null</code> if there is none, in which case this
     *            behaves the same as {@link #tearDown()}.
     * @throws Throwable
     *             The testFailure, if not <code>null</code>; otherwise a clean up
     *             failure, if one occurred.
     */
    @Override
    protected void tearDown(final Throwable testFailure) throws Throwable
    {
        if (testFailure == null)
        {
            cleanupData();
            return;
        }

        try
        {
            cleanupData();
        } catch (final Throwable cleanupFailure)
        {
            if (cleanupFailure != testFailure)
            {
                testFailure.addSuppressed(cleanupFailure);
            }
        }
        throw testFailure;
    }

    /**
     * Use the provided databaseTester to prep the database with the provided
     * prep dataset. See {@link org.dbunit.IDatabaseTester#onSetup()}.
     * <p>
     * Executes against the connection shared with verifyData() and
     * cleanupData() for this test's lifecycle rather than a fresh one, and
     * leaves it open; cleanupData() closes it. See #800.
     *
     * @throws Exception if preparing the data fails.
     */
    public void setupData() throws Exception
    {
        requireDatabaseTester();
        log.info("setupData: setting prep dataset and inserting rows");

        try
        {
            final IDatabaseTester reusableTester =
                    makeReusableConnectionDatabaseTester(getDataSet());
            reusableTester.onSetup();
        } catch (final Exception e)
        {
            log.error("setupData: Exception with setting up data:", e);
            throw e;
        }
    }

    /**
     * Fails fast, the same way for every lifecycle step, when no databaseTester was set.
     *
     * @throws IllegalStateException If databaseTester is null.
     */
    private void requireDatabaseTester()
    {
        if (databaseTester == null)
        {
            throw new IllegalStateException(DATABASE_TESTER_IS_NULL_MSG);
        }
    }

    @Override
    protected DatabaseOperation getSetUpOperation() throws Exception
    {
        requireDatabaseTester();
        return databaseTester.getSetUpOperation();
    }

    @Override
    protected DatabaseOperation getTearDownOperation() throws Exception
    {
        requireDatabaseTester();
        return databaseTester.getTearDownOperation();
    }

    /**
     * {@inheritDoc} Uses the connection from the provided databaseTester,
     * shared with setupData() and cleanupData() for this test's lifecycle.
     * Left open on return; cleanupData() closes it. See #800.
     */
    @Override
    public void verifyData() throws Exception
    {
        requireDatabaseTester();

        final IDatabaseConnection reusableConnection = getReusableConnection();

        final DatabaseConfig config = reusableConnection.getConfig();
        expectedDataSetAndVerifyTableDefinitionVerifier.verify(verifyTableDefs,
                expectedDataSet, config);

        try
        {
            final int tableDefsCount = verifyTableDefs.length;
            if (tableDefsCount == 0)
            {
                log.info("verifyData: No tables to verify as"
                        + " no VerifyTableDefinitions specified");
            } else
            {
                log.info(
                        "verifyData: about to verify {} tables"
                                + " using verifyTableDefinitions={}",
                        tableDefsCount, verifyTableDefs);
            }

            for (int i = 0; i < tableDefsCount; i++)
            {
                final VerifyTableDefinition td = verifyTableDefs[i];
                verifyData(reusableConnection, td);
            }
        } catch (final Exception e)
        {
            log.error("verifyData: Exception:", e);
            throw e;
        }
    }

    /**
     * Verifies a single table's actual data against its expected data.
     *
     * @param connection the database connection to load the actual data from.
     * @param verifyTableDefinition the table definition to verify.
     * @throws Exception if verifying the table fails.
     */
    protected void verifyData(final IDatabaseConnection connection,
            final VerifyTableDefinition verifyTableDefinition) throws Exception
    {
        final String tableName = verifyTableDefinition.getTableName();
        log.debug("verifyData: Verifying table '{}'", tableName);

        final String[] excludeColumns =
                verifyTableDefinition.getColumnExclusionFilters();
        final String[] includeColumns =
                verifyTableDefinition.getColumnInclusionFilters();
        final Map<String, ValueComparer> columnValueComparers =
                verifyTableDefinition.getColumnValueComparers();
        final ValueComparer defaultValueComparer =
                verifyTableDefinition.getDefaultValueComparer();
        final boolean sortOnFilteredColumnsOnly =
                verifyTableDefinition.isSortOnFilteredColumnsOnly();

        final ITable expectedTable = loadTableDataFromDataSet(tableName);
        final ITable actualTable;
        if (sortOnFilteredColumnsOnly
                && !isLoadTableDataFromDatabaseOverridden())
        {
            actualTable = loadComparedColumnsFromDatabase(tableName,
                    connection, excludeColumns, includeColumns);
        } else
        {
            actualTable = loadTableDataFromDatabase(tableName, connection);
        }

        if (sortOnFilteredColumnsOnly)
        {
            verifyData(expectedTable, actualTable, excludeColumns,
                    includeColumns, defaultValueComparer, columnValueComparers,
                    true);
        } else
        {
            verifyData(expectedTable, actualTable, excludeColumns,
                    includeColumns, defaultValueComparer,
                    columnValueComparers);
        }
    }

    /**
     * Loads the given table's expected data from the expected dataset.
     *
     * @param tableName the name of the table to load.
     * @return the table's expected data.
     * @throws DataSetException if loading the table fails.
     */
    public ITable loadTableDataFromDataSet(final String tableName)
            throws DataSetException
    {
        ITable table = null;

        final String methodName = "loadTableDataFromDataSet";

        log.debug("{}: Loading table {} from expected dataset", methodName,
                tableName);
        try
        {
            table = expectedDataSet.getTable(tableName);
        } catch (final Exception e)
        {
            final String msg = methodName + ": Problem obtaining table '"
                    + tableName + "' from expected dataset";
            log.error(msg, e);
            throw new DataSetException(msg, e);
        }
        return table;
    }

    /**
     * Loads the given table's actual data from the database.
     *
     * @param tableName the name of the table to load.
     * @param connection the database connection to load the data from.
     * @return the table's actual data.
     * @throws Exception if loading the table fails.
     */
    public ITable loadTableDataFromDatabase(final String tableName,
            final IDatabaseConnection connection) throws Exception
    {
        ITable table = null;

        final String methodName = "loadTableDataFromDatabase";

        log.debug("{}: Loading table {} from database", methodName, tableName);
        try
        {
            table = connection.createTable(tableName);
        } catch (final Exception e)
        {
            final String msg = methodName + ": Problem obtaining table '"
                    + tableName + "' from database";
            log.error(msg, e);
            throw new DataSetException(msg, e);
        }
        return table;
    }

    /**
     * Loads the given table's actual data from the database selecting only
     * the columns that survive the exclude and include column filters, which
     * are the only ones that play a part when sorting only on the filtered
     * columns and comparing. An excluded column, often a large one such as a
     * BLOB or CLOB, is not fetched. The columns come from the connection's
     * cached {@link org.dbunit.database.DatabaseDataSet}, which the set up
     * operation has already initialized, and the table is made by the
     * connection's configured
     * {@link DatabaseConfig#PROPERTY_RESULTSET_TABLE_FACTORY}. When the
     * table's columns cannot be determined that way, or all of them are
     * filtered out, which would leave nothing to select, it loads every column
     * as {@link #loadTableDataFromDatabase(String, IDatabaseConnection)} does.
     *
     * @param tableName
     *            The name of the table to load.
     * @param connection
     *            The database connection to load the data from.
     * @param excludeColumns
     *            The column names to exclude; null or empty to exclude none.
     * @param includeColumns
     *            The column names to only include; null or empty to include
     *            all.
     * @return The table's actual data for the compared columns.
     * @throws Exception
     *             If loading the table fails.
     */
    private ITable loadComparedColumnsFromDatabase(final String tableName,
            final IDatabaseConnection connection, final String[] excludeColumns,
            final String[] includeColumns) throws Exception
    {
        final String methodName = "loadComparedColumnsFromDatabase";

        final ITableMetaData comparedMetaData = findComparedColumns(tableName,
                connection, excludeColumns, includeColumns);
        if (comparedMetaData == null)
        {
            return loadTableDataFromDatabase(tableName, connection);
        }

        log.debug("{}: Loading table {} from database, selecting only the"
                + " compared columns", methodName, tableName);
        try
        {
            final IResultSetTableFactory tableFactory =
                    (IResultSetTableFactory) connection.getConfig().getProperty(
                            DatabaseConfig.PROPERTY_RESULTSET_TABLE_FACTORY);
            return tableFactory.createTable(comparedMetaData, connection);
        } catch (final Exception e)
        {
            final String msg = methodName + ": Problem obtaining table '"
                    + tableName + "' from database";
            log.error(msg, e);
            throw new DataSetException(msg, e);
        }
    }

    /**
     * Finds the given table's columns that survive the exclude and include
     * column filters.
     *
     * @param tableName
     *            The name of the table.
     * @param connection
     *            The database connection whose dataset describes the table.
     * @param excludeColumns
     *            The column names to exclude; null or empty to exclude none.
     * @param includeColumns
     *            The column names to only include; null or empty to include
     *            all.
     * @return The metadata of just those columns, or null when it cannot be
     *         determined from the connection's dataset or no column is left.
     */
    private ITableMetaData findComparedColumns(final String tableName,
            final IDatabaseConnection connection, final String[] excludeColumns,
            final String[] includeColumns)
    {
        try
        {
            final IDataSet databaseDataSet = connection.createDataSet();
            if (databaseDataSet == null)
            {
                return null;
            }

            final ITableMetaData databaseMetaData =
                    databaseDataSet.getTableMetaData(tableName);
            final ITableMetaData comparedMetaData = new FilteredTableMetaData(
                    databaseMetaData,
                    makeColumnFilter(excludeColumns, includeColumns));
            if (comparedMetaData.getColumns().length == 0)
            {
                log.debug("findComparedColumns: No column of table {} is"
                        + " compared, selecting every column", tableName);
                return null;
            }
            return comparedMetaData;
        } catch (final SQLException | DataSetException e)
        {
            log.debug("findComparedColumns: Cannot determine the columns of"
                    + " table {}, selecting every column", tableName, e);
            return null;
        }
    }

    /**
     * Returns whether a subclass overrides
     * {@link #loadTableDataFromDatabase(String, IDatabaseConnection)}, for
     * example to add a where clause, in which case it, not this class,
     * decides what is loaded.
     *
     * @return True if a subclass overrides it.
     */
    private boolean isLoadTableDataFromDatabaseOverridden()
    {
        try
        {
            final Class<?> declaringClass = getClass().getMethod(
                    "loadTableDataFromDatabase", String.class,
                    IDatabaseConnection.class).getDeclaringClass();
            return declaringClass != DefaultPrepAndExpectedTestCase.class;
        } catch (final NoSuchMethodException e)
        {
            return true;
        }
    }

    /**
     * For the specified expected and actual tables (and excluding and including
     * the specified columns), verify the actual data is as expected.
     *
     * @param expectedTable
     *            The expected table to compare the actual table to.
     * @param actualTable
     *            The actual table to compare to the expected table.
     * @param excludeColumns
     *            The column names to exclude from comparison. See
     *            {@link org.dbunit.dataset.filter.DefaultColumnFilter#excludeColumn(String)}
     *            .
     * @param includeColumns
     *            The column names to only include in comparison. See
     *            {@link org.dbunit.dataset.filter.DefaultColumnFilter#includeColumn(String)}
     *            .
     * @param defaultValueComparer
     *            {@link ValueComparer} to use with column value comparisons
     *            when the column name for the table is not in the
     *            columnValueComparers {@link Map}. Can be <code>null</code> and
     *            will default.
     * @param columnValueComparers
     *            {@link Map} of {@link ValueComparer}s to use for specific
     *            columns. Key is column name, value is the
     *            {@link ValueComparer}. Can be <code>null</code> and will
     *            default to defaultValueComparer for all columns in all tables.
     * @throws DatabaseUnitException if the tables' row counts, columns, or data do not match.
     * @see #verifyData(ITable, ITable, String[], String[], ValueComparer, Map, boolean)
     *      to also control whether sorting considers only the filtered
     *      columns; this overload always sorts by all native columns.
     */
    protected void verifyData(final ITable expectedTable,
            final ITable actualTable, final String[] excludeColumns,
            final String[] includeColumns,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers)
            throws DatabaseUnitException
    {
        verifyData(expectedTable, actualTable, excludeColumns, includeColumns,
                defaultValueComparer, columnValueComparers, false);
    }

    /**
     * For the specified expected and actual tables (and excluding and including
     * the specified columns), verify the actual data is as expected.
     *
     * @param expectedTable
     *            The expected table to compare the actual table to.
     * @param actualTable
     *            The actual table to compare to the expected table.
     * @param excludeColumns
     *            The column names to exclude from comparison. See
     *            {@link org.dbunit.dataset.filter.DefaultColumnFilter#excludeColumn(String)}
     *            .
     * @param includeColumns
     *            The column names to only include in comparison. See
     *            {@link org.dbunit.dataset.filter.DefaultColumnFilter#includeColumn(String)}
     *            .
     * @param defaultValueComparer
     *            {@link ValueComparer} to use with column value comparisons
     *            when the column name for the table is not in the
     *            columnValueComparers {@link Map}. Can be <code>null</code> and
     *            will default.
     * @param columnValueComparers
     *            {@link Map} of {@link ValueComparer}s to use for specific
     *            columns. Key is column name, value is the
     *            {@link ValueComparer}. Can be <code>null</code> and will
     *            default to defaultValueComparer for all columns in all tables.
     * @param sortOnFilteredColumnsOnly
     *            True to sort the expected and actual tables by only the
     *            columns that survive excludeColumns/includeColumns, instead
     *            of by all of the actual table's native columns; see
     *            {@link VerifyTableDefinition#isSortOnFilteredColumnsOnly()}.
     *            When false and the expected table omits columns of the actual
     *            table, so the two sort by different columns, a failed
     *            comparison that passes when sorted by only the filtered
     *            columns carries a suppressed exception naming this as the
     *            remedy.
     * @throws DatabaseUnitException if the tables' row counts, columns, or data do not match.
     * @since 3.5.0
     */
    protected void verifyData(final ITable expectedTable,
            final ITable actualTable, final String[] excludeColumns,
            final String[] includeColumns,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers,
            final boolean sortOnFilteredColumnsOnly)
            throws DatabaseUnitException
    {
        final String methodName = "verifyData";

        final ITableMetaData actualTableMetaData =
                actualTable.getTableMetaData();
        final ITableMetaData expectedTableMetaData =
                expectedTable.getTableMetaData();

        final Column[] actualTableColumns = actualTableMetaData.getColumns();
        final Column[] expectedTableColumns = makeExpectedTableColumns(
                actualTableColumns, expectedTableMetaData);
        final String tableName = actualTableMetaData.getTableName();
        final boolean sortKeysDiffer = !sortOnFilteredColumnsOnly
                && expectedTableColumns.length < actualTableColumns.length;

        final Column[] actualSortColumns;
        final Column[] expectedSortColumns;
        if (sortOnFilteredColumnsOnly)
        {
            log.debug("{}: Sorting using only filtered columns", methodName);
            actualSortColumns = filterColumns(actualTableColumns,
                    excludeColumns, includeColumns, tableName);
            expectedSortColumns = filterColumns(expectedTableColumns,
                    excludeColumns, includeColumns, tableName);
        } else
        {
            log.debug("{}: Sorting using all columns", methodName);
            actualSortColumns = actualTableColumns;
            expectedSortColumns = expectedTableColumns;
        }

        final SortedTable expectedSortedTable =
                sortByColumns(expectedTable, expectedSortColumns, true);
        log.trace("{}: Sorted expected table={}", methodName,
                expectedSortedTable);

        final SortedTable actualSortedTable =
                sortByColumns(actualTable, actualSortColumns, false);
        log.trace("{}: Sorted actual table={}", methodName, actualSortedTable);

        // Filter out the columns from the expected and actual results
        log.debug(
                "{}: Applying column exclude and include filters to sorted expected table",
                methodName);
        final ITable expectedFilteredTable = applyColumnFilters(
                expectedSortedTable, excludeColumns, includeColumns);
        log.debug(
                "{}: Applying column exclude and include filters to sorted actual table",
                methodName);
        final ITable actualFilteredTable = applyColumnFilters(actualSortedTable,
                excludeColumns, includeColumns);

        log.debug("{}: Creating additionalColumnInfo for expected table",
                methodName);
        final Column[] allAdditionalColumnInfo =
                makeAdditionalColumnInfo(expectedTable, excludeColumns);
        final Column[] additionalColumnInfo =
                columnsNamedIn(allAdditionalColumnInfo, actualTableColumns);
        log.trace("{}: additionalColumnInfo={}", methodName,
                additionalColumnInfo);

        logSortedTables(expectedSortedTable, actualSortedTable);

        log.debug("{}: Comparing expected table to actual table", methodName);
        try
        {
            compareData(expectedFilteredTable, actualFilteredTable,
                    additionalColumnInfo, defaultValueComparer,
                    columnValueComparers);
        } catch (final AssertionError e)
        {
            if (sortKeysDiffer)
            {
                final boolean passesWhenSortedOnFilteredColumnsOnly =
                        passesWhenSortedOnFilteredColumnsOnly(expectedTable,
                                actualTable, excludeColumns, includeColumns,
                                defaultValueComparer, columnValueComparers);
                if (passesWhenSortedOnFilteredColumnsOnly)
                {
                    final DatabaseUnitRuntimeException hint = makeSortHint(
                            tableName, actualTableColumns.length,
                            expectedTableColumns.length);
                    e.addSuppressed(hint);
                    // also logged: a build that trims stack traces, for
                    // example Surefire with trimStackTrace, drops suppressed
                    log.warn(hint.getMessage());
                }
            }
            throw e;
        }
    }

    /**
     * Sorts the table by the given columns, comparing the values as the
     * columns' data types do instead of as text.
     *
     * @param table
     *            The table to sort.
     * @param sortColumns
     *            The columns to sort by.
     * @param useSpecifiedColumns
     *            True to use the column definitions of sortColumns in the
     *            sorted table, as the expected table does to follow the actual
     *            table's; false to use the table's own.
     * @return The sorted view of the table.
     * @throws DataSetException
     *             If a sort column does not exist in the table.
     */
    private SortedTable sortByColumns(final ITable table,
            final Column[] sortColumns, final boolean useSpecifiedColumns)
            throws DataSetException
    {
        final SortedTable sortedTable =
                new SortedTable(table, sortColumns, useSpecifiedColumns);
        sortedTable.setUseComparable(true);
        return sortedTable;
    }

    /**
     * Returns whether comparing the tables passes when both are sorted by only
     * the columns that survive the exclude and include column filters, as
     * {@code sortOnFilteredColumnsOnly} sorts them. It compares with a fresh
     * {@link DefaultFailureHandler} and no additional column info, instead of
     * through {@link #compareData} and the {@link #failureHandler}, so that it
     * neither runs a subclass's comparison nor records differences in a
     * handler that collects them. It does not compare tables of different
     * columns or row counts, which cannot pass. Whatever it throws means that
     * it does not pass: this check explains a failure and must never replace
     * it.
     *
     * @param expectedTable
     *            The expected table.
     * @param actualTable
     *            The actual table.
     * @param excludeColumns
     *            The column names to exclude from comparison.
     * @param includeColumns
     *            The column names to only include in comparison.
     * @param defaultValueComparer
     *            The value comparer for columns without one in
     *            columnValueComparers; can be null.
     * @param columnValueComparers
     *            The value comparers for specific columns; can be null.
     * @return True if the comparison passes with that sort.
     */
    private boolean passesWhenSortedOnFilteredColumnsOnly(
            final ITable expectedTable, final ITable actualTable,
            final String[] excludeColumns, final String[] includeColumns,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers)
    {
        final String methodName = "passesWhenSortedOnFilteredColumnsOnly";
        try
        {
            final ITableMetaData actualTableMetaData =
                    actualTable.getTableMetaData();
            final ITableMetaData expectedTableMetaData =
                    expectedTable.getTableMetaData();
            final Column[] actualTableColumns =
                    actualTableMetaData.getColumns();
            final Column[] expectedTableColumns = makeExpectedTableColumns(
                    actualTableColumns, expectedTableMetaData);
            final String tableName = actualTableMetaData.getTableName();

            final Column[] actualSortColumns = filterColumns(
                    actualTableColumns, excludeColumns, includeColumns,
                    tableName);
            final Column[] expectedSortColumns = filterColumns(
                    expectedTableColumns, excludeColumns, includeColumns,
                    tableName);
            final SortedTable expectedSortedTable =
                    sortByColumns(expectedTable, expectedSortColumns, true);
            final SortedTable actualSortedTable =
                    sortByColumns(actualTable, actualSortColumns, false);
            final ITable expectedFilteredTable = applyColumnFilters(
                    expectedSortedTable, excludeColumns, includeColumns);
            final ITable actualFilteredTable = applyColumnFilters(
                    actualSortedTable, excludeColumns, includeColumns);

            if (!haveSameColumnsAndRowCount(expectedFilteredTable,
                    actualFilteredTable))
            {
                return false;
            }

            Assertion.assertWithValueComparer(expectedFilteredTable,
                    actualFilteredTable, new DefaultFailureHandler(),
                    defaultValueComparer, columnValueComparers);
            return true;
        } catch (final AssertionError | Exception e)
        {
            log.debug("{}: The comparison does not pass sorted by only the"
                    + " filtered columns", methodName, e);
            return false;
        }
    }

    /**
     * Returns whether the tables have the same columns, without regard to case,
     * and the same number of rows. Comparing tables that differ in either fails
     * before any value is compared, and logs an error.
     *
     * @param expectedTable
     *            The expected table.
     * @param actualTable
     *            The actual table.
     * @return True if both have the same columns and row count.
     * @throws DataSetException
     *             If the columns cannot be retrieved.
     */
    private boolean haveSameColumnsAndRowCount(final ITable expectedTable,
            final ITable actualTable) throws DataSetException
    {
        final ITableMetaData expectedMetaData =
                expectedTable.getTableMetaData();
        final ITableMetaData actualMetaData = actualTable.getTableMetaData();
        final Columns.ColumnDiff columnDiff =
                Columns.getColumnDiff(expectedMetaData, actualMetaData);
        final boolean sameColumns = !columnDiff.hasDifference();
        final int expectedRowCount = expectedTable.getRowCount();
        final int actualRowCount = actualTable.getRowCount();
        final boolean sameRowCount = expectedRowCount == actualRowCount;
        return sameColumns && sameRowCount;
    }

    /**
     * Makes the hint for a failed comparison that passes when sorted by only
     * the filtered columns: the actual table was sorted by all of its columns
     * and the expected table by only the columns it declares, which can compare
     * rows that are equal against different counterparts.
     *
     * @param tableName
     *            The name of the table that failed the comparison.
     * @param actualColumnCount
     *            The number of columns the actual table was sorted by.
     * @param expectedColumnCount
     *            The number of columns the expected table was sorted by.
     * @return The hint, to attach to the failure as suppressed.
     */
    private DatabaseUnitRuntimeException makeSortHint(final String tableName,
            final int actualColumnCount, final int expectedColumnCount)
    {
        final String message = "verifyData: The comparison of table '"
                + tableName + "' failed, but it passes when both tables are"
                + " sorted by only the compared columns. The actual table was"
                + " sorted by all " + actualColumnCount
                + " of its columns and the expected table by only the "
                + expectedColumnCount + " it declares, so rows that are equal"
                + " on the compared columns can be matched against different"
                + " counterparts, for example when the columns the expected"
                + " table omits are generated. Sort by only the compared"
                + " columns with setSortOnFilteredColumnsOnly(true) on the"
                + " table's VerifyTableDefinition, or sortOnFilteredColumnsOnly"
                + " = true on its @DbUnitVerifyTable.";
        return new DatabaseUnitRuntimeException(message);
    }

    /**
     * Reduces the given columns to those that survive the given exclude and
     * include column filters, using the same matching semantics - including
     * {@link DefaultColumnFilter}'s wildcard pattern support - as
     * {@link #applyColumnFilters(ITable, String[], String[])}, so the sort
     * key always matches the columns that end up compared.
     *
     * @param columns
     *            The columns to filter.
     * @param excludeColumns
     *            The column names to exclude; null or empty to exclude none.
     * @param includeColumns
     *            The column names to only include; null to include all.
     * @param tableName
     *            The table name, which may be null; passed only to
     *            {@link DefaultColumnFilter#accept(String, Column)} for its
     *            debug logging.
     * @return The filtered columns, in columns' original order.
     */
    private Column[] filterColumns(final Column[] columns,
            final String[] excludeColumns, final String[] includeColumns,
            final String tableName)
    {
        final DefaultColumnFilter columnFilter =
                makeColumnFilter(excludeColumns, includeColumns);
        return FilteredTableMetaData.getFilteredColumns(tableName, columns,
                columnFilter);
    }

    /**
     * Makes the column filter for the given exclude and include column
     * patterns, which {@link DefaultColumnFilter} matches without regard to
     * case and with its wildcard support.
     *
     * @param excludeColumns
     *            The column names to exclude; null or empty to exclude none.
     * @param includeColumns
     *            The column names to only include; null or empty to include
     *            all.
     * @return The filter accepting the columns that are included and not
     *         excluded.
     */
    private DefaultColumnFilter makeColumnFilter(final String[] excludeColumns,
            final String[] includeColumns)
    {
        final DefaultColumnFilter columnFilter = new DefaultColumnFilter();
        if (includeColumns != null)
        {
            for (final String includeColumn : includeColumns)
            {
                columnFilter.includeColumn(includeColumn);
            }
        }
        if (excludeColumns != null)
        {
            for (final String excludeColumn : excludeColumns)
            {
                columnFilter.excludeColumn(excludeColumn);
            }
        }
        return columnFilter;
    }

    /**
     * Make the expected table's columns from the actual table's column
     * definitions, so the expected data is sorted and compared the same way as
     * the actual data whichever types and column order its source declared.
     * With no expected columns, none are made.
     *
     * @throws DataSetException If the expected table's columns cannot be retrieved.
     */
    private Column[] makeExpectedTableColumns(final Column[] actualColumns,
            final ITableMetaData expectedTableMetaData) throws DataSetException
    {
        final Column[] expectedColumns = expectedTableMetaData.getColumns();
        return makeExpectedTableColumns(actualColumns, expectedColumns);
    }

    /**
     * Make expected Column[] from actual table column definitions so expected
     * data comparisons use data types from database (and expected data columns
     * handled same as actual data in comparisons). Don't include columns from
     * actual that are not in expected.
     */
    private Column[] makeExpectedTableColumns(final Column[] actualColumns,
            final Column[] expectedColumns)
    {
        return columnsNamedIn(actualColumns, expectedColumns);
    }

    /**
     * Keeps the columns whose name, without regard to case, is also the name of
     * one of the given other columns.
     *
     * @param columns
     *            The columns to keep from.
     * @param namedColumns
     *            The columns whose names decide which of columns to keep.
     * @return The kept columns, in columns' original order.
     */
    private Column[] columnsNamedIn(final Column[] columns,
            final Column[] namedColumns)
    {
        final Set<String> names =
                Arrays.stream(namedColumns).map(Column::getColumnName)
                        .map(name -> name.toLowerCase(Locale.ENGLISH))
                        .collect(Collectors.toSet());

        final List<Column> keptColumns = Arrays.stream(columns)
                .filter(col -> names.contains(
                        col.getColumnName().toLowerCase(Locale.ENGLISH)))
                .collect(Collectors.toList());
        return keptColumns.toArray(new Column[keptColumns.size()]);
    }

    private void logSortedTables(final SortedTable expectedSortedTable,
            final SortedTable actualSortedTable)
    {
        if (log.isTraceEnabled())
        {
            logSortedTable("expectedSortedTable", expectedSortedTable);
            logSortedTable("actualSortedTable", actualSortedTable);
        }
    }

    private void logSortedTable(final String tableTypeName,
            final SortedTable table)
    {
        final String methodName = "logSortedTable:";
        final Column[] sortColumns = table.getSortColumns();
        log.trace("{} {} sortColumns={}", methodName, tableTypeName,
                sortColumns);
        try
        {
            final String tableContents = tableFormatter.format(table);
            log.trace("{} {} tableContents={}", methodName, tableTypeName,
                    tableContents);
        } catch (final DataSetException e)
        {
            log.error("{} Error trying to log table={}", methodName,
                    tableTypeName, e);
        }
    }

    /**
     * Compare the tables, enables easy overriding.
     * <p>
     * Uses {@link #failureHandler} when set; otherwise defers to
     * {@link Assertion#assertWithValueComparer(ITable, ITable, Column[], ValueComparer, Map)}'s
     * own {@link org.dbunit.assertion.DefaultFailureHandler}, configured with
     * additionalColumnInfo.
     *
     * @param expectedTable the table containing all expected results.
     * @param actualTable the table containing all actual results.
     * @param additionalColumnInfo the additional columns to include in failure messages.
     * @param defaultValueComparer the value comparer used when no more specific comparer is configured.
     * @param columnValueComparers the per-column value comparers to use.
     * @throws DatabaseUnitException if the tables' row counts, columns, or data do not match.
     */
    protected void compareData(final ITable expectedTable,
            final ITable actualTable, final Column[] additionalColumnInfo,
            final ValueComparer defaultValueComparer,
            final Map<String, ValueComparer> columnValueComparers)
            throws DatabaseUnitException
    {
        if (failureHandler == null)
        {
            Assertion.assertWithValueComparer(expectedTable, actualTable,
                    additionalColumnInfo, defaultValueComparer,
                    columnValueComparers);
        } else
        {
            Assertion.assertWithValueComparer(expectedTable, actualTable,
                    failureHandler, defaultValueComparer,
                    columnValueComparers);
        }
    }

    /**
     * Don't add excluded columns to additionalColumnInfo as they are not
     * compared. The verification then drops, from what this returns, the
     * columns the actual table does not have, for example one that was not
     * loaded from the database, as the fail message would show a not found
     * message for them.
     *
     * @param expectedTable
     *            Not null.
     * @param excludeColumns
     *            Nullable.
     * @return the additional column info, excluding excludeColumns.
     * @throws DataSetException if the expected table's columns cannot be retrieved.
     */
    protected Column[] makeAdditionalColumnInfo(final ITable expectedTable,
            final String[] excludeColumns) throws DataSetException
    {
        final Column[] allColumns =
                expectedTable.getTableMetaData().getColumns();

        return excludeColumns == null ? allColumns
                : makeAdditionalColumnInfo(excludeColumns, allColumns);
    }

    /**
     * Don't add excluded columns to additionalColumnInfo as they are not
     * compared. The exclude patterns match the way the column filters applied
     * to the compared tables do: without regard to case, and with
     * {@link DefaultColumnFilter}'s wildcard support. The verification then
     * drops, from what this returns, the columns the actual table does not
     * have, for example one that was not loaded from the database, as the fail
     * message would show a not found message for them.
     *
     * @param excludeColumns
     *            Not null.
     * @param allColumns
     *            Not null.
     * @return the additional column info, excluding excludeColumns.
     */
    protected Column[] makeAdditionalColumnInfo(final String[] excludeColumns,
            final Column[] allColumns)
    {
        return filterColumns(allColumns, excludeColumns, null, null);
    }

    /**
     * Make a <code>IDataSet</code> from the specified files with case sensitive
     * table names as false.
     *
     * @param dataFiles
     *            Represents the array of dbUnit data files.
     * @param dataFilesName
     *            Concept name of the data files, e.g. prep, expected.
     * @return The composite dataset.
     * @throws DataSetException
     *             On dbUnit errors.
     */
    public IDataSet makeCompositeDataSet(final String[] dataFiles,
            final String dataFilesName) throws DataSetException
    {
        return makeCompositeDataSet(dataFiles, dataFilesName, false);
    }

    /**
     * Make a <code>IDataSet</code> from the specified files.
     *
     * @param dataFiles
     *            Represents the array of dbUnit data files; null is treated as
     *            none.
     * @param dataFilesName
     *            Concept name of the data files, e.g. prep, expected.
     * @param isCaseSensitiveTableNames
     *            true if case sensitive table names is on.
     * @return The composite dataset.
     * @throws DataSetException
     *             On dbUnit errors.
     */
    public IDataSet makeCompositeDataSet(final String[] dataFiles,
            final String dataFilesName, final boolean isCaseSensitiveTableNames)
            throws DataSetException
    {
        if (dataFileLoader == null)
        {
            throw new IllegalStateException(
                    "dataFileLoader is null; must configure or set it first");
        }

        final String[] files;
        if (dataFiles == null)
        {
            log.debug("makeCompositeDataSet: {} dataFiles is null, using none",
                    dataFilesName);
            files = new String[0];
        } else
        {
            files = dataFiles;
        }

        final int count = files.length;
        log.debug("makeCompositeDataSet: {} dataFiles count={}", dataFilesName,
                count);
        if (count == 0)
        {
            log.info("makeCompositeDataSet: Specified zero {} data files",
                    dataFilesName);
        }

        final IDataSet[] dataSets = new IDataSet[count];
        for (int i = 0; i < count; i++)
        {
            dataSets[i] = dataFileLoader.load(files[i]);
        }

        return new CompositeDataSet(dataSets, true, isCaseSensitiveTableNames);
    }

    /**
     * Apply the specified exclude and include column filters to the specified
     * table.
     *
     * @param table
     *            The table to apply the filters to.
     * @param excludeColumns
     *            The exclude filters; use null or empty array to mean exclude
     *            none.
     * @param includeColumns
     *            The include filters; use null or empty array to mean include
     *            all.
     * @return The filtered table.
     * @throws DataSetException if applying the filters fails.
     */
    public ITable applyColumnFilters(final ITable table,
            final String[] excludeColumns, final String[] includeColumns)
            throws DataSetException
    {
        if (table == null)
        {
            throw new IllegalArgumentException("table is null");
        }

        ITable filteredTable = table;

        // note: a filter with no include patterns accepts every column, so an
        // empty inclusion array includes all, the same as null
        if (includeColumns == null)
        {
            log.debug("applyColumnFilters: including columns=(all)");
        } else
        {
            log.debug("applyColumnFilters: including columns='{}'",
                    Arrays.toString(includeColumns));
        }

        if (excludeColumns == null || excludeColumns.length == 0)
        {
            log.debug("applyColumnFilters: excluding columns=(none)");
        } else
        {
            log.debug("applyColumnFilters: excluding columns='{}'",
                    Arrays.toString(excludeColumns));
        }

        // one filter over the unfiltered table, not one wrapper per filter: a
        // failure message looks a dropped column up only one wrapper deep
        if (includeColumns != null
                || (excludeColumns != null && excludeColumns.length > 0))
        {
            final DefaultColumnFilter columnFilter =
                    makeColumnFilter(excludeColumns, includeColumns);
            filteredTable = new ColumnFilterTable(table, columnFilter);
        }

        return filteredTable;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IDataSet getPrepDataset()
    {
        return prepDataSet;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IDataSet getExpectedDataset()
    {
        return expectedDataSet;
    }

    /**
     * Get the databaseTester.
     *
     * @see #databaseTester
     *
     * @return The databaseTester.
     */
    @Override
    public IDatabaseTester getDatabaseTester()
    {
        return databaseTester;
    }

    /**
     * Set the databaseTester. Replacing a different one first releases the
     * shared connection acquired from it, closing it only when
     * {@link #cleanupData()} would, and forgets what was resolved from it, so
     * the next step acquires its connection and case sensitivity setting from
     * the new one.
     *
     * @see #databaseTester
     *
     * @param databaseTester
     *            The databaseTester to set.
     */
    @Override
    public void setDatabaseTester(final IDatabaseTester databaseTester)
    {
        if (databaseTester == this.databaseTester)
        {
            return;
        }

        releaseReplacedTestersConnection();
        this.databaseTester = databaseTester;
        this.cachedIsCaseSensitiveTableNames = null;
    }

    /**
     * Release the shared connection acquired from the databaseTester about to
     * be replaced, closing it only when this instance owns it, and forget it
     * so the next step acquires one from the new databaseTester. Called before
     * the field is reassigned because {@link ConnectionOwnership} reads the
     * outgoing databaseTester's listener to decide whether to close. A close
     * failure is logged, not thrown, as the connection is being discarded
     * either way.
     */
    private void releaseReplacedTestersConnection()
    {
        if (!reusableConnectionHolder.isResolved())
        {
            return;
        }

        try
        {
            closeReusableConnection();
        } catch (final Exception e)
        {
            log.warn("setDatabaseTester: releasing the replaced tester's"
                    + " connection failed", e);
        }
        reusableConnectionHolder.discardOnFailure();
    }

    /**
     * Get whether the connection lookupFeatureValue() and cleanupData() are
     * done with is closed.
     *
     * @see #closeConnectionAfterTest
     *
     * @return True if it is closed, false if not.
     * @since 3.4.0
     */
    @Override
    public boolean isCloseConnectionAfterTest()
    {
        return closeConnectionAfterTest;
    }

    /**
     * Set whether the connection lookupFeatureValue() and cleanupData() are
     * done with is closed. Default is true. Set to false when databaseTester
     * shares a {@link org.dbunit.database.CachingConnectionProvider} across
     * test methods, so this instance does not close a connection other tests
     * still expect to reuse. False requires a databaseTester that hands out
     * the same connection on repeated calls: with one that hands out a new
     * connection each time, nothing closes the connection left open after a
     * test, and a step that fails leaves open the connection this instance
     * forgets. A warning is logged when databaseTester is a
     * {@link JdbcDatabaseTester} without a CachingConnectionProvider.
     *
     * @see #closeConnectionAfterTest
     *
     * @param closeConnectionAfterTest
     *            True to close it, false to leave it open.
     * @since 3.4.0
     */
    @Override
    public void setCloseConnectionAfterTest(
            final boolean closeConnectionAfterTest)
    {
        this.closeConnectionAfterTest = closeConnectionAfterTest;
    }

    /**
     * Get the dataFileLoader.
     *
     * @see #dataFileLoader
     *
     * @return The dataFileLoader.
     */
    @Override
    public DataFileLoader getDataFileLoader()
    {
        return dataFileLoader;
    }

    /**
     * Set the dataFileLoader.
     *
     * @see #dataFileLoader
     *
     * @param dataFileLoader
     *            The dataFileLoader to set.
     */
    @Override
    public void setDataFileLoader(final DataFileLoader dataFileLoader)
    {
        this.dataFileLoader = dataFileLoader;
    }

    /**
     * Set the prepDs.
     *
     * @see #prepDataSet
     *
     * @param prepDataSet
     *            The prepDs to set.
     */
    public void setPrepDs(final IDataSet prepDataSet)
    {
        this.prepDataSet = prepDataSet;
    }

    /**
     * Set the expectedDs.
     *
     * @see #expectedDataSet
     *
     * @param expectedDataSet
     *            The expectedDs to set.
     */
    public void setExpectedDs(final IDataSet expectedDataSet)
    {
        this.expectedDataSet = expectedDataSet;
    }

    /**
     * Get the tableDefs.
     *
     * @see #verifyTableDefs
     *
     * @return The tableDefs.
     */
    public VerifyTableDefinition[] getVerifyTableDefs()
    {
        return verifyTableDefs;
    }

    /**
     * Set the tableDefs.
     *
     * @see #verifyTableDefs
     *
     * @param verifyTableDefs
     *            The tableDefs to set.
     */
    public void setVerifyTableDefs(
            final VerifyTableDefinition[] verifyTableDefs)
    {
        this.verifyTableDefs = verifyTableDefs;
    }

    /**
     * Returns the verifier used to check that verify table definitions and the expected dataset agree.
     *
     * @return the verifier used to check that verify table definitions and the expected dataset agree.
     */
    public ExpectedDataSetAndVerifyTableDefinitionVerifier getExpectedDataSetAndVerifyTableDefinitionVerifier()
    {
        return expectedDataSetAndVerifyTableDefinitionVerifier;
    }

    /**
     * Sets the verifier used to check that verify table definitions and the expected dataset agree.
     *
     * @param expectedDataSetAndVerifyTableDefinitionVerifier the verifier to use.
     */
    public void setExpectedDataSetAndVerifyTableDefinitionVerifier(
            final ExpectedDataSetAndVerifyTableDefinitionVerifier expectedDataSetAndVerifyTableDefinitionVerifier)
    {
        this.expectedDataSetAndVerifyTableDefinitionVerifier =
                expectedDataSetAndVerifyTableDefinitionVerifier;
    }

    /**
     * Get the failureHandler.
     *
     * @see #failureHandler
     *
     * @return The failureHandler.
     * @since 3.5.0
     */
    @Override
    public FailureHandler getFailureHandler()
    {
        return failureHandler;
    }

    /**
     * Set the failureHandler.
     *
     * @see #failureHandler
     *
     * @param failureHandler
     *            The failureHandler to set.
     * @since 3.5.0
     */
    @Override
    public void setFailureHandler(final FailureHandler failureHandler)
    {
        this.failureHandler = failureHandler;
    }

    /**
     * Get the RowCountCheck in use.
     *
     * @see #rowCountChecker
     *
     * @return The RowCountCheck, or null if none has been resolved or set yet.
     * @since 3.6.0
     */
    public RowCountCheck getRowCountCheck()
    {
        return rowCountChecker.getRowCountCheck();
    }

    /**
     * Set the RowCountCheck, overriding the one otherwise lazily built from the shared
     * connection's DatabaseConfig.
     *
     * @see #rowCountChecker
     *
     * @param rowCountCheck
     *            The RowCountCheck to use.
     * @since 3.6.0
     */
    public void setRowCountCheck(final RowCountCheck rowCountCheck)
    {
        rowCountChecker.setRowCountCheck(rowCountCheck);
    }

    /**
     * Set the enabled flag and excluded table patterns to resolve a RowCountCheck from, instead
     * of the shared connection's DatabaseConfig - the values an annotation such as
     * {@code @DbUnitRowCountCheck} declares.
     *
     * @see #rowCountChecker
     *
     * @param enabled
     *            Whether the check is enabled.
     * @param exclude
     *            The excluded table patterns; null is treated as empty (excludes none).
     * @since 3.6.0
     */
    @Override
    public void setRowCountCheckOverride(final boolean enabled, final String[] exclude)
    {
        rowCountChecker.setEnabledOverride(enabled, exclude);
    }

    /**
     * Clear a previously set enabled flag and excluded table patterns override, returning to
     * resolving a RowCountCheck from the shared connection's DatabaseConfig.
     *
     * <p>A caller reusing one instance across several tests - e.g. one held by a
     * {@code @DbUnitTestCase} static field - must call this once a test that set an override is
     * over, so that test's override does not silently carry over onto the next one.
     *
     * @see #rowCountChecker
     * @since 3.6.0
     */
    @Override
    public void clearRowCountCheckOverride()
    {
        rowCountChecker.clearEnabledOverride();
    }

    /**
     * Get the DatabaseConfig property name/value pairs applied to the connection shared by
     * setupData(), verifyData() and cleanupData().
     *
     * @see #setDatabaseConfigProperties(Properties)
     *
     * @return A copy of the properties, or null if none are set.
     * @since 3.6.0
     */
    @Override
    public Properties getDatabaseConfigProperties()
    {
        if (databaseConfigProperties == null)
        {
            return null;
        }

        final Properties copy = new Properties();
        copy.putAll(databaseConfigProperties);
        return copy;
    }

    /**
     * Set DatabaseConfig property name/value pairs to apply to the connection shared by
     * setupData(), verifyData() and cleanupData(), every time {@link #getConnection()}
     * resolves it.
     *
     * <p>
     * Puts back, first, whatever the previous properties replaced, so that changing them - as
     * the annotation runtime does to restore an injected instance's own properties after a
     * test - leaves nothing of the old values on a connection kept open.
     *
     * @see #databaseConfigProperties
     *
     * @param databaseConfigProperties
     *            The properties to apply; null or empty applies none.
     * @since 3.6.0
     */
    @Override
    public void setDatabaseConfigProperties(final Properties databaseConfigProperties)
    {
        restoreDatabaseConfig();
        if (databaseConfigProperties == null)
        {
            this.databaseConfigProperties = null;
            this.databaseConfigOverrides = new DatabaseConfigOverrides(new Properties());
        } else
        {
            final Properties copy = new Properties();
            copy.putAll(databaseConfigProperties);
            this.databaseConfigProperties = copy;
            this.databaseConfigOverrides = new DatabaseConfigOverrides(copy);
        }
    }

    /**
     * A blanket {@link ConnectionPreservingOperationListener} over this instance's own listener
     * that tells the databaseTester's listener about each connection retrieved before its
     * delegate, the instance's own listener, hears of it, and puts this instance's
     * values back on the connection in between.
     *
     * @since 3.6.0
     */
    private static final class TestersListenerFirstOperationListener
            extends ConnectionPreservingOperationListener
    {
        private final IOperationListener testersListener;
        private final Consumer<IDatabaseConnection> afterTestersListener;

        private TestersListenerFirstOperationListener(final IOperationListener testersListener,
                final Consumer<IDatabaseConnection> afterTestersListener,
                final IOperationListener ownListener)
        {
            super(ownListener);
            this.testersListener = testersListener;
            this.afterTestersListener = afterTestersListener;
        }

        @Override
        public void connectionRetrieved(final IDatabaseConnection connection)
        {
            testersListener.connectionRetrieved(connection);
            afterTestersListener.accept(connection);
            super.connectionRetrieved(connection);
        }
    }

    /**
     * {@link IDatabaseTester} that runs setUp/tearDown operations against a
     * connection supplied by the given {@link Callable} instead of calling
     * {@code getConnection()} on a wrapped {@link IDatabaseTester}, so
     * repeated {@link #onSetup()}/{@link #onTearDown()} calls driven through
     * this instance share whatever connection the supplier itself caches
     * (here, {@link DefaultPrepAndExpectedTestCase#getReusableConnection()})
     * rather than each opening a new one.
     *
     * @since 3.4.0
     */
    private static final class ReusableConnectionDatabaseTester
            extends AbstractDatabaseTester
    {
        private final Callable<IDatabaseConnection> connectionSupplier;

        /**
         * Create new instance with the specified connection supplier.
         *
         * @param connectionSupplier
         *            Supplies the connection to use; invoked once per
         *            {@link #getConnection()} call, so it is responsible for
         *            any caching of its own.
         */
        private ReusableConnectionDatabaseTester(
                final Callable<IDatabaseConnection> connectionSupplier)
        {
            this.connectionSupplier = connectionSupplier;
        }

        @Override
        public IDatabaseConnection getConnection() throws Exception
        {
            return connectionSupplier.call();
        }
    }
}