/*

Derby - Class org.apache.derbyTesting.functionTests.tests.upgradeTests.UpgradeTrajectoryTest

Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License.  You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/
package org.apache.derbyTesting.functionTests.tests.upgradeTests;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Properties;
import javax.sql.DataSource;
import junit.extensions.TestSetup;
import junit.framework.Test;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.BaseTestSuite;
import org.apache.derbyTesting.junit.ClassLoaderTestSetup;
import org.apache.derbyTesting.junit.JDBCClient;
import org.apache.derbyTesting.junit.JDBCClientSetup;
import org.apache.derbyTesting.junit.JDBCDataSource;
import org.apache.derbyTesting.junit.SecurityManagerSetup;
import org.apache.derbyTesting.junit.SupportFilesSetup;
import org.apache.derbyTesting.junit.SystemPropertyTestSetup;
import org.apache.derbyTesting.junit.TestConfiguration;

/**
 * <p>
 * Test upgrade trajectories. This test compares the metadata in
 * upgraded databases to the metadata in databases created from scratch.
 * Given a collection of releases, this test does the following:
 * </p>
 *
 * <ul>
 * <li>Builds either a minimal set of trajectories or the full set of possible
 * trajectories:
 *  <ul>
 *   <li>By default, just builds a minimal set of trajectories. These are all
 *   trajectories which begin with some release, then upgrade through all
 *   intermediate releases to the highest release in the original collection of
 *   all releases. A set of N releases gives rise to N-1 minimal trajectories.</li>
 *
 *   <li>Otherwise, if the system property {@code derbyTesting.allTrajectories}
 *   is set to true, builds the set of all upgrade trajectories possible on
 *   that collection of releases. An upgrade trajectory is a sorted subset of
 *   those releases. Each subset is sorted in ascending release order. We
 *   exclude the vacuous empty subset and the uninteresting singleton
 *   subsets. A set of N releases gives rise to ((2**N) - N) - 1 hard-upgrade
 *   trajectories.</li>
 *  </ul>
 * </li>
 * <li>For each trajectory, we create two databases:
 *  <ul>
 *   <li>A starting point database created with the first release in the
 *   trajectory. This database is then upgraded through all of the intermediate
 *   releases in the trajectory until it is at the level of the last release in
 *   the trajectory.</li>
 *   <li>An ending point database created with the last release in the
 *   trajectory.</li>
 *  </ul>
 *</li>
 * <li>We then compare the metadata in the starting point and ending point
 * databases.</li>
 * </ul>
 *
 * <p>
 * By default we don't consider soft-upgrades. Also by default, we consider
 * trajectories with more than one release from the same branch. You can
 * parameterize or customize some constants (see below) if you want to change
 * these decisions.
 * </p>
 *
 * <p>
 * By default we consider all  trajectories possible on the collection
 * of releases listed in {@link OldVersions}. If you want to consider
 * a different  collection of  releases, you  can override  the {@code
 * OldVersions}  collection  by  setting the  system  property  {@code
 * "derbyTesting.oldVersionsPath"}. Here, for instance, is the command
 * line to run this test against a customized list of releases:
 * </p>
 *
 * <blockquote><pre>
 *  java -XX:MaxPermSize=128M -Xmx512m \
 *  -DderbyTesting.oldReleasePath=/Users/me/myDerbyReleaseDirectory \
 *  -DderbyTesting.oldVersionsPath=/Users/me/fileContainingMyListOfTastyReleases \
 *  junit.textui.TestRunner org.apache.derbyTesting.functionTests.tests.upgradeTests.UpgradeTrajectoryTest
 * </pre></blockquote>
 *
 * <p>
 * For extra verbose output, you can set the "derby.tests.debug" property too:
 * </p>
 *
 * <blockquote><pre>
 *  java -XX:MaxPermSize=128M -Xmx512m \
 *  -DderbyTesting.oldReleasePath=/Users/me/myDerbyReleaseDirectory \
 *  -DderbyTesting.oldVersionsPath=/Users/me/fileContainingMyListOfTastyReleases \
 *  -Dderby.tests.debug=true \
 *  junit.textui.TestRunner org.apache.derbyTesting.functionTests.tests.upgradeTests.UpgradeTrajectoryTest
 * </pre></blockquote>
 *
 * <p>
 * Here is the command line to run all upgrade trajectories against a customized list of releases:
 * </p>
 *
 * <blockquote><pre>
 *  java -XX:MaxPermSize=128M -Xmx512m \
 *  -DderbyTesting.allTrajectories=true \
 *  -DderbyTesting.oldReleasePath=/Users/me/myDerbyReleaseDirectory \
 *  -DderbyTesting.oldVersionsPath=/Users/me/fileContainingMyListOfTastyReleases \
 *  junit.textui.TestRunner org.apache.derbyTesting.functionTests.tests.upgradeTests.UpgradeTrajectoryTest
 * </pre></blockquote>
 *
 * <p>
 * If you need to test a particular trajectory, you can hand-edit
 * {@code makeSampleTrajectories()} and uncomment the call to it.
 * </p>
 *
 */
public class UpgradeTrajectoryTest extends BaseJDBCTestCase
{
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // CONSTANTS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    public static final String ALL_TRAJECTORIES_PROPERTY = "derbyTesting.allTrajectories";

    public static Version VERSION_10_0_2_1 = new Version( 10, 0, 2, 1 );
    public static Version VERSION_10_1_3_1 = new Version( 10, 1, 3, 1 );
    public static Version VERSION_10_2_2_0 = new Version( 10, 2, 2, 0 );
    public static Version VERSION_10_5_1_1 = new Version( 10, 5, 1, 1 );
    public static Version VERSION_10_6_0_0 = new Version( 10, 6, 0, 0 );

