/*
 *
 * Derby - Class org.apache.derbyTesting.junit.JDBC
 *
 * 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.junit;

import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;

import junit.framework.Assert;
import junit.framework.AssertionFailedError;

/**
 * JDBC utility methods for the JUnit tests.
 * Note that JSR 169 is a subset of JDBC 3 and
 * JDBC 3 is a subset of JDBC 4.
 * The base level for the Derby tests is JSR 169.
 */
public class JDBC {
    
    /**
     * Helper class whose <code>equals()</code> method returns
     * <code>true</code> for all strings on this format: SQL061021105830900
     */
    public static class GeneratedId {
        public boolean equals(Object o) {
            String tmpstr = (String)o;
            boolean b = true;
            if (!(o instanceof String))
                b = false;
            if (!(tmpstr.startsWith("SQL")))
                b = false;
            if (tmpstr.length() != 18)
                b = false;
            for (int i=3 ; i<18 ; i++)
            {
                if (Character.isDigit(tmpstr.charAt(i)))
                    continue;
                else
                {
                    b = false;
                    break;
                }
            }
            return b;
        }
        public String toString() {
            return "xxxxGENERATED-IDxxxx";
        }
    }

    /**
     * Constant to pass to DatabaseMetaData.getTables() to fetch
     * just tables.
     */
    public static final String[] GET_TABLES_TABLE = new String[] {"TABLE"};
    /**
     * Constant to pass to DatabaseMetaData.getTables() to fetch
     * just views.
     */
    public static final String[] GET_TABLES_VIEW = new String[] {"VIEW"};
    /**
     * Constant to pass to DatabaseMetaData.getTables() to fetch
     * just synonyms.
     */
    public static final String[] GET_TABLES_SYNONYM =
        new String[] {"SYNONYM"};
    
    /**
     * Types.SQLXML value without having to compile with JDBC4.
     */
    public static final int SQLXML = 2009;
	
    /**
     * Tell if we are allowed to use DriverManager to create database
     * connections.
     */
    private static final boolean HAVE_DRIVER
                           = haveClass("java.sql.Driver");
    
    /**
     * Does the Savepoint class exist, indicates
     * JDBC 3 (or JSR 169). 
     */
    private static final boolean HAVE_SAVEPOINT
                           = haveClass("java.sql.Savepoint");

    /**
     * Does the java.sql.SQLXML class exist, indicates JDBC 4. 
     */
    private static final boolean HAVE_SQLXML
                           = haveClass("java.sql.SQLXML");

    /** Is the Lucene core jar file on the classpath */
    public  static  final   boolean HAVE_LUCENE_CORE =
        haveClass( "org.apache.lucene.analysis.Analyzer" );

    /** Is the Lucene analyzer jar file on the classpath */
    public  static  final   boolean HAVE_LUCENE_ANALYZERS =
        haveClass( "org.apache.lucene.analysis.en.EnglishAnalyzer" );

    /** Is the Lucene queryparser jar file on the classpath */
    public  static  final   boolean HAVE_LUCENE_QUERYPARSER =
        haveClass( "org.apache.lucene.queryparser.surround.parser.QueryParser" );

    /** Is the json-simple core jar file on the classpath */
    public  static  final   boolean HAVE_JSON_SIMPLE =
        haveClass( "org.json.simple.JSONArray" );

    /**
     * Does java.sql.ResultSet implement java.lang.AutoCloseable?
     * Indicates JDBC 4.1.
     */
    private static final boolean HAVE_AUTO_CLOSEABLE_RESULT_SET;
    static {
        boolean autoCloseable;
        try {
            Class<?> acClass = Class.forName("java.lang.AutoCloseable");
            autoCloseable = acClass.isAssignableFrom(ResultSet.class);
        } catch (Throwable t) {
            autoCloseable = false;
        }
        HAVE_AUTO_CLOSEABLE_RESULT_SET = autoCloseable;
    }

    private static final boolean HAVE_REFERENCEABLE;
    static {
        boolean ok = false;
        try {
            Class.forName("javax.naming.Referenceable");
            ok = true;
        } catch (Throwable t) {
        }
        HAVE_REFERENCEABLE = ok;
    }

    private static final boolean HAVE_SQLTYPE;
    static {
        boolean ok = false;
        try {
            Class.forName("java.sql.SQLType");
            ok = true;
        } catch (Throwable t) {
        }
        HAVE_SQLTYPE = ok;
    }
    
    /**
     * Is javax.management.MBeanServer available? Indicates whether the
     * JVM supports the Java Management Extensions (JMX).
     */
    private static final boolean HAVE_MBEAN_SERVER =
            haveClass("javax.management.MBeanServer");

    /**
     * Can we load a specific class, use this to determine JDBC level.
     * @param className Class to attempt load on.
     * @return true if class can be loaded, false otherwise.
     */
    static boolean haveClass(String className)
    {
        try {
            Class.forName(className);
            return true;
        } catch (Throwable e) {
        	return false;
        }    	
    }

    /**
     * Return true if the virtual machine environment supports JDBC 4.2 or
     * later.
     */
    public static boolean vmSupportsJDBC42() {
        return vmSupportsJDBC41() && HAVE_SQLTYPE;
    }

    /**
     * Return true if the virtual machine environment supports JDBC 4.1 or
     * later. JDBC 4.1 is a superset of JDBC 4.0 and of JSR-169.
     */
    public static boolean vmSupportsJDBC41() {
        return vmSupportsJDBC4() && HAVE_AUTO_CLOSEABLE_RESULT_SET;
    }

 	/**
	 * Return true if the virtual machine environment
	 * supports JDBC4 or later. JDBC 4 is a superset
     * of JDBC 3 and of JSR169.
     * <BR>
     * This method returns true in a JDBC 4 environment
     * and false in a JDBC 3 or JSR 169 environment.
	 */
	public static boolean vmSupportsJDBC4()
	{
		return HAVE_DRIVER
	       && HAVE_SQLXML;
	}
 	/**
	 * Return true if the virtual machine environment
	 * supports JDBC3 or later. JDBC 3 is a super-set of JSR169
     * and a subset of JDBC 4.
     * <BR>
     * This method will return true in a JDBC 3 or JDBC 4
     * environment, but false in a JSR169 environment.
	 */
	public static boolean vmSupportsJDBC3()
	{
		return HAVE_DRIVER
		       && HAVE_SAVEPOINT;
	}

	/**
	 * Return true if the virtual machine environment
	 * supports JSR169. JSR169 is a subset of JDBC 3
     * and hence a subset of JDBC 4 as well.
     * <BR>
     * This method returns true only in a JSR 169
     * environment.
	 */
	public static boolean vmSupportsJSR169()
	{
		return !HAVE_DRIVER
		       && HAVE_SAVEPOINT;
	}	
	
    /**
     * @return {@code true} if JNDI is available.
     */
    public static boolean vmSupportsJNDI() {
        return HAVE_REFERENCEABLE;
    }

    /**
     * Return true if the JVM supports the Java Management Extensions (JMX).
     */
    public static boolean vmSupportsJMX() {
        return HAVE_MBEAN_SERVER;
    }

	/**
	 * Rollback and close a connection for cleanup.
	 * Test code that is expecting Connection.close to succeed
	 * normally should just call conn.close().
	 * 
	 * <P>
	 * If conn is not-null and isClosed() returns false
	 * then both rollback and close will be called.
	 * If both methods throw exceptions
	 * then they will be chained together and thrown.
	 * @throws SQLException Error closing connection.
	 */
	public static void cleanup(Connection conn) throws SQLException
	{
		if (conn == null)
			return;
		if (conn.isClosed())
			return;
		
		SQLException sqle = null;
		try {
			conn.rollback();
		} catch (SQLException e) {
			sqle = e;
		}
		
		try {
			conn.close();
		} catch (SQLException e) {
			if (sqle == null)
			    sqle = e;
			else
				sqle.setNextException(e);
			throw sqle;
		}
	}
	