    public static Version.Trajectory TRAJECTORY_10_0_2_1_TO_10_1_3_1 = new Version.Trajectory( new Version[] { VERSION_10_0_2_1, VERSION_10_1_3_1 } );

    public static String BRANCH_10_0 = "10.0";
    public static String BRANCH_10_1 = "10.1";
    public static String BRANCH_10_2 = "10.2";
    public static String BRANCH_10_3 = "10.3";
    public static String BRANCH_10_4 = "10.4";
    public static String BRANCH_10_5 = "10.5";

    public static final String UPGRADED_DATABASE = "old_database";
    public static final String VIRGIN_DATABASE = "new_database";
    public static final String COMPARISON_DATABASE = "comparison_database";
    private static final String DUMMY_NUMBER = "123";
    private static final String DUMMY_STRING = "BLAHBLAH";
    private static final String DUMMY_TIMESTAMP = "123456";

    private static final String DERBY_4214_1 = "RETURNS VARCHAR(32672)";
    private static final String DERBY_4214_2 = "RETURNS VARCHAR(10890)";
    private static final String DERBY_4215 = "SYSCS_INPLACE_COMPRESS_TABLE";

    //
    // Parameterize or change these switches if you want to alter the set of
    // trajectories which we consider:
    //
    private static final boolean TRJ_IGNORE_SOFT_UPGRADE = true;
    private static final boolean TRJ_SAME_BRANCH_NEIGHBORS = false;

    private static final String SYSALIASES = "SYSALIASES";
    private static final String SYSCONGLOMERATES = "SYSCONGLOMERATES";
    private static final String SYSSTATEMENTS = "SYSSTATEMENTS";
    private static final String SYSROUTINEPERMS = "SYSROUTINEPERMS";

    private static final String CONGLOMERATENUMBER = "CONGLOMERATENUMBER";
    private static final String ALIAS = "ALIAS";
    private static final String ALIASID = "ALIASID";
    private static final String SPECIFICNAME = "SPECIFICNAME";
    private static final String STMTID = "STMTID";
    private static final String STMTNAME = "STMTNAME";
    private static final String TEXT = "TEXT";
    private static final String ROUTINEPERMSID = "ROUTINEPERMSID";
    private static final String LASTCOMPILED = "LASTCOMPILED";

    private static final boolean LOQUACIOUS = false;

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // STATE
    //
    ///////////////////////////////////////////////////////////////////////////////////

    private Version.Trajectory _trajectory;
    private String             _trajectoryName;
    private boolean[]          _hardUpgradeRequests;
    
    private HashMap<String,String>     _unstableColumnValues = new HashMap<String,String>();

    private static ThreadLocal<ClassLoader> _originalClassLoader = new ThreadLocal<ClassLoader>();

    // these are the system tables which must be read first in order to
    // prep the mapping of unstable identifiers
    private static String[] INITIAL_TABLES = new String[]
    {
        "SYSALIASES",
    };


    ///////////////////////////////////////////////////////////////////////////////////
    //
    // CONSTRUCTOR
    //
    ///////////////////////////////////////////////////////////////////////////////////

    public UpgradeTrajectoryTest( Version.Trajectory trajectory, boolean[] hardUpgradeRequests )
    {
        super( "testTrajectory" );

        _trajectory = trajectory;
        _hardUpgradeRequests = hardUpgradeRequests;
    }

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // JUnit BEHAVIOR
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * Return the suite of tests.
     */
    public static Test suite()
    {
        BaseTestSuite suite = new BaseTestSuite("Upgrade trajectory test");

        addTrajectories( suite );
        
        Test sfs = new SupportFilesSetup((Test) suite);

        return SecurityManagerSetup.noSecurityManager( sfs );
    }

    /**
     * <p>
     * Add all of the trajectories we intend to test.
     * </p>
     */
    private static void addTrajectories( BaseTestSuite suite )
    {
        Version.Trajectory[] trajectories = makeTrajectories();
        int                  count = trajectories.length;

        println( "Found " + count + " trajectories." );

        for ( int i = 0; i < count; i++ )
        {
            // right now, we're only testing hard upgrade.
            addHardUpgradeOnlyTrajectory( suite, trajectories[ i ] );
        }
    }
    
    /**
     * <p>
     * Make all known upgrade trajectories. This amounts to every non-vacuous
     * subset of the known versions.
     * </p>
     */
    private static Version.Trajectory[] makeTrajectories()
    {
        Version[]   supportedVersions = getSupportedVersions();
        Version.Trajectory[] result;

        //
        // If requested to, we construct the power set of supported versions, throwing
        // out the non-upgradable subsets, viz., the empty set and the
        // singletons. We include hard upgrades between releases on the same
        // branch.
        //
        // By default we don't test all trajectories. We only test the
        // trajectories which are complete sequences from a starting point up to
        // the last release.
        //
        if ( shouldBuildAllTrajectories() ) { result = buildPowerSet( supportedVersions ); }
        else { result = buildMinimalSet( supportedVersions ); }
        
        //
        // Uncomment this line if you just want to test a couple sample
        // trajectories.
        //
        //result = makeSampleTrajectories();
        
        return result;
    }
    
    /**
     * <p>
     * Return true if we should build all trajectories.
     * </p>
     */
    private static boolean shouldBuildAllTrajectories()
    {
        Boolean bool = Boolean.valueOf(getSystemProperty( ALL_TRAJECTORIES_PROPERTY ));

        return bool.booleanValue();
    }
    
    /**
     * <p>
     * Sample trajectory for debugging this program.
     * </p>
     */
    private static Version.Trajectory[] makeSampleTrajectories()
    {
        return new Version.Trajectory[]
        {
            new Version.Trajectory( new Version[] { new Version( 10, 0, 2, 1), new Version( 10, 1, 3, 1 ) } ),
            new Version.Trajectory( new Version[] { new Version( 10, 0, 2, 1), new Version( 10, 3, 3, 0 ) } ),
            new Version.Trajectory( new Version[] { new Version( 10, 0, 2, 1), new Version( 10, 3, 3, 0 ), new Version( 10, 5, 1, 1 ) } ),
            new Version.Trajectory( new Version[] { new Version( 10, 0, 2, 1), new Version( 10, 3, 3, 0 ), new Version( 10, 6, 0, 0 ) } ),
            new Version.Trajectory( new Version[] { new Version( 10, 0, 2, 1), new Version( 10, 5, 1, 1 ) } ),
            new Version.Trajectory( new Version[] { new Version( 10, 4, 2, 1), new Version( 10, 5, 1, 1 ) } ),
        };

    }
    
    /**
     * <p>
     * Get the supported versions.
     * </p>
     */
    private static Version[] getSupportedVersions()
    {
        int[][]      raw = OldVersions.getSupportedVersions();
        int          count = raw.length;
        Version[] result = new Version[ count ];

        for ( int i = 0; i < count; i++ ) { result[ i ] = new Version( raw[ i ] ); }

        return result;
    }
    
    /**
     * <p>
     * Add only the test case which hard-upgrades along all edges of the Trajectory.
     * </p>
     */
    private static void addHardUpgradeOnlyTrajectory(
        BaseTestSuite suite, Version.Trajectory trajectory )
    {
        // a valid trajectory must have a start point and a different end point
        int       versionCount = trajectory.getVersionCount();
        if ( versionCount < 2 ) { return; }
        
        boolean[] hardUpgradeRequests = new boolean[ versionCount ];

        // the start point is always hard
        for ( int i = 0; i < versionCount; i++ ) { hardUpgradeRequests[ i ] = true; }

        addTrajectory( suite, trajectory, hardUpgradeRequests );
    }

    /**
     * <p>
     * Add a single trajectory to the suite, looping through all combinations of
     * hard and softupgrade.
     * </p>
     */
    private static void addTrajectory(
        BaseTestSuite suite, Version.Trajectory trajectory )
    {
        // a valid trajectory must have a start point and a different end point
        int       versionCount = trajectory.getVersionCount();
        if ( versionCount < 2 ) { return; }
        
        boolean[] hardUpgradeRequests = new boolean[ versionCount ];

        // the start point is always hard unless you parameterize the following constant
        hardUpgradeRequests[ 0 ] = TRJ_IGNORE_SOFT_UPGRADE;

        addTrajectory( suite, trajectory, hardUpgradeRequests, 1 );
    }

    private static void addTrajectory(
        BaseTestSuite suite,
        Version.Trajectory trajectory,
        boolean[] hardUpgradeRequests,
        int idx )
    {
        if ( idx >= trajectory.getVersionCount() )
        {
            addTrajectory( suite, trajectory, hardUpgradeRequests );
        }
        else
        {
            boolean[] hard = clone( hardUpgradeRequests );
            boolean[] soft = clone( hardUpgradeRequests );
            
            hard[ idx ] = true;
            addTrajectory( suite, trajectory, hard, idx + 1 );

            soft[ idx ] = false;
            addTrajectory( suite, trajectory, soft, idx + 1 );
        }
    }

    private static boolean[] clone( boolean[] input )
    {
        int       count = input.length;
        boolean[] output = new boolean[ count ];

        for ( int i = 0; i < count; i++ ) { output[ i ] = input[ i ]; }

        return output;
    }

    /**
     * <p>
     * Add a single trajectory to the suite, with upgrade instructions.
     * </p>
     */
    private static void addTrajectory(
        BaseTestSuite suite,
        Version.Trajectory trajectory,
        boolean[] hardUpgradeRequests )
    {
        UpgradeTrajectoryTest utt = new UpgradeTrajectoryTest( trajectory, hardUpgradeRequests );
        TestSetup setup = TestConfiguration.additionalDatabaseDecorator( utt, UPGRADED_DATABASE );
        setup =  TestConfiguration.additionalDatabaseDecorator( setup, VIRGIN_DATABASE );
        setup =  TestConfiguration.additionalDatabaseDecorator( setup, COMPARISON_DATABASE );

        Properties preReleaseUpgrade = new Properties();
        preReleaseUpgrade.setProperty( "derby.database.allowPreReleaseUpgrade", "true");
        
        setup = new SystemPropertyTestSetup(setup, preReleaseUpgrade );

        // If the first release in the trajectory pre-dates the release which
        // introduced JDBC4, force the client to be the JDBC3 client. This
        // prevents us from falling through and picking up the JDBC4 data source from
        // the system classpath rather than picking up a datasource from
        // the version-specific classloader.
        if ( trajectory.getVersion( 0 ).compareTo( VERSION_10_2_2_0 ) < 0 )
        {
            setup = new JDBCClientSetup( setup, JDBCClient.EMBEDDED_30 );
        }
        
        suite.addTest( setup );
    }

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // TESTS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * <p>
     * Test a single trajectory.
     * </p>
     */
    public void testTrajectory() throws Exception
    {
        _trajectoryName = _trajectory.toString() + ' ' + stringifyUpgradeRequests();

        println( "Testing trajectory: " + _trajectoryName );

        // there must be at least 2 versions in a trajectory, otherwise the
        // test is vacuous
        int  versionCount = _trajectory.getVersionCount();
        assertTrue( _trajectoryName, versionCount > 1 );

        // remember the original class loader
        saveOriginalClassLoader();

        try {
            Version startDataVersion = _trajectory.getVersion( 0 );
            Version endDataVersion = startDataVersion;

            createDatabase( startDataVersion, UPGRADED_DATABASE );

            // now upgrade the database through the whole chain
            for ( int i = 1; i < versionCount; i++ )
            {
                Version nextSoftwareVersion = _trajectory.getVersion( i );
                boolean hardUpgrade = _hardUpgradeRequests[ i ];

                if ( hardUpgrade ) { endDataVersion = nextSoftwareVersion; }

                upgradeDatabase( nextSoftwareVersion, endDataVersion, hardUpgrade, UPGRADED_DATABASE );
            }

            createDatabase( endDataVersion, VIRGIN_DATABASE );

            if ( LOQUACIOUS) { println( "    End version is " + endDataVersion ); }

            compareDatabases( endDataVersion, UPGRADED_DATABASE, VIRGIN_DATABASE );
        }
        finally
        {
            restoreOriginalClassLoader();

            // reboot the databases so that DropDatabaseSetup can remove them
            bootDatabase( UPGRADED_DATABASE );
            bootDatabase( VIRGIN_DATABASE );
        }
    }