	/**
	 * Drop a database schema by dropping all objects in it
	 * and then executing DROP SCHEMA. If the schema is
	 * APP it is cleaned but DROP SCHEMA is not executed.
	 * 
	 * TODO: Handle dependencies by looping in some intelligent
	 * way until everything can be dropped.
	 * 

	 * 
	 * @param dmd DatabaseMetaData object for database
	 * @param schema Name of the schema
	 * @throws SQLException database error
	 */
	public static void dropSchema(DatabaseMetaData dmd, String schema) throws SQLException
	{		
		Connection conn = dmd.getConnection();
		Assert.assertFalse(conn.getAutoCommit());
		Statement s = dmd.getConnection().createStatement();

        // Triggers
        PreparedStatement pstr = conn.prepareStatement(
                "SELECT TRIGGERNAME FROM SYS.SYSSCHEMAS S, SYS.SYSTRIGGERS T "
                + "WHERE S.SCHEMAID = T.SCHEMAID AND SCHEMANAME = ?");
        pstr.setString(1, schema);
        ResultSet trrs = pstr.executeQuery();
        while (trrs.next()) {
            String trigger = trrs.getString(1);
            s.execute("DROP TRIGGER " + JDBC.escape(schema, trigger));
        }
        trrs.close();
        pstr.close();

        // Functions - not supported by JDBC meta data until JDBC 4
        // Need to use the CHAR() function on A.ALIASTYPE
        // so that the compare will work in any schema.
        PreparedStatement psf = conn.prepareStatement(
                "SELECT ALIAS FROM SYS.SYSALIASES A, SYS.SYSSCHEMAS S" +
                " WHERE A.SCHEMAID = S.SCHEMAID " +
                " AND CHAR(A.ALIASTYPE) = ? " +
                " AND S.SCHEMANAME = ?");
        psf.setString(1, "F" );
        psf.setString(2, schema);
        ResultSet rs = psf.executeQuery();
        dropUsingDMD(s, rs, schema, "ALIAS", "FUNCTION");        

		// Procedures
		rs = dmd.getProcedures((String) null,
				schema, (String) null);
		
		dropUsingDMD(s, rs, schema, "PROCEDURE_NAME", "PROCEDURE");
		
		// Views
		rs = dmd.getTables((String) null, schema, (String) null,
                GET_TABLES_VIEW);
		
		dropUsingDMD(s, rs, schema, "TABLE_NAME", "VIEW");
		
		// Tables
		rs = dmd.getTables((String) null, schema, (String) null,
                GET_TABLES_TABLE);
		
		dropUsingDMD(s, rs, schema, "TABLE_NAME", "TABLE");
        
        // At this point there may be tables left due to
        // foreign key constraints leading to a dependency loop.
        // Drop any constraints that remain and then drop the tables.
        // If there are no tables then this should be a quick no-op.
        ResultSet table_rs = dmd.getTables((String) null, schema, (String) null,
                GET_TABLES_TABLE);

        while (table_rs.next()) {
            String tablename = table_rs.getString("TABLE_NAME");
            rs = dmd.getExportedKeys((String) null, schema, tablename);
            while (rs.next()) {
                short keyPosition = rs.getShort("KEY_SEQ");
                if (keyPosition != 1)
                    continue;
                String fkName = rs.getString("FK_NAME");
                // No name, probably can't happen but couldn't drop it anyway.
                if (fkName == null)
                    continue;
                String fkSchema = rs.getString("FKTABLE_SCHEM");
                String fkTable = rs.getString("FKTABLE_NAME");

                String ddl = "ALTER TABLE " +
                    JDBC.escape(fkSchema, fkTable) +
                    " DROP FOREIGN KEY " +
                    JDBC.escape(fkName);
                s.executeUpdate(ddl);
            }
            rs.close();
        }
        table_rs.close();
        conn.commit();
                
        // Tables (again)
        rs = dmd.getTables((String) null, schema, (String) null,
                GET_TABLES_TABLE);        
        dropUsingDMD(s, rs, schema, "TABLE_NAME", "TABLE");

        // drop UDTs
        psf.setString(1, "A" );
        psf.setString(2, schema);
        rs = psf.executeQuery();
        dropUsingDMD(s, rs, schema, "ALIAS", "TYPE");        
  
        // drop aggregates
        psf.setString(1, "G" );
        psf.setString(2, schema);
        rs = psf.executeQuery();
        dropUsingDMD(s, rs, schema, "ALIAS", "DERBY AGGREGATE");        
        psf.close();
  
        // Synonyms - need work around for DERBY-1790 where
        // passing a table type of SYNONYM fails.
        rs = dmd.getTables((String) null, schema, (String) null,
                GET_TABLES_SYNONYM);
        
        dropUsingDMD(s, rs, schema, "TABLE_NAME", "SYNONYM");
                
        // sequences
        if ( sysSequencesExists( conn ) )
        {
            psf = conn.prepareStatement
                (
                 "SELECT SEQUENCENAME FROM SYS.SYSSEQUENCES A, SYS.SYSSCHEMAS S" +
                 " WHERE A.SCHEMAID = S.SCHEMAID " +
                 " AND S.SCHEMANAME = ?");
            psf.setString(1, schema);
            rs = psf.executeQuery();
            dropUsingDMD(s, rs, schema, "SEQUENCENAME", "SEQUENCE");
            psf.close();
        }

		// Finally drop the schema if it is not APP
		if (!schema.equals("APP")) {
			s.executeUpdate("DROP SCHEMA " + JDBC.escape(schema) + " RESTRICT");
		}
		conn.commit();
		s.close();
	}

    /**
     * Return true if the SYSSEQUENCES table exists.
     */
    private static boolean sysSequencesExists( Connection conn ) throws SQLException
    {
        PreparedStatement ps = null;
        ResultSet rs =  null;
        try {
            ps = conn.prepareStatement
                (
                 "select count(*) from sys.systables t, sys.sysschemas s\n" +
                 "where t.schemaid = s.schemaid\n" +
                 "and ( cast(s.schemaname as varchar(128)))= 'SYS'\n" +
                 "and ( cast(t.tablename as varchar(128))) = 'SYSSEQUENCES'" );
            rs = ps.executeQuery();
            rs.next();
            return ( rs.getInt( 1 ) > 0 );
        }
        finally
        {
            if ( rs != null ) { rs.close(); }
            if ( ps != null ) { ps.close(); }
        }
    }
	
	/**
	 * DROP a set of objects based upon a ResultSet from a
	 * DatabaseMetaData call.
	 * 
	 * TODO: Handle errors to ensure all objects are dropped,
	 * probably requires interaction with its caller.
	 * 
	 * @param s Statement object used to execute the DROP commands.
	 * @param rs DatabaseMetaData ResultSet
	 * @param schema Schema the objects are contained in
	 * @param mdColumn The column name used to extract the object's
	 * name from rs
	 * @param dropType The keyword to use after DROP in the SQL statement
	 * @throws SQLException database errors.
	 */
	private static void dropUsingDMD(
			Statement s, ResultSet rs, String schema,
			String mdColumn,
			String dropType) throws SQLException
	{
		String dropLeadIn = "DROP " + dropType + " ";
		
        // First collect the set of DROP SQL statements.
        ArrayList<String> ddl = new ArrayList<String>();
		while (rs.next())
		{
            String objectName = rs.getString(mdColumn);
            String raw = dropLeadIn + JDBC.escape(schema, objectName);
            if (
                "TYPE".equals( dropType )  ||
                "SEQUENCE".equals( dropType ) ||
                "DERBY AGGREGATE".equals( dropType )
                )
            { raw = raw + " restrict "; }
            ddl.add( raw );
		}
		rs.close();
        if (ddl.isEmpty())
            return;
                
        // Execute them as a complete batch, hoping they will all succeed.
        s.clearBatch();
        int batchCount = 0;
        for (Iterator i = ddl.iterator(); i.hasNext(); )
        {
            Object sql = i.next();
            if (sql != null) {
                s.addBatch(sql.toString());
                batchCount++;
            }
        }

		int[] results;
        boolean hadError;
		try {
		    results = s.executeBatch();
		    Assert.assertNotNull(results);
		    Assert.assertEquals("Incorrect result length from executeBatch",
		    		batchCount, results.length);
            hadError = false;
		} catch (BatchUpdateException batchException) {
			results = batchException.getUpdateCounts();
			Assert.assertNotNull(results);
			Assert.assertTrue("Too many results in BatchUpdateException",
					results.length <= batchCount);
            hadError = true;
		}
		
        // Remove any statements from the list that succeeded.
		boolean didDrop = false;
		for (int i = 0; i < results.length; i++)
		{
			int result = results[i];
			if (result == Statement.EXECUTE_FAILED)
				hadError = true;
			else if (result == Statement.SUCCESS_NO_INFO || result >= 0) {
				didDrop = true;
				ddl.set(i, null);
			}
			else
				Assert.fail("Negative executeBatch status");
		}
        s.clearBatch();
        if (didDrop) {
            // Commit any work we did do.
            s.getConnection().commit();
        }

        // If we had failures drop them as individual statements
        // until there are none left or none succeed. We need to
        // do this because the batch processing stops at the first
        // error. This copes with the simple case where there
        // are objects of the same type that depend on each other
        // and a different drop order will allow all or most
        // to be dropped.
        if (hadError) {
            do {
                hadError = false;
                didDrop = false;
                for (ListIterator<String> i = ddl.listIterator(); i.hasNext();) {
                    String sql = i.next();
                    if (sql != null) {
                        try {
                            s.executeUpdate(sql);
                            i.set(null);
                            didDrop = true;
                        } catch (SQLException e) {
                            hadError = true;
                        }
                    }
                }
                if (didDrop)
                    s.getConnection().commit();
            } while (hadError && didDrop);
        }
	}
	