    /**
     * <p>
     * Compare the metadata in two databases.
     * </p>
     */
    private void compareDatabases( Version version, String leftDatabaseName, String rightDatabaseName )
        throws Exception
    {
        ClassLoaderTestSetup.setThreadLoader( version.getClassLoader() );

        DataSource leftDS = makeDataSource( leftDatabaseName );
        DataSource rightDS = makeDataSource( rightDatabaseName );
        DataSource comparisonDS = bootDatabase( COMPARISON_DATABASE );
        Connection leftConn = leftDS.getConnection();
        Connection rightConn = rightDS.getConnection();
        Connection comparisonConn = comparisonDS.getConnection();

        //        compareQueries( leftConn, rightConn, 2, "select stmtname, lastcompiled from sys.sysroutineperms order by stmtname" );
        //        boolean b = true;
        //        if ( b ) { return; }


        try {

            goodStatement( comparisonConn, "create schema " + leftDatabaseName );
            goodStatement( comparisonConn, "create schema " + rightDatabaseName );

            compareResults
                ( leftConn, rightConn, comparisonConn, leftDatabaseName, rightDatabaseName, "first_table", "select tablename from sys.systables where tabletype = 'S'" );

            // compare the tables which must come first
            int initialTableCount = INITIAL_TABLES.length;
            for ( int i = 0; i < initialTableCount; i++ )
            {
                String systemTableName = INITIAL_TABLES[ i ];
                compareResults
                    ( leftConn, rightConn, comparisonConn, leftDatabaseName, rightDatabaseName, systemTableName, "select * from sys." + systemTableName );
            }

            // now compare the other tables
            ArrayList systemTables = listSystemTables( comparisonConn );
            int          count = systemTables.size();

            for ( int i = 0; i < count; i++ )
            {
                String systemTableName = (String) systemTables.get( i );
                compareResults
                    ( leftConn, rightConn, comparisonConn, leftDatabaseName, rightDatabaseName, systemTableName, "select * from sys." + systemTableName );
            }

        }
        finally
        {
            shutdownDatabase( leftDS );
            shutdownDatabase( rightDS );
            shutdownDatabase( comparisonDS );
        }

    }
    
    /**
     * <p>
     * Get the names of all the system tables in the database except for the
     * list of initial tables.
     * </p>
     */
    private ArrayList<String> listSystemTables
        ( Connection conn )
        throws Exception
    {
        ArrayList<String> result = new ArrayList<String>();

        StringBuffer buffer = new StringBuffer();

        buffer.append( "select tablename from sys.systables where tabletype = 'S' and tablename != 'SYSDUMMY1'" );
        int initialTableCount = INITIAL_TABLES.length;
        for ( int i = 0; i < initialTableCount; i++ )
        {
            buffer.append( " and tablename != '" );
            buffer.append( INITIAL_TABLES[ i ] );
            buffer.append( "'" );
        }
        buffer.append( " order by tablename" );
        
        PreparedStatement ps = chattyPrepare( conn,  buffer.toString() );
        ResultSet               rs = ps.executeQuery();

        while ( rs.next() )
        {
            result.add( rs.getString( 1 ) );
        }

        rs.close();
        ps.close();

        return result;
    }
    