	/**
	 * Assert all columns in the ResultSetMetaData match the
	 * table's defintion through DatabaseMetadDta. Only works
	 * if the complete select list correspond to columns from
	 * base tables.
	 * <BR>
	 * Does not require that the complete set of any table's columns are
	 * returned.
	 * @throws SQLException 
	 * 
	 */
	public static void assertMetaDataMatch(DatabaseMetaData dmd,
			ResultSetMetaData rsmd) throws SQLException
	{
		for (int col = 1; col <= rsmd.getColumnCount(); col++)
		{
			// Only expect a single column back
		    ResultSet column = dmd.getColumns(
		    		rsmd.getCatalogName(col),
		    		rsmd.getSchemaName(col),
		    		rsmd.getTableName(col),
		    		rsmd.getColumnName(col));
		    
		    Assert.assertTrue("Column missing " + rsmd.getColumnName(col),
		    		column.next());
		    
		    Assert.assertEquals(column.getInt("DATA_TYPE"),
		    		rsmd.getColumnType(col));
		    
		    Assert.assertEquals(column.getInt("NULLABLE"),
		    		rsmd.isNullable(col));
		    
		    Assert.assertEquals(column.getString("TYPE_NAME"),
		    		rsmd.getColumnTypeName(col));
		    
		    column.close();
		}
	}
    
    /**
     * Assert a result set is empty.
     * If the result set is not empty it will
     * be drained before the check to see if
     * it is empty.
     * The ResultSet is closed by this method.

     */
    public static void assertEmpty(ResultSet rs)
    throws SQLException
    {
        assertDrainResults(rs, 0);
    }
	
    /**
     * 
     * @param rs
     */
    public static void assertClosed(ResultSet rs)
    {
        try { 
            rs.next();
            Assert.fail("ResultSet not closed");
        }catch (SQLException sqle){
            Assert.assertEquals("XCL16", sqle.getSQLState());
        }
        
        
    }
    
    /**
     * Assert that no warnings were returned from a JDBC getWarnings()
     * method such as Connection.getWarnings. Reports the contents
     * of the warning if it is not null.
     * @param warning Warning that should be null.
     */
    public static void assertNoWarnings(SQLWarning warning)
    {
        if (warning == null)
            return;
        
        Assert.fail("Expected no SQLWarnings - got: " + warning.getSQLState() 
                + " " + warning.getMessage());
    }
    
    /**
     * Assert that the statement has no more results(getMoreResults) and it 
     * indeed does not return any resultsets(by checking getResultSet). 
     * Also, ensure that update count is -1.
     * @param s Statement holding no results.
     * @throws SQLException Exception checking results.
     */
    public static void assertNoMoreResults(Statement s) throws SQLException
    {
    	Assert.assertFalse(s.getMoreResults());
        Assert.assertTrue(s.getUpdateCount() == -1);
        Assert.assertNull(s.getResultSet());
    }
    
    /**
     * Assert that a ResultSet representing generated keys is non-null
     * and of the correct type. This method leaves the ResultSet
     * open and does not fetch any date from it.
     * 
     * @param description For assert messages
     * @param keys ResultSet returned from getGeneratedKeys().
     * @throws SQLException
     */
    public static void assertGeneratedKeyResultSet(
            String description, ResultSet keys) throws SQLException
    {
        
        Assert.assertNotNull(description, keys);
        
        // Requirements from section 13.6 JDBC 4 specification
        Assert.assertEquals(
                description + 
                " - Required CONCUR_READ_ONLY for generated key result sets",
                ResultSet.CONCUR_READ_ONLY, keys.getConcurrency());
        
        int type = keys.getType();
        if ( (type != ResultSet.TYPE_FORWARD_ONLY) &&
             (type != ResultSet.TYPE_SCROLL_INSENSITIVE))
        {
            Assert.fail(description +
                    " - Invalid type for generated key result set" + type);
        }
        
        

    }
    
    /**
     * Drain a ResultSet and assert it has at least one row.
     *
     * The ResultSet is closed by this method.
     */
    public static void assertDrainResultsHasData(ResultSet rs)
    throws SQLException
    {
        int rowCount = assertDrainResults(rs, -1);
        Assert.assertTrue("ResultSet expected to have data", rowCount > 0);
    }    
    
	/**
	 * Drain a single ResultSet by reading all of its
	 * rows and columns. Each column is accessed using
	 * getString() and asserted that the returned value
	 * matches the state of ResultSet.wasNull().
	 *
	 * Provides simple testing of the ResultSet when the
	 * contents are not important.
     * 
     * The ResultSet is closed by this method.
	 *
	 * @param rs Result set to drain.
     * @return the number of rows seen.

	 * @throws SQLException
	 */
	public static int assertDrainResults(ResultSet rs)
	    throws SQLException
	{
		return assertDrainResults(rs, -1);
	}

	/**
	 * Does the work of assertDrainResults() as described
	 * above.  If the received row count is non-negative,
	 * this method also asserts that the number of rows
	 * in the result set matches the received row count.
     * 
     * The ResultSet is closed by this method.
	 *
	 * @param rs Result set to drain.
	 * @param expectedRows If non-negative, indicates how
	 *  many rows we expected to see in the result set.
     *  @return the number of rows seen.
	 * @throws SQLException
	 */
	public static int assertDrainResults(ResultSet rs,
	    int expectedRows) throws SQLException
	{
		ResultSetMetaData rsmd = rs.getMetaData();
        List<List<String>> seen = new ArrayList<List<String>>();
        List<String> seenRow = new ArrayList<String>();
		
		int rows = 0;
		while (rs.next()) {
			for (int col = 1; col <= rsmd.getColumnCount(); col++)
			{
				String s = rs.getString(col);
                seenRow.add(s);
				Assert.assertEquals(s == null, rs.wasNull());
                if (rs.wasNull())
                    assertResultColumnNullable(rs, seen, seenRow, col);
			}
			rows++;
            seen.add(new ArrayList<String>(seenRow));
            seenRow.clear();
		}
		rs.close();

        if (expectedRows >= 0) {
            try {
                Assert.assertEquals("Unexpected row count:", expectedRows, rows);
            } catch (AssertionFailedError e) {
                throw addRsToReport(e, rsmd, seen, seenRow, rs);
            }
        }
        
        return rows;
	}
    