    /**
     * <p>
     * Compare the results of a query in two databases.
     * </p>
     */
    private void compareResults
        ( Connection leftConn, Connection rightConn, Connection comparisonConn, String leftSchema, String rightSchema, String tableName, String query )
        throws Exception
    {
        String leftTableName = leftSchema + "." + tableName;
        String rightTableName = rightSchema + "." + tableName;
        StringBuffer columnList = new StringBuffer();
        StringBuffer insertList = new StringBuffer();
        ArrayList<String> columnNames = new ArrayList<String>();

        PreparedStatement leftPS = chattyPrepare( leftConn, query );
        PreparedStatement rightPS = chattyPrepare( rightConn, query );
        ResultSet leftSelect = leftPS.executeQuery();
        ResultSet rightSelect = rightPS.executeQuery();
        PreparedStatement leftInsert = null;
        PreparedStatement rightInsert = null;

        try {
            //
            // First create the tables to hold the left and right results.
            //
            ResultSetMetaData leftRSMD = leftSelect.getMetaData();
            ResultSetMetaData rightRSMD = rightSelect.getMetaData();
            int columnCount = leftRSMD.getColumnCount();
            String sampleColumnName = null;

            assertEquals( _trajectoryName, leftRSMD.getColumnCount(), rightRSMD.getColumnCount() );

            columnList.append( "\n( " );
            insertList.append( "\n( " );
            for ( int i = 1; i <= columnCount; i++ )
            {
                if ( i > 1 )
                {
                    columnList.append( ", " );
                    insertList.append( ", " );
                }
                String columnName = leftRSMD.getColumnName(  i );
                if ( i == 1 ) { sampleColumnName = columnName; }
                columnNames.add( columnName );
                
                assertEquals( _trajectoryName, leftRSMD.getColumnName( i ), rightRSMD.getColumnName( i ) );
                assertEquals( _trajectoryName, leftRSMD.getColumnType( i ), rightRSMD.getColumnType( i  ) );

                columnList.append( columnName );
                columnList.append( " varchar( 10000 )" );
                insertList.append( "?" );
            }
            columnList.append( "\n)" );
            insertList.append( "\n)" );

            String colList = columnList.toString();
            String insList = insertList.toString();

            goodStatement( comparisonConn, "create table " + leftTableName + colList );
            goodStatement( comparisonConn, "create table " + rightTableName + colList );
            
            leftInsert = chattyPrepare( comparisonConn, "insert into " + leftTableName + " values " + insList );
            rightInsert = chattyPrepare( comparisonConn, "insert into " + rightTableName + " values " + insList );

            // now loop through the metadata rows, copying them into the
            // comparison tables
            int leftCount = stuffTable( tableName, leftSelect, leftInsert );
            int rightCount = stuffTable( tableName, rightSelect, rightInsert );

            if (
                (!suffersDERBY_4215( tableName )) &&
                (!suffersDERBY_4216( tableName ))
                )
            { assertEquals( _trajectoryName + ": " + tableName, leftCount, rightCount ); }
            else { assertTrue( _trajectoryName + ": " + tableName, leftCount != rightCount ); }

            // now compare the copied metadata, using left joins
            leftJoin( comparisonConn, columnNames, tableName, leftTableName, rightTableName );
            if ( !suffersDERBY_4216( tableName ) )
            { leftJoin( comparisonConn, columnNames, tableName, rightTableName, leftTableName ); }
        }
        finally
        {
            leftSelect.close();
            rightSelect.close();
            leftPS.close();
            rightPS.close();

            if ( leftInsert != null ) { leftInsert.close(); }
            if ( rightInsert != null ) { rightInsert.close(); }
        }
        
    }

    /**
     * <p>
     * Copy metadata from a source table into a target table.
     * </p>
     */
    private int stuffTable( String tableName, ResultSet select, PreparedStatement insert ) throws Exception
    {
        ResultSetMetaData rsmd = select.getMetaData();
        int  columnCount = rsmd.getColumnCount();
        int  rowCount = 0;

        String[] columnNames = new String[ columnCount ];
        for ( int i = 0; i < columnCount; i++ ) { columnNames[ i ] = rsmd.getColumnName( i + 1 ); }

        String[] row = new String[ columnCount ];

        while ( select.next() )
        {
            for ( int i = 0; i < columnCount; i++ )
            {
                row[ i ] = select.getString( i + 1 );
            }
            normalizeRow( tableName, columnNames, row );
            
            for ( int i = 0; i < columnCount; i++ )
            {
                insert.setString( i + 1, row[ i ]  );
            }
            insert.executeUpdate();
            rowCount++;
        }

        return rowCount;
    }
    
    /**
     * <p>
     * Left join two tables and verify that the result is empty.
     * </p>
     */
    private void leftJoin( Connection conn, ArrayList columnNames, String tableName, String leftTableName, String rightTableName )
        throws Exception
    {
        PreparedStatement selectPS = null;
        ResultSet select = null;

        String sampleColumn = (String) columnNames.get( 0 );
        StringBuffer buffer = new StringBuffer();

        buffer.append( "select *\nfrom " );
        buffer.append( leftTableName );
        buffer.append( " l left join " );
        buffer.append( rightTableName );
        buffer.append( " r\non " );
        int count = columnNames.size();
        for ( int i = 0; i < count; i++ )
        {
            if ( i > 0 ) { buffer.append( "and " ); }

            String columnName = (String) columnNames.get( i );
            buffer.append( "( ( l." );
            buffer.append( columnName );
            buffer.append( " = r." );
            buffer.append( columnName );
            buffer.append( " ) or ( l." );
            buffer.append( columnName );
            buffer.append( " is null and r." );
            buffer.append( columnName );
            buffer.append( " is null ) )\n" );
        }
        buffer.append( "where r." );
        buffer.append( sampleColumn );
        buffer.append( " is null" );
        

        try {
            selectPS = chattyPrepare
                (
                 conn,
                 buffer.toString()
                 );
            select = selectPS.executeQuery();

            String expected = "";
            String actual = filterKnownProblems( tableName, printResultSet( select ) );

            assertEquals( _trajectoryName + ": " + leftTableName + " vs. " + rightTableName, expected, actual );
        }
        finally
        {
            if ( select != null ) { select.close(); }
            if ( selectPS != null ) { selectPS.close(); }
        }

    }
    
    /**
     * <p>
     * Create a database using the indicated version of Derby.
     * </p>
     */
    private void createDatabase( Version version, String logicalDatabaseName )
        throws Exception
    {
        ClassLoaderTestSetup.setThreadLoader( version.getClassLoader() );

        DataSource ds = bootDatabase( logicalDatabaseName );

        Connection conn = ds.getConnection();

        vetDBVersion( version, version, conn );

        shutdownDatabase( ds );
    }

    /**
     * <p>
     * Upgrade a database to the indicated version of Derby.
     * </p>
     */
    private void upgradeDatabase( Version softwareVersion, Version dataVersion, boolean hardUpgrade, String logicalDatabaseName )
        throws Exception
    {
        ClassLoaderTestSetup.setThreadLoader(softwareVersion.getClassLoader());

        DataSource ds = upgradeDatabase( logicalDatabaseName, hardUpgrade );

        Connection conn = ds.getConnection();

        vetDBVersion( softwareVersion, dataVersion, conn );

        shutdownDatabase( ds );
    }

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // MINIONS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * <p>
     * Boot a database.
     * </p>
     */
    private DataSource bootDatabase( String logicalDatabaseName )
        throws Exception
    {
        DataSource ds = makeDataSource( logicalDatabaseName );
        Method     setMethod = ds.getClass().getMethod( "setCreateDatabase", new Class[] { String.class } );
        
        setMethod.invoke( ds, new Object[] { "create" } );
        
        Connection conn = ds.getConnection();
        
        return ds;
    }
    
    /**
     * <p>
     * Hard-upgrade a database.
     * </p>
     */
    private DataSource upgradeDatabase( String logicalDatabaseName, boolean hardUpgrade )
        throws Exception
    {
        DataSource ds = makeDataSource( logicalDatabaseName );

        if ( hardUpgrade )
        {
            Method     setMethod = ds.getClass().getMethod( "setConnectionAttributes", new Class[] { String.class } );
        
            setMethod.invoke( ds, new Object[] { "upgrade=true" } );
        }
        
        Connection conn = ds.getConnection();
        
        return ds;
    }

    /**
     * <p>
     * Make a DataSource given a logical database name.
     * </p>
     */
    private DataSource makeDataSource( String logicalDatabaseName )
        throws Exception
    {
        return JDBCDataSource.getDataSourceLogical( logicalDatabaseName );
    }

    /**
     * <p>
     * Shutdown a database.
     * </p>
     */
    private void shutdownDatabase( DataSource ds )
    {
        JDBCDataSource.shutdownDatabase( ds );
    }
    
    /**
     * <p>
     * Verify that the database has the expected version.
     * </p>
     */
    private void vetDBVersion( Version softwareVersion, Version dataVersion, Connection conn )
        throws Exception
    {
        String expectedSoftwareVersion = softwareVersion.toString();
        String expectedDataVersion = dataVersion.getBranchID();
        String actualSoftwareVersion = trimDriverVersion( conn.getMetaData().getDriverVersion() );
        String actualDataVersion = getDataVersion( conn );

        assertEquals( _trajectoryName, expectedSoftwareVersion, actualSoftwareVersion );
        assertEquals( _trajectoryName, expectedDataVersion, actualDataVersion );
    }

    /**
     * <p>
     * Get the version of the data in the database.
     * </p>
     */
    private String getDataVersion( Connection conn )
        throws Exception
    {
        PreparedStatement ps = null;
        ResultSet               rs = null;

        try {
            ps = conn.prepareStatement( "values syscs_util.syscs_get_database_property('DataDictionaryVersion')" );
            rs = ps.executeQuery();

            rs.next();

            return rs.getString( 1 );
        }
        catch (SQLException se)
        {
            printStackTrace( se );
            return null;
        }
        finally
        {
            if ( rs != null ) { rs.close(); }
            if ( ps != null ) { ps.close(); }
        }
    }

    /**
     * <p>
     * Strip the trailing subversion stamp from a Derby version number.
     * </p>
     */
    private String trimDriverVersion( String driverVersion)
    {
        int idx = driverVersion.indexOf( ' ' );
        if ( idx > 0 ) { return driverVersion.substring( 0, idx ); }
        else { return driverVersion; }
    }

    private void saveOriginalClassLoader()
    {
        // remember the original class loader so that we can reset
        if ( _originalClassLoader.get() == null ) { 
            _originalClassLoader.set( ClassLoaderTestSetup.getThreadLoader() ); 
        }
    }
    private void restoreOriginalClassLoader()
    {
        ClassLoaderTestSetup.setThreadLoader(
                (ClassLoader) _originalClassLoader.get() );
    }

    private String stringifyUpgradeRequests()
    {
        StringBuffer buffer = new StringBuffer();
        int          count = _hardUpgradeRequests.length;

        buffer.append( "( " );

        for ( int i = 0; i < count; i++ )
        {
            if ( i > 0 ) { buffer.append( ", " ); }
            if ( _hardUpgradeRequests[ i ] ) { buffer.append( "hard" ); }
            else { buffer.append( "soft" ); }
        }

        buffer.append( " )" );

        return buffer.toString();
    }

    /**
     * Run good DDL.
     */
    protected void    goodStatement( Connection conn, String ddl ) throws SQLException
    {
        PreparedStatement    ps = chattyPrepare( conn, ddl );

        ps.execute();
        ps.close();
    }
    
    /**
     * Prepare a statement and report its sql text.
     */
    protected PreparedStatement   chattyPrepare( Connection conn, String text )
        throws SQLException
    {
        if ( LOQUACIOUS) { println( "Preparing statement:\n\t" + text ); }
        
        return conn.prepareStatement( text );
    }