    /**
     * Assert that a column is nullable in its ResultSetMetaData.
     * Used when a utility method checking the contents of a
     * ResultSet sees a NULL value. If the value is NULL then
     * the column's definition in ResultSetMetaData must allow NULLs
     * (or not disallow NULLS).
     * @param rs the resultSet
     * @param seen The set of entirely read rows so far
     * @param seenRow The set of read columns in the current row so far
     * @param col Position of column just fetched that was NULL.
     * @throws SQLException Error accessing meta data
     */
    private static void assertResultColumnNullable(
            ResultSet rs,
            List<List<String>> seen,
            List<String> seenRow,
            int col)
    throws SQLException
    {
        final ResultSetMetaData rsmd = rs.getMetaData();

        try {
            Assert.assertFalse(rsmd.isNullable(col) == ResultSetMetaData.columnNoNulls);
        } catch (AssertionFailedError e) {
            throw addRsToReport(e, rsmd, seen, seenRow, rs);
        }
    }
	
    /**
     * Takes a result set and an array of expected colum names (as
     * Strings)  and asserts that the column names in the result
     * set metadata match the number, order, and names of those
     * in the array.
     *
     * @param rs ResultSet for which we're checking column names.
     * @param expectedColNames Array of expected column names.
     */
    public static void assertColumnNames(ResultSet rs,
        String... expectedColNames) throws SQLException
    {
        ResultSetMetaData rsmd = rs.getMetaData();
        int actualCols = rsmd.getColumnCount();

        Assert.assertEquals("Unexpected column count:",
            expectedColNames.length, rsmd.getColumnCount());

        for (int i = 0; i < actualCols; i++)
        {
            Assert.assertEquals("Column names do not match:",
                expectedColNames[i], rsmd.getColumnName(i+1));
        }
    }
    /**
     * Takes a result set and an array of expected colum names (as
     * Strings)  and asserts that the column names in the result
     * set metadata match the number, order, and names of those
     * in the array.
     * Does the same for column types
     *      *
     * @param rs ResultSet for which we're checking column names.
     * @param expectedTypes Array of expected types for the columns.
     * @param expectedColNames Array of expected column names.
     */
    public static void assertDatabaseMetaDataColumns(ResultSet rs,
            int[] expectedTypes, 
            String... expectedColNames) throws SQLException
    {
        assertDatabaseMetaDataColumns(rs, null, expectedTypes, expectedColNames);
    }    
    /**
     * Takes a result set and an array of expected colum names (as
     * Strings)  and asserts that the column names in the result
     * set metadata match the number, order, and names of those
     * in the array.
     * Does the same for column types and nullability.
     * 
     * This is a variation of JDBC.assertColumnNames, here we only compare
     * the expected columns and ignore additional ones, because columns 
     * returned from DatabaseMetaData can be added to with newer JDBC versions.
     * If the ResultSet can not change over time, JDBC.assertColumnNames 
     * should be used. See DERBY-6180.
     *
     * @param rs ResultSet for which we're checking column names.
     * @param expectedTypes Array of expected types for the columns.
     * @param nullability Array of expected nullability values for the columns.
     * @param expectedColNames Array of expected column names.
     */
    public static void assertDatabaseMetaDataColumns(ResultSet rs,
            boolean[] nullability, int[] expectedTypes, 
            String... expectedColNames) throws SQLException
    {
        ResultSetMetaData rsmd = rs.getMetaData();
        int actualCols = rsmd.getColumnCount();
        String actualColNames="[";
        for (int c = 0; c < rsmd.getColumnCount(); c++) {
            if (c==0)
                actualColNames=actualColNames + rsmd.getColumnName(c+1);
            else
                actualColNames=actualColNames + ", " + rsmd.getColumnName(c+1);
        }
        actualColNames=actualColNames + "]";
        
        if (nullability != null)
            Assert.assertEquals("Number of items in expected ColumnNames and " +
                "expected nullability arrays don't match; fix up the test", 
                expectedColNames.length, nullability.length);
        if (expectedTypes != null)
            Assert.assertEquals("Number of items in expected ColumnNames and " +
                "expected ColumnTypes arrays don't match; fix up the test", 
                expectedColNames.length, expectedTypes.length);
        
        if (expectedColNames.length == rsmd.getColumnCount()) {
            // the count is the same, expect the names to be the same too
            for (int i = 0; i < actualCols; i++)
            {
                Assert.assertEquals("Column names do not match:",
                        expectedColNames[i], rsmd.getColumnName(i+1));
                if (expectedTypes != null)
                    Assert.assertEquals("Column types do not match for column " 
                        + (i+1),
                        expectedTypes[i], rsmd.getColumnType(i+1));
                if (nullability != null) {
                    int expected = nullability[i] ?
                            ResultSetMetaData.columnNullable : 
                                ResultSetMetaData.columnNoNulls;
                    Assert.assertEquals(
                            "Column nullability do not match for column " +
                            (i+1), expected, rsmd.isNullable(i+1));
                }
            }
        }
        else
        {
            for (int i = 0; i < expectedColNames.length; i++)
            {
                String expectedColName = expectedColNames[i];
                boolean found=false;
                for (int j = 0 ; j < rsmd.getColumnCount(); j++)
                {
                    if (expectedColNames[i].equalsIgnoreCase(
                            rsmd.getColumnName(j+1)))
                    {
                        found = true;
                        if (expectedTypes != null)
                            Assert.assertEquals("Column Type does not match for column " +
                                expectedColNames[i] + "(" + (i) + ")",
                                expectedTypes[i], rsmd.getColumnType(j+1));
                        if (nullability != null) {
                            int expected = nullability[i] ?
                                ResultSetMetaData.columnNullable : 
                                    ResultSetMetaData.columnNoNulls;
                        Assert.assertEquals(
                            "Column nullability does not match for " +
                            "column " + (i), expected, rsmd.isNullable(j+1));
                        }
                        break;
                    }
                    else {
                        continue;
                    }
                }
                Assert.assertTrue("Missing an expected column: " + expectedColName + 
                        "\n Expected: " + Arrays.toString(expectedColNames) +
                        "\n Actual  : " + actualColNames,
                        found);
            }
        }
    }
    /**
     * Takes a result set and an array of expected column types
     * from java.sql.Types 
     * and asserts that the column types in the result
     * set metadata match the number, order, and names of those
     * in the array.
     * 
     * No length information for variable length types
     * can be passed. For ResultSets from JDBC DatabaseMetaData
     * the specification only indicates the types of the
     * columns, not the length.
     *
     * @param rs ResultSet for which we're checking column names.
     * @param expectedTypes Array of expected column types.
     */
    public static void assertColumnTypes(ResultSet rs,
        int[] expectedTypes) throws SQLException
    {
        ResultSetMetaData rsmd = rs.getMetaData();
        int actualCols = rsmd.getColumnCount();

        Assert.assertEquals("Unexpected column count:",
                expectedTypes.length, rsmd.getColumnCount());

        for (int i = 0; i < actualCols; i++)
        {
            Assert.assertEquals("Column types do not match for column " + (i+1),
                    expectedTypes[i], rsmd.getColumnType(i+1));
        }
    }
    /**
     * Takes a Prepared Statement and an array of expected parameter types
     * from java.sql.Types 
     * and asserts that the parameter types in the ParamterMetaData
     * match the number and order of those
     * in the array.
     * @param ps PreparedStatement for which we're checking parameter names.
     * @param expectedTypes Array of expected parameter types.
     */
    public static void assertParameterTypes (PreparedStatement ps,
	        int[] expectedTypes) throws Exception
	    {
            if ( vmSupportsJSR169() )
            {
                Assert.fail( "The assertParameterTypes() method only works on platforms which support ParameterMetaData." );
            }

            Object pmd = ps.getClass().getMethod( "getParameterMetaData", null ).invoke(  ps, null );
            int actualParams = ((Integer) pmd.getClass().getMethod( "getParameterCount", null ).invoke( pmd, null )).intValue();

	        Assert.assertEquals("Unexpected parameter count:",
	                expectedTypes.length, actualParams );

            Method  getParameterType = pmd.getClass().getMethod( "getParameterType", new Class[] { Integer.TYPE } );

	        for (int i = 0; i < actualParams; i++)
	        {
	            Assert.assertEquals
                    ("Types do not match for parameter " + (i+1),
                     expectedTypes[i],
                     ((Integer) getParameterType.invoke( pmd, new Object[] { i + 1 } )).intValue()
                     );
	        }
	    }
    