    /**
     * Stringify a result set.
     */
	private	String	printResultSet( ResultSet rs )
		throws SQLException
	{
		if ( rs == null )
		{
			return "Null ResultSet!";
		}

		ResultSetMetaData	rsmd = rs.getMetaData();
		int					count = rsmd.getColumnCount();
		StringBuffer		buffer = new StringBuffer();

		while ( rs.next() )
		{
			for ( int i = 1; i <= count; i++ )
			{
                if ( i > 1 ) { buffer.append( " | " ); }
				buffer.append( rs.getString( i ) );
			}
			buffer.append( "\n" );
		}

		return buffer.toString();
	}

    /**
     * <p>
     * Replace values which are known to be unstable.
     * </p>
     */
    private void normalizeRow( String tableName, String[] columnNames, String[] row )
    {
        int count = row.length;

        for ( int i = 0; i < count; i++ )
        {
            String value = row[ i ];

            if ( isColumn( SYSCONGLOMERATES, CONGLOMERATENUMBER, tableName, columnNames[ i ] ) ) { value = DUMMY_NUMBER; }
            else if ( isColumn( SYSALIASES, ALIASID, tableName, columnNames[ i ] ) )
            {
                String original = value;
                value = getColumnValue( ALIAS, columnNames, row );
                _unstableColumnValues.put( original, value );
            }
            else if ( isColumn( SYSALIASES, SPECIFICNAME, tableName, columnNames[ i ] ) )
            {
                String original = value;
                value = getColumnValue( ALIAS, columnNames, row );
                _unstableColumnValues.put( original, value );
            }
            else if ( isColumn( SYSSTATEMENTS, STMTID, tableName, columnNames[ i ] ) )
            {
                String original = value;
                value = getColumnValue( STMTNAME, columnNames, row );
                _unstableColumnValues.put( original, value );
            }
            else if ( isColumn( SYSSTATEMENTS, LASTCOMPILED, tableName, columnNames[ i ] ) )
            {
                value = DUMMY_TIMESTAMP;
            }
            else if ( isColumn( SYSSTATEMENTS, TEXT, tableName, columnNames[ i ] ) && suffersDERBY_4216( tableName ) )
            {
                value = DUMMY_STRING;
            }
            else if ( isColumn( SYSROUTINEPERMS, ALIASID, tableName, columnNames[ i ] ) )
            {
                // replace with corresponding value that was substituted into SYSALIASES
                value = (String) _unstableColumnValues.get( value );
            }
            else if ( isColumn( SYSROUTINEPERMS, ROUTINEPERMSID, tableName, columnNames[ i ] ) )
            {
                value = DUMMY_STRING;
            }
            
            row[ i ] = value;
        }
    }

    /**
     * <p>
     * Return true if we are dealing with the indicated column.
     * </p>
     */
    private boolean isColumn( String expectedTableName, String expectedColumnName, String actualTableName, String actualColumnName )
    {
        return ( expectedTableName.equals( actualTableName ) && expectedColumnName.equals( actualColumnName ) );
    }
    
    /**
     * <p>
     * Return the value for the indicated columns.
     * </p>
     */
    private String getColumnValue( String columnName, String[] columnNames, String[] row )
    {
        int  count = columnNames.length;
        for ( int i = 0; i < count; i++ )
        {
            if ( columnName.equals( columnNames[ i ] ) ) { return row[ i ]; }
        }

        return null;
    }
    
    /**
     * <p>
     * Return true if the string looks like a UUID.
     * </p>
     */
    private boolean isUUID( String raw )
    {
        if (
            ( raw != null ) &&
            ( raw.length() == 36 ) &&
            ( raw.charAt( 8 ) == '-' ) &&
            ( raw.charAt( 13 ) == '-' ) &&
            ( raw.charAt( 18 ) == '-' )
            )
        { return true; }
        else { return false; }
    }

    /**
     * <p>
     * Debug method to print out a result set.
     * </p>
     */
    private void printQueryResults( Connection conn, String query )
        throws Exception
    {
        PreparedStatement ps = chattyPrepare( conn, query );
        ResultSet               rs = ps.executeQuery();

        if ( LOQUACIOUS) { println( printResultSet( rs ) ); }

        rs.close();
        ps.close();
    }

    /**
     * <p>
     * Debug method to compare the results of a query on two databases.
     * </p>
     */
    private void compareQueries( Connection leftConn, Connection rightConn, int colCount, String query )
        throws Exception
    {
        PreparedStatement leftPS = chattyPrepare( leftConn, query );
        PreparedStatement rightPS = chattyPrepare( rightConn, query );
        ResultSet               leftRS = leftPS.executeQuery();
        ResultSet               rightRS = rightPS.executeQuery();
        while( leftRS.next() )
        {
            rightRS.next();

            for ( int i = 1; i <= colCount; i++ )
            {
                String leftValue = leftRS.getString( i );
                String rightValue = rightRS.getString( i );
                boolean unequal = false;

                if ( leftValue == null )
                {
                    if ( rightValue != null ) { unequal = true; }
                }
                else if ( !leftValue.equals( rightValue ) ) { unequal = true; }

                if ( unequal )
                {
                    if ( LOQUACIOUS) { println( "Column values different for column " + i + ". Left = " + leftValue + ", right = " + rightValue ); }
                }
            }
        }

        leftRS.close();
        rightRS.close();
        leftPS.close();
        rightPS.close();
    }
    
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // POWER SET BUILDING MINIONS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * <p>
     * Builds the minimal set of trajectories for the supported versions. This
     * is all trajectories which start at some release, then hard upgrade to
     * every subsequent release up to the very last release. For a set of N
     * releases, there are N-1 non-vacuous trajectories of this shape.
     * </p>
     */
    private static Version.Trajectory[] buildMinimalSet( Version[] supportedVersions )
    {
        ArrayList<Version.Trajectory>   trajectoryList = new ArrayList<Version.Trajectory>();
        int  versionCount = supportedVersions.length;
        boolean[]  include = new boolean[ versionCount ];

        for ( int i = 0; i < versionCount; i++ ) { include[ i ] = true; }
        addSubset( supportedVersions, trajectoryList, include, false ); 

        for ( int i = 0; i < versionCount - 1; i++ )
        {
            include[ i ] = false;
            addSubset( supportedVersions, trajectoryList, include, false ); 
        }

        return squeezeArray( trajectoryList );
    }
    