    /**
     * Check the nullability of the column definitions for
     * the ResultSet matches the expected values.
     * @param rs
     * @param nullability
     * @throws SQLException 
     */
    public static void assertNullability(ResultSet rs,
            boolean[] nullability) throws SQLException
    {
        ResultSetMetaData rsmd = rs.getMetaData();
        int actualCols = rsmd.getColumnCount();

        Assert.assertEquals("Unexpected column count:",
                nullability.length, rsmd.getColumnCount());

        for (int i = 0; i < actualCols; i++)
        {
            int expected = nullability[i] ?
               ResultSetMetaData.columnNullable : ResultSetMetaData.columnNoNulls;
            Assert.assertEquals("Column nullability do not match for column " + (i+1),
                    expected, rsmd.isNullable(i+1));
        }       
    }
    /**
     * Asserts a ResultSet returns a single row with a single
     * column equal to the passed in String value. The value can
     * be null to indicate SQL NULL. The comparision is made
     * using assertFullResultSet in trimmed string mode.
     *  As a side effect, this method closes the ResultSet.
     */
    public static void assertSingleValueResultSet(ResultSet rs,
            String value) throws SQLException
    {
        String[] row = new String[] {value};
        String[][] set = new String[][] {row};
        assertFullResultSet(rs, set);
    }
    
    /**
     * assertFullResultSet() using trimmed string comparisions.
     * Equal to
     * <code>
     * assertFullResultSet(rs, expectedRows, true)
     * </code>
     *  As a side effect, this method closes the ResultSet.
     */
    public static void assertFullResultSet(ResultSet rs,
            String [][] expectedRows)
            throws SQLException
     {
        assertFullResultSet(rs, expectedRows, true);
     }

    /**
     * Takes a result set and a two-dimensional array and asserts
     * that the rows and columns in the result set match the number,
     * order, and values of those in the array.  Each row in
     * the array is compared with the corresponding row in the
     * result set. As a side effect, this method closes the ResultSet.
     *
     * Will throw an assertion failure if any of the following
     * is true:
     *
     *  1. Expected vs actual number of columns doesn't match
     *  2. Expected vs actual number of rows doesn't match
     *  3. Any column in any row of the result set does not "equal"
     *     the corresponding column in the expected 2-d array.  If
     *     "allAsTrimmedStrings" is true then the result set value
     *     will be retrieved as a String and compared, via the ".equals()"
     *     method, to the corresponding object in the array (with the
     *     assumption being that the objects in the array are all 
     *     Strings).  Otherwise the result set value will be retrieved
     *     and compared as an Object, which is useful when asserting
     *     the JDBC types of the columns in addition to their values.
     *
     * NOTE: It follows from #3 that the order of the rows in the
     * in received result set must match the order of the rows in
     * the received 2-d array.  Otherwise the result will be an
     * assertion failure.
     *
     * @param rs The actual result set.
     * @param expectedRows 2-Dimensional array of objects representing
     *  the expected result set.
     * @param allAsTrimmedStrings Whether or not to fetch (and compare)
     *  all values from the actual result set as trimmed Strings; if
     *  false the values will be fetched and compared as Objects.  For
     *  more on how this parameter is used, see assertRowInResultSet().
     */
    public static void assertFullResultSet(ResultSet rs,
        Object [][] expectedRows, boolean allAsTrimmedStrings)
        throws SQLException
    {
        assertFullResultSet( rs, expectedRows, allAsTrimmedStrings, true );
    }

    /**
     * Takes a result set and a two-dimensional array and asserts
     * that the rows and columns in the result set match the number,
     * order, and values of those in the array.  Each row in
     * the array is compared with the corresponding row in the
     * result set.
     *
     * Will throw an assertion failure if any of the following
     * is true:
     *
     *  1. Expected vs actual number of columns doesn't match
     *  2. Expected vs actual number of rows doesn't match
     *  3. Any column in any row of the result set does not "equal"
     *     the corresponding column in the expected 2-d array.  If
     *     "allAsTrimmedStrings" is true then the result set value
     *     will be retrieved as a String and compared, via the ".equals()"
     *     method, to the corresponding object in the array (with the
     *     assumption being that the objects in the array are all 
     *     Strings).  Otherwise the result set value will be retrieved
     *     and compared as an Object, which is useful when asserting
     *     the JDBC types of the columns in addition to their values.
     *
     * NOTE: It follows from #3 that the order of the rows in the
     * in received result set must match the order of the rows in
     * the received 2-d array.  Otherwise the result will be an
     * assertion failure.
     *
     * @param rs The actual result set.
     * @param expectedRows 2-Dimensional array of objects representing
     *  the expected result set.
     * @param allAsTrimmedStrings Whether or not to fetch (and compare)
     *  all values from the actual result set as trimmed Strings; if
     *  false the values will be fetched and compared as Objects.  For
     *  more on how this parameter is used, see assertRowInResultSet().
     * @param closeResultSet If true, the ResultSet is closed on the way out.
     */
    public static void assertFullResultSet(ResultSet rs,
        Object [][] expectedRows, boolean allAsTrimmedStrings, boolean closeResultSet)
        throws SQLException
    {
        assertFullResultSetMinion(rs, expectedRows, allAsTrimmedStrings,
                                  closeResultSet, null);
    }


    /**
     * assertFullResultSet() using trimmed string comparisons.
     * Equal to
     * <code>
     * assertFullResultSet(rs, expectedRows, true)
     * </code>
     *
     * As a side effect, this method closes the result set.
     * <p/>
     * Additionally, also assert that the given warnings are seen.  The array
     * {@code warnings} should contain null or a warning (SQLState string). The
     * array entry is asserted against the result set after having read the
     * corresponding row in the result set. <b>NOTE: only asserted for embedded
     * result sets, cf DERBY-159</b>
     * <p/>
     * For now, we only look at the first warning if there is a chain
     * of warnings.
     */
    public static void assertFullResultSet(ResultSet rs,
        Object [][] expectedRows, String[] warnings)
        throws SQLException
    {
        assertFullResultSetMinion(rs, expectedRows, true, true, warnings);
    }


    private static void assertFullResultSetMinion(
        ResultSet rs,
        Object [][] expectedRows,
        boolean allAsTrimmedStrings,
        boolean closeResultSet,
        String[] warnings)
        throws SQLException
    {
        int rows;
        ResultSetMetaData rsmd = rs.getMetaData();

        List<List<String>> seen = new ArrayList<List<String>>();
        List<String> seenRow = new ArrayList<String>();

        // Assert that we have the right number of columns. If we expect an
        // empty result set, the expected column count is unknown, so don't
        // check.
        if (expectedRows.length > 0) {
            try {
                Assert.assertEquals("Unexpected column count:",
                        expectedRows[0].length, rsmd.getColumnCount());
            } catch (AssertionFailedError e) {
                throw addRsToReport(e, rsmd, seen, seenRow, rs);
            }
        }

        for (rows = 0; rs.next(); rows++)
        {

            // Assert warnings on result set, but only for embedded, cf
            // DERBY-159.
            if (TestConfiguration.getCurrent().getJDBCClient().isEmbedded() &&
                warnings != null) {

                SQLWarning w = rs.getWarnings();
                String wstr = null;

                if (w != null) {
                    wstr = w.getSQLState();
                }

                try {
                    Assert.assertEquals(
                            "Warning assertion error on row " + (rows+1),
                            warnings[rows],
                            wstr);
                } catch (AssertionFailedError e) {
                    throw addRsToReport(e, rsmd, seen, seenRow, rs);
                }
            }

            /* If we have more actual rows than expected rows, don't
             * try to assert the row.  Instead just keep iterating
             * to see exactly how many rows the actual result set has.
             */
            if (rows < expectedRows.length)
            {
                // dumps itself in any assertion seem
                assertRowInResultSet(rs, seen, seenRow, rows + 1,
                    expectedRows[rows], allAsTrimmedStrings);
            }

            seen.add(new ArrayList<String>(seenRow));
            seenRow.clear();
        }

        if ( closeResultSet ) { rs.close(); }

        // And finally, assert the row count.
        try {
            Assert.assertEquals("Unexpected row count:", expectedRows.length, rows);
        } catch (AssertionFailedError e) {
            throw addRsToReport(e, rsmd, seen, seenRow, rs);
        }
    }


    /**
     * Similar to assertFullResultSet(...) above, except that this
     * method takes a BitSet and checks the received expectedRows
     * against the columns referenced by the BitSet.  So the assumption
     * here is that expectedRows will only have as many columns as
     * there are "true" bits in the received BitSet.
     *
     * This method is useful when we expect there to be a specific
     * ordering on some column OC in the result set, but do not care
     * about the ordering of the non-OC columns when OC is the
     * same across rows.  Ex.  If we have the following results with
     * an expected ordering on column J:
     *
     *   I    J
     *   -    -
     *   a    1
     *   b    1
     *   c    2
     *   c    2
     *
     * Then this method allows us to verify that J is sorted as
     * "1, 1, 2, 2" without having to worry about whether or not
     * (a,1) comes before (b,1).  The caller would simply pass in
     * a BitSet whose content was {1} and an expectedRows array
     * of {{"1"},{"1"},{"2"},{"2"}}.
     *
     * For now this method always does comparisons with
     * "asTrimmedStrings" set to true, and always closes
     * the result set.
     */
    public static void assertPartialResultSet(ResultSet rs,
        Object [][] expectedRows, BitSet colsToCheck)
        throws SQLException
    {
        int rows;
        final List<List<String>> seen = new ArrayList<List<String>>();
        final List<String> seenRow = new ArrayList<String>();
        final ResultSetMetaData rsmd = rs.getMetaData();

        // Assert that we have the right number of columns. If we expect an
        // empty result set, the expected column count is unknown, so don't
        // check.
        if (expectedRows.length > 0) {
            Assert.assertEquals("Unexpected column count:",
                expectedRows[0].length, colsToCheck.cardinality());
        }

        for (rows = 0; rs.next(); rows++)
        {
            /* If we have more actual rows than expected rows, don't
             * try to assert the row.  Instead just keep iterating
             * to see exactly how many rows the actual result set has.
             */
            if (rows < expectedRows.length)
            {
                assertRowInResultSet(rs, seen, seenRow, rows + 1,
                    expectedRows[rows], true, colsToCheck);
            }
            seen.add(new ArrayList<String>(seenRow));
            seenRow.clear();
        }

        rs.close();

        // And finally, assert the row count.
        try {
            Assert.assertEquals("Unexpected row count:", expectedRows.length, rows);
        } catch (AssertionFailedError e) {
            throw addRsToReport(e, rsmd, seen, seenRow, rs);
        }
    }

    /**
     * Assert that every column in the current row of the received
     * result set matches the corresponding column in the received
     * array.  This means that the order of the columns in the result
     * set must match the order of the values in expectedRow.
     *
     * <p>
     * If the expected value for a given row/column is a SQL NULL,
     * then the corresponding value in the array should be a Java
     * null.
     *
     * <p>
     * If a given row/column could have different values (for instance,
     * because it contains a timestamp), the expected value of that
     * row/column could be an object whose <code>equals()</code> method
     * returns <code>true</code> for all acceptable values. (This does
     * not work if one of the acceptable values is <code>null</code>.)
     *
     * @param rs Result set whose current row we'll check.
     * @param seen The set of entirely read rows so far (IN semantics)
     * @param seenRow The set of read columns in the current row so far
     *                (OUT semantics)
     * @param rowNum Row number (w.r.t expected rows) that we're
     *  checking.
     * @param expectedRow Array of objects representing the expected
     *  values for the current row.
     * @param asTrimmedStrings Whether or not to fetch and compare
     *  all values from "rs" as trimmed Strings.  If true then the
     *  value from rs.getString() AND the expected value will both
     *  be trimmed before comparison.  If such trimming is not
     *  desired (ex. if we were testing the padding of CHAR columns)
     *  then this param should be FALSE and the expected values in
     *  the received array should include the expected whitespace.  
     *  If for example the caller wants to check the padding of a
     *  CHAR(8) column, asTrimmedStrings should be FALSE and the
     *  expected row should contain the expected padding, such as
     *  "FRED    ".
     * @throws java.sql.SQLException
     */
    private static void assertRowInResultSet(
            ResultSet rs,
            List<List<String>> seen,
            List<String> seenRow,
            int rowNum,
            Object [] expectedRow,
            boolean asTrimmedStrings) throws SQLException
    {
        assertRowInResultSet(
            rs, seen, seenRow, rowNum, expectedRow, asTrimmedStrings, (BitSet)null);
    }

    /**
     * See assertRowInResultSet(...) above.
     *
     * @param colsToCheck If non-null then for every bit b
     *   that is set in colsToCheck, we'll compare the (b+1)-th column
     *   of the received result set's current row to the i-th column
     *   of expectedRow, where 0 &lt;= i &lt; # bits set in colsToCheck.
     *   So if colsToCheck is { 0, 3 } then expectedRow should have
     *   two objects and we'll check that:
     *
     *     expectedRow[0].equals(rs.getXXX(1));
     *     expectedRow[1].equals(rs.getXXX(4));
     *
     *   If colsToCheck is null then the (i+1)-th column in the
     *   result set is compared to the i-th column in expectedRow,
     *   where 0 &lt;= i &lt; expectedRow.length.
     */
    private static void assertRowInResultSet(
            ResultSet rs,
            List<List<String>> seen,
            List<String> seenRow,
            int rowNum,
            Object [] expectedRow,
            boolean asTrimmedStrings,
            BitSet colsToCheck) throws SQLException
    {
        int cPos = 0;
        ResultSetMetaData rsmd = rs.getMetaData();
        for (int i = 0; i < expectedRow.length; i++)
        {
            cPos = (colsToCheck == null)
                ? (i+1)
                : colsToCheck.nextSetBit(cPos) + 1;

            Object obj;
            if (asTrimmedStrings)
            {
                // Trim the expected value, if non-null.
                if (expectedRow[i] != null)
                    expectedRow[i] = ((String)expectedRow[i]).trim();

                /* Different clients can return different values for
                 * boolean columns--namely, 0/1 vs false/true.  So in
                 * order to keep things uniform, take boolean columns
                 * and get the JDBC string version.  Note: since
                 * Derby doesn't have a BOOLEAN type, we assume that
                 * if the column's type is SMALLINT and the expected
                 * value's string form is "true" or "false", then the
                 * column is intended to be a mock boolean column.
                 */
                if ((expectedRow[i] != null)
                    && (rsmd.getColumnType(cPos) == Types.SMALLINT))
                {
                    String s = expectedRow[i].toString();
                    if (s.equals("true") || s.equals("false"))
                        obj = (rs.getShort(cPos) == 0) ? "false" : "true";
                    else
                        obj = rs.getString(cPos);
                }
                else
                {
                    obj = rs.getString(cPos);

                }
                
                // Trim the rs string.
                if (obj != null)
                    obj = ((String)obj).trim();

            }
            else {
                obj = rs.getObject(cPos);
            }

            seenRow.add(obj == null ? "null" : obj.toString());

            boolean ok = (rs.wasNull() && (expectedRow[i] == null))
                || (!rs.wasNull()
                    && (expectedRow[i] != null)
                    && (expectedRow[i].equals(obj)
                        || (obj instanceof byte[] // Assumes byte arrays
                            && Arrays.equals((byte[] )obj,
                                             (byte[] )expectedRow[i]))));
            if (!ok)
            {
                Object expected = expectedRow[i];
                Object found = obj;
                if (obj instanceof byte[]) {
                    expected = bytesToString((byte[] )expectedRow[i]);
                    found = bytesToString((byte[] )obj);
                }

                try {
                    Assert.fail("Column value mismatch @ column '" +
                            rsmd.getColumnName(cPos) + "', row " + rowNum +
                            ":\n    Expected: >" + expected +
                            "<\n    Found:    >" + found + "<");
                } catch (AssertionFailedError e) {
                    throw addRsToReport(e, rsmd, seen, seenRow, rs);
                }
            }
            
            if (rs.wasNull()) {
                assertResultColumnNullable(rs, seen, seenRow, cPos);
            }
        }
    }
    