    /**
     * <p>
     * Build the power set of all supported versions.
     * </p>
     */
    private static Version.Trajectory[] buildPowerSet( Version[] supportedVersions )
    {
        ArrayList<Version.Trajectory>   trajectoryList = new ArrayList<Version.Trajectory>();
        int            versionCount = supportedVersions.length;
        boolean[]  include = new boolean[ versionCount ];

        buildPowerSetMinion( supportedVersions, trajectoryList, include, 0, TRJ_SAME_BRANCH_NEIGHBORS );

        return squeezeArray( trajectoryList );
    }
    
    /**
     * <p>
     * Turn a list of trajectories into an array.
     * </p>
     */
    private static Version.Trajectory[] squeezeArray( ArrayList<Version.Trajectory> trajectoryList )
    {
        Version.Trajectory[] result = new Version.Trajectory[ trajectoryList.size() ];
        trajectoryList.toArray( result );

        return result;
    }
    
    /**
     * <p>
     * Recursive workhorse to build the power set of supported versions. If
     * requested, we also prune out all trajectories which have adjacent
     * versions from the same branch. If we're hard-upgrading, between releases,
     * then these SHOULD be uninteresting combinations.
     * </p>
     */
    private static void buildPowerSetMinion
        ( Version[] supportedVersions, ArrayList<Version.Trajectory> result, boolean[] include, int idx, boolean removeSameBranchNeighbors )
    {
        int  versionCount = supportedVersions.length;

        if ( idx >= versionCount ) { addSubset( supportedVersions, result, include, removeSameBranchNeighbors ); }
        else
        {
            include[ idx ] = true;
            buildPowerSetMinion( supportedVersions, result, include, idx + 1, removeSameBranchNeighbors );
            
            include[ idx ] = false;
            buildPowerSetMinion( supportedVersions, result, include, idx + 1, removeSameBranchNeighbors );
        }
    }
    
    /**
     * <p>
     * Add a subset to the evolving list of subsets of supported versions. Throw
     * out the empty set and singletons. We sort each trajectory so that its
     * versions are in ascending order.
     * </p>
     */
    private static void addSubset
        ( Version[] supportedVersions, ArrayList<Version.Trajectory> result, boolean[] include, boolean removeSameBranchNeighbors )
    {
        int  versionCount = supportedVersions.length;

        ArrayList<Version> seed = new ArrayList<Version>();
        Version   previousVersion = null;
        
        for ( int i = 0; i < versionCount; i++ )
        {
            Version  thisVersion = supportedVersions[ i  ];

            if ( include[ i ] )
            {
                //
                // If adjacent version are from the same branch, remove them if
                // requested to.
                //
                if ( removeSameBranchNeighbors && ( previousVersion != null ) )
                {
                    if ( previousVersion.getBranchID().equals( thisVersion.getBranchID() ) )
                    {
                        continue;
                    }
                }
                
                previousVersion = thisVersion;                
                seed.add( thisVersion );
            }
        }
        int  seedSize = seed.size();
        
        if ( seedSize > 1 )
        {
            Version[] subset = new Version[ seedSize ];
            seed.toArray( subset );
            
            result.add( (new Version.Trajectory( subset )).sort() );
        }
    }

    /**
     * <p>
     * Return empty string if passed in result is a known problem.
     * </p>
     */
    private String filterKnownProblems( String tableName, String actual )
    {
        if (
            _trajectory.endsAt( BRANCH_10_5 ) &&
            ( contains( actual, DERBY_4214_1 ) || contains( actual, DERBY_4214_2 ) )
           )
        { return ""; }

        if (
            suffersDERBY_4215( tableName ) &&
            ( contains( actual, DERBY_4215 ) )
           )
        { return ""; }

        return actual;
    }

    /**
     * <p>
     * Return true if the conditions of DERBY-4215 exist.
     * </p>
     */
    private boolean suffersDERBY_4215( String tableName )
    {
        return
                 (
                     SYSROUTINEPERMS.equals( tableName ) &&
                     _trajectory.startsAt( BRANCH_10_0 ) &&
                     ( !_trajectory.contains( BRANCH_10_1 ) ) &&
                     (
                      _trajectory.contains( BRANCH_10_2 ) ||
                      _trajectory.contains( BRANCH_10_3 ) ||
                      _trajectory.contains( BRANCH_10_4 )
                      ) &&
                     ( _trajectory.getEndingVersion().compareTo( VERSION_10_6_0_0 )  < 0 )
                 );
    }
    
    /**
     * <p>
     * Return true if the conditions of DERBY-4216 exist.
     * </p>
     */
    private boolean suffersDERBY_4216( String tableName )
    {
        return
                 (
                     SYSSTATEMENTS.equals( tableName ) &&
                     _trajectory.startsAt( BRANCH_10_0 ) &&
                     _trajectory.endsAt( BRANCH_10_1 )
                 );
    }
    
    private boolean contains( String left, String right )
    {
        return ( left.indexOf( right ) >= 0 );
    }
    
    
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // INNER CLASSES
    //
    ///////////////////////////////////////////////////////////////////////////////////


}