    /**
     * Assert two result sets have the same contents.
     * MetaData is determined from rs1, thus if rs2 has extra
     * columns they will be ignored. The metadata for the
     * two ResultSets are not compared.
     * <BR>
     * The compete ResultSet is walked for both ResultSets,
     * and they are both closed.
     * <BR>
     * Columns are compared as primitive ints or longs, Blob,
     * Clobs or as Strings.
     * @throws IOException 
     */
    public static void assertSameContents(ResultSet rs1, ResultSet rs2)
            throws SQLException, IOException {
        ResultSetMetaData rsmd = rs1.getMetaData();
        int columnCount = rsmd.getColumnCount();
        while (rs1.next()) {
            Assert.assertTrue(rs2.next());
            for (int col = 1; col <= columnCount; col++) {
                switch (rsmd.getColumnType(col)) {
                case Types.SMALLINT:
                case Types.INTEGER:
                    Assert.assertEquals(rs1.getInt(col), rs2.getInt(col));
                    break;
                case Types.BIGINT:
                    Assert.assertEquals(rs1.getLong(col), rs2.getLong(col));
                    break;
                case Types.BLOB:
                    BaseJDBCTestCase.assertEquals(rs1.getBlob(col),
                            rs2.getBlob(col));
                    break;
                case Types.CLOB:
                    BaseJDBCTestCase.assertEquals(rs1.getClob(col),
                            rs2.getClob(col));
                    break;
                default:
                    Assert.assertEquals(rs1.getString(col), rs2.getString(col));
                    break;
                }
                Assert.assertEquals(rs1.wasNull(), rs2.wasNull());
            }
        }
        Assert.assertFalse(rs2.next());
        
        rs1.close();
        rs2.close();
    }

    /**
     * Assert that the ResultSet contains the same rows as the specified
     * two-dimensional array. The order of the results is ignored. Convert the
     * results to trimmed strings before comparing. The ResultSet object will
     * be closed.
     *
     * @param rs the ResultSet to check
     * @param expectedRows the expected rows
     */
    public static void assertUnorderedResultSet(
            ResultSet rs, String[][] expectedRows) throws SQLException {
        assertUnorderedResultSet(rs, expectedRows, true);
    }

    /**
     * Assert that the ResultSet contains the same rows as the specified
     * two-dimensional array. The order of the results is ignored. Objects are
     * read out of the ResultSet with the <code>getObject()</code> method and
     * compared with <code>equals()</code>. If the
     * <code>asTrimmedStrings</code> is <code>true</code>, the objects are read
     * with <code>getString()</code> and trimmed before they are compared. The
     * ResultSet object will be closed when this method returns.
     *
     * @param rs the ResultSet to check
     * @param expectedRows the expected rows
     * @param asTrimmedStrings whether the object should be compared as trimmed
     * strings
     */
    public static void assertUnorderedResultSet(
            ResultSet rs, Object[][] expectedRows, boolean asTrimmedStrings)
                throws SQLException {
        assertRSContains(rs, expectedRows, asTrimmedStrings, true);
    }

    /**
     * Asserts that the {@code ResultSet} contains the rows specified by the
     * two-dimensional array.
     * <p>
     * The order of the rows are ignored, and there may be more rows in the
     * result set than in the array. All values are compared as trimmed strings.
     *
     * @param rs the result set to check
     * @param expectedRows the rows that must exist in the result set
     * @throws SQLException if accessing the result set fails
     */
    public static void assertResultSetContains(
                ResultSet rs, Object[][] expectedRows)
            throws SQLException {
        assertRSContains(rs, expectedRows, true, false);
    }

    /**
     * Asserts that the {@code ResultSet} contains the rows specified by the
     * two-dimensional array.
     *
     * @param rs the result set to check
     * @param expectedRows the rows that must exist in the result set
     * @param asTrimmedStrings whether the objects should be compared as
     *      trimmed strings
     * @param rowCountsMustMatch whether the number of rows must be the same in
     *      the result set and the array of expected rows
     * @throws SQLException if accessing the result set fails
     */
    private static void assertRSContains(
                ResultSet rs, Object[][] expectedRows, boolean asTrimmedStrings,
                boolean rowCountsMustMatch)
            throws SQLException {
        if (expectedRows.length == 0) {
            if (rowCountsMustMatch) {
                assertEmpty(rs);
            }
            return;
        }

        ResultSetMetaData rsmd = rs.getMetaData();
        List<List<String>> seen = new ArrayList<List<String>>();
        List<String> seenRow = new ArrayList<String>();

        try {
            Assert.assertEquals("Unexpected column count",
                    expectedRows[0].length, rsmd.getColumnCount());
        } catch (AssertionFailedError e) {
            throw addRsToReport(e, rsmd, seen, seenRow, rs);
        }

        List<List<String>> expected =
                new ArrayList<List<String>>(expectedRows.length);
        for (int i = 0; i < expectedRows.length; i++) {
            Assert.assertEquals("Different column count in expectedRows",
                                expectedRows[0].length, expectedRows[i].length);
            List<String> row = new ArrayList<String>(expectedRows[i].length);

            for (int j = 0; j < expectedRows[i].length; j++) {
                String val = (String) expectedRows[i][j];
                row.add(asTrimmedStrings ?
                        (val == null ? null : val.trim()) :
                        val);
            }
            expected.add(row);
        }

        List<List<String>> actual =
                new ArrayList<List<String>>(expectedRows.length);
        while (rs.next()) {
            List<String> row = new ArrayList<String>(expectedRows[0].length);
            for (int i = 1; i <= expectedRows[0].length; i++) {
                String s = rs.getString(i);
                seenRow.add(s);

                row.add(asTrimmedStrings ?
                        (s == null ? null : s.trim()) :
                        s);

                if (rs.wasNull()) {
                    assertResultColumnNullable(rs, seen, seenRow, i);
                }
            }
            actual.add(row);
            seen.add(new ArrayList<String>(seenRow));
            seenRow.clear();
        }
        rs.close();

        try {
            if (rowCountsMustMatch) {
                String message = "Unexpected row count, expected: " +
                        expectedRows.length + ", actual: " + actual.size() + "\n" +
                        "\t expected rows: \n\t\t" + expected +
                        "\n\t actual result: \n\t\t" + actual + "\n";
                Assert.assertEquals(message,
                        expectedRows.length, actual.size());
            }
            if ( !actual.containsAll(expected) )
            {
                expected.removeAll( actual );
                BaseTestCase.println
                        ( "These expected rows don't appear in the actual result: " + expected );
                String message = "Missing rows in ResultSet; \n\t expected rows: \n\t\t"
                        + expected + "\n\t actual result: \n\t\t" + actual;
                Assert.fail( message );
            }
        } catch (AssertionFailedError e) {
            throw addRsToReport(e, rsmd, seen, seenRow, rs);
        }
    }

    /**
     * Asserts that the current schema is the same as the one specified.
     *
     * @param con connection to check schema in
     * @param schema expected schema name
     * @throws SQLException if something goes wrong
     */
    public static void assertCurrentSchema(Connection con, String schema)
            throws SQLException {
        Statement stmt = con.createStatement();
        try {
            JDBC.assertSingleValueResultSet(
                    stmt.executeQuery("VALUES CURRENT SCHEMA"), schema);
        } finally {
            stmt.close();
        }
    }


    /**
     * Asserts that the current user is the same as the one specified.
     *
     * @param con connection to check schema in
     * @param user expected user name
     * @throws SQLException if something goes wrong
     */
    public static void assertCurrentUser(Connection con, String user)
            throws SQLException {
        Statement stmt = con.createStatement();
        try {
            JDBC.assertSingleValueResultSet(
                    stmt.executeQuery("VALUES CURRENT_USER"), user);
        } finally {
            stmt.close();
        }
    }

    /**
     * Convert byte array to String.
     * Each byte is converted to a hexadecimal string representation.
     *
     * @param ba Byte array to be converted.
     * @return Hexadecimal string representation. Returns null on null input.
     */
    private static String bytesToString(byte[] ba)
    {
        if (ba == null) return null;
        StringBuffer s = new StringBuffer();
        for (int i = 0; i < ba.length; ++i) {
            s.append(Integer.toHexString(ba[i] & 0x00ff));
        }
        return s.toString();
    }

	/**
	 * Escape a non-qualified name so that it is suitable
	 * for use in a SQL query executed by JDBC.
	 */
	public static String escape(String name)
	{
        StringBuffer buffer = new StringBuffer(name.length() + 2);
        buffer.append('"');
        for (int i = 0; i < name.length(); i++) {
            char c = name.charAt(i);
            // escape double quote characters with an extra double quote
            if (c == '"') buffer.append('"');
            buffer.append(c);
        }
        buffer.append('"');
        return buffer.toString();
	}	


	/**
     * Compress 2 adjacent (single or double) quotes into a single (s or d)
     * quote when found in the middle of a String.
     *
     * NOTE:  """" or '''' will be compressed into "" or ''.
     * This function assumes that the leading and trailing quote from a
     * string or delimited identifier have already been removed.
     * @param source string to be compressed
     * @param quotes string containing two single or double quotes.
     * @return String where quotes have been compressed
     */
    private static String compressQuotes(String source, String quotes)
    {
        String  result = source;
        int         index;

        /* Find the first occurrence of adjacent quotes. */
        index = result.indexOf(quotes);

        /* Replace each occurrence with a single quote and begin the
         * search for the next occurrence from where we left off.
         */
        while (index != -1) {
            result = result.substring(0, index + 1) +
                     result.substring(index + 2);
            index = result.indexOf(quotes, index + 1);
        }

        return result;
    }


    /**
     * Convert a SQL identifier to case normal form.
     *
     * Normalize a SQL identifer, up-casing if <regular identifer>,
     * and handling of <delimited identifer> (SQL 2003, section 5.2).
     * The normal form is used internally in Derby.
     */
    public static String identifierToCNF(String id)
    {
        if (id == null || id.length() == 0) {
            return id;
        }

        if (id.charAt(0) == '"' &&
                id.length() >= 3   &&
                id.charAt(id.length() - 1) == '"') {
            // assume syntax is OK, thats is, any quotes inside are doubled:

            return compressQuotes(
                id.substring(1, id.length() - 1), "\"\"");

        } else {
            return id.toUpperCase(Locale.ENGLISH);
        }
    }


    /**
	 * Escape a schama-qualified name so that it is suitable
	 * for use in a SQL query executed by JDBC.
	 */
	public static String escape(String schema, String name)
	{
        return escape(schema) + "." + escape(name);
	}
         
        /**
         * Return Type name from jdbc type
         * 
         * @param jdbcType  jdbc type to translate
         */
        public static String sqlNameFromJdbc(int jdbcType) {
            switch (jdbcType) {
            case Types.BIT          :  return "Types.BIT";
            case Types.BOOLEAN  : return "Types.BOOLEAN";
            case Types.TINYINT      :  return "Types.TINYINT";
            case Types.SMALLINT     :  return "SMALLINT";
            case Types.INTEGER      :  return "INTEGER";
            case Types.BIGINT       :  return "BIGINT";
            
            case Types.FLOAT        :  return "Types.FLOAT";
            case Types.REAL         :  return "REAL";
            case Types.DOUBLE       :  return "DOUBLE";
            
            case Types.NUMERIC      :  return "Types.NUMERIC";
            case Types.DECIMAL      :  return "DECIMAL";
            
            case Types.CHAR         :  return "CHAR";
            case Types.VARCHAR      :  return "VARCHAR";
            case Types.LONGVARCHAR  :  return "LONG VARCHAR";
            case Types.CLOB         :  return "CLOB";
            
            case Types.DATE         :  return "DATE";
            case Types.TIME         :  return "TIME";
            case Types.TIMESTAMP    :  return "TIMESTAMP";
            
            case Types.BINARY       :  return "CHAR () FOR BIT DATA";
            case Types.VARBINARY    :  return "VARCHAR () FOR BIT DATA";
            case Types.LONGVARBINARY:  return "LONG VARCHAR FOR BIT DATA";
            case Types.BLOB         :  return "BLOB";

            case Types.OTHER        :  return "Types.OTHER";
            case Types.NULL         :  return "Types.NULL";
            default : return String.valueOf(jdbcType);
                }
        }

    /**
     * Get run-time statistics and check that a sequence of string exist in the
     * statistics, using the given statement.
     * <p/>
     * For the format of the strings, see RuntimeStatisticsParser#assertSequence
     *
     * @see RuntimeStatisticsParser#assertSequence
     *
     * @param s the statement presumed to just have been executed, and for
     *        which we want to check the run-time statistics
     * @param sequence the sequnce of strings we expect to see in the run-time
     *        statistics
     * @throws SQLException standard
     */
    public static void checkPlan(Statement s, String[] sequence)
            throws SQLException {

        ResultSet rs = s.executeQuery(
                "values SYSCS_UTIL.SYSCS_GET_RUNTIMESTATISTICS()");
        rs.next();

        String rts = rs.getString(1);
        rs.close();

        RuntimeStatisticsParser rtsp = new RuntimeStatisticsParser(rts);
        rtsp.assertSequence(sequence);
    }

    private static AssertionFailedError addRsToReport(
            AssertionFailedError afe,
            ResultSetMetaData rsmd,
            List<List<String>> seen,
            List<String> seenRow,
            ResultSet rs) throws SQLException {

        try {
            if (rs == null) {
                return BaseTestCase.newAssertionFailedError(
                        afe.getMessage() + "\n<NULL>", afe);
            }

            final int c = rsmd.getColumnCount();
            StringBuilder heading = new StringBuilder("    ");
            StringBuilder underline = new StringBuilder("    ");

            // Display column headings
            for (int i=1; i<= c; i++) {
                if (i > 1) {
                    heading.append(",");
                    underline.append(" ");
                }

                int len = heading.length();
                heading.append(rsmd.getColumnLabel(i));
                len = heading.length() - len;

                for (int j = len; j > 0; j--) {
                    underline.append("-");
                }
            }

            heading.append("\n");
            underline.append("\n");

            StringBuilder rowImg = new StringBuilder();
            rowImg.append(afe.getMessage()).
                   append("\n\n").
                   append(heading.toString()).
                   append(underline.toString());

            if (!rs.isClosed()) {
                final int s = seenRow.size();

                // Get any rest of columns of current row
                for (int i=0; i < c - s; i++) {
                    String column = null;

                    try {
                        column = rs.getString(s + i + 1);
                    } catch (SQLException e) {
                        // We may not yet have called next?
                        if (e.getSQLState().equals("24000")) {
                            if (rs.next()) {
                                column = rs.getString(s + i + 1);
                            } else {
                                break;
                            }
                        }
                    }
                    seenRow.add(column);
                }

                if (seenRow.size() > 0) {
                    seen.add(new ArrayList<String>(seenRow));
                    seenRow.clear();
                }

                // Get any remaining rows
                while (rs.next()) {
                    for (int i = 0; i < c; i++) {
                        seenRow.add(rs.getString(i + 1));
                    }
                    seen.add(new ArrayList<String>(seenRow));
                    seenRow.clear();
                }
            }

            // Display data
            for (List<String> row : seen) {
                rowImg.append("   ").
                       append(row.toString()).
                       append("\n");
            }

            return BaseTestCase.newAssertionFailedError(rowImg.toString(), afe);

        } catch (Throwable t) {
            // Something went wrong when adding the ResultSet to the error
            // message. Don't return this error, as it would shadow the
            // original error. Print it to help debugging.
            BaseTestCase.printStackTrace(t);

            // Return the original error.
            return afe;
        }
    }
}
