/*
 * Copyright (c) 2002, 2026, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2.0, as published by
 * the Free Software Foundation.
 *
 * This program is designed to work with certain software that is licensed under separate terms, as designated in a particular file or component or in
 * included license documentation. The authors of MySQL hereby grant you an additional permission to link the program and your derivative works with the
 * separately licensed software that they have either included with the program or referenced in the documentation.
 *
 * Without limiting anything contained in the foregoing, this file, which is part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
 * version 1.0, a copy of which can be found at http://oss.oracle.com/licenses/universal-foss-exception.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

package testsuite.regression;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharArrayReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Array;
import java.sql.BatchUpdateException;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DataTruncation;
import java.sql.Date;
import java.sql.NClob;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.ToIntFunction;

import javax.sql.XAConnection;
import javax.sql.rowset.serial.SerialBlob;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import com.mysql.cj.CharsetMappingWrapper;
import com.mysql.cj.ClientPreparedQuery;
import com.mysql.cj.MysqlConnection;
import com.mysql.cj.MysqlType;
import com.mysql.cj.PreparedQuery;
import com.mysql.cj.Query;
import com.mysql.cj.ServerPreparedQuery;
import com.mysql.cj.Session;
import com.mysql.cj.conf.PropertyDefinitions;
import com.mysql.cj.conf.PropertyDefinitions.DatabaseTerm;
import com.mysql.cj.conf.PropertyDefinitions.SslMode;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.conf.PropertySet;
import com.mysql.cj.exceptions.CJCommunicationsException;
import com.mysql.cj.exceptions.ExceptionFactory;
import com.mysql.cj.exceptions.ExceptionInterceptor;
import com.mysql.cj.exceptions.MysqlErrorNumbers;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.interceptors.QueryInterceptor;
import com.mysql.cj.jdbc.ClientPreparedStatement;
import com.mysql.cj.jdbc.CloseOption;
import com.mysql.cj.jdbc.ConnectionImpl;
import com.mysql.cj.jdbc.JdbcConnection;
import com.mysql.cj.jdbc.JdbcPreparedStatement;
import com.mysql.cj.jdbc.JdbcStatement;
import com.mysql.cj.jdbc.MysqlConnectionPoolDataSource;
import com.mysql.cj.jdbc.MysqlXADataSource;
import com.mysql.cj.jdbc.ParameterBindings;
import com.mysql.cj.jdbc.ServerPreparedStatement;
import com.mysql.cj.jdbc.StatementImpl;
import com.mysql.cj.jdbc.exceptions.CommunicationsException;
import com.mysql.cj.jdbc.exceptions.MySQLQueryInterruptedException;
import com.mysql.cj.jdbc.exceptions.MySQLTimeoutException;
import com.mysql.cj.jdbc.exceptions.MysqlDataTruncation;
import com.mysql.cj.jdbc.ha.ReplicationConnection;
import com.mysql.cj.jdbc.interceptors.ResultSetScannerInterceptor;
import com.mysql.cj.jdbc.result.CachedResultSetMetaData;
import com.mysql.cj.jdbc.result.ResultSetInternalMethods;
import com.mysql.cj.log.Log;
import com.mysql.cj.protocol.ColumnDefinition;
import com.mysql.cj.protocol.Message;
import com.mysql.cj.protocol.Resultset;
import com.mysql.cj.protocol.ResultsetRows;
import com.mysql.cj.protocol.ServerSession;
import com.mysql.cj.protocol.a.NativeServerSession;
import com.mysql.cj.util.LRUCache;
import com.mysql.cj.util.StringUtils;
import com.mysql.cj.util.TimeUtil;

import testsuite.BaseQueryInterceptor;
import testsuite.BaseTestCase;
import testsuite.UnreliableSocketFactory;
import testsuite.simple.StatementsTest;

/**
 * Regression tests for the Statement class
 */
public class StatementRegressionTest extends BaseTestCase {

    class PrepareThread extends Thread {

        Connection c;

        PrepareThread(Connection cn) {
            this.c = cn;
        }

        @Override
        public void run() {
            for (int i = 0; i < 20; i++) // force this to end eventually
            {
                try {
                    this.c.prepareStatement("SELECT 1");
                    StatementRegressionTest.this.testServerPrepStmtDeadlockCounter++;
                    Thread.sleep(400);
                } catch (SQLException sqlEx) {
                    throw new RuntimeException(sqlEx);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    static int count = 0;

    static int nextID = 1; // The next ID we expected to generate

    /*
     * Each row in this table is to be converted into a single REPLACE statement. If the value is zero, a new record is to be created using then autoincrement
     * feature. If the value is non-zero, the existing row of that value is to be replace with, obviously, the same key. I expect one Generated Key for each
     * zero value - but I would accept one key for each value, with non-zero values coming back as themselves.
     */
    static final int[][] tests = { { 0 }, // generate 1
            { 1, 0, 0 }, // update 1, generate 2, 3
            { 2, 0, 0, }, // update 2, generate 3, 4
    };

    protected int testServerPrepStmtDeadlockCounter = 0;

    private void addBatchItems(Statement statement, PreparedStatement pStmt, String tableName, int i) throws SQLException {
        pStmt.setString(1, "ps_batch_" + i);
        pStmt.setString(2, "ps_batch_" + i);
        pStmt.addBatch();

        statement.addBatch("INSERT INTO " + tableName + " (strdata1, strdata2) VALUES (\"s_batch_" + i + "\",\"s_batch_" + i + "\")");
    }

    private void createGGKTables() throws Exception {
        // Delete and recreate table
        dropGGKTables();
        createTable("testggk", "(id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,val INT NOT NULL)", "MYISAM");
    }

    private void doGGKTestPreparedStatement(int[] values, boolean useUpdate) throws Exception {
        // Generate the the multiple replace command
        StringBuilder cmd = new StringBuilder("REPLACE INTO testggk VALUES ");
        int newKeys = 0;

        for (int i = 0; i < values.length; i++) {
            cmd.append("(");

            if (values[i] == 0) {
                cmd.append("NULL");
                newKeys += 1;
            } else {
                cmd.append(values[i]);
            }

            cmd.append(", ");
            cmd.append(count++);
            cmd.append("), ");
        }

        cmd.setLength(cmd.length() - 2); // trim the final ", "

        // execute and print it
        System.out.println(cmd.toString());

        PreparedStatement pStmt = this.conn.prepareStatement(cmd.toString(), Statement.RETURN_GENERATED_KEYS);

        if (useUpdate) {
            pStmt.executeUpdate();
        } else {
            pStmt.execute();
        }

        // print out what actually happened
        System.out.println("Expect " + newKeys + " generated keys, starting from " + nextID);

        this.rs = pStmt.getGeneratedKeys();
        StringBuilder res = new StringBuilder("Got keys");

        int[] generatedKeys = new int[newKeys];
        int i = 0;

        while (this.rs.next()) {
            if (i < generatedKeys.length) {
                generatedKeys[i] = this.rs.getInt(1);
            }

            i++;

            res.append(" " + this.rs.getInt(1));
        }

        int numberOfGeneratedKeys = i;

        assertTrue(numberOfGeneratedKeys == newKeys,
                "Didn't retrieve expected number of generated keys, expected " + newKeys + ", found " + numberOfGeneratedKeys);
        assertTrue(generatedKeys[0] == nextID, "Keys didn't start with correct sequence: ");

        System.out.println(res.toString());

        // Read and print the new state of the table
        this.rs = this.stmt.executeQuery("SELECT id, val FROM testggk");
        System.out.println("New table contents ");

        while (this.rs.next()) {
            System.out.println("Id " + this.rs.getString(1) + " val " + this.rs.getString(2));
        }

        // Tidy up
        System.out.println("");
        nextID += newKeys;
    }

    private void doGGKTestStatement(int[] values, boolean useUpdate) throws Exception {
        // Generate the the multiple replace command
        StringBuilder cmd = new StringBuilder("REPLACE INTO testggk VALUES ");
        int newKeys = 0;

        for (int i = 0; i < values.length; i++) {
            cmd.append("(");

            if (values[i] == 0) {
                cmd.append("NULL");
                newKeys += 1;
            } else {
                cmd.append(values[i]);
            }

            cmd.append(", ");
            cmd.append(count++);
            cmd.append("), ");
        }

        cmd.setLength(cmd.length() - 2); // trim the final ", "

        // execute and print it
        System.out.println(cmd.toString());

        if (useUpdate) {
            this.stmt.executeUpdate(cmd.toString(), Statement.RETURN_GENERATED_KEYS);
        } else {
            this.stmt.execute(cmd.toString(), Statement.RETURN_GENERATED_KEYS);
        }

        // print out what actually happened
        System.out.println("Expect " + newKeys + " generated keys, starting from " + nextID);

        this.rs = this.stmt.getGeneratedKeys();
        StringBuilder res = new StringBuilder("Got keys");

        int[] generatedKeys = new int[newKeys];
        int i = 0;

        while (this.rs.next()) {
            if (i < generatedKeys.length) {
                generatedKeys[i] = this.rs.getInt(1);
            }

            i++;

            res.append(" " + this.rs.getInt(1));
        }

        int numberOfGeneratedKeys = i;

        assertTrue(numberOfGeneratedKeys == newKeys,
                "Didn't retrieve expected number of generated keys, expected " + newKeys + ", found " + numberOfGeneratedKeys);
        assertTrue(generatedKeys[0] == nextID, "Keys didn't start with correct sequence: ");

        System.out.println(res.toString());

        // Read and print the new state of the table
        this.rs = this.stmt.executeQuery("SELECT id, val FROM testggk");
        System.out.println("New table contents ");

        while (this.rs.next()) {
            System.out.println("Id " + this.rs.getString(1) + " val " + this.rs.getString(2));
        }

        // Tidy up
        System.out.println("");
        nextID += newKeys;
    }

    private void dropGGKTables() throws Exception {
        this.stmt.executeUpdate("DROP TABLE IF EXISTS testggk");
    }

    /**
     * @param pStmt
     * @param catId
     * @throws SQLException
     */
    private void execQueryBug5191(PreparedStatement pStmt, int catId) throws SQLException {
        pStmt.setInt(1, catId);

        this.rs = pStmt.executeQuery();

        assertTrue(this.rs.next());
        assertTrue(this.rs.next());
        // assertTrue(rs.next());

        assertFalse(this.rs.next());
    }

    /**
     * @param continueBatchOnError
     * @throws SQLException
     */
    private void innerBug6823(boolean continueBatchOnError) throws SQLException {
        Properties continueBatchOnErrorProps = new Properties();
        continueBatchOnErrorProps.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        continueBatchOnErrorProps.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        continueBatchOnErrorProps.setProperty(PropertyKey.continueBatchOnError.getKeyName(), String.valueOf(continueBatchOnError));
        this.conn = getConnectionWithProps(continueBatchOnErrorProps);
        Statement statement = this.conn.createStatement();

        String tableName = "testBug6823";

        createTable(tableName,
                "(id int not null primary key auto_increment, strdata1 varchar(255) not null, strdata2 varchar(255), UNIQUE INDEX (strdata1(100)))");

        PreparedStatement pStmt = this.conn.prepareStatement("INSERT INTO " + tableName + " (strdata1, strdata2) VALUES (?,?)");

        int c = 0;
        addBatchItems(statement, pStmt, tableName, ++c);
        addBatchItems(statement, pStmt, tableName, ++c);
        addBatchItems(statement, pStmt, tableName, ++c);
        addBatchItems(statement, pStmt, tableName, c); // duplicate entry
        addBatchItems(statement, pStmt, tableName, ++c);
        addBatchItems(statement, pStmt, tableName, ++c);

        int expectedUpdateCounts = continueBatchOnError ? 6 : 3;

        BatchUpdateException e1 = null;
        BatchUpdateException e2 = null;

        int[] updateCountsPstmt = null;
        try {
            updateCountsPstmt = pStmt.executeBatch();
        } catch (BatchUpdateException e) {
            e1 = e;
            updateCountsPstmt = e1.getUpdateCounts();
        }

        int[] updateCountsStmt = null;
        try {
            updateCountsStmt = statement.executeBatch();
        } catch (BatchUpdateException e) {
            e2 = e;
            updateCountsStmt = e1.getUpdateCounts();
        }

        assertNotNull(e1);
        assertNotNull(e2);

        assertEquals(expectedUpdateCounts, updateCountsPstmt.length);
        assertEquals(expectedUpdateCounts, updateCountsStmt.length);

        if (continueBatchOnError) {
            assertTrue(updateCountsPstmt[3] == Statement.EXECUTE_FAILED);
            assertTrue(updateCountsStmt[3] == Statement.EXECUTE_FAILED);
        }

        int psRows = 0;
        this.rs = this.stmt.executeQuery("SELECT * from " + tableName + " WHERE strdata1 like \"ps_%\"");
        while (this.rs.next()) {
            psRows++;
        }
        assertTrue(psRows > 0);

        int sRows = 0;
        this.rs = this.stmt.executeQuery("SELECT * from " + tableName + " WHERE strdata1 like \"s_%\"");
        while (this.rs.next()) {
            sRows++;
        }
        assertTrue(sRows > 0);

        assertTrue(psRows == sRows, psRows + "!=" + sRows);
    }

    /**
     * Tests fix for BUG#10155, double quotes not recognized when parsing client-side prepared statements.
     *
     * @throws Exception
     */
    @Test
    public void testBug10155() throws Exception {
        this.conn.prepareStatement("SELECT \"Test question mark? Test single quote'\"").executeQuery().close();
    }

    /**
     * Tests fix for BUG#10630, Statement.getWarnings() fails with NPE if statement has been closed.
     *
     * @throws Exception
     */
    @Test
    public void testBug10630() throws Exception {
        Connection conn2 = null;
        Statement stmt2 = null;

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            conn2 = getConnectionWithProps(props);
            stmt2 = conn2.createStatement();

            conn2.close();
            stmt2.getWarnings();
            fail("Should've caught an exception here");
        } catch (SQLException sqlEx) {
            assertEquals(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, sqlEx.getSQLState());
        } finally {
            if (stmt2 != null) {
                stmt2.close();
            }

            if (conn2 != null) {
                conn2.close();
            }
        }
    }

    /**
     * Tests fix for BUG#11115, Varbinary data corrupted when using server-side prepared statements.
     *
     * @throws Exception
     */
    @Test
    public void testBug11115() throws Exception {
        String tableName = "testBug11115";

        createTable(tableName, "(pwd VARBINARY(30)) DEFAULT CHARACTER SET utf8", "InnoDB");

        byte[] bytesToTest = new byte[] { 17, 120, -1, -73, -5 };

        PreparedStatement insStmt = this.conn.prepareStatement("INSERT INTO " + tableName + " (pwd) VALUES (?)");
        insStmt.setBytes(1, bytesToTest);
        insStmt.executeUpdate();

        this.rs = this.stmt.executeQuery("SELECT pwd FROM " + tableName);
        this.rs.next();

        byte[] fromDatabase = this.rs.getBytes(1);

        assertEquals(bytesToTest.length, fromDatabase.length);

        for (int i = 0; i < bytesToTest.length; i++) {
            assertEquals(bytesToTest[i], fromDatabase[i]);
        }

        this.rs = this.conn.prepareStatement("SELECT pwd FROM " + tableName).executeQuery();
        this.rs.next();

        fromDatabase = this.rs.getBytes(1);

        assertEquals(bytesToTest.length, fromDatabase.length);

        for (int i = 0; i < bytesToTest.length; i++) {
            assertEquals(bytesToTest[i], fromDatabase[i]);
        }
    }

    @Test
    public void testBug11540() throws Exception {
        Locale originalLocale = Locale.getDefault();
        Connection thaiConn = null;
        Statement thaiStmt = null;
        PreparedStatement thaiPrepStmt = null;

        try {
            createTable("testBug11540", "(field1 DATE, field2 DATETIME)");
            this.stmt.executeUpdate("INSERT INTO testBug11540 VALUES (NOW(), NOW())");
            Locale.setDefault(new Locale("th", "TH"));
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.jdbcCompliantTruncation.getKeyName(), "false");
            props.setProperty(PropertyKey.connectionTimeZone.getKeyName(), "LOCAL");

            thaiConn = getConnectionWithProps(props);
            thaiStmt = thaiConn.createStatement();

            this.rs = thaiStmt.executeQuery("SELECT field1, field2 FROM testBug11540");
            this.rs.next();

            Date origDate = this.rs.getDate(1);
            Timestamp origTimestamp = this.rs.getTimestamp(2);
            this.rs.close();

            thaiStmt.executeUpdate("TRUNCATE TABLE testBug11540");

            thaiPrepStmt = ((com.mysql.cj.jdbc.JdbcConnection) thaiConn).clientPrepareStatement("INSERT INTO testBug11540 VALUES (?,?)");
            thaiPrepStmt.setDate(1, origDate);
            thaiPrepStmt.setTimestamp(2, origTimestamp);
            thaiPrepStmt.executeUpdate();

            this.rs = thaiStmt.executeQuery("SELECT field1, field2 FROM testBug11540");
            this.rs.next();

            Date testDate = this.rs.getDate(1);
            Timestamp testTimestamp = this.rs.getTimestamp(2);
            this.rs.close();

            props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.connectionTimeZone.getKeyName(), "LOCAL");
            Connection testConn = getConnectionWithProps(props);
            TimeZone serverTz = ((MysqlConnection) testConn).getSession().getServerSession().getSessionTimeZone();
            Timestamp expTs = Timestamp.from(origTimestamp.toInstant().atZone(ZoneId.systemDefault()).withZoneSameInstant(serverTz.toZoneId())
                    .withZoneSameLocal(ZoneId.systemDefault()).toInstant());
            assertEquals(origDate, testDate);
            assertEquals(expTs, testTimestamp);

        } finally {
            Locale.setDefault(originalLocale);
        }
    }

    /**
     * Tests fix for BUG#11663, autoGenerateTestcaseScript uses bogus parameter names for server-side prepared statements.
     *
     * @throws Exception
     */
    @Test
    public void testBug11663() throws Exception {
        if (((com.mysql.cj.jdbc.JdbcConnection) this.conn).getPropertySet().getBooleanProperty(PropertyKey.useServerPrepStmts).getValue()) {
            Connection testcaseGenCon = null;
            PrintStream oldErr = System.err;

            try {
                createTable("testBug11663", "(field1 int)");

                Properties props = new Properties();
                props.setProperty(PropertyKey.autoGenerateTestcaseScript.getKeyName(), "true");
                testcaseGenCon = getConnectionWithProps(props);
                ByteArrayOutputStream testStream = new ByteArrayOutputStream();
                PrintStream testErr = new PrintStream(testStream);
                System.setErr(testErr);
                this.pstmt = testcaseGenCon.prepareStatement("SELECT field1 FROM testBug11663 WHERE field1=?");
                this.pstmt.setInt(1, 1);
                this.pstmt.execute();
                System.setErr(oldErr);
                String testString = new String(testStream.toByteArray());

                int setIndex = testString.indexOf("SET @debug_stmt_param");
                int equalsIndex = testString.indexOf("=", setIndex);
                String paramName = testString.substring(setIndex + 4, equalsIndex);

                int usingIndex = testString.indexOf("USING " + paramName, equalsIndex);

                assertTrue(usingIndex != -1);
            } finally {
                System.setErr(oldErr);

                if (this.pstmt != null) {
                    this.pstmt.close();
                    this.pstmt = null;
                }

                if (testcaseGenCon != null) {
                    testcaseGenCon.close();
                }

            }
        }
    }

    /**
     * Tests fix for BUG#11798 - Pstmt.setObject(...., Types.BOOLEAN) throws exception.
     *
     * @throws Exception
     */
    @Test
    public void testBug11798() throws Exception {
        try {
            this.pstmt = this.conn.prepareStatement("SELECT ?");
            this.pstmt.setObject(1, Boolean.TRUE, Types.BOOLEAN);
            this.pstmt.setObject(1, new BigDecimal("1"), Types.BOOLEAN);
            this.pstmt.setObject(1, "true", Types.BOOLEAN);
        } finally {
            if (this.pstmt != null) {
                this.pstmt.close();
                this.pstmt = null;
            }
        }
    }

    /**
     * Tests fix for BUG#13255 - Reconnect during middle of executeBatch()
     * should not happen.
     *
     * @throws Exception
     */
    @Test
    public void testBug13255() throws Exception {
        createTable("testBug13255", "(field_1 int)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.autoReconnect.getKeyName(), "true");

        Connection reconnectConn = null;
        Statement reconnectStmt = null;
        PreparedStatement reconnectPStmt = null;

        try {
            reconnectConn = getConnectionWithProps(props);
            reconnectStmt = reconnectConn.createStatement();

            String connectionId = getSingleIndexedValueWithQuery(reconnectConn, 1, "SELECT CONNECTION_ID()").toString();

            reconnectStmt.addBatch("INSERT INTO testBug13255 VALUES (1)");
            reconnectStmt.addBatch("INSERT INTO testBug13255 VALUES (2)");
            reconnectStmt.addBatch("KILL " + connectionId);

            for (int i = 0; i < 100; i++) {
                reconnectStmt.addBatch("INSERT INTO testBug13255 VALUES (" + i + ")");
            }

            try {
                reconnectStmt.executeBatch();
            } catch (SQLException sqlEx) {
                // We expect this...we killed the connection
            }

            assertEquals(2, getRowCount("testBug13255"));

            this.stmt.executeUpdate("TRUNCATE TABLE testBug13255");

            reconnectConn.close();

            reconnectConn = getConnectionWithProps(props);

            connectionId = getSingleIndexedValueWithQuery(reconnectConn, 1, "SELECT CONNECTION_ID()").toString();

            reconnectPStmt = reconnectConn.prepareStatement("INSERT INTO testBug13255 VALUES (?)");
            reconnectPStmt.setInt(1, 1);
            reconnectPStmt.addBatch();
            reconnectPStmt.setInt(1, 2);
            reconnectPStmt.addBatch();
            reconnectPStmt.addBatch("KILL " + connectionId);

            for (int i = 3; i < 100; i++) {
                reconnectPStmt.setInt(1, i);
                reconnectPStmt.addBatch();
            }

            try {
                reconnectPStmt.executeBatch();
            } catch (SQLException sqlEx) {
                // We expect this...we killed the connection
            }

            assertEquals(2, getRowCount("testBug13255"));

        } finally {
            if (reconnectStmt != null) {
                reconnectStmt.close();
            }

            if (reconnectConn != null) {
                reconnectConn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#15024 - Driver incorrectly closes streams passed as
     * arguments to PreparedStatements.
     *
     * @throws Exception
     */
    @Test
    public void testBug15024() throws Exception {
        createTable("testBug15024", "(field1 BLOB)");

        try {
            this.pstmt = this.conn.prepareStatement("INSERT INTO testBug15024 VALUES (?)");
            testStreamsForBug15024(false, false);

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useConfigs.getKeyName(), "3-0-Compat");

            Connection compatConn = null;

            try {
                compatConn = getConnectionWithProps(props);

                this.pstmt = compatConn.prepareStatement("INSERT INTO testBug15024 VALUES (?)");
                testStreamsForBug15024(true, false);
            } finally {
                if (compatConn != null) {
                    compatConn.close();
                }
            }
        } finally {
            if (this.pstmt != null) {
                PreparedStatement toClose = this.pstmt;
                this.pstmt = null;

                toClose.close();
            }
        }
    }

    /**
     * PreparedStatement should call EscapeProcessor.escapeSQL?
     *
     * @throws Exception
     */
    @Test
    public void testBug15141() throws Exception {
        try {
            createTable("testBug15141", "(field1 VARCHAR(32))");
            this.stmt.executeUpdate("INSERT INTO testBug15141 VALUES ('abc')");

            this.pstmt = this.conn.prepareStatement("select {d '1997-05-24'} FROM testBug15141");
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals("1997-05-24", this.rs.getString(1));
            this.rs.close();
            this.rs = null;
            this.pstmt.close();
            this.pstmt = null;

            this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement("select {d '1997-05-24'} FROM testBug15141");
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals("1997-05-24", this.rs.getString(1));
            this.rs.close();
            this.rs = null;
            this.pstmt.close();
            this.pstmt = null;
        } finally {
            if (this.rs != null) {
                ResultSet toCloseRs = this.rs;
                this.rs = null;
                toCloseRs.close();
            }

            if (this.pstmt != null) {
                PreparedStatement toClosePstmt = this.pstmt;
                this.pstmt = null;
                toClosePstmt.close();
            }
        }
    }

    /**
     * Tests fix for BUG#18041 - Server-side prepared statements don't cause
     * truncation exceptions to be thrown.
     *
     * @throws Exception
     */
    @Test
    public void testBug18041() throws Exception {
        createTable("testBug18041", "(`a` tinyint(4) NOT NULL, `b` char(4) default NULL)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.jdbcCompliantTruncation.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");

        Connection truncConn = null;
        PreparedStatement stm = null;

        try {
            truncConn = getConnectionWithProps(props);

            stm = truncConn.prepareStatement("insert into testBug18041 values (?,?)");
            stm.setInt(1, 1000);
            stm.setString(2, "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn");
            stm.executeUpdate();
            fail("Truncation exception should have been thrown");
        } catch (DataTruncation truncEx) {
            // we expect this
        } finally {
            if (truncConn != null) {
                truncConn.close();
            }
        }
    }

    private void testStreamsForBug15024(boolean shouldBeClosedStream, boolean shouldBeClosedReader) throws SQLException {
        IsClosedInputStream bIn = new IsClosedInputStream(new byte[4]);
        IsClosedReader readerIn = new IsClosedReader("abcdef");

        this.pstmt.setBinaryStream(1, bIn, 4);
        this.pstmt.execute();
        assertEquals(shouldBeClosedStream, bIn.isClosed());

        this.pstmt.setCharacterStream(1, readerIn, 6);
        this.pstmt.execute();
        assertEquals(shouldBeClosedReader, readerIn.isClosed());

        this.pstmt.close();
    }

    class IsClosedReader extends StringReader {

        boolean isClosed = false;

        public IsClosedReader(String arg0) {
            super(arg0);
        }

        @Override
        public void close() {
            super.close();

            this.isClosed = true;
        }

        public boolean isClosed() {
            return this.isClosed;
        }

    }

    class IsClosedInputStream extends ByteArrayInputStream {

        boolean isClosed = false;

        public IsClosedInputStream(byte[] arg0, int arg1, int arg2) {
            super(arg0, arg1, arg2);
        }

        public IsClosedInputStream(byte[] arg0) {
            super(arg0);
        }

        @Override
        public void close() throws IOException {
            super.close();
            this.isClosed = true;
        }

        public boolean isClosed() {
            return this.isClosed;
        }

    }

    /**
     * Tests fix for BUG#1774 -- Truncated words after double quote
     *
     * @throws Exception
     */
    @Test
    public void testBug1774() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug1774");
            this.stmt.executeUpdate("CREATE TABLE testBug1774 (field1 VARCHAR(255))");

            PreparedStatement pStmt = this.conn.prepareStatement("INSERT INTO testBug1774 VALUES (?)");

            String testString = "The word contains \" character";

            pStmt.setString(1, testString);
            pStmt.executeUpdate();

            this.rs = this.stmt.executeQuery("SELECT * FROM testBug1774");
            this.rs.next();
            assertEquals(this.rs.getString(1), testString);
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug1774");
        }
    }

    /**
     * Tests fix for BUG#1901 -- PreparedStatement.setObject(int, Object, int,
     * int) doesn't support CLOB or BLOB types.
     *
     * @throws Exception
     */
    @Test
    public void testBug1901() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug1901");
            this.stmt.executeUpdate("CREATE TABLE testBug1901 (field1 VARCHAR(255))");
            this.stmt.executeUpdate("INSERT INTO testBug1901 VALUES ('aaa')");

            this.rs = this.stmt.executeQuery("SELECT field1 FROM testBug1901");
            this.rs.next();

            Clob valueAsClob = this.rs.getClob(1);
            Blob valueAsBlob = this.rs.getBlob(1);

            PreparedStatement pStmt = this.conn.prepareStatement("INSERT INTO testBug1901 VALUES (?)");
            pStmt.setObject(1, valueAsClob, java.sql.Types.CLOB, 0);
            pStmt.executeUpdate();
            pStmt.setObject(1, valueAsBlob, java.sql.Types.BLOB, 0);
            pStmt.executeUpdate();
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug1901");
        }
    }

    /**
     * Tests fix for BUG#1933 -- Driver property 'maxRows' has no effect.
     *
     * @throws Exception
     */
    @Test
    public void testBug1933() throws Exception {
        Connection maxRowsConn = null;
        PreparedStatement maxRowsPrepStmt = null;
        Statement maxRowsStmt = null;

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.maxRows.getKeyName(), "1");

            maxRowsConn = getConnectionWithProps(props);

            maxRowsStmt = maxRowsConn.createStatement();

            assertTrue(maxRowsStmt.getMaxRows() == 1);

            this.rs = maxRowsStmt.executeQuery("SELECT 1 UNION SELECT 2");

            this.rs.next();

            maxRowsPrepStmt = maxRowsConn.prepareStatement("SELECT 1 UNION SELECT 2");

            assertTrue(maxRowsPrepStmt.getMaxRows() == 1);

            this.rs = maxRowsPrepStmt.executeQuery();

            this.rs.next();

            assertTrue(!this.rs.next());

            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");

            maxRowsConn.close();
            maxRowsConn = getConnectionWithProps(props);

            maxRowsPrepStmt = maxRowsConn.prepareStatement("SELECT 1 UNION SELECT 2");

            assertTrue(maxRowsPrepStmt.getMaxRows() == 1);

            this.rs = maxRowsPrepStmt.executeQuery();

            this.rs.next();

            assertTrue(!this.rs.next());
        } finally {
            if (maxRowsConn != null) {
                maxRowsConn.close();
            }
        }
    }

    /**
     * Tests the fix for BUG#1934 -- prepareStatement dies silently when
     * encountering Statement.RETURN_GENERATED_KEY
     *
     * @throws Exception
     */
    @Test
    public void testBug1934() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug1934");
            this.stmt.executeUpdate("CREATE TABLE testBug1934 (field1 INT)");

            System.out.println("Before prepareStatement()");

            this.pstmt = this.conn.prepareStatement("INSERT INTO testBug1934 VALUES (?)", java.sql.Statement.RETURN_GENERATED_KEYS);

            assertTrue(this.pstmt != null);

            System.out.println("After prepareStatement() - " + this.pstmt);
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug1934");
        }
    }

    /**
     * Tests fix for BUG#1958 - Improper bounds checking on
     * PreparedStatement.setFoo().
     *
     * @throws Exception
     */
    @Test
    public void testBug1958() throws Exception {
        PreparedStatement pStmt = null;

        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug1958");
            this.stmt.executeUpdate("CREATE TABLE testBug1958 (field1 int)");

            pStmt = this.conn.prepareStatement("SELECT * FROM testBug1958 WHERE field1 IN (?, ?, ?)");

            try {
                pStmt.setInt(4, 1);
            } catch (SQLException sqlEx) {
                assertTrue(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT.equals(sqlEx.getSQLState()));
            }
        } finally {
            if (pStmt != null) {
                pStmt.close();
            }

            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug1958");
        }
    }

    /**
     * Tests the fix for BUG#2606, server-side prepared statements not returning
     * datatype YEAR correctly.
     *
     * @throws Exception
     */
    @Test
    public void testBug2606() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug2606");
            this.stmt.executeUpdate("CREATE TABLE testBug2606(year_field YEAR)");
            this.stmt.executeUpdate("INSERT INTO testBug2606 VALUES (2004)");

            PreparedStatement yrPstmt = this.conn.prepareStatement("SELECT year_field FROM testBug2606");

            this.rs = yrPstmt.executeQuery();

            assertTrue(this.rs.next());

            assertEquals("2004-01-01", this.rs.getDate(1).toString());
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug2606");
        }
    }

    /**
     * Tests the fix for BUG#2671, nulls encoded incorrectly in server-side prepared statements.
     *
     * @throws Exception
     */
    @Test
    public void testBug2671() throws Exception {
        createTable("test3",
                "(`field1` int(8) NOT NULL auto_increment, `field2` int(8) unsigned zerofill default NULL,"
                        + " `field3` varchar(30) binary NOT NULL default '', `field4` varchar(100) default NULL, `field5` datetime NULL default NULL,"
                        + " PRIMARY KEY  (`field1`), UNIQUE KEY `unq_id` (`field2`), UNIQUE KEY  (`field3`)) CHARACTER SET utf8",
                "InnoDB");

        this.stmt.executeUpdate("insert into test3 (field1, field3, field4) values (1, 'blewis', 'Bob Lewis')");

        String query = "UPDATE test3 SET field2=?, field3=?, field4=?, field5=? WHERE field1 = ?";

        java.sql.Date mydate = null;

        this.pstmt = this.conn.prepareStatement(query);

        this.pstmt.setInt(1, 13);
        this.pstmt.setString(2, "abc");
        this.pstmt.setString(3, "def");
        this.pstmt.setDate(4, mydate);
        this.pstmt.setInt(5, 1);

        int retval = this.pstmt.executeUpdate();
        assertEquals(1, retval);
    }

    /**
     * Tests fix for BUG#3103 -- java.util.Date not accepted as parameter to
     * PreparedStatement.setObject().
     *
     * @throws Exception
     *
     * @deprecated uses deprecated methods of Date class
     */
    @Deprecated
    @Test
    public void testBug3103() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3103");

            if (versionMeetsMinimum(5, 6, 4)) {
                this.stmt.executeUpdate("CREATE TABLE testBug3103 (field1 TIMESTAMP(3))");
            } else {
                this.stmt.executeUpdate("CREATE TABLE testBug3103 (field1 TIMESTAMP)");
            }

            PreparedStatement pStmt = this.conn.prepareStatement("INSERT INTO testBug3103 VALUES (?)");

            java.util.Date utilDate = new java.util.Date();

            pStmt.setObject(1, utilDate);
            pStmt.executeUpdate();

            this.rs = this.stmt.executeQuery("SELECT field1 FROM testBug3103");
            this.rs.next();

            java.util.Date retrUtilDate = new java.util.Date(this.rs.getTimestamp(1).getTime());

            // We can only compare on the day/month/year hour/minute/second interval, because the timestamp has added milliseconds to the internal date...
            assertTrue(utilDate.getMonth() == retrUtilDate.getMonth() && utilDate.getDate() == retrUtilDate.getDate()
                    && utilDate.getYear() == retrUtilDate.getYear() && utilDate.getHours() == retrUtilDate.getHours()
                    && utilDate.getMinutes() == retrUtilDate.getMinutes() && utilDate.getSeconds() == retrUtilDate.getSeconds(), "Dates not equal");
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3103");
        }
    }

    /**
     * Tests fix for BUG#3520
     *
     * @throws Exception
     *             ...
     */
    @Test
    public void testBug3520() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS t");
            this.stmt.executeUpdate("CREATE TABLE t (s1 int,primary key (s1))");
            this.stmt.executeUpdate("INSERT INTO t VALUES (1)");
            this.stmt.executeUpdate("INSERT INTO t VALUES (1)");
        } catch (SQLException sqlEx) {
            System.out.println(sqlEx.getSQLState());
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS t");
        }
    }

    /**
     * Tests fix for BUG#3557 -- UpdatableResultSet not picking up default values
     *
     * @throws Exception
     */
    @Test
    public void testBug3557() throws Exception {
        boolean populateDefaults = ((JdbcConnection) this.conn).getPropertySet().getBooleanProperty(PropertyKey.populateInsertRowWithDefaultValues).getValue();

        try {
            ((JdbcConnection) this.conn).getPropertySet().getBooleanProperty(PropertyKey.populateInsertRowWithDefaultValues).setValue(true);

            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3557");

            this.stmt.executeUpdate(
                    "CREATE TABLE testBug3557 (`a` varchar(255) NOT NULL default 'XYZ', `b` varchar(255) default '123', PRIMARY KEY  (`a`(100)))");

            Statement updStmt = this.conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
            this.rs = updStmt.executeQuery("SELECT * FROM testBug3557");

            assertTrue(this.rs.getConcurrency() == ResultSet.CONCUR_UPDATABLE);

            this.rs.moveToInsertRow();

            assertEquals("XYZ", this.rs.getObject(1));
            assertEquals("123", this.rs.getObject(2));
        } finally {
            ((JdbcConnection) this.conn).getPropertySet().getBooleanProperty(PropertyKey.populateInsertRowWithDefaultValues).setValue(populateDefaults);

            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3557");
        }
    }

    /**
     * Tests fix for BUG#3620 -- Timezone not respected correctly.
     *
     * @throws SQLException
     *
     */
    @Test
    @Disabled("this test is working in c/J 5.1 but fails here; disabled for later analysis")
    public void testBug3620new() throws SQLException { // TODO: should replace testBug3620()
        final long epsillon = 3000; // allow 3 seconds time difference

        TimeZone defaultTimeZone = TimeZone.getDefault();
        try {
            TimeZone.setDefault(TimeZone.getTimeZone("America/Chicago"));

            createTable("testBug3620", "(field1 TIMESTAMP) ENGINE=InnoDB");

            Properties props = new Properties();
            props.put(PropertyKey.cacheDefaultTimeZone.getKeyName(), "false");

            Connection connNoTz = getConnectionWithProps(props);
            PreparedStatement tsPstmt = connNoTz.prepareStatement("INSERT INTO testBug3620 VALUES (?)");

            Calendar pointInTime = Calendar.getInstance();
            pointInTime.set(2004, 02, 29, 10, 0, 0);
            long pointInTimeOffset = pointInTime.getTimeZone().getRawOffset();
            Timestamp ts = new Timestamp(pointInTime.getTime().getTime());

            tsPstmt.setTimestamp(1, ts);
            tsPstmt.executeUpdate();

            this.rs = connNoTz.createStatement().executeQuery("SELECT field1 FROM testBug3620");
            this.rs.next();
            String tsValueAsString = new String(this.rs.getBytes(1));
            Timestamp tsValueAsTimestamp = this.rs.getTimestamp(1);
            System.out.println("Timestamp as String, inserted with no calendar: " + tsValueAsString.toString());
            System.out.println("Timestamp as Timestamp, inserted with no calendar: " + tsValueAsTimestamp);

            connNoTz.createStatement().executeUpdate("DELETE FROM testBug3620");

            Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

            props.put("useTimezone", "true");
            props.put("serverTimezone", "UTC");

            Connection connWithTz = getConnectionWithProps(props);
            Statement tsStmt = connWithTz.createStatement();
            tsPstmt = connWithTz.prepareStatement("INSERT INTO testBug3620 VALUES (?)");

            tsPstmt.setTimestamp(1, ts, cal);
            tsPstmt.executeUpdate();

            this.rs = connNoTz.createStatement().executeQuery("SELECT field1 FROM testBug3620");
            this.rs.next();
            tsValueAsString = new String(this.rs.getBytes(1));
            tsValueAsTimestamp = this.rs.getTimestamp(1);
            System.out.println("Timestamp as String, inserted with UTC calendar: " + tsValueAsString.toString());
            System.out.println("Timestamp as Timestamp, inserted with UTC calendar: " + tsValueAsTimestamp);

            this.rs = tsStmt.executeQuery("SELECT field1 FROM testBug3620");
            this.rs.next();
            Timestamp tsValueUTC = this.rs.getTimestamp(1, cal);
            System.out.println("Timestamp specifying UTC calendar from statement: " + tsValueUTC.toString());

            // We use this testcase with other vendors, JDBC spec requires result set fields can only be read once, although MySQL doesn't require this ;)
            this.rs = tsStmt.executeQuery("SELECT field1 FROM testBug3620");
            this.rs.next();
            Timestamp tsValueStmtNoCal = this.rs.getTimestamp(1);
            System.out.println("Timestamp specifying no calendar from statement: " + tsValueStmtNoCal.toString());

            PreparedStatement tsPstmtRetr = connWithTz.prepareStatement("SELECT field1 FROM testBug3620");
            this.rs = tsPstmtRetr.executeQuery();
            this.rs.next();
            Timestamp tsValuePstmtUTC = this.rs.getTimestamp(1, cal);
            System.out.println("Timestamp specifying UTC calendar from prepared statement: " + tsValuePstmtUTC.toString());

            // We use this testcase with other vendors, JDBC spec requires result set fields can only be read once, although MySQL doesn't require this ;)
            this.rs = tsPstmtRetr.executeQuery();
            this.rs.next();
            Timestamp tsValuePstmtNoCal = this.rs.getTimestamp(1);
            System.out.println("Timestamp specifying no calendar from prepared statement: " + tsValuePstmtNoCal.toString());

            long stmtDeltaTWithCal = tsValueStmtNoCal.getTime() - ts.getTime();
            long deltaOrig = Math.abs(stmtDeltaTWithCal - pointInTimeOffset);
            assertTrue(deltaOrig < epsillon, "Difference between original timestamp and timestamp retrieved using java.sql.Statement "
                    + "set in database using UTC calendar is not ~= " + epsillon + " it is actually " + deltaOrig);

            long pStmtDeltaTWithCal = tsValuePstmtNoCal.getTime() - ts.getTime();
            deltaOrig = Math.abs(pStmtDeltaTWithCal - pointInTimeOffset);
            assertTrue(deltaOrig < epsillon, "Difference between original timestamp and timestamp retrieved using java.sql.PreparedStatement "
                    + "set in database using UTC calendar is not ~= " + epsillon + ", it is actually " + deltaOrig);

            System.out.println("Difference between original ts and ts with no calendar: " + (tsValuePstmtNoCal.getTime() - ts.getTime()) + ", offset should be "
                    + pointInTimeOffset);
        } finally {
            TimeZone.setDefault(defaultTimeZone);
        }
    }

    /**
     * Tests that DataTruncation is thrown when data is truncated.
     *
     * @throws Exception
     */
    @Test
    public void testBug3697() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3697");
            this.stmt.executeUpdate("CREATE TABLE testBug3697 (field1 VARCHAR(255))");

            StringBuilder updateBuf = new StringBuilder("INSERT INTO testBug3697 VALUES ('");

            for (int i = 0; i < 512; i++) {
                updateBuf.append("A");
            }

            updateBuf.append("')");

            try {
                this.stmt.executeUpdate(updateBuf.toString());
            } catch (DataTruncation dtEx) {
                // This is an expected exception....
            }

            SQLWarning warningChain = this.stmt.getWarnings();

            System.out.println(warningChain);
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3697");
        }
    }

    /**
     * Tests fix for BUG#3804, data truncation on server should throw
     * DataTruncation exception.
     *
     * @throws Exception
     */
    @Test
    public void testBug3804() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3804");
            this.stmt.executeUpdate("CREATE TABLE testBug3804 (field1 VARCHAR(5))");

            boolean caughtTruncation = false;

            try {
                this.stmt.executeUpdate("INSERT INTO testBug3804 VALUES ('1234567')");
            } catch (DataTruncation truncationEx) {
                caughtTruncation = true;
                System.out.println(truncationEx);
            }

            assertTrue(caughtTruncation, "Data truncation exception should've been thrown");
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3804");
        }
    }

    /**
     * Tests BUG#3873 - PreparedStatement.executeBatch() not returning all
     * generated keys (even though that's not JDBC compliant).
     *
     * @throws Exception
     */
    @Test
    public void testBug3873() throws Exception {
        PreparedStatement batchStmt = null;

        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3873");
            this.stmt.executeUpdate("CREATE TABLE testBug3873 (keyField INT NOT NULL PRIMARY KEY AUTO_INCREMENT, dataField VARCHAR(32))");
            batchStmt = this.conn.prepareStatement("INSERT INTO testBug3873 (dataField) VALUES (?)", Statement.RETURN_GENERATED_KEYS);
            batchStmt.setString(1, "abc");
            batchStmt.addBatch();
            batchStmt.setString(1, "def");
            batchStmt.addBatch();
            batchStmt.setString(1, "ghi");
            batchStmt.addBatch();

            @SuppressWarnings("unused")
            int[] updateCounts = batchStmt.executeBatch();

            this.rs = batchStmt.getGeneratedKeys();

            while (this.rs.next()) {
                System.out.println(this.rs.getInt(1));
            }

            this.rs = batchStmt.getGeneratedKeys();
            assertTrue(this.rs.next());
            assertTrue(1 == this.rs.getInt(1));
            assertTrue(this.rs.next());
            assertTrue(2 == this.rs.getInt(1));
            assertTrue(this.rs.next());
            assertTrue(3 == this.rs.getInt(1));
            assertTrue(!this.rs.next());
        } finally {
            if (batchStmt != null) {
                batchStmt.close();
            }

            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3873");
        }
    }

    /**
     * Tests fix for BUG#4119 -- misbehavior in a managed environment from
     * MVCSoft JDO
     *
     * @throws Exception
     */
    @Test
    public void testBug4119() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4119");
            this.stmt.executeUpdate("CREATE TABLE `testBug4119` (`field1` varchar(255) NOT NULL default '', `field2` bigint(20) default NULL,"
                    + "`field3` int(11) default NULL, `field4` datetime default NULL, `field5` varchar(75) default NULL,"
                    + "`field6` varchar(75) default NULL, `field7` varchar(75) default NULL, `field8` datetime default NULL,"
                    + " PRIMARY KEY  (`field1`(100)))");

            PreparedStatement pStmt = this.conn.prepareStatement(
                    "insert into testBug4119 (field2, field3, field4, field5, field6, field7, field8, field1) values (?, ?, ?, ?, ?, ?, ?, ?)");

            pStmt.setString(1, "0");
            pStmt.setString(2, "0");
            pStmt.setTimestamp(3, new java.sql.Timestamp(System.currentTimeMillis()));
            pStmt.setString(4, "ABC");
            pStmt.setString(5, "DEF");
            pStmt.setString(6, "AA");
            pStmt.setTimestamp(7, new java.sql.Timestamp(System.currentTimeMillis()));
            pStmt.setString(8, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
            pStmt.executeUpdate();
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4119");
        }
    }

    /**
     * Tests fix for BUG#4311 - Error in JDBC retrieval of mediumint column when
     * using prepared statements and binary result sets.
     *
     * @throws Exception
     */
    @Test
    public void testBug4311() throws Exception {
        try {
            int lowValue = -8388608;
            int highValue = 8388607;

            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4311");
            this.stmt.executeUpdate("CREATE TABLE testBug4311 (low MEDIUMINT, high MEDIUMINT)");
            this.stmt.executeUpdate("INSERT INTO testBug4311 VALUES (" + lowValue + ", " + highValue + ")");

            PreparedStatement pStmt = this.conn.prepareStatement("SELECT low, high FROM testBug4311");
            this.rs = pStmt.executeQuery();
            assertTrue(this.rs.next());
            assertTrue(this.rs.getInt(1) == lowValue);
            assertTrue(this.rs.getInt(2) == highValue);
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4311");
        }
    }

    /**
     * Tests fix for BUG#4510 -- Statement.getGeneratedKeys() fails when key >
     * 32767
     *
     * @throws Exception
     */
    @Test
    public void testBug4510() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4510");
            this.stmt.executeUpdate("CREATE TABLE testBug4510 (field1 INT NOT NULL PRIMARY KEY AUTO_INCREMENT, field2 VARCHAR(100))");
            this.stmt.executeUpdate("INSERT INTO testBug4510 (field1, field2) VALUES (32767, 'bar')");

            PreparedStatement p = this.conn.prepareStatement("insert into testBug4510 (field2) values (?)", Statement.RETURN_GENERATED_KEYS);

            p.setString(1, "blah");

            p.executeUpdate();

            ResultSet genKeysRs = p.getGeneratedKeys();
            genKeysRs.next();
            System.out.println("Id: " + genKeysRs.getInt(1));
            genKeysRs.close();
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4510");
        }
    }

    /**
     * Server doesn't accept everything as a server-side prepared statement, so
     * by default we scan for stuff it can't handle.
     *
     * @throws SQLException
     */
    @Test
    public void testBug4718() throws SQLException {
        if (((com.mysql.cj.jdbc.JdbcConnection) this.conn).getPropertySet().getBooleanProperty(PropertyKey.useServerPrepStmts).getValue()) {
            this.pstmt = this.conn.prepareStatement("SELECT 1 LIMIT ?");
            assertTrue(this.pstmt instanceof ClientPreparedStatement);

            this.pstmt = this.conn.prepareStatement("SELECT 1 LIMIT 1");
            assertTrue(this.pstmt instanceof com.mysql.cj.jdbc.ServerPreparedStatement);

            this.pstmt = this.conn.prepareStatement("SELECT 1 LIMIT 1, ?");
            assertTrue(this.pstmt instanceof ClientPreparedStatement);

            try {
                this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4718");
                this.stmt.executeUpdate("CREATE TABLE testBug4718 (field1 char(32))");

                this.pstmt = this.conn.prepareStatement("ALTER TABLE testBug4718 ADD INDEX (field1)");
                assertTrue(this.pstmt instanceof ClientPreparedStatement);

                this.pstmt = this.conn.prepareStatement("SELECT 1");
                assertTrue(this.pstmt instanceof ServerPreparedStatement);

                this.pstmt = this.conn.prepareStatement("UPDATE testBug4718 SET field1=1");
                assertTrue(this.pstmt instanceof ServerPreparedStatement);

                this.pstmt = this.conn.prepareStatement("UPDATE testBug4718 SET field1=1 LIMIT 1");
                assertTrue(this.pstmt instanceof ServerPreparedStatement);

                this.pstmt = this.conn.prepareStatement("UPDATE testBug4718 SET field1=1 LIMIT ?");
                assertTrue(this.pstmt instanceof ClientPreparedStatement);

                this.pstmt = this.conn.prepareStatement("UPDATE testBug4718 SET field1='Will we ignore LIMIT ?,?'");
                assertTrue(this.pstmt instanceof ServerPreparedStatement);

            } finally {
                this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4718");
            }
        }
    }

    /**
     * Tests fix for BUG#5012 -- ServerPreparedStatements dealing with return of
     * DECIMAL type don't work.
     *
     * @throws Exception
     */
    @Test
    public void testBug5012() throws Exception {
        PreparedStatement pStmt = null;
        String valueAsString = "12345.12";

        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug5012");
            this.stmt.executeUpdate("CREATE TABLE testBug5012(field1 DECIMAL(10,2))");
            this.stmt.executeUpdate("INSERT INTO testBug5012 VALUES (" + valueAsString + ")");

            pStmt = this.conn.prepareStatement("SELECT field1 FROM testBug5012");
            this.rs = pStmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(new BigDecimal(valueAsString), this.rs.getBigDecimal(1));
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug5012");

            if (pStmt != null) {
                pStmt.close();
            }
        }
    }

    /**
     * Tests fix for BUG#5133 -- PreparedStatement.toString() doesn't return
     * correct value if no parameters are present in statement.
     *
     * @throws Exception
     */
    @Test
    public void testBug5133() throws Exception {
        String query = "SELECT 1";
        String output = this.conn.prepareStatement(query).toString();
        System.out.println(output);

        assertTrue(output.indexOf(query) != -1);
    }

    /**
     * Tests for BUG#5191 -- PreparedStatement.executeQuery() gives
     * OutOfMemoryError
     *
     * @throws Exception
     */
    @Test
    public void testBug5191() throws Exception {
        PreparedStatement pStmt = null;

        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug5191Q");
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug5191C");

            this.stmt.executeUpdate("CREATE TABLE testBug5191Q (QuestionId int NOT NULL AUTO_INCREMENT, Text VARCHAR(200), PRIMARY KEY(QuestionId))");

            this.stmt.executeUpdate("CREATE TABLE testBug5191C (CategoryId int, QuestionId int)");

            String[] questions = new String[] { "What is your name?", "What is your quest?", "What is the airspeed velocity of an unladen swollow?",
                    "How many roads must a man walk?", "Where's the tea?", };

            for (int i = 0; i < questions.length; i++) {
                this.stmt.executeUpdate("INSERT INTO testBug5191Q(Text) VALUES (\"" + questions[i] + "\")");
                int catagory = i < 3 ? 0 : i;

                this.stmt.executeUpdate("INSERT INTO testBug5191C (CategoryId, QuestionId) VALUES (" + catagory + ", " + i + ")");
                /*
                 * this.stmt.executeUpdate("INSERT INTO testBug5191C" +
                 * "(CategoryId, QuestionId) VALUES (" + catagory + ", (SELECT
                 * testBug5191Q.QuestionId" + " FROM testBug5191Q " + "WHERE
                 * testBug5191Q.Text LIKE '" + questions[i] + "'))");
                 */
            }

            pStmt = this.conn.prepareStatement(
                    "SELECT qc.QuestionId, q.Text FROM testBug5191Q q, testBug5191C qc WHERE qc.CategoryId = ? AND q.QuestionId = qc.QuestionId");

            int catId = 0;
            for (int i = 0; i < 100; i++) {
                execQueryBug5191(pStmt, catId);
            }

        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug5191Q");
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug5191C");

            if (pStmt != null) {
                pStmt.close();
            }
        }
    }

    /**
     * Tests for BUG#5235, ClassCastException on all-zero date field when
     * zeroDatetimeBehavior is 'CONVERT_TO_NULL'.
     *
     * @throws Exception
     */
    @Test
    public void testBug5235() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.zeroDateTimeBehavior.getKeyName(), "CONVERT_TO_NULL");
        if (versionMeetsMinimum(5, 7, 4)) {
            props.setProperty(PropertyKey.jdbcCompliantTruncation.getKeyName(), "false");
        }

        if (versionMeetsMinimum(5, 7, 5)) {
            String sqlMode = getMysqlVariable("sql_mode");
            if (sqlMode.contains("STRICT_TRANS_TABLES")) {
                sqlMode = removeSqlMode("STRICT_TRANS_TABLES", sqlMode);
                props.setProperty(PropertyKey.sessionVariables.getKeyName(), "sql_mode='" + sqlMode + "'");
            }
        }

        Connection convertToNullConn = getConnectionWithProps(props);
        Statement convertToNullStmt = convertToNullConn.createStatement();
        try {
            convertToNullStmt.executeUpdate("DROP TABLE IF EXISTS testBug5235");
            convertToNullStmt.executeUpdate("CREATE TABLE testBug5235(field1 DATE)");
            convertToNullStmt.executeUpdate("INSERT INTO testBug5235 (field1) VALUES ('0000-00-00')");

            PreparedStatement ps = convertToNullConn.prepareStatement("SELECT field1 FROM testBug5235");
            this.rs = ps.executeQuery();

            if (this.rs.next()) {
                assertNull(this.rs.getObject("field1"));
            }
        } finally {
            convertToNullStmt.executeUpdate("DROP TABLE IF EXISTS testBug5235");
        }
    }

    @Test
    public void testBug5450() throws Exception {
        String table = "testBug5450";
        String column = "policyname";

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.characterEncoding.getKeyName(), "utf-8");

            Connection utf8Conn = getConnectionWithProps(props);
            Statement utfStmt = utf8Conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);

            this.stmt.executeUpdate("DROP TABLE IF EXISTS " + table);

            this.stmt.executeUpdate("CREATE TABLE " + table + "(policyid int NOT NULL AUTO_INCREMENT, " + column + " VARCHAR(200), "
                    + "PRIMARY KEY(policyid)) DEFAULT CHARACTER SET utf8");

            String pname0 = "inserted \uac00 - foo - \u4e00";

            utfStmt.executeUpdate("INSERT INTO " + table + "(" + column + ") VALUES (\"" + pname0 + "\")");

            this.rs = utfStmt.executeQuery("SELECT " + column + " FROM " + table);

            this.rs.first();
            String pname1 = this.rs.getString(column);

            assertEquals(pname0, pname1);
            byte[] bytes = this.rs.getBytes(column);

            String pname2 = new String(bytes, "utf-8");
            assertEquals(pname1, pname2);

            utfStmt.executeUpdate("delete from " + table + " where " + column + " like 'insert%'");

            PreparedStatement s1 = utf8Conn.prepareStatement("insert into " + table + "(" + column + ") values (?)");

            s1.setString(1, pname0);
            s1.executeUpdate();

            String byteesque = "byte " + pname0;
            byte[] newbytes = byteesque.getBytes("utf-8");

            s1.setBytes(1, newbytes);
            s1.executeUpdate();

            this.rs = utfStmt.executeQuery("select " + column + " from " + table + " where " + column + " like 'insert%'");
            this.rs.first();
            String pname3 = this.rs.getString(column);
            assertEquals(pname0, pname3);

            this.rs = utfStmt.executeQuery("select " + column + " from " + table + " where " + column + " like 'byte insert%'");
            this.rs.first();

            String pname4 = this.rs.getString(column);
            assertEquals(byteesque, pname4);

        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS " + table);
        }
    }

    @Test
    public void testBug5510() throws Exception {
        createTable("`testBug5510`",
                "(`a` bigint(20) NOT NULL auto_increment, `b` varchar(64) default NULL, `c` varchar(64) default NULL,"
                        + "`d` varchar(255) default NULL, `e` int(11) default NULL, `f` varchar(32) default NULL, `g` varchar(32) default NULL,"
                        + "`h` varchar(80) default NULL, `i` varchar(255) default NULL, `j` varchar(255) default NULL, `k` varchar(255) default NULL,"
                        + "`l` varchar(32) default NULL, `m` varchar(32) default NULL, `n` timestamp NOT NULL default CURRENT_TIMESTAMP on update"
                        + " CURRENT_TIMESTAMP, `o` int(11) default NULL, `p` int(11) default NULL, PRIMARY KEY  (`a`)) DEFAULT CHARSET=latin1",
                "InnoDB ");
        PreparedStatement pStmt = this.conn.prepareStatement("INSERT INTO testBug5510 (a) VALUES (?)");
        pStmt.setNull(1, 0);
        pStmt.executeUpdate();
    }

    @Test
    public void testBug6823() throws SQLException {
        innerBug6823(true);
        innerBug6823(false);
    }

    @Test
    public void testBug7461() throws Exception {
        String tableName = "testBug7461";

        try {
            createTable(tableName, "(field1 varchar(4))");
            File tempFile = File.createTempFile("mysql-test", ".txt");
            tempFile.deleteOnExit();

            FileOutputStream fOut = new FileOutputStream(tempFile);
            fOut.write("abcdefghijklmnop".getBytes());
            fOut.close();

            try {
                this.stmt.executeQuery("LOAD DATA LOCAL INFILE '" + tempFile.toString() + "' INTO TABLE " + tableName);
            } catch (SQLException sqlEx) {
                this.stmt.getWarnings();
            }

        } finally {
            dropTable(tableName);
        }
    }

    @Test
    public void testBug8181() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug8181");
            this.stmt.executeUpdate("CREATE TABLE testBug8181(col1 VARCHAR(20),col2 INT)");

            this.pstmt = this.conn.prepareStatement("INSERT INTO testBug8181(col1,col2) VALUES(?,?)");

            for (int i = 0; i < 20; i++) {
                this.pstmt.setString(1, "Test " + i);
                this.pstmt.setInt(2, i);
                this.pstmt.addBatch();
            }

            this.pstmt.executeBatch();

        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug8181");

            if (this.pstmt != null) {
                this.pstmt.close();
            }
        }
    }

    /**
     * Tests fix for BUG#8487 - PreparedStatements not creating streaming result
     * sets.
     *
     * @throws Exception
     */
    @Test
    public void testBug8487() throws Exception {
        try {
            this.pstmt = this.conn.prepareStatement("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);

            this.pstmt.setFetchSize(Integer.MIN_VALUE);
            this.rs = this.pstmt.executeQuery();
            try {
                this.rs = this.conn.createStatement().executeQuery("SELECT 2");
                fail("Should have caught a streaming exception here");
            } catch (SQLException sqlEx) {
                assertTrue(sqlEx.getMessage() != null && sqlEx.getMessage().indexOf("Streaming") != -1);
            }

        } finally {
            if (this.rs != null) {
                while (this.rs.next()) {
                }

                this.rs.close();
            }

            if (this.pstmt != null) {
                this.pstmt.close();
            }
        }
    }

    /**
     * Tests multiple statement support with fix for BUG#9704.
     *
     * @throws Exception
     */
    @Test
    public void testBug9704() throws Exception {
        Connection multiStmtConn = null;
        Statement multiStmt = null;

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), "true");

            multiStmtConn = getConnectionWithProps(props);

            multiStmt = multiStmtConn.createStatement();

            multiStmt.executeUpdate("DROP TABLE IF EXISTS testMultiStatements");
            multiStmt.executeUpdate("CREATE TABLE testMultiStatements (field1 VARCHAR(255), field2 INT, field3 DOUBLE)");
            multiStmt.executeUpdate("INSERT INTO testMultiStatements VALUES ('abcd', 1, 2)");

            multiStmt.execute("SELECT field1 FROM testMultiStatements WHERE field1='abcd'; UPDATE testMultiStatements SET field3=3;"
                    + "SELECT field3 FROM testMultiStatements WHERE field3=3");

            this.rs = multiStmt.getResultSet();

            assertTrue(this.rs.next());

            assertTrue("abcd".equals(this.rs.getString(1)));
            this.rs.close();

            // Next should be an update count...
            assertTrue(!multiStmt.getMoreResults());

            assertTrue(multiStmt.getUpdateCount() == 1, "Update count was " + multiStmt.getUpdateCount() + ", expected 1");

            assertTrue(multiStmt.getMoreResults());

            this.rs = multiStmt.getResultSet();

            assertTrue(this.rs.next());

            assertTrue(this.rs.getDouble(1) == 3);

            // End of multi results
            assertTrue(!multiStmt.getMoreResults());
            assertTrue(multiStmt.getUpdateCount() == -1);
        } finally {
            if (multiStmt != null) {
                multiStmt.executeUpdate("DROP TABLE IF EXISTS testMultiStatements");

                multiStmt.close();
            }

            if (multiStmtConn != null) {
                multiStmtConn.close();
            }
        }
    }

    /**
     * Tests that you can close a statement twice without an NPE.
     *
     * @throws Exception
     */
    @Test
    public void testCloseTwice() throws Exception {
        Statement closeMe = this.conn.createStatement();
        closeMe.close();
        closeMe.close();
    }

    /**
     * Tests all forms of statements influencing getGeneratedKeys().
     *
     * @throws Exception
     */
    @Test
    public void testGetGeneratedKeysAllCases() throws Exception {
        System.out.println("Using Statement.executeUpdate()\n");

        try {
            createGGKTables();

            // Do the tests
            for (int i = 0; i < tests.length; i++) {
                doGGKTestStatement(tests[i], true);
            }
        } finally {
            dropGGKTables();
        }

        nextID = 1;
        count = 0;

        System.out.println("Using Statement.execute()\n");

        try {
            createGGKTables();

            // Do the tests
            for (int i = 0; i < tests.length; i++) {
                doGGKTestStatement(tests[i], false);
            }
        } finally {
            dropGGKTables();
        }

        nextID = 1;
        count = 0;

        System.out.println("Using PreparedStatement.executeUpdate()\n");

        try {
            createGGKTables();

            // Do the tests
            for (int i = 0; i < tests.length; i++) {
                doGGKTestPreparedStatement(tests[i], true);
            }
        } finally {
            dropGGKTables();
        }

        nextID = 1;
        count = 0;

        System.out.println("Using PreparedStatement.execute()\n");

        try {
            createGGKTables();

            // Do the tests
            for (int i = 0; i < tests.length; i++) {
                doGGKTestPreparedStatement(tests[i], false);
            }
        } finally {
            dropGGKTables();
        }
    }

    /**
     * Tests that max_rows and 'limit' don't cause exceptions to be thrown.
     *
     * @throws Exception
     */
    @Test
    public void testLimitAndMaxRows() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testMaxRowsAndLimit");
            this.stmt.executeUpdate("CREATE TABLE testMaxRowsAndLimit(limitField INT)");

            for (int i = 0; i < 500; i++) {
                this.stmt.executeUpdate("INSERT INTO testMaxRowsAndLimit VALUES (" + i + ")");
            }

            this.stmt.setMaxRows(250);
            this.rs = this.stmt.executeQuery("SELECT limitField FROM testMaxRowsAndLimit");
        } finally {
            this.stmt.setMaxRows(0);

            this.stmt.executeUpdate("DROP TABLE IF EXISTS testMaxRowsAndLimit");
        }
    }

    /*
     * public void testBug9595() throws Exception { double[] vals = new double[]
     * {52.21, 52.22, 52.23, 52.24};
     *
     * createTable("testBug9595", "(field1 DECIMAL(10,2), sortField INT)");
     *
     * this.pstmt = this.conn.prepareStatement("INSERT INTO testBug9595 VALUES
     * (?, ?)"); // Try setting as doubles for (int i = 0; i < vals.length; i++)
     * { this.pstmt.setDouble(1, vals[i]); this.pstmt.setInt(2, i);
     * this.pstmt.executeUpdate(); }
     *
     * this.pstmt = this.conn.prepareStatement("SELECT field1 FROM testBug9595
     * ORDER BY sortField"); this.rs = this.pstmt.executeQuery();
     *
     * int i = 0;
     *
     * while (this.rs.next()) { double valToTest = vals[i++];
     *
     * assertEquals(this.rs.getDouble(1), valToTest, 0.001);
     * assertEquals(this.rs.getBigDecimal(1).doubleValue(), valToTest, 0.001); }
     *
     * this.pstmt = this.conn.prepareStatement("INSERT INTO testBug9595 VALUES
     * (?, ?)");
     *
     * this.stmt.executeUpdate("TRUNCATE TABLE testBug9595"); // Now, as
     * BigDecimals for (i = 0; i < vals.length; i++) { BigDecimal foo = new
     * BigDecimal(vals[i]);
     *
     * this.pstmt.setObject(1, foo, Types.DECIMAL, 2); this.pstmt.setInt(2, i);
     * this.pstmt.executeUpdate(); }
     *
     * this.pstmt = this.conn.prepareStatement("SELECT field1 FROM testBug9595
     * ORDER BY sortField"); this.rs = this.pstmt.executeQuery();
     *
     * i = 0;
     *
     * while (this.rs.next()) { double valToTest = vals[i++];
     * System.out.println(this.rs.getString(1));
     * assertEquals(this.rs.getDouble(1), valToTest, 0.001);
     * assertEquals(this.rs.getBigDecimal(1).doubleValue(), valToTest, 0.001); }
     * }
     */

    /**
     * Tests that 'LOAD DATA LOCAL INFILE' works
     *
     * @throws Exception
     */
    @Test
    public void testLoadData() throws Exception {
        assumeTrue(supportsLoadLocalInfile(this.stmt), "This test requires the server started with --local-infile=ON");

        createTable("loadDataRegress", "(field1 int, field2 int)");

        //int maxAllowedPacket = 1048576;

        File tempFile = File.createTempFile("mysql", ".txt");

        // tempFile.deleteOnExit();
        System.out.println(tempFile);

        Writer out = new FileWriter(tempFile);

        int localCount = 0;
        int rowCount = 128; // maxAllowedPacket * 4;

        for (int i = 0; i < rowCount; i++) {
            out.write(localCount++ + "\t" + localCount++ + "\n");
        }

        out.close();

        StringBuilder fileNameBuf = null;

        if (File.separatorChar == '\\') {
            fileNameBuf = new StringBuilder();

            String fileName = tempFile.getAbsolutePath();
            int fileNameLength = fileName.length();

            for (int i = 0; i < fileNameLength; i++) {
                char c = fileName.charAt(i);

                if (c == '\\') {
                    fileNameBuf.append("/");
                } else {
                    fileNameBuf.append(c);
                }
            }
        } else {
            fileNameBuf = new StringBuilder(tempFile.getAbsolutePath());
        }
        final String fileName = fileNameBuf.toString();

        assertThrows(SQLSyntaxErrorException.class,
                versionMeetsMinimum(8, 0, 19) ? "Loading local data is disabled;.*" : "The used command is not allowed with this MySQL version", () -> {
                    this.stmt.executeUpdate("LOAD DATA LOCAL INFILE '" + fileName + "' INTO TABLE loadDataRegress CHARACTER SET "
                            + CharsetMappingWrapper.getStaticMysqlCharsetForJavaEncoding(
                                    ((MysqlConnection) this.conn).getPropertySet().getStringProperty(PropertyKey.characterEncoding).getValue(),
                                    this.serverVersion));
                    return null;
                });

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.allowLoadLocalInfile.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);
        int updateCount = testConn.createStatement()
                .executeUpdate("LOAD DATA LOCAL INFILE '" + fileNameBuf.toString() + "' INTO TABLE loadDataRegress CHARACTER SET "
                        + CharsetMappingWrapper.getStaticMysqlCharsetForJavaEncoding(
                                ((MysqlConnection) this.conn).getPropertySet().getStringProperty(PropertyKey.characterEncoding).getValue(),
                                this.serverVersion));
        assertTrue(updateCount == rowCount);
    }

    @Test
    public void testNullClob() throws Exception {
        createTable("testNullClob", "(field1 TEXT NULL)");

        PreparedStatement pStmt = null;

        try {
            pStmt = this.conn.prepareStatement("INSERT INTO testNullClob VALUES (?)");
            pStmt.setClob(1, (Clob) null);
            pStmt.executeUpdate();
        } finally {
            if (pStmt != null) {
                pStmt.close();
            }
        }
    }

    /**
     * Tests fix for BUG#1658
     *
     * @throws Exception
     */
    @Test
    public void testParameterBoundsCheck() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testParameterBoundsCheck");
            this.stmt.executeUpdate("CREATE TABLE testParameterBoundsCheck(f1 int, f2 int, f3 int, f4 int, f5 int)");

            PreparedStatement _pstmt = this.conn.prepareStatement("UPDATE testParameterBoundsCheck SET f1=?, f2=?,f3=?,f4=? WHERE f5=?");

            _pstmt.setString(1, "");
            _pstmt.setString(2, "");

            try {
                _pstmt.setString(25, "");
            } catch (SQLException sqlEx) {
                assertTrue(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT.equals(sqlEx.getSQLState()));
            }
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testParameterBoundsCheck");
        }
    }

    @Test
    public void testPStmtTypesBug() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testPStmtTypesBug");
            this.stmt.executeUpdate("CREATE TABLE testPStmtTypesBug(field1 INT)");
            this.pstmt = this.conn.prepareStatement("INSERT INTO testPStmtTypesBug VALUES (?)");
            this.pstmt.setObject(1, null, Types.INTEGER);
            this.pstmt.addBatch();
            this.pstmt.setInt(1, 1);
            this.pstmt.addBatch();
            this.pstmt.executeBatch();

        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testPStmtTypesBug");
        }
    }

    /**
     * Tests for BUG#9288, parameter index out of range if LIKE, ESCAPE '\'
     * present in query.
     *
     * @throws Exception
     */
    /*
     * public void testBug9288() throws Exception { String tableName =
     * "testBug9288"; PreparedStatement pStmt = null;
     *
     * try { createTable(tableName, "(field1 VARCHAR(32), field2 INT)"); pStmt =
     * ((com.mysql.jdbc.Connection)this.conn).clientPrepareStatement( "SELECT
     * COUNT(1) FROM " + tableName + " WHERE " + "field1 LIKE '%' ESCAPE '\\'
     * AND " + "field2 > ?"); pStmt.setInt(1, 0);
     *
     * this.rs = pStmt.executeQuery(); } finally { if (this.rs != null) {
     * this.rs.close(); this.rs = null; }
     *
     * if (pStmt != null) { pStmt.close(); } } }
     */

    /*
     * public void testBug10999() throws Exception { if (versionMeetsMinimum(5,
     * 0, 5)) {
     *
     * String tableName = "testBug10999"; String updateTrigName =
     * "testBug10999Update"; String insertTrigName = "testBug10999Insert"; try {
     * createTable(tableName, "(pkfield INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
     * field1 VARCHAR(32))");
     *
     * try { this.stmt.executeUpdate("DROP TRIGGER " + updateTrigName); } catch
     * (SQLException sqlEx) { // ignore for now }
     *
     * this.stmt.executeUpdate("CREATE TRIGGER " + updateTrigName + " AFTER
     * UPDATE ON " + tableName + " FOR EACH ROW " + "BEGIN " + "END");
     *
     * try { this.stmt.executeUpdate("DROP TRIGGER " + insertTrigName); } catch
     * (SQLException sqlEx) { // ignore }
     *
     * this.stmt.executeUpdate("CREATE TRIGGER " + insertTrigName + " AFTER
     * INSERT ON " + tableName + " FOR EACH ROW " + " BEGIN " + "END");
     *
     * this.conn.setAutoCommit(false);
     *
     * String updateSQL = "INSERT INTO " + tableName + " (field1) VALUES
     * ('abcdefg')"; int rowCount = this.stmt.executeUpdate(updateSQL,
     * Statement.RETURN_GENERATED_KEYS);
     *
     * this.rs = stmt.getGeneratedKeys(); if (rs.next()) {
     * System.out.println(rs.getInt(1)); int id = rs.getInt(1); //if
     * (log.isDebugEnabled()) // log.debug("Retrieved ID = " + id); } //else {
     * //log.error("Can't retrieve ID with getGeneratedKeys."); // Retrieve ID
     * using a SELECT statement instead. // querySQL = "SELECT id from tab1
     * WHERE ...";
     *
     * //if (log.isDebugEnabled()) // log.debug(querySQL);
     *
     * //rs = stmt.executeQuery(querySQL); this.rs =
     * this.stmt.executeQuery("SELECT pkfield FROM " + tableName); } finally {
     * this.conn.setAutoCommit(true);
     *
     * try { this.stmt.executeUpdate("DROP TRIGGER IF EXISTS " +
     * insertTrigName); } catch (SQLException sqlEx) { // ignore }
     *
     * try { this.stmt.executeUpdate("DROP TRIGGER IF EXISTS " +
     * updateTrigName); } catch (SQLException sqlEx) { // ignore } } } }
     */

    /**
     * Tests that binary dates/times are encoded/decoded correctly.
     *
     * @throws Exception
     *
     * @deprecated because we need to use this particular constructor for the
     *             date class, as Calendar-constructed dates don't pass the
     *             .equals() test :(
     */
    @Deprecated
    @Test
    public void testServerPrepStmtAndDate() throws Exception {
        createTable("testServerPrepStmtAndDate",
                "(`P_ID` int(10) NOT NULL default '0', `R_Date` date default NULL, UNIQUE KEY `P_ID` (`P_ID`), KEY `R_Date` (`R_Date`))");
        Date dt = new java.sql.Date(102, 1, 2); // Note, this represents the date 2002-02-02

        PreparedStatement pStmt2 = this.conn.prepareStatement("INSERT INTO testServerPrepStmtAndDate (P_ID, R_Date) VALUES (171576, ?)");
        pStmt2.setDate(1, dt);
        pStmt2.executeUpdate();
        pStmt2.close();

        this.rs = this.stmt.executeQuery("SELECT R_Date FROM testServerPrepStmtAndDate");
        this.rs.next();

        System.out.println("Date that was stored (as String) " + this.rs.getString(1)); // comes back as 2002-02-02

        PreparedStatement pStmt = this.conn.prepareStatement("Select P_ID,R_Date from testServerPrepStmtAndDate Where R_Date = ?   and P_ID = 171576");
        pStmt.setDate(1, dt);

        this.rs = pStmt.executeQuery();

        assertTrue(this.rs.next());

        assertEquals("171576", this.rs.getString(1));

        assertEquals(dt, this.rs.getDate(2));
    }

    @Test
    public void testServerPrepStmtDeadlock() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        Connection c = getConnectionWithProps(props);

        Thread testThread1 = new PrepareThread(c);
        Thread testThread2 = new PrepareThread(c);
        testThread1.start();
        testThread2.start();
        Thread.sleep(30000);
        assertTrue(this.testServerPrepStmtDeadlockCounter >= 10);
    }

    /**
     * Tests PreparedStatement.setCharacterStream() to ensure it accepts > 4K
     * streams
     *
     * @throws Exception
     */
    @Test
    public void testSetCharacterStream() throws Exception {
        try {
            ((com.mysql.cj.jdbc.JdbcConnection) this.conn).getPropertySet().getBooleanProperty(PropertyKey.traceProtocol).setValue(true);

            this.stmt.executeUpdate("DROP TABLE IF EXISTS charStreamRegressTest");
            this.stmt.executeUpdate("CREATE TABLE charStreamRegressTest(field1 text)");

            this.pstmt = this.conn.prepareStatement("INSERT INTO charStreamRegressTest VALUES (?)");

            // char[] charBuf = new char[16384];
            char[] charBuf = new char[32];

            for (int i = 0; i < charBuf.length; i++) {
                charBuf[i] = 'A';
            }

            CharArrayReader reader = new CharArrayReader(charBuf);

            this.pstmt.setCharacterStream(1, reader, charBuf.length);
            this.pstmt.executeUpdate();

            this.rs = this.stmt.executeQuery("SELECT LENGTH(field1) FROM charStreamRegressTest");

            this.rs.next();

            System.out.println("Character stream length: " + this.rs.getString(1));

            this.rs = this.stmt.executeQuery("SELECT field1 FROM charStreamRegressTest");

            this.rs.next();

            String result = this.rs.getString(1);

            assertTrue(result.length() == charBuf.length);

            this.stmt.execute("TRUNCATE TABLE charStreamRegressTest");

            // Test that EOF is not thrown
            reader = new CharArrayReader(charBuf);
            this.pstmt.clearParameters();
            this.pstmt.setCharacterStream(1, reader, charBuf.length);
            this.pstmt.executeUpdate();

            this.rs = this.stmt.executeQuery("SELECT LENGTH(field1) FROM charStreamRegressTest");

            this.rs.next();

            System.out.println("Character stream length: " + this.rs.getString(1));

            this.rs = this.stmt.executeQuery("SELECT field1 FROM charStreamRegressTest");

            this.rs.next();

            result = this.rs.getString(1);

            assertTrue(result.length() == charBuf.length, "Retrieved value of length " + result.length() + " != length of inserted value " + charBuf.length);

            // Test single quotes inside identifers
            this.stmt.executeUpdate("DROP TABLE IF EXISTS `charStream'RegressTest`");
            this.stmt.executeUpdate("CREATE TABLE `charStream'RegressTest`(field1 text)");

            this.pstmt = this.conn.prepareStatement("INSERT INTO `charStream'RegressTest` VALUES (?)");

            reader = new CharArrayReader(charBuf);
            this.pstmt.setCharacterStream(1, reader, charBuf.length * 2);
            this.pstmt.executeUpdate();

            this.rs = this.stmt.executeQuery("SELECT field1 FROM `charStream'RegressTest`");

            this.rs.next();

            result = this.rs.getString(1);

            assertTrue(result.length() == charBuf.length, "Retrieved value of length " + result.length() + " != length of inserted value " + charBuf.length);
        } finally {
            ((com.mysql.cj.jdbc.JdbcConnection) this.conn).getPropertySet().getBooleanProperty(PropertyKey.traceProtocol).setValue(false);

            if (this.rs != null) {
                try {
                    this.rs.close();
                } catch (Exception ex) {
                    // ignore
                }

                this.rs = null;
            }

            this.stmt.executeUpdate("DROP TABLE IF EXISTS `charStream'RegressTest`");
            this.stmt.executeUpdate("DROP TABLE IF EXISTS charStreamRegressTest");
        }
    }

    /**
     * Tests a bug where Statement.setFetchSize() does not work for values other
     * than 0 or Integer.MIN_VALUE
     *
     * @throws Exception
     */
    @Test
    public void testSetFetchSize() throws Exception {
        int oldFetchSize = this.stmt.getFetchSize();

        try {
            this.stmt.setFetchSize(10);
        } finally {
            this.stmt.setFetchSize(oldFetchSize);
        }
    }

    /**
     * Tests fix for BUG#907
     *
     * @throws Exception
     */
    @Test
    public void testSetMaxRows() throws Exception {
        Statement maxRowsStmt = null;

        try {
            maxRowsStmt = this.conn.createStatement();
            maxRowsStmt.setMaxRows(1);
            this.rs = maxRowsStmt.executeQuery("SELECT 1");
        } finally {
            if (maxRowsStmt != null) {
                maxRowsStmt.close();
            }
        }
    }

    /**
     * Tests for timestamp NPEs occuring in binary-format timestamps.
     *
     * @throws Exception
     *
     * @deprecated yes, we know we are using deprecated methods here :)
     */
    @Deprecated
    @Test
    public void testTimestampNPE() throws Exception {
        try {
            Timestamp ts = new Timestamp(System.currentTimeMillis());

            this.stmt.executeUpdate("DROP TABLE IF EXISTS testTimestampNPE");
            this.stmt.executeUpdate("CREATE TABLE testTimestampNPE (field1 TIMESTAMP)");

            this.pstmt = this.conn.prepareStatement("INSERT INTO testTimestampNPE VALUES (?)");
            this.pstmt.setTimestamp(1, ts);
            this.pstmt.executeUpdate();

            this.pstmt = this.conn.prepareStatement("SELECT field1 FROM testTimestampNPE");

            this.rs = this.pstmt.executeQuery();

            this.rs.next();

            System.out.println(this.rs.getString(1));

            this.rs.getDate(1);

            Timestamp rTs = this.rs.getTimestamp(1);
            assertTrue(rTs.getYear() == ts.getYear(), "Retrieved year of " + rTs.getYear() + " does not match " + ts.getYear());
            assertTrue(rTs.getMonth() == ts.getMonth(), "Retrieved month of " + rTs.getMonth() + " does not match " + ts.getMonth());
            assertTrue(rTs.getDate() == ts.getDate(), "Retrieved date of " + rTs.getDate() + " does not match " + ts.getDate());

            this.stmt.executeUpdate("DROP TABLE IF EXISTS testTimestampNPE");

        } finally {
        }
    }

    @Test
    public void testTruncationWithChar() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testTruncationWithChar");
            this.stmt.executeUpdate("CREATE TABLE testTruncationWithChar (field1 char(2))");

            this.pstmt = this.conn.prepareStatement("INSERT INTO testTruncationWithChar VALUES (?)");
            this.pstmt.setString(1, "00");
            this.pstmt.executeUpdate();
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testTruncationWithChar");
        }
    }

    /**
     * Tests fix for updatable streams being supported in updatable result sets.
     *
     * @throws Exception
     */
    @Test
    public void testUpdatableStream() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS updateStreamTest");
            this.stmt.executeUpdate("CREATE TABLE updateStreamTest (keyField INT NOT NULL AUTO_INCREMENT PRIMARY KEY, field1 BLOB)");

            int streamLength = 16385;
            byte[] streamData = new byte[streamLength];

            /* create an updatable statement */
            Statement updStmt = this.conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);

            /* fill the resultset with some values */
            ResultSet updRs = updStmt.executeQuery("SELECT * FROM updateStreamTest");

            /* move to insertRow */
            updRs.moveToInsertRow();

            /* update the table */
            updRs.updateBinaryStream("field1", new ByteArrayInputStream(streamData), streamLength);

            updRs.insertRow();

            assertTrue(updRs.next());
            assertEquals(1, updRs.getInt(1));
            assertTrue(Arrays.equals(streamData, updRs.getBytes(2)));

        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS updateStreamTest");
        }
    }

    /**
     * Tests fix for BUG#15383 - PreparedStatement.setObject() serializes BigInteger as object, rather than sending as numeric value (and is thus not
     * complementary to .getObject() on an UNSIGNED LONG type).
     *
     * @throws Exception
     */
    @Test
    public void testBug15383() throws Exception {
        createTable("testBug15383", "(id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,value BIGINT UNSIGNED NULL DEFAULT 0,PRIMARY KEY(id))", "InnoDB");

        this.stmt.executeUpdate("INSERT INTO testBug15383(value) VALUES(1)");

        Statement updatableStmt = this.conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);

        try {
            this.rs = updatableStmt.executeQuery("SELECT * from testBug15383");

            assertTrue(this.rs.next());

            Object bigIntObj = this.rs.getObject("value");
            assertEquals("java.math.BigInteger", bigIntObj.getClass().getName());

            this.rs.updateObject("value", new BigInteger("3"));
            this.rs.updateRow();

            assertEquals("3", this.rs.getString("value"));
        } finally {
            if (this.rs != null) {
                ResultSet toClose = this.rs;
                this.rs = null;
                toClose.close();
            }

            if (updatableStmt != null) {
                updatableStmt.close();
            }
        }
    }

    /**
     * Tests fix for BUG#17099 - Statement.getGeneratedKeys() throws NPE when no query has been processed.
     *
     * @throws Exception
     */
    @Test
    public void testBug17099() throws Exception {
        PreparedStatement pStmt = this.conn.prepareStatement("SELECT 1", Statement.RETURN_GENERATED_KEYS);
        assertNotNull(pStmt.getGeneratedKeys());

        pStmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement("SELECT 1", Statement.RETURN_GENERATED_KEYS);
        assertNotNull(pStmt.getGeneratedKeys());
    }

    /**
     * Tests fix for BUG#17587 - clearParameters() on a closed prepared statement causes NPE.
     *
     * @throws Exception
     */
    @Test
    public void testBug17587() throws Exception {
        createTable("testBug17857", "(field1 int)");
        PreparedStatement pStmt = null;

        try {
            pStmt = this.conn.prepareStatement("INSERT INTO testBug17857 VALUES (?)");
            pStmt.close();
            try {
                pStmt.clearParameters();
            } catch (SQLException sqlEx) {
                assertEquals(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, sqlEx.getSQLState());
            }

            pStmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement("INSERT INTO testBug17857 VALUES (?)");
            pStmt.close();
            try {
                pStmt.clearParameters();
            } catch (SQLException sqlEx) {
                assertEquals(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, sqlEx.getSQLState());
            }

        } finally {
            if (pStmt != null) {
                pStmt.close();
            }
        }
    }

    /**
     * Tests fix for BUG#19615, PreparedStatement.setObject(int, Object, int) doesn't respect scale of BigDecimals.
     *
     * @throws Exception
     */
    @Test
    public void testBug19615() throws Exception {
        createTable("testBug19615", "(field1 DECIMAL(19, 12))");

        BigDecimal dec = new BigDecimal("1.234567");

        this.pstmt = this.conn.prepareStatement("INSERT INTO testBug19615 VALUES (?)");
        this.pstmt.setObject(1, dec, Types.DECIMAL);
        this.pstmt.executeUpdate();
        this.pstmt.close();

        this.rs = this.stmt.executeQuery("SELECT field1 FROM testBug19615");
        this.rs.next();
        assertEquals(dec, this.rs.getBigDecimal(1).setScale(6));
        this.rs.close();
        this.stmt.executeUpdate("TRUNCATE TABLE testBug19615");

        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement("INSERT INTO testBug19615 VALUES (?)");
        this.pstmt.setObject(1, dec, Types.DECIMAL);
        this.pstmt.executeUpdate();
        this.pstmt.close();

        this.rs = this.stmt.executeQuery("SELECT field1 FROM testBug19615");
        this.rs.next();
        assertEquals(dec, this.rs.getBigDecimal(1).setScale(6));
        this.rs.close();
    }

    /**
     * Tests fix for BUG#20029 - NPE thrown from executeBatch().
     *
     * @throws Exception
     */
    @Test
    public void testBug20029() throws Exception {
        createTable("testBug20029", "(field1 int)");

        long initialTimeout = 20; // may need to raise this depending on environment we try and do this automatically in this testcase

        for (int i = 0; i < 10; i++) {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            final Connection toBeKilledConn = getConnectionWithProps(props);
            final long timeout = initialTimeout;
            PreparedStatement toBeKilledPstmt = null;

            try {
                toBeKilledPstmt = ((com.mysql.cj.jdbc.JdbcConnection) toBeKilledConn).clientPrepareStatement("INSERT INTO testBug20029 VALUES (?)");

                for (int j = 0; j < 1000; j++) {
                    toBeKilledPstmt.setInt(1, j);
                    toBeKilledPstmt.addBatch();
                }

                Thread t = new Thread() {

                    @Override
                    public void run() {
                        try {
                            sleep(timeout);
                            toBeKilledConn.close();
                        } catch (Throwable thr) {

                        }
                    }

                };

                t.start();

                try {
                    if (!toBeKilledConn.isClosed()) {
                        initialTimeout *= 2;
                        continue;
                    }

                    toBeKilledPstmt.executeBatch();
                    fail("Should've caught a SQLException for the statement being closed here");
                } catch (SQLException sqlEx) {
                    assertEquals("08003", sqlEx.getSQLState());
                    break;
                }

                fail("Connection didn't close while in the middle of PreparedStatement.executeBatch()");
            } finally {
                if (toBeKilledPstmt != null) {
                    toBeKilledPstmt.close();
                }

                if (toBeKilledConn != null) {
                    toBeKilledConn.close();
                }
            }
        }
    }

    /**
     * Fixes BUG#20687 - Can't pool server-side prepared statements, exception raised when re-using them.
     *
     * @throws Exception
     */
    @Test
    public void testBug20687() throws Exception {
        createTable("testBug20687", "(field1 int)");
        Connection poolingConn = null;

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        PreparedStatement pstmt1 = null;
        PreparedStatement pstmt2 = null;

        try {
            poolingConn = getConnectionWithProps(props);
            pstmt1 = poolingConn.prepareStatement("SELECT field1 FROM testBug20687");
            this.rs = pstmt1.executeQuery();
            pstmt1.close();

            pstmt2 = poolingConn.prepareStatement("SELECT field1 FROM testBug20687");
            this.rs = pstmt2.executeQuery();
            assertTrue(pstmt1 == pstmt2);
            pstmt2.close();
        } finally {
            if (pstmt1 != null) {
                pstmt1.close();
            }

            if (pstmt2 != null) {
                pstmt2.close();
            }

            if (poolingConn != null) {
                poolingConn.close();
            }
        }
    }

    @Test
    public void testLikeWithBackslashes() throws Exception {
        Connection noBackslashEscapesConn = null;

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.sessionVariables.getKeyName(), "sql_mode=NO_BACKSLASH_ESCAPES");

            noBackslashEscapesConn = getConnectionWithProps(props);

            createTable("X_TEST",
                    "(userName varchar(32) not null, ivalue integer, CNAME varchar(255), bvalue CHAR(1), svalue varchar(255), ACTIVE CHAR(1), primary key (userName)) DEFAULT CHARSET=latin1");

            String insert_sql = "insert into X_TEST (ivalue, CNAME, bvalue, svalue, ACTIVE, userName) values (?, ?, ?, ?, ?, ?)";

            this.pstmt = noBackslashEscapesConn.prepareStatement(insert_sql);
            this.pstmt.setInt(1, 0);
            this.pstmt.setString(2, "c:\\jetson");
            this.pstmt.setInt(3, 1);
            this.pstmt.setString(4, "c:\\jetson");
            this.pstmt.setInt(5, 1);
            this.pstmt.setString(6, "c:\\jetson");
            this.pstmt.execute();

            String select_sql = "select user0_.userName as userName0_0_, user0_.ivalue as ivalue0_0_, user0_.CNAME as CNAME0_0_, user0_.bvalue as bvalue0_0_, user0_.svalue as svalue0_0_, user0_.ACTIVE as ACTIVE0_0_ from X_TEST user0_ where user0_.userName like ?";
            this.pstmt = noBackslashEscapesConn.prepareStatement(select_sql);
            this.pstmt.setString(1, "c:\\j%");
            // if we comment out the previous line and uncomment the following, the like clause matches
            // this.pstmt.setString(1,"c:\\\\j%");
            System.out.println("about to execute query " + select_sql);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
        } finally {
            if (noBackslashEscapesConn != null) {
                noBackslashEscapesConn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#20650 - Statement.cancel() causes NullPointerException if underlying connection has been closed due to server failure.
     *
     * @throws Exception
     */
    @Test
    public void testBug20650() throws Exception {
        Connection closedConn = null;
        Statement cancelStmt = null;

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            closedConn = getConnectionWithProps(props);
            cancelStmt = closedConn.createStatement();

            closedConn.close();

            cancelStmt.cancel();
        } finally {
            if (cancelStmt != null) {
                cancelStmt.close();
            }

            if (closedConn != null && !closedConn.isClosed()) {
                closedConn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#20888 - escape of quotes in client-side prepared statements parsing not respected.
     *
     * @throws Exception
     */
    @Test
    public void testBug20888() throws Exception {
        String s = "SELECT 'What do you think about D\\'Artanian''?', \"What do you think about D\\\"Artanian\"\"?\"";
        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement(s);

        this.rs = this.pstmt.executeQuery();
        this.rs.next();
        assertEquals(this.rs.getString(1), "What do you think about D'Artanian'?");
        assertEquals(this.rs.getString(2), "What do you think about D\"Artanian\"?");
    }

    /**
     * Tests Bug#21207 - Driver throws NPE when tracing prepared statements that have been closed (in asSQL()).
     *
     * @throws Exception
     */
    @Test
    public void testBug21207() throws Exception {
        this.pstmt = this.conn.prepareStatement("SELECT 1");
        this.pstmt.close();
        this.pstmt.toString(); // this used to cause an NPE
    }

    /**
     * Tests BUG#21438, server-side PS fails when using jdbcCompliantTruncation. If either is set to FALSE (&useServerPrepStmts=false or
     * &jdbcCompliantTruncation=false) test succedes.
     *
     * @throws Exception
     */
    @SuppressWarnings("deprecation")
    @Test
    public void testBug21438() throws Exception {
        createTable("testBug21438", "(t_id int(10), test_date timestamp NOT NULL,primary key t_pk (t_id));");

        assertEquals(1, this.stmt.executeUpdate("insert into testBug21438 values (1,NOW());"));

        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn)
                .serverPrepareStatement("UPDATE testBug21438 SET test_date=ADDDATE(?,INTERVAL 1 YEAR) WHERE t_id=1;");
        Timestamp ts = new Timestamp(System.currentTimeMillis());
        ts.setNanos(999999999);

        this.pstmt.setTimestamp(1, ts);

        assertEquals(1, this.pstmt.executeUpdate());

        Timestamp future = (Timestamp) getSingleIndexedValueWithQuery(1, "SELECT test_date FROM testBug21438");
        assertEquals(future.getYear() - ts.getYear(), 1);
    }

    /**
     * Tests fix for BUG#22359 - Driver was using millis for Statement.setQueryTimeout() when spec says argument is seconds.
     *
     * @throws Exception
     */
    @Test
    public void testBug22359() throws Exception {
        Statement timeoutStmt = null;

        try {
            timeoutStmt = this.conn.createStatement();
            timeoutStmt.setQueryTimeout(2);

            long begin = System.currentTimeMillis();

            try {
                timeoutStmt.execute("SELECT SLEEP(30)");
                fail("Query didn't time out");
            } catch (MySQLTimeoutException timeoutEx) {
                long end = System.currentTimeMillis();

                assertTrue(end - begin > 1000);
            }
        } finally {
            if (timeoutStmt != null) {
                timeoutStmt.close();
            }
        }
    }

    /**
     * Tests fix for BUG#22290 - Driver issues truncation on write exception when it shouldn't (due to sending big decimal incorrectly to server with
     * server-side prepared statement).
     *
     * @throws Exception
     */
    @Test
    public void testBug22290() throws Exception {
        createTable("testbug22290", "(`id` int(11) NOT NULL default '1',`cost` decimal(10,2) NOT NULL,PRIMARY KEY  (`id`)) DEFAULT CHARSET=utf8", "InnoDB");
        assertEquals(this.stmt.executeUpdate("INSERT INTO testbug22290 (`id`,`cost`) VALUES (1,'1.00')"), 1);

        Connection configuredConn = null;

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.sessionVariables.getKeyName(), "sql_mode='STRICT_TRANS_TABLES'");

            configuredConn = getConnectionWithProps(props);

            this.pstmt = configuredConn.prepareStatement("update testbug22290 set cost = cost + ? where id = 1");
            this.pstmt.setBigDecimal(1, new BigDecimal("1.11"));
            assertEquals(this.pstmt.executeUpdate(), 1);

            assertEquals(this.stmt.executeUpdate("UPDATE testbug22290 SET cost='1.00'"), 1);
            this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) configuredConn).clientPrepareStatement("update testbug22290 set cost = cost + ? where id = 1");
            this.pstmt.setBigDecimal(1, new BigDecimal("1.11"));
            assertEquals(this.pstmt.executeUpdate(), 1);
        } finally {
            if (configuredConn != null) {
                configuredConn.close();
            }
        }
    }

    @Test
    public void testClientPreparedSetBoolean() throws Exception {
        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement("SELECT ?");
        this.pstmt.setBoolean(1, false);
        assertEquals("SELECT 0", this.pstmt.toString().substring(this.pstmt.toString().indexOf("SELECT")));
        this.pstmt.setBoolean(1, true);
        assertEquals("SELECT 1", this.pstmt.toString().substring(this.pstmt.toString().indexOf("SELECT")));
    }

    /**
     * Tests fix for BUG#24360 .setFetchSize() breaks prepared SHOW and other commands.
     *
     * @throws Exception
     */
    @Test
    public void testBug24360() throws Exception {
        Connection c = null;

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");

        try {
            c = getConnectionWithProps(props);

            this.pstmt = c.prepareStatement("SHOW PROCESSLIST");
            this.pstmt.setFetchSize(5);
            this.pstmt.execute();
        } finally {
            if (c != null) {
                c.close();
            }
        }
    }

    /**
     * Tests fix for BUG#24344 - useJDBCCompliantTimezoneShift with server-side prepared statements gives different behavior than when using client-side
     * prepared statements. (this is now fixed if moving from server-side prepared statements to client-side prepared statements by setting
     * "useSSPSCompatibleTimezoneShift" to "true", as the driver can't tell if this is a new deployment that never used server-side prepared statements, or if
     * it is an existing deployment that is switching to client-side prepared statements from server-side prepared statements.
     *
     * Note: The properties 'useJDBCCompliantTimezoneShift' and 'useSSPSCompatibleTimezoneShift' no longer exist in Connector/J 6.0.
     *
     * @throws Exception
     */
    @Test
    public void testBug24344() throws Exception {
        super.createTable("testBug24344", "(i INT AUTO_INCREMENT, t1 DATETIME, PRIMARY KEY (i)) ENGINE = MyISAM");

        Connection conn2 = null;

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
            props.setProperty(PropertyKey.preserveInstants.getKeyName(), "false");
            conn2 = getConnectionWithProps(props);
            this.pstmt = conn2.prepareStatement("INSERT INTO testBug24344 (t1) VALUES (?)");
            Calendar c = Calendar.getInstance();
            c.set(Calendar.MILLISECOND, 789);
            this.pstmt.setTimestamp(1, new Timestamp(c.getTime().getTime()));
            this.pstmt.execute();
            this.pstmt.close();
            conn2.close();

            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
            props.setProperty(PropertyKey.preserveInstants.getKeyName(), "false");
            conn2 = getConnectionWithProps(props);
            this.pstmt = conn2.prepareStatement("INSERT INTO testBug24344 (t1) VALUES (?)");
            this.pstmt.setTimestamp(1, new Timestamp(c.getTime().getTime()));
            this.pstmt.execute();
            this.pstmt.close();

            Statement s = conn2.createStatement();
            this.rs = s.executeQuery("SELECT t1 FROM testBug24344 ORDER BY i ASC");

            Timestamp[] dates = new Timestamp[2];

            int i = 0;
            while (this.rs.next()) {
                dates[i++] = this.rs.getTimestamp(1);
            }

            assertEquals(2, i, "Number of rows should be 2.");
            assertEquals(dates[0], dates[1]);
        } finally {
            if (conn2 != null) {
                conn2.close();
            }
        }
    }

    /**
     * Tests fix for BUG#25073 - rewriting batched statements leaks internal statement instances, and causes a memory leak.
     *
     * @throws Exception
     */
    @Test
    public void testBug25073() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        Connection multiConn = getConnectionWithProps(props);
        createTable("testBug25073", "(pk_field INT PRIMARY KEY NOT NULL AUTO_INCREMENT, field1 INT)");
        Statement multiStmt = multiConn.createStatement();
        multiStmt.addBatch("INSERT INTO testBug25073(field1) VALUES (1)");
        multiStmt.addBatch("INSERT INTO testBug25073(field1) VALUES (2)");
        multiStmt.addBatch("INSERT INTO testBug25073(field1) VALUES (3)");
        multiStmt.addBatch("INSERT INTO testBug25073(field1) VALUES (4)");
        multiStmt.addBatch("UPDATE testBug25073 SET field1=5 WHERE field1=1");
        multiStmt.addBatch("UPDATE testBug25073 SET field1=6 WHERE field1=2 OR field1=3");

        int beforeOpenStatementCount = ((com.mysql.cj.jdbc.JdbcConnection) multiConn).getActiveStatementCount();

        multiStmt.executeBatch();

        int afterOpenStatementCount = ((com.mysql.cj.jdbc.JdbcConnection) multiConn).getActiveStatementCount();

        assertEquals(beforeOpenStatementCount, afterOpenStatementCount);

        createTable("testBug25073", "(pk_field INT PRIMARY KEY NOT NULL AUTO_INCREMENT, field1 INT)");
        props.clear();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        props.setProperty(PropertyKey.maxAllowedPacket.getKeyName(), "1024");
        props.setProperty(PropertyKey.dumpQueriesOnException.getKeyName(), "true");
        props.setProperty(PropertyKey.maxQuerySizeToLog.getKeyName(), String.valueOf(1024 * 1024 * 2));
        multiConn = getConnectionWithProps(props);
        multiStmt = multiConn.createStatement();

        for (int i = 0; i < 1000; i++) {
            multiStmt.addBatch("INSERT INTO testBug25073(field1) VALUES (" + i + ")");
        }

        beforeOpenStatementCount = ((com.mysql.cj.jdbc.JdbcConnection) multiConn).getActiveStatementCount();

        multiStmt.executeBatch();

        afterOpenStatementCount = ((com.mysql.cj.jdbc.JdbcConnection) multiConn).getActiveStatementCount();

        assertEquals(beforeOpenStatementCount, afterOpenStatementCount);

        createTable("testBug25073", "(pk_field INT PRIMARY KEY NOT NULL AUTO_INCREMENT, field1 INT)");

        props.clear();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        props.setProperty(PropertyKey.dumpQueriesOnException.getKeyName(), "true");
        props.setProperty(PropertyKey.maxQuerySizeToLog.getKeyName(), String.valueOf(1024 * 1024 * 2));
        multiConn = getConnectionWithProps(props);
        PreparedStatement pStmt = multiConn.prepareStatement("INSERT INTO testBug25073(field1) VALUES (?)", Statement.RETURN_GENERATED_KEYS);

        for (int i = 0; i < 1000; i++) {
            pStmt.setInt(1, i);
            pStmt.addBatch();
        }

        beforeOpenStatementCount = ((com.mysql.cj.jdbc.JdbcConnection) multiConn).getActiveStatementCount();

        pStmt.executeBatch();

        afterOpenStatementCount = ((com.mysql.cj.jdbc.JdbcConnection) multiConn).getActiveStatementCount();

        assertEquals(beforeOpenStatementCount, afterOpenStatementCount);

        createTable("testBug25073", "(pk_field INT PRIMARY KEY NOT NULL AUTO_INCREMENT, field1 INT)");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        props.setProperty(PropertyKey.maxAllowedPacket.getKeyName(), "1024");
        props.setProperty(PropertyKey.dumpQueriesOnException.getKeyName(), "true");
        props.setProperty(PropertyKey.maxQuerySizeToLog.getKeyName(), String.valueOf(1024 * 1024 * 2));
        multiConn = getConnectionWithProps(props);
        pStmt = multiConn.prepareStatement("INSERT INTO testBug25073(field1) VALUES (?)", Statement.RETURN_GENERATED_KEYS);

        for (int i = 0; i < 1000; i++) {
            pStmt.setInt(1, i);
            pStmt.addBatch();
        }

        beforeOpenStatementCount = ((com.mysql.cj.jdbc.JdbcConnection) multiConn).getActiveStatementCount();

        pStmt.executeBatch();

        afterOpenStatementCount = ((com.mysql.cj.jdbc.JdbcConnection) multiConn).getActiveStatementCount();

        assertEquals(beforeOpenStatementCount, afterOpenStatementCount);
    }

    /**
     * Tests fix for BUG#25009 - Results from updates not handled correctly in multi-statement queries.
     *
     * @throws Exception
     */
    @Test
    public void testBug25009() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), "true");

        Connection multiConn = getConnectionWithProps(props);
        createTable("testBug25009", "(field1 INT)");

        try {
            Statement multiStmt = multiConn.createStatement();
            multiStmt.execute("SELECT 1;SET @a=1; SET @b=2; SET @c=3; INSERT INTO testBug25009 VALUES (1)");

            assertEquals(-1, multiStmt.getUpdateCount());

            this.rs = multiStmt.getResultSet();
            assertTrue(this.rs.next());
            assertEquals(multiStmt.getMoreResults(), false);

            for (int i = 0; i < 3; i++) {
                assertEquals(0, multiStmt.getUpdateCount());
                assertEquals(multiStmt.getMoreResults(), false);
            }

            assertEquals(1, multiStmt.getUpdateCount());

            this.rs = multiStmt.executeQuery("SELECT field1 FROM testBug25009");
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));

        } finally {
            if (multiConn != null) {
                multiConn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#25025 - Client-side prepared statement parser gets confused by in-line (slash-star) comments and therefore can't rewrite batched
     * statements or reliably detect type of statements when they're used.
     *
     * @throws Exception
     */
    @Test
    public void testBug25025() throws Exception {
        Connection multiConn = null;

        createTable("testBug25025", "(field1 INT)");

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");

            multiConn = getConnectionWithProps(props);

            this.pstmt = multiConn
                    .prepareStatement("/* insert foo.bar.baz INSERT INTO foo VALUES (?,?,?,?) to trick parser */ INSERT into testBug25025 VALUES (?)");
            this.pstmt.setInt(1, 1);
            this.pstmt.addBatch();
            this.pstmt.setInt(1, 2);
            this.pstmt.addBatch();
            this.pstmt.setInt(1, 3);
            this.pstmt.addBatch();

            int[] counts = this.pstmt.executeBatch();

            assertEquals(3, counts.length);
            assertEquals(Statement.SUCCESS_NO_INFO, counts[0]);
            assertEquals(Statement.SUCCESS_NO_INFO, counts[1]);
            assertEquals(Statement.SUCCESS_NO_INFO, counts[2]);
            assertEquals(true, ((ClientPreparedStatement) this.pstmt).getQueryInfo().isRewritableWithMultiValuesClause());
        } finally {
            if (multiConn != null) {
                multiConn.close();
            }
        }
    }

    @Test
    public void testBustedGGKWithPSExecute() throws Exception {
        createTable("sequence", "(sequence_name VARCHAR(32) NOT NULL PRIMARY KEY, next_val BIGINT NOT NULL)");

        // Populate with the initial value
        this.stmt.executeUpdate("INSERT INTO sequence VALUES ('test-sequence', 1234)");

        // Atomic operation to increment and return next value
        PreparedStatement pStmt = null;

        try {
            pStmt = this.conn.prepareStatement("UPDATE sequence SET next_val=LAST_INSERT_ID(next_val + ?) WHERE sequence_name = ?",
                    Statement.RETURN_GENERATED_KEYS);

            pStmt.setInt(1, 4);
            pStmt.setString(2, "test-sequence");
            pStmt.execute();

            this.rs = pStmt.getGeneratedKeys();
            this.rs.next();
            assertEquals(1238, this.rs.getLong(1));
        } finally {
            if (pStmt != null) {
                pStmt.close();
            }
        }
    }

    /**
     * Tests fix for BUG#28256 - When connection is in read-only mode, queries that are parentheized incorrectly identified as DML.
     *
     * @throws Exception
     */
    @Test
    public void testBug28256() throws Exception {
        try {
            this.conn.setReadOnly(true);
            this.stmt.execute("(SELECT 1) UNION (SELECT 2)");
            this.conn.prepareStatement("(SELECT 1) UNION (SELECT 2)").execute();
            ((com.mysql.cj.jdbc.JdbcConnection) this.conn).serverPrepareStatement("(SELECT 1) UNION (SELECT 2)").execute();
        } finally {
            this.conn.setReadOnly(false);
        }
    }

    /**
     * Tests fix for BUG#28469 - PreparedStatement.getMetaData() for statements containing leading one-line comments is not returned correctly.
     *
     * As part of this fix, we also overhauled detection of DML for executeQuery() and SELECTs for executeUpdate() in plain and prepared statements to be aware
     * of the same types of comments.
     *
     * @throws Exception
     */
    @Test
    public void testBug28469() throws Exception {
        PreparedStatement commentStmt = null;

        try {
            String[] statementsToTest = { "-- COMMENT\nSELECT 1", "# COMMENT\nSELECT 1", "/* comment */ SELECT 1" };

            for (int i = 0; i < statementsToTest.length; i++) {
                commentStmt = this.conn.prepareStatement(statementsToTest[i]);
                assertNotNull(commentStmt.getMetaData());
                PreparedStatement localCommentStmt = commentStmt;
                assertThrows("Should not be able to call executeUpdate() on a SELECT statement!", SQLException.class, () -> {
                    localCommentStmt.executeUpdate();
                    return null;
                });

                this.rs = commentStmt.executeQuery();
                this.rs.next();
                assertEquals(1, this.rs.getInt(1));
            }

            createTable("testBug28469", "(field1 INT)");

            String[] updatesToTest = { "-- COMMENT\nUPDATE testBug28469 SET field1 = 2", "# COMMENT\nUPDATE testBug28469 SET field1 = 2",
                    "/* comment */ UPDATE testBug28469 SET field1 = 2" };

            for (int i = 0; i < updatesToTest.length; i++) {
                commentStmt = this.conn.prepareStatement(updatesToTest[i]);
                PreparedStatement localCommentStmt = commentStmt;
                assertNull(commentStmt.getMetaData());

                assertThrows("Should not be able to call executeQuery() on a SELECT statement!", SQLException.class, () -> {
                    localCommentStmt.executeQuery();
                    return null;
                });
                int localI = i;
                assertThrows("Should not be able to call executeQuery() on a SELECT statement!", SQLException.class, () -> {
                    this.stmt.executeQuery(updatesToTest[localI]);
                    return null;
                });
            }
        } finally {
            if (commentStmt != null) {
                commentStmt.close();
            }
        }
    }

    /**
     * Tests error with slash-star comment at EOL
     *
     * @throws Exception
     */
    @Test
    public void testCommentParsing() throws Exception {
        createTable("PERSON", "(NAME VARCHAR(32), PERID VARCHAR(32))");

        this.pstmt = this.conn.prepareStatement("SELECT NAME AS name2749_0_, PERID AS perid2749_0_ FROM PERSON WHERE PERID=? /*FOR UPDATE*/");
    }

    /**
     * Tests fix for BUG#28851 - parser in client-side prepared statements eats character following '/' if it's not a multi-line comment.
     *
     * @throws Exception
     */
    @Test
    public void testBug28851() throws Exception {
        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement("SELECT 1/?");
        this.pstmt.setInt(1, 1);
        this.rs = this.pstmt.executeQuery();

        assertTrue(this.rs.next());

        assertEquals(1, this.rs.getInt(1));
    }

    /**
     * Tests fix for BUG#28596 - parser in client-side prepared statements runs to end of statement, rather than end-of-line for '#' comments.
     *
     * Also added support for '--' single-line comments
     *
     * @throws Exception
     */
    @Test
    public void testBug28596() throws Exception {
        String query = "SELECT #\n?, #\n? #?\r\n,-- abcdefg \n?";

        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement(query);
        this.pstmt.setInt(1, 1);
        this.pstmt.setInt(2, 2);
        this.pstmt.setInt(3, 3);

        assertEquals(3, this.pstmt.getParameterMetaData().getParameterCount());
        this.rs = this.pstmt.executeQuery();

        assertTrue(this.rs.next());

        assertEquals(1, this.rs.getInt(1));
        assertEquals(2, this.rs.getInt(2));
        assertEquals(3, this.rs.getInt(3));
    }

    /**
     * Tests fix for BUG#30550 - executeBatch() on an empty batch when there are no elements in the batch causes a divide-by-zero error when rewriting is
     * enabled.
     *
     * @throws Exception
     */
    @Test
    public void testBug30550() throws Exception {
        createTable("testBug30550", "(field1 int)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        Connection rewriteConn = getConnectionWithProps(props);
        PreparedStatement batchPStmt = null;
        Statement batchStmt = null;

        try {
            batchStmt = rewriteConn.createStatement();
            assertEquals(0, batchStmt.executeBatch().length);

            batchStmt.addBatch("INSERT INTO testBug30550 VALUES (1)");
            int[] counts = batchStmt.executeBatch();
            assertEquals(1, counts.length);
            assertEquals(1, counts[0]);
            assertEquals(0, batchStmt.executeBatch().length);

            batchPStmt = rewriteConn.prepareStatement("INSERT INTO testBug30550 VALUES (?)");
            batchPStmt.setInt(1, 1);
            assertEquals(0, batchPStmt.executeBatch().length);
            batchPStmt.addBatch();
            counts = batchPStmt.executeBatch();
            assertEquals(1, counts.length);
            assertEquals(1, counts[0]);
            assertEquals(0, batchPStmt.executeBatch().length);
        } finally {
            if (batchPStmt != null) {
                batchPStmt.close();
            }

            if (batchStmt != null) {
                batchStmt.close();
            }
            if (rewriteConn != null) {
                rewriteConn.close();
            }
        }
    }

    /**
     * Tests fix for Bug#27412 - cached metadata with PreparedStatement.execute() throws NullPointerException.
     *
     * @throws Exception
     */
    @Test
    public void testBug27412() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.cacheResultSetMetadata.getKeyName(), "true");
        Connection conn2 = getConnectionWithProps(props);
        PreparedStatement pstm = conn2.prepareStatement("SELECT 1");
        try {
            assertTrue(pstm.execute());
        } finally {
            pstm.close();
            conn2.close();
        }
    }

    @Test
    public void testBustedGGKColumnNames() throws Exception {
        createTable("testBustedGGKColumnNames", "(field1 int primary key auto_increment)");
        this.stmt.executeUpdate("INSERT INTO testBustedGGKColumnNames VALUES (null)", Statement.RETURN_GENERATED_KEYS);
        assertEquals("GENERATED_KEY", this.stmt.getGeneratedKeys().getMetaData().getColumnName(1));

        this.pstmt = this.conn.prepareStatement("INSERT INTO testBustedGGKColumnNames VALUES (null)", Statement.RETURN_GENERATED_KEYS);
        this.pstmt.executeUpdate();
        assertEquals("GENERATED_KEY", this.pstmt.getGeneratedKeys().getMetaData().getColumnName(1));

        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).serverPrepareStatement("INSERT INTO testBustedGGKColumnNames VALUES (null)",
                Statement.RETURN_GENERATED_KEYS);
        this.pstmt.executeUpdate();
        assertEquals("GENERATED_KEY", this.pstmt.getGeneratedKeys().getMetaData().getColumnName(1));
    }

    @Test
    public void testLancesBitMappingBug() throws Exception {
        createTable("Bit_TabXXX", "( `MAX_VAL` BIT default NULL, `MIN_VAL` BIT default NULL, `NULL_VAL` BIT default NULL) DEFAULT CHARSET=latin1", "InnoDB");

        // add Bit_In_MinXXX procedure
        createProcedure("Bit_In_MinXXX", "(MIN_PARAM TINYINT(1)) begin update Bit_TabXXX set MIN_VAL=MIN_PARAM; end");

        createProcedure("Bit_In_MaxXXX", "(MAX_PARAM TINYINT(1)) begin update Bit_TabXXX set MAX_VAL=MAX_PARAM; end");

        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        String sPrepStmt = "{call Bit_In_MinXXX(?)}";
        this.pstmt = this.conn.prepareStatement(sPrepStmt);
        this.pstmt.setObject(1, "true", java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("true", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, "false", java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("false", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, "1", java.sql.Types.BIT); // fails
        this.pstmt.executeUpdate();
        assertEquals("true", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, "0", java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("false", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, Boolean.TRUE, java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("true", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, Boolean.FALSE, java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("false", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, new Boolean(true), java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("true", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, new Boolean(false), java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("false", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, new Byte("1"), java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("true", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
        this.stmt.execute("TRUNCATE TABLE Bit_TabXXX");
        this.stmt.execute("insert into Bit_TabXXX values(null,0,null)");

        this.pstmt.setObject(1, new Byte("0"), java.sql.Types.BIT);
        this.pstmt.executeUpdate();
        assertEquals("false", getSingleIndexedValueWithQuery(1, "SELECT MIN_VAL FROM Bit_TabXXX").toString());
    }

    /**
     * Tests fix for BUG#32577 - no way to store two timestamp/datetime values that happens over the DST switchover, as the hours end up being the same when
     * sent as the literal that MySQL requires.
     *
     * Note that to get this scenario to work with MySQL (since it doesn't support per-value timezones), you need to configure your server (or session) to be in
     * UTC. This will cause the driver to always convert to/from the server and client timezone consistently.
     *
     * @throws Exception
     */
    @Test
    public void testBug32577() throws Exception {
        createTable("testBug32577", "(id INT, field_datetime DATETIME, field_timestamp TIMESTAMP)");
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.sessionVariables.getKeyName(), "time_zone='+0:00'");
        props.setProperty(PropertyKey.connectionTimeZone.getKeyName(), "UTC");

        Connection nonLegacyConn = getConnectionWithProps(props);

        try {
            long earlier = 1194154200000L;
            long later = 1194157800000L;

            this.pstmt = nonLegacyConn.prepareStatement("INSERT INTO testBug32577 VALUES (?,?,?)");
            Timestamp ts = new Timestamp(earlier);
            this.pstmt.setInt(1, 1);
            this.pstmt.setTimestamp(2, ts);
            this.pstmt.setTimestamp(3, ts);
            this.pstmt.executeUpdate();

            ts = new Timestamp(later);
            this.pstmt.setInt(1, 2);
            this.pstmt.setTimestamp(2, ts);
            this.pstmt.setTimestamp(3, ts);
            this.pstmt.executeUpdate();

            this.rs = nonLegacyConn.createStatement()
                    .executeQuery("SELECT id, field_datetime, field_timestamp , UNIX_TIMESTAMP(field_datetime), UNIX_TIMESTAMP(field_timestamp) "
                            + "FROM testBug32577 ORDER BY id ASC");

            this.rs.next();

            //java.util.Date date1 = new Date(this.rs.getTimestamp(2).getTime());
            Timestamp ts1 = this.rs.getTimestamp(3);
            long datetimeSeconds1 = this.rs.getLong(4) * 1000;
            long timestampSeconds1 = this.rs.getLong(5) * 1000;

            this.rs.next();

            //java.util.Date date2 = new Date(this.rs.getTimestamp(2).getTime());
            Timestamp ts2 = this.rs.getTimestamp(3);
            long datetimeSeconds2 = this.rs.getLong(4) * 1000;
            long timestampSeconds2 = this.rs.getLong(5) * 1000;

            assertEquals(later, datetimeSeconds2);
            assertEquals(later, timestampSeconds2);
            assertEquals(earlier, datetimeSeconds1);
            assertEquals(earlier, timestampSeconds1);

            SimpleDateFormat sdf = TimeUtil.getSimpleDateFormat(null, "MM/dd/yyyy HH:mm z", TimeZone.getTimeZone("America/New_York"));
            System.out.println(sdf.format(ts2));
            System.out.println(sdf.format(ts1));
        } finally {
            if (nonLegacyConn != null) {
                nonLegacyConn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#30508 - ResultSet returned by Statement.getGeneratedKeys() is not closed automatically when statement that created it is closed.
     *
     * @throws Exception
     */
    @Test
    public void testBug30508() throws Exception {
        createTable("testBug30508", "(k INT PRIMARY KEY NOT NULL AUTO_INCREMENT, p VARCHAR(32))");
        try {
            Statement ggkStatement = this.conn.createStatement();
            ggkStatement.executeUpdate("INSERT INTO testBug30508 (p) VALUES ('abc')", Statement.RETURN_GENERATED_KEYS);

            this.rs = ggkStatement.getGeneratedKeys();
            ggkStatement.close();

            this.rs.next();
            fail("Should've had an exception here");
        } catch (SQLException sqlEx) {
            assertEquals("S1000", sqlEx.getSQLState());
        }

        try {
            this.pstmt = this.conn.prepareStatement("SELECT 1", Statement.RETURN_GENERATED_KEYS);
            this.rs = this.pstmt.getGeneratedKeys();
            this.pstmt.close();
            this.rs.next();
            fail("Should've had an exception here");
        } catch (SQLException sqlEx) {
            assertEquals("S1000", sqlEx.getSQLState());
        }

        createProcedure("testBug30508", "() BEGIN SELECT 1; END");

        try {
            this.pstmt = this.conn.prepareCall("{CALL testBug30508()}");
            this.rs = this.pstmt.getGeneratedKeys();
            this.pstmt.close();
            this.rs.next();
            fail("Should've had an exception here");
        } catch (SQLException sqlEx) {
            assertEquals("S1000", sqlEx.getSQLState());
        }
    }

    @Test
    public void testMoreLanceBugs() throws Exception {
        createTable("Bit_Tab", "( `MAX_VAL` BIT default NULL, `MIN_VAL` BIT default NULL, `NULL_VAL` BIT default NULL) DEFAULT CHARSET=latin1", "InnoDB");
        // this.stmt.execute("insert into Bit_Tab values(null,0,null)");
        createProcedure("Bit_Proc", "(out MAX_PARAM TINYINT, out MIN_PARAM TINYINT, out NULL_PARAM TINYINT) "
                + "begin select MAX_VAL, MIN_VAL, NULL_VAL  into MAX_PARAM, MIN_PARAM, NULL_PARAM from Bit_Tab; end ");

        Boolean minBooleanVal;
        Boolean oRetVal;
        String Min_Val_Query = "SELECT MIN_VAL from Bit_Tab";
        String Min_Insert = "insert into Bit_Tab values(1,0,null)";
        // System.out.println("Value to insert=" + extractVal(Min_Insert,1));
        CallableStatement cstmt;

        this.stmt.executeUpdate("delete from Bit_Tab");
        this.stmt.executeUpdate(Min_Insert);
        cstmt = this.conn.prepareCall("{call Bit_Proc(?,?,?)}");

        cstmt.registerOutParameter(1, java.sql.Types.BIT);
        cstmt.registerOutParameter(2, java.sql.Types.BIT);
        cstmt.registerOutParameter(3, java.sql.Types.BIT);

        cstmt.executeUpdate();

        boolean bRetVal = cstmt.getBoolean(2);
        oRetVal = new Boolean(bRetVal);
        minBooleanVal = new Boolean("false");
        this.rs = this.stmt.executeQuery(Min_Val_Query);
        assertEquals(minBooleanVal, oRetVal);
    }

    @Test
    public void testBug33823() throws Exception {
        ResultSetInternalMethods resultSetInternalMethods = new ResultSetInternalMethods() {

            @Override
            public void clearNextResultset() {
            }

            @Override
            public char getFirstCharOfQuery() {
                return 0;
            }

            @Override
            public ResultSetInternalMethods getNextResultset() {
                return null;
            }

            @Override
            public Object getObjectStoredProc(int columnIndex, int desiredSqlType) throws SQLException {
                return null;
            }

            @Override
            public Object getObjectStoredProc(int i, Map<Object, Object> map, int desiredSqlType) throws SQLException {
                return null;
            }

            @Override
            public Object getObjectStoredProc(String columnName, int desiredSqlType) throws SQLException {
                return null;
            }

            @Override
            public Object getObjectStoredProc(String colName, Map<Object, Object> map, int desiredSqlType) throws SQLException {
                return null;
            }

            @Override
            public String getServerInfo() {
                return null;
            }

            @Override
            public long getUpdateCount() {
                return 0;
            }

            @Override
            public long getUpdateID() {
                return 0;
            }

            @Override
            public void initializeWithMetadata() throws SQLException {
            }

            @Override
            public void populateCachedMetaData(CachedResultSetMetaData cachedMetaData) throws SQLException {
            }

            @Override
            public void doClose(CloseOption... options) throws SQLException {
            }

            @Override
            public boolean isClosed() {
                return false;
            }

            @Override
            public boolean hasRows() {
                return false;
            }

            @Override
            public void setFirstCharOfQuery(char firstCharUpperCase) {
            }

            @Override
            public void setOwningStatement(JdbcStatement owningStatement) {
            }

            @Override
            public void setStatementUsedForFetchingRows(JdbcPreparedStatement stmt) {
            }

            @Override
            public void setWrapperStatement(Statement wrapperStatement) {
            }

            @Override
            public boolean absolute(int row) throws SQLException {
                return false;
            }

            @Override
            public void afterLast() throws SQLException {
            }

            @Override
            public void beforeFirst() throws SQLException {
            }

            @Override
            public void cancelRowUpdates() throws SQLException {
            }

            @Override
            public void clearWarnings() throws SQLException {
            }

            @Override
            public void close() throws SQLException {
            }

            @Override
            public void deleteRow() throws SQLException {
            }

            @Override
            public int findColumn(String columnName) throws SQLException {
                return 0;
            }

            @Override
            public boolean first() throws SQLException {
                return false;
            }

            @Override
            public Array getArray(int i) throws SQLException {
                return null;
            }

            @Override
            public Array getArray(String colName) throws SQLException {
                return null;
            }

            @Override
            public InputStream getAsciiStream(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public InputStream getAsciiStream(String columnName) throws SQLException {
                return null;
            }

            @Override
            public BigDecimal getBigDecimal(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public BigDecimal getBigDecimal(String columnName) throws SQLException {
                return null;
            }

            @Override
            @Deprecated
            public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
                return null;
            }

            @Override
            @Deprecated
            public BigDecimal getBigDecimal(String columnName, int scale) throws SQLException {
                return null;
            }

            @Override
            public InputStream getBinaryStream(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public InputStream getBinaryStream(String columnName) throws SQLException {
                return null;
            }

            @Override
            public Blob getBlob(int i) throws SQLException {
                return null;
            }

            @Override
            public Blob getBlob(String colName) throws SQLException {
                return null;
            }

            @Override
            public boolean getBoolean(int columnIndex) throws SQLException {
                return false;
            }

            @Override
            public boolean getBoolean(String columnName) throws SQLException {
                return false;
            }

            @Override
            public byte getByte(int columnIndex) throws SQLException {
                return 0;
            }

            @Override
            public byte getByte(String columnName) throws SQLException {
                return 0;
            }

            @Override
            public byte[] getBytes(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public byte[] getBytes(String columnName) throws SQLException {
                return null;
            }

            @Override
            public Reader getCharacterStream(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public Reader getCharacterStream(String columnName) throws SQLException {
                return null;
            }

            @Override
            public Clob getClob(int i) throws SQLException {
                return null;
            }

            @Override
            public Clob getClob(String colName) throws SQLException {
                return null;
            }

            @Override
            public int getConcurrency() throws SQLException {
                return 0;
            }

            @Override
            public String getCursorName() throws SQLException {
                return null;
            }

            @Override
            public Date getDate(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public Date getDate(String columnName) throws SQLException {
                return null;
            }

            @Override
            public Date getDate(int columnIndex, Calendar cal) throws SQLException {
                return null;
            }

            @Override
            public Date getDate(String columnName, Calendar cal) throws SQLException {
                return null;
            }

            @Override
            public double getDouble(int columnIndex) throws SQLException {
                return 0;
            }

            @Override
            public double getDouble(String columnName) throws SQLException {
                return 0;
            }

            @Override
            public int getFetchDirection() throws SQLException {
                return 0;
            }

            @Override
            public int getFetchSize() throws SQLException {
                return 0;
            }

            @Override
            public float getFloat(int columnIndex) throws SQLException {
                return 0;
            }

            @Override
            public float getFloat(String columnName) throws SQLException {
                return 0;
            }

            @Override
            public int getInt(int columnIndex) throws SQLException {
                return 0;
            }

            @Override
            public int getInt(String columnName) throws SQLException {
                return 0;
            }

            @Override
            public long getLong(int columnIndex) throws SQLException {
                return 0;
            }

            @Override
            public long getLong(String columnName) throws SQLException {
                return 0;
            }

            @Override
            public ResultSetMetaData getMetaData() throws SQLException {
                return null;
            }

            @Override
            public Object getObject(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public Object getObject(String columnName) throws SQLException {
                return null;
            }

            @Override
            public Object getObject(int arg0, Map<String, Class<?>> arg1) throws SQLException {
                return null;
            }

            @Override
            public Object getObject(String arg0, Map<String, Class<?>> arg1) throws SQLException {
                return null;
            }

            @Override
            public Ref getRef(int i) throws SQLException {
                return null;
            }

            @Override
            public Ref getRef(String colName) throws SQLException {
                return null;
            }

            @Override
            public int getRow() throws SQLException {
                return 0;
            }

            @Override
            public short getShort(int columnIndex) throws SQLException {
                return 0;
            }

            @Override
            public short getShort(String columnName) throws SQLException {
                return 0;
            }

            @Override
            public Statement getStatement() throws SQLException {
                return null;
            }

            @Override
            public String getString(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public String getString(String columnName) throws SQLException {
                return null;
            }

            @Override
            public Time getTime(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public Time getTime(String columnName) throws SQLException {
                return null;
            }

            @Override
            public Time getTime(int columnIndex, Calendar cal) throws SQLException {
                return null;
            }

            @Override
            public Time getTime(String columnName, Calendar cal) throws SQLException {
                return null;
            }

            @Override
            public Timestamp getTimestamp(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public Timestamp getTimestamp(String columnName) throws SQLException {
                return null;
            }

            @Override
            public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException {
                return null;
            }

            @Override
            public Timestamp getTimestamp(String columnName, Calendar cal) throws SQLException {
                return null;
            }

            @Override
            public int getType() throws SQLException {
                return 0;
            }

            @Override
            public URL getURL(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public URL getURL(String columnName) throws SQLException {
                return null;
            }

            @Override
            @Deprecated
            public InputStream getUnicodeStream(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            @Deprecated
            public InputStream getUnicodeStream(String columnName) throws SQLException {
                return null;
            }

            @Override
            public SQLWarning getWarnings() throws SQLException {
                return null;
            }

            @Override
            public void insertRow() throws SQLException {
            }

            @Override
            public boolean isAfterLast() throws SQLException {
                return false;
            }

            @Override
            public boolean isBeforeFirst() throws SQLException {
                return false;
            }

            @Override
            public boolean isFirst() throws SQLException {
                return false;
            }

            @Override
            public boolean isLast() throws SQLException {
                return false;
            }

            @Override
            public boolean last() throws SQLException {
                return false;
            }

            @Override
            public void moveToCurrentRow() throws SQLException {
            }

            @Override
            public void moveToInsertRow() throws SQLException {
            }

            @Override
            public boolean next() throws SQLException {
                return false;
            }

            @Override
            public boolean previous() throws SQLException {
                return false;
            }

            @Override
            public void refreshRow() throws SQLException {
            }

            @Override
            public boolean relative(int rows) throws SQLException {
                return false;
            }

            @Override
            public boolean rowDeleted() throws SQLException {
                return false;
            }

            @Override
            public boolean rowInserted() throws SQLException {
                return false;
            }

            @Override
            public boolean rowUpdated() throws SQLException {
                return false;
            }

            @Override
            public void setFetchDirection(int direction) throws SQLException {
            }

            @Override
            public void setFetchSize(int rows) throws SQLException {
            }

            @Override
            public void updateArray(int columnIndex, Array x) throws SQLException {
            }

            @Override
            public void updateArray(String columnName, Array x) throws SQLException {
            }

            @Override
            public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException {
            }

            @Override
            public void updateAsciiStream(String columnName, InputStream x, int length) throws SQLException {
            }

            @Override
            public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException {
            }

            @Override
            public void updateBigDecimal(String columnName, BigDecimal x) throws SQLException {
            }

            @Override
            public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException {
            }

            @Override
            public void updateBinaryStream(String columnName, InputStream x, int length) throws SQLException {
            }

            @Override
            public void updateBlob(int columnIndex, Blob x) throws SQLException {
            }

            @Override
            public void updateBlob(String columnName, Blob x) throws SQLException {
            }

            @Override
            public void updateBoolean(int columnIndex, boolean x) throws SQLException {
            }

            @Override
            public void updateBoolean(String columnName, boolean x) throws SQLException {
            }

            @Override
            public void updateByte(int columnIndex, byte x) throws SQLException {
            }

            @Override
            public void updateByte(String columnName, byte x) throws SQLException {
            }

            @Override
            public void updateBytes(int columnIndex, byte[] x) throws SQLException {
            }

            @Override
            public void updateBytes(String columnName, byte[] x) throws SQLException {
            }

            @Override
            public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException {
            }

            @Override
            public void updateCharacterStream(String columnName, Reader reader, int length) throws SQLException {
            }

            @Override
            public void updateClob(int columnIndex, Clob x) throws SQLException {
            }

            @Override
            public void updateClob(String columnName, Clob x) throws SQLException {
            }

            @Override
            public void updateDate(int columnIndex, Date x) throws SQLException {
            }

            @Override
            public void updateDate(String columnName, Date x) throws SQLException {
            }

            @Override
            public void updateDouble(int columnIndex, double x) throws SQLException {
            }

            @Override
            public void updateDouble(String columnName, double x) throws SQLException {
            }

            @Override
            public void updateFloat(int columnIndex, float x) throws SQLException {
            }

            @Override
            public void updateFloat(String columnName, float x) throws SQLException {
            }

            @Override
            public void updateInt(int columnIndex, int x) throws SQLException {
            }

            @Override
            public void updateInt(String columnName, int x) throws SQLException {
            }

            @Override
            public void updateLong(int columnIndex, long x) throws SQLException {
            }

            @Override
            public void updateLong(String columnName, long x) throws SQLException {
            }

            @Override
            public void updateNull(int columnIndex) throws SQLException {
            }

            @Override
            public void updateNull(String columnName) throws SQLException {
            }

            @Override
            public void updateObject(int columnIndex, Object x) throws SQLException {
            }

            @Override
            public void updateObject(String columnName, Object x) throws SQLException {
            }

            @Override
            public void updateObject(int columnIndex, Object x, int scale) throws SQLException {
            }

            @Override
            public void updateObject(String columnName, Object x, int scale) throws SQLException {
            }

            @Override
            public void updateRef(int columnIndex, Ref x) throws SQLException {
            }

            @Override
            public void updateRef(String columnName, Ref x) throws SQLException {
            }

            @Override
            public void updateRow() throws SQLException {
            }

            @Override
            public void updateShort(int columnIndex, short x) throws SQLException {
            }

            @Override
            public void updateShort(String columnName, short x) throws SQLException {
            }

            @Override
            public void updateString(int columnIndex, String x) throws SQLException {
            }

            @Override
            public void updateString(String columnName, String x) throws SQLException {
            }

            @Override
            public void updateTime(int columnIndex, Time x) throws SQLException {
            }

            @Override
            public void updateTime(String columnName, Time x) throws SQLException {
            }

            @Override
            public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException {
            }

            @Override
            public void updateTimestamp(String columnName, Timestamp x) throws SQLException {
            }

            @Override
            public boolean wasNull() throws SQLException {
                return false;
            }

            @Override
            public RowId getRowId(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public RowId getRowId(String columnLabel) throws SQLException {
                return null;
            }

            @Override
            public void updateRowId(int columnIndex, RowId x) throws SQLException {
            }

            @Override
            public void updateRowId(String columnLabel, RowId x) throws SQLException {
            }

            @Override
            public int getHoldability() throws SQLException {
                return 0;
            }

            @Override
            public void updateNString(int columnIndex, String nString) throws SQLException {
            }

            @Override
            public void updateNString(String columnLabel, String nString) throws SQLException {
            }

            @Override
            public void updateNClob(int columnIndex, NClob nClob) throws SQLException {
            }

            @Override
            public void updateNClob(String columnLabel, NClob nClob) throws SQLException {
            }

            @Override
            public NClob getNClob(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public NClob getNClob(String columnLabel) throws SQLException {
                return null;
            }

            @Override
            public SQLXML getSQLXML(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public SQLXML getSQLXML(String columnLabel) throws SQLException {
                return null;
            }

            @Override
            public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException {
            }

            @Override
            public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException {
            }

            @Override
            public String getNString(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public String getNString(String columnLabel) throws SQLException {
                return null;
            }

            @Override
            public Reader getNCharacterStream(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public Reader getNCharacterStream(String columnLabel) throws SQLException {
                return null;
            }

            @Override
            public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException {
            }

            @Override
            public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException {
            }

            @Override
            public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException {
            }

            @Override
            public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException {
            }

            @Override
            public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException {
            }

            @Override
            public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException {
            }

            @Override
            public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException {
            }

            @Override
            public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException {
            }

            @Override
            public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException {
            }

            @Override
            public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException {
            }

            @Override
            public void updateClob(int columnIndex, Reader reader, long length) throws SQLException {
            }

            @Override
            public void updateClob(String columnLabel, Reader reader, long length) throws SQLException {
            }

            @Override
            public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException {
            }

            @Override
            public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException {
            }

            @Override
            public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException {
            }

            @Override
            public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException {
            }

            @Override
            public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException {
            }

            @Override
            public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException {
            }

            @Override
            public void updateCharacterStream(int columnIndex, Reader x) throws SQLException {
            }

            @Override
            public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException {
            }

            @Override
            public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException {
            }

            @Override
            public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException {
            }

            @Override
            public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException {
            }

            @Override
            public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException {
            }

            @Override
            public void updateClob(int columnIndex, Reader reader) throws SQLException {
            }

            @Override
            public void updateClob(String columnLabel, Reader reader) throws SQLException {
            }

            @Override
            public void updateNClob(int columnIndex, Reader reader) throws SQLException {
            }

            @Override
            public void updateNClob(String columnLabel, Reader reader) throws SQLException {
            }

            @Override
            public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
                return null;
            }

            @Override
            public <T> T getObject(String columnLabel, Class<T> type) throws SQLException {
                return null;
            }

            @Override
            public <T> T unwrap(Class<T> iface) throws SQLException {
                return null;
            }

            @Override
            public boolean isWrapperFor(Class<?> iface) throws SQLException {
                return false;
            }

            @Override
            public BigInteger getBigInteger(int columnIndex) throws SQLException {
                return null;
            }

            @Override
            public void closeOwner() {
            }

            @Override
            public MysqlConnection getConnection() {
                return null;
            }

            @Override
            public Session getSession() {
                return null;
            }

            @Override
            public String getPointOfOrigin() {
                return null;
            }

            @Override
            public int getOwnerFetchSize() {
                return 0;
            }

            @Override
            public Query getOwningQuery() {
                return null;
            }

            @Override
            public int getOwningStatementMaxRows() {
                return 0;
            }

            @Override
            public int getOwningStatementFetchSize() {
                return 0;
            }

            @Override
            public long getOwningStatementServerId() {
                return 0;
            }

            @Override
            public int getResultId() {
                return 0;
            }

            @Override
            public void initRowsWithMetadata() {
            }

            @Override
            public ColumnDefinition getColumnDefinition() {
                return null;
            }

            @Override
            public void setColumnDefinition(ColumnDefinition metadata) {
            }

            @Override
            public void setNextResultset(Resultset nextResultset) {
            }

            @Override
            public ResultsetRows getRows() {
                return null;
            }

            @Override
            public Lock getLock() {
                return null;
            }

        };

        resultSetInternalMethods.close();
    }

    /**
     * Tests fix for BUG#34093 - Statements with batched values do not return correct values for getGeneratedKeys() when "rewriteBatchedStatements" is set to
     * "true", and the statement has an "ON DUPLICATE KEY UPDATE" clause.
     *
     * @throws Exception
     */
    @Test
    public void testBug34093() throws Exception {
        Connection rewriteConn = null;

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        rewriteConn = getConnectionWithProps(props);

        checkBug34093(rewriteConn);

        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        rewriteConn = getConnectionWithProps(props);

        checkBug34093(rewriteConn);
    }

    private void checkBug34093(Connection rewriteConn) throws Exception {
        try {
            String ddl = "(autoIncId INT NOT NULL PRIMARY KEY AUTO_INCREMENT, uniqueTextKey VARCHAR(255), UNIQUE KEY (uniqueTextKey(100)))";

            String[] sequence = { "c", "a", "d", "b" };
            String sql = "insert into testBug30493 (uniqueTextKey) values (?) on duplicate key UPDATE autoIncId = last_insert_id( autoIncId )";
            String tablePrimeSql = "INSERT INTO testBug30493 (uniqueTextKey) VALUES ('a'), ('b'), ('c'), ('d')";

            // setup the rewritten and non-written statements
            Statement stmts[] = new Statement[2];
            PreparedStatement pstmts[] = new PreparedStatement[2];
            stmts[0] = this.conn.createStatement();
            stmts[1] = rewriteConn.createStatement();
            pstmts[0] = this.conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            pstmts[1] = rewriteConn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            for (int i = 0; i < sequence.length; ++i) {
                String sqlLiteral = sql.replaceFirst("\\?", "'" + sequence[i] + "'");
                stmts[0].addBatch(sqlLiteral);
                stmts[1].addBatch(sqlLiteral);
                pstmts[0].setString(1, sequence[i]);
                pstmts[0].addBatch();
                pstmts[1].setString(1, sequence[i]);
                pstmts[1].addBatch();
            }

            // run the test once for Statement, and once for PreparedStatement
            Statement stmtSets[][] = new Statement[2][];
            stmtSets[0] = stmts;
            stmtSets[1] = pstmts;

            for (int stmtSet = 0; stmtSet < 2; ++stmtSet) {
                Statement testStmts[] = stmtSets[stmtSet];
                createTable("testBug30493", ddl);
                this.stmt.executeUpdate(tablePrimeSql);

                int nonRwUpdateCounts[] = testStmts[0].executeBatch();

                ResultSet nonRewrittenRsKeys = testStmts[0].getGeneratedKeys();

                createTable("testBug30493", ddl);
                this.stmt.executeUpdate(tablePrimeSql);
                int expectedUpdateCount = versionMeetsMinimum(5, 5, 16) ? 1 : 2; // behavior changed by fix of Bug#46675, affects servers starting from 5.5.16 and 5.6.3

                int rwUpdateCounts[] = testStmts[1].executeBatch();
                ResultSet rewrittenRsKeys = testStmts[1].getGeneratedKeys();
                for (int i = 0; i < 4; ++i) {
                    assertEquals(expectedUpdateCount, nonRwUpdateCounts[i]);
                    assertEquals(expectedUpdateCount, rwUpdateCounts[i]);
                }

                assertResultSetLength(nonRewrittenRsKeys, 4);
                assertResultSetLength(rewrittenRsKeys, 4);

                assertResultSetsEqual(nonRewrittenRsKeys, rewrittenRsKeys);
            }
        } finally {
            if (rewriteConn != null) {
                rewriteConn.close();
            }
        }
    }

    @Test
    public void testBug34093_nonbatch() throws Exception {
        Connection rewriteConn = null;

        try {
            String ddl = "(autoIncId INT NOT NULL PRIMARY KEY AUTO_INCREMENT, uniqueTextKey VARCHAR(255) UNIQUE KEY)";

            String sql = "insert into testBug30493 (uniqueTextKey) values ('c') on duplicate key UPDATE autoIncId = last_insert_id( autoIncId )";
            String tablePrimeSql = "INSERT INTO testBug30493 (uniqueTextKey) VALUES ('a'), ('b'), ('c'), ('d')";

            try {
                createTable("testBug30493", ddl);
            } catch (SQLException sqlEx) {
                if (sqlEx.getMessage().indexOf("max key length") != -1) {
                    createTable("testBug30493", "(autoIncId INT NOT NULL PRIMARY KEY AUTO_INCREMENT, uniqueTextKey VARCHAR(180) UNIQUE KEY)");
                }
            }
            this.stmt.executeUpdate(tablePrimeSql);

            Statement stmt1 = this.conn.createStatement();
            stmt1.execute(sql, Statement.RETURN_GENERATED_KEYS);
            int expectedUpdateCount = versionMeetsMinimum(5, 5, 16) ? 1 : 2; // behavior changed by fix of Bug#46675, affects servers starting from 5.5.16 and 5.6.3

            assertEquals(expectedUpdateCount, stmt1.getUpdateCount());
            ResultSet stmtKeys = stmt1.getGeneratedKeys();
            assertResultSetLength(stmtKeys, 1);

            try {
                createTable("testBug30493", ddl);
            } catch (SQLException sqlEx) {
                if (sqlEx.getMessage().indexOf("max key length") != -1) {
                    createTable("testBug30493", "(autoIncId INT NOT NULL PRIMARY KEY AUTO_INCREMENT, uniqueTextKey VARCHAR(180) UNIQUE KEY)");
                }
            }
            this.stmt.executeUpdate(tablePrimeSql);

            this.pstmt = this.conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            this.pstmt.execute();
            assertEquals(expectedUpdateCount, this.pstmt.getUpdateCount());
            ResultSet pstmtKeys = this.pstmt.getGeneratedKeys();
            assertResultSetLength(pstmtKeys, 1);

            assertResultSetsEqual(stmtKeys, pstmtKeys);
        } finally {
            if (rewriteConn != null) {
                rewriteConn.close();
            }
        }
    }

    @Test
    public void testBug34518() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useCursorFetch.getKeyName(), "true");
        Connection fetchConn = getConnectionWithProps(props);
        Statement fetchStmt = fetchConn.createStatement();

        int stmtCount = ((com.mysql.cj.jdbc.JdbcConnection) fetchConn).getActiveStatementCount();

        fetchStmt.setFetchSize(100);
        this.rs = fetchStmt.executeQuery("SELECT 1");

        assertEquals(((com.mysql.cj.jdbc.JdbcConnection) fetchConn).getActiveStatementCount(), stmtCount + 1);
        this.rs.close();
        assertEquals(((com.mysql.cj.jdbc.JdbcConnection) fetchConn).getActiveStatementCount(), stmtCount);
    }

    @Test
    public void testBug35170() throws Exception {
        Statement stt = null;

        try {
            stt = this.conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
            stt.setFetchSize(Integer.MIN_VALUE);
            this.rs = stt.executeQuery("select 1");
            this.rs.next();
            while (!this.rs.isAfterLast()) {
                this.rs.getString(1);
                this.rs.next();
            }
        } finally {
            if (stt != null) {
                stt.close();
            }
        }
    }

    /*
     * public void testBug35307() throws Exception { createTable("testBug35307",
     * "(`id` int(11) unsigned NOT NULL auto_increment," +
     * "`field` varchar(20) NOT NULL," + "`date` datetime NOT NULL," +
     * "PRIMARY KEY  (`id`)" + ") ENGINE=MyISAM DEFAULT CHARSET=latin1");
     *
     * this.stmt.executeUpdate("INSERT INTO testBug35307 (field) values ('works')"
     * ); }
     */

    @Test
    public void testBug35666() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.logSlowQueries.getKeyName(), "true");
        Connection loggingConn = getConnectionWithProps(props);
        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) loggingConn).serverPrepareStatement("SELECT SLEEP(4)");
        this.pstmt.execute();
    }

    @Test
    public void testDeadlockBatchBehavior() throws Exception {
        try {
            createTable("t1", "(id INTEGER, x INTEGER)", "INNODB");
            createTable("t2", "(id INTEGER, x INTEGER)", "INNODB");
            this.stmt.executeUpdate("INSERT INTO t1 VALUES (0, 0)");

            this.conn.setAutoCommit(false);
            this.rs = this.conn.createStatement().executeQuery("SELECT * FROM t1 WHERE id=0 FOR UPDATE");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.includeInnodbStatusInDeadlockExceptions.getKeyName(), "true");
            final Connection deadlockConn = getConnectionWithProps(props);
            deadlockConn.setAutoCommit(false);

            final Statement deadlockStmt = deadlockConn.createStatement();
            deadlockStmt.executeUpdate("INSERT INTO t2 VALUES (1, 0)");
            this.rs = deadlockStmt.executeQuery("SELECT * FROM t2 WHERE id=0 FOR UPDATE");

            new Thread() {

                @Override
                public void run() {
                    try {
                        deadlockStmt.addBatch("INSERT INTO t2 VALUES (1, 0)");
                        deadlockStmt.addBatch("INSERT INTO t2 VALUES (2, 0)");
                        deadlockStmt.addBatch("UPDATE t1 SET x=2 WHERE id=0");
                        deadlockStmt.executeBatch();
                    } catch (SQLException sqlEx) {
                        sqlEx.printStackTrace();
                        try {
                            deadlockConn.rollback();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }

            }.run();

            this.stmt.executeUpdate("INSERT INTO t1 VALUES (0, 0)");

        } catch (BatchUpdateException sqlEx) {
            int[] updateCounts = sqlEx.getUpdateCounts();
            for (int i = 0; i < updateCounts.length; i++) {
                System.out.println(updateCounts[i]);
            }
        } finally {
            this.conn.rollback();
            this.conn.setAutoCommit(true);
        }
    }

    @Test
    public void testBug39352() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useAffectedRows.getKeyName(), "true");
        Connection affectedRowsConn = getConnectionWithProps(props);

        try {

            createTable("bug39352", "(id INT PRIMARY KEY, data VARCHAR(100))");
            assertEquals(1, this.stmt.executeUpdate("INSERT INTO bug39352 (id,data) values (1,'a')"));
            int rowsAffected = this.stmt.executeUpdate("INSERT INTO bug39352 (id, data) VALUES(2, 'bb') ON DUPLICATE KEY UPDATE data=values(data)");
            assertEquals(1, rowsAffected, "First UPD failed");

            rowsAffected = affectedRowsConn.createStatement()
                    .executeUpdate("INSERT INTO bug39352 (id, data) VALUES(2, 'bbb') ON DUPLICATE KEY UPDATE data=values(data)");
            assertEquals(2, rowsAffected, "2nd UPD failed");

            rowsAffected = affectedRowsConn.createStatement()
                    .executeUpdate("INSERT INTO bug39352 (id, data) VALUES(2, 'bbb') ON DUPLICATE KEY UPDATE data=values(data)");
            assertEquals(0, rowsAffected, "3rd UPD failed");

        } finally {
            affectedRowsConn.close();
        }
    }

    @Test
    public void testBug38747() throws Exception {
        try {
            this.conn.setReadOnly(true);
            this.pstmt = this.conn.prepareStatement("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
            this.pstmt.setFetchSize(Integer.MIN_VALUE);

            this.rs = this.pstmt.executeQuery();

            while (this.rs.next()) {
            }

            this.rs.close();
            this.pstmt.close();

        } finally {
            this.conn.setReadOnly(false);
        }
    }

    @Test
    public void testBug39956() throws Exception {
        ResultSet enginesRs = this.conn.createStatement().executeQuery("SHOW ENGINES");

        while (enginesRs.next()) {
            if ("YES".equalsIgnoreCase(enginesRs.getString("Support")) || "DEFAULT".equalsIgnoreCase(enginesRs.getString("Support"))) {

                String engineName = enginesRs.getString("Engine");

                if ("CSV".equalsIgnoreCase(engineName) || "BLACKHOLE".equalsIgnoreCase(engineName) || "FEDERATED".equalsIgnoreCase(engineName)
                        || "MRG_MYISAM".equalsIgnoreCase(engineName) || "PARTITION".equalsIgnoreCase(engineName) || "EXAMPLE".equalsIgnoreCase(engineName)
                        || "PERFORMANCE_SCHEMA".equalsIgnoreCase(engineName) || engineName.endsWith("_SCHEMA")) {
                    continue; // not supported
                }

                String tableName = "testBug39956_" + engineName;

                Properties props = new Properties();
                props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
                props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
                props.setProperty(PropertyKey.sessionVariables.getKeyName(), "auto_increment_increment=2");
                Connection twoConn = getConnectionWithProps(props);

                try {
                    for (int i = 0; i < 2; i++) {
                        createTable(tableName, "(k int primary key auto_increment, p varchar(4)) ENGINE=" + engineName);

                        ((com.mysql.cj.jdbc.JdbcConnection) twoConn).getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).setValue(i == 1);

                        this.pstmt = twoConn.prepareStatement("INSERT INTO " + tableName + " (p) VALUES (?)", Statement.RETURN_GENERATED_KEYS);
                        this.pstmt.setString(1, "a");
                        this.pstmt.addBatch();
                        this.pstmt.setString(1, "b");
                        this.pstmt.addBatch();
                        this.pstmt.executeBatch();

                        this.rs = this.pstmt.getGeneratedKeys();

                        this.rs.next();
                        assertEquals(1, this.rs.getInt(1), "For engine " + engineName + (i == 1 ? " rewritten " : " plain "));
                        this.rs.next();
                        assertEquals(3, this.rs.getInt(1), "For engine " + engineName + (i == 1 ? " rewritten " : " plain "));

                        createTable(tableName, "(k int primary key auto_increment, p varchar(4)) ENGINE=" + engineName);
                        Statement twoStmt = twoConn.createStatement();
                        for (int j = 0; j < 10; j++) {
                            twoStmt.addBatch("INSERT INTO " + tableName + " (p) VALUES ('" + j + "')");
                        }

                        twoStmt.executeBatch(); // No getGeneratedKeys() support in JDBC spec, but we allow it...might have to rewrite test if/when we don't
                        this.rs = twoStmt.getGeneratedKeys();

                        int key = 1;

                        for (int j = 0; j < 10; j++) {
                            this.rs.next();
                            assertEquals(key, this.rs.getInt(1), "For engine " + engineName + (i == 1 ? " rewritten " : " plain "));
                            key += 2;
                        }
                    }
                } finally {
                    if (twoConn != null) {
                        twoConn.close();
                    }
                }
            }
        }
    }

    @Test
    public void testBug34185() throws Exception {
        this.rs = this.stmt.executeQuery("SELECT 1");

        try {
            this.stmt.getGeneratedKeys();
            fail("Expected exception");
        } catch (SQLException sqlEx) {
            assertEquals(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, sqlEx.getSQLState());
        }

        this.pstmt = this.conn.prepareStatement("SELECT 1");

        try {
            this.pstmt.execute();
            this.pstmt.getGeneratedKeys();
            fail("Expected exception");
        } catch (SQLException sqlEx) {
            assertEquals(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT, sqlEx.getSQLState());
        }
    }

    @Test
    public void testBug41161() throws Exception {
        createTable("testBug41161", "(a int, b int)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        Connection rewriteConn = getConnectionWithProps(props);

        try {
            this.pstmt = rewriteConn.prepareStatement("INSERT INTO testBug41161 (a, b) VALUES (?, ?, ?)");
            this.pstmt.setInt(1, 1);
            this.pstmt.setInt(2, 1);

            try {
                this.pstmt.addBatch();
                fail("Should have thrown an exception");
            } catch (SQLException sqlEx) {
                assertEquals("07001", sqlEx.getSQLState());
            }

            this.pstmt.executeBatch(); // NPE when this bug exists
        } finally {
            rewriteConn.close();
        }
    }

    /**
     * Ensures that cases listed in Bug#41448 actually work - we don't think there's a bug here right now
     *
     * @throws Exception
     */
    @Test
    public void testBug41448() throws Exception {
        createTable("testBug41448", "(pk INT PRIMARY KEY AUTO_INCREMENT, field1 VARCHAR(4))");

        this.stmt.executeUpdate("INSERT INTO testBug41448 (field1) VALUES ('abc')", Statement.RETURN_GENERATED_KEYS);
        this.stmt.getGeneratedKeys();

        this.stmt.executeUpdate("INSERT INTO testBug41448 (field1) VALUES ('def')", new int[] { 1 });
        this.stmt.getGeneratedKeys();

        this.stmt.executeUpdate("INSERT INTO testBug41448 (field1) VALUES ('ghi')", new String[] { "pk" });
        this.stmt.getGeneratedKeys();

        this.stmt.executeUpdate("INSERT INTO testBug41448 (field1) VALUES ('ghi')");

        assertThrows(SQLException.class, () -> {
            this.stmt.getGeneratedKeys();
            return null;
        });

        this.stmt.execute("INSERT INTO testBug41448 (field1) VALUES ('jkl')", Statement.RETURN_GENERATED_KEYS);
        this.stmt.getGeneratedKeys();

        this.stmt.execute("INSERT INTO testBug41448 (field1) VALUES ('mno')", new int[] { 1 });
        this.stmt.getGeneratedKeys();

        this.stmt.execute("INSERT INTO testBug41448 (field1) VALUES ('pqr')", new String[] { "pk" });
        this.stmt.getGeneratedKeys();

        this.stmt.execute("INSERT INTO testBug41448 (field1) VALUES ('stu')");

        assertThrows(SQLException.class, () -> {
            this.stmt.getGeneratedKeys();
            return null;
        });

        this.pstmt = this.conn.prepareStatement("INSERT INTO testBug41448 (field1) VALUES (?)", Statement.RETURN_GENERATED_KEYS);
        this.pstmt.setString(1, "abc");
        this.pstmt.executeUpdate();
        this.pstmt.getGeneratedKeys();
        this.pstmt.execute();
        this.pstmt.getGeneratedKeys();

        this.pstmt = this.conn.prepareStatement("INSERT INTO testBug41448 (field1) VALUES (?)", new int[] { 1 });
        this.pstmt.setString(1, "abc");
        this.pstmt.executeUpdate();
        this.pstmt.getGeneratedKeys();
        this.pstmt.execute();
        this.pstmt.getGeneratedKeys();

        this.pstmt = this.conn.prepareStatement("INSERT INTO testBug41448 (field1) VALUES (?)", new String[] { "pk" });
        this.pstmt.setString(1, "abc");
        this.pstmt.executeUpdate();
        this.pstmt.getGeneratedKeys();
        this.pstmt.execute();
        this.pstmt.getGeneratedKeys();

        this.pstmt = this.conn.prepareStatement("INSERT INTO testBug41448 (field1) VALUES (?)");
        this.pstmt.setString(1, "abc");
        this.pstmt.executeUpdate();
        assertThrows(SQLException.class, () -> {
            this.pstmt.getGeneratedKeys();
            return null;
        });

        this.pstmt.execute();

        assertThrows(SQLException.class, () -> {
            this.pstmt.getGeneratedKeys();
            return null;
        });
    }

    @Test
    public void testBug48172() throws Exception {
        createTable("testBatchInsert", "(a INT PRIMARY KEY AUTO_INCREMENT)");
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        props.setProperty(PropertyKey.dumpQueriesOnException.getKeyName(), "true");
        Connection rewriteConn = getConnectionWithProps(props);
        assertEquals("0", getSingleIndexedValueWithQuery(rewriteConn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());

        this.pstmt = rewriteConn.prepareStatement("INSERT INTO testBatchInsert VALUES (?)");
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.executeBatch();

        assertEquals("1", getSingleIndexedValueWithQuery(rewriteConn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());
        this.pstmt = rewriteConn.prepareStatement("INSERT INTO `testBatchInsert`VALUES (?)");
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.executeBatch();

        assertEquals("2", getSingleIndexedValueWithQuery(rewriteConn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());

        this.pstmt = rewriteConn.prepareStatement("INSERT INTO testBatchInsert VALUES(?)");
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.executeBatch();

        assertEquals("3", getSingleIndexedValueWithQuery(rewriteConn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());

        this.pstmt = rewriteConn.prepareStatement("INSERT INTO testBatchInsert VALUES\n(?)");
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.setNull(1, java.sql.Types.INTEGER);
        this.pstmt.addBatch();
        this.pstmt.executeBatch();

        assertEquals("4", getSingleIndexedValueWithQuery(rewriteConn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());
    }

    /**
     * Tests fix for Bug#41532 - regression in performance for batched inserts when using ON DUPLICATE KEY UPDATE
     *
     * @throws Exception
     */
    @Test
    public void testBug41532() throws Exception {
        this.rs = this.stmt.executeQuery("SHOW VARIABLES LIKE 'max_allowed_packet'");
        this.rs.next();
        long len = 1024 * 1024 * 2;
        long defaultMaxAllowedPacket = this.rs.getInt(2);
        boolean changeMaxAllowedPacket = defaultMaxAllowedPacket < len;

        createTable("testBug41532", "(ID INTEGER, S1 VARCHAR(100), S2 VARCHAR(100), S3 VARCHAR(100), D1 DATETIME, D2 DATETIME, D3 DATETIME, "
                + "N1 DECIMAL(28,6), N2 DECIMAL(28,6), N3 DECIMAL(28,6), UNIQUE KEY UNIQUE_KEY_TEST_DUPLICATE (ID) )");

        try {
            if (changeMaxAllowedPacket) {
                this.stmt.executeUpdate("SET GLOBAL max_allowed_packet=" + len);
            }

            int numTests = 5000;
            Connection rewriteConn = getConnectionWithProps(
                    "useSSL=false,allowPublicKeyRetrieval=true,rewriteBatchedStatements=true,dumpQueriesOnException=true");

            assertEquals("0", getSingleIndexedValueWithQuery(rewriteConn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());
            long batchedTime = timeBatch(rewriteConn, numTests);
            assertEquals("1", getSingleIndexedValueWithQuery(rewriteConn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());

            this.stmt.executeUpdate("TRUNCATE TABLE testBug41532");

            assertEquals("0", getSingleIndexedValueWithQuery(this.conn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());
            long unbatchedTime = timeBatch(this.conn, numTests);
            assertEquals(String.valueOf(numTests), getSingleIndexedValueWithQuery(this.conn, 2, "SHOW SESSION STATUS LIKE 'Com_insert'").toString());
            assertTrue(batchedTime < unbatchedTime);

            rewriteConn = getConnectionWithProps(
                    "useSSL=false,allowPublicKeyRetrieval=true,rewriteBatchedStatements=true,useCursorFetch=true,defaultFetchSize=10000");
            timeBatch(rewriteConn, numTests);
        } finally {
            if (changeMaxAllowedPacket) {
                this.stmt.executeUpdate("SET GLOBAL max_allowed_packet=" + defaultMaxAllowedPacket);
            }
        }
    }

    private long timeBatch(Connection c, int numberOfRows) throws SQLException {
        this.pstmt = c.prepareStatement("INSERT INTO testBug41532(ID, S1, S2, S3, D1, D2, D3, N1, N2, N3) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
                + " ON DUPLICATE KEY UPDATE S1 = VALUES(S1), S2 = VALUES(S2), S3 = VALUES(S3), D1 = VALUES(D1), D2 ="
                + " VALUES(D2), D3 = VALUES(D3), N1 = N1 + VALUES(N1), N2 = N2 + VALUES(N2), N2 = N2 + VALUES(N2)");
        c.setAutoCommit(false);
        c.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
        Date d1 = new Date(currentTimeMillis());
        Date d2 = new Date(currentTimeMillis() + 1000000);
        Date d3 = new Date(currentTimeMillis() + 1250000);

        for (int i = 0; i < numberOfRows; i++) {
            this.pstmt.setObject(1, new Integer(i), Types.INTEGER);
            this.pstmt.setObject(2, String.valueOf(i), Types.VARCHAR);
            this.pstmt.setObject(3, String.valueOf(i * 0.1), Types.VARCHAR);
            this.pstmt.setObject(4, String.valueOf(i / 3), Types.VARCHAR);
            this.pstmt.setObject(5, new Timestamp(d1.getTime()), Types.TIMESTAMP);
            this.pstmt.setObject(6, new Timestamp(d2.getTime()), Types.TIMESTAMP);
            this.pstmt.setObject(7, new Timestamp(d3.getTime()), Types.TIMESTAMP);
            this.pstmt.setObject(8, new BigDecimal(i + 0.1), Types.DECIMAL);
            this.pstmt.setObject(9, new BigDecimal(i * 0.1), Types.DECIMAL);
            this.pstmt.setObject(10, new BigDecimal(i / 3), Types.DECIMAL);
            this.pstmt.addBatch();
        }
        long startTime = currentTimeMillis();
        this.pstmt.executeBatch();
        c.commit();
        long stopTime = currentTimeMillis();

        this.rs = this.conn.createStatement().executeQuery("SELECT COUNT(*) FROM testBug41532");
        assertTrue(this.rs.next());
        assertEquals(numberOfRows, this.rs.getInt(1));

        return stopTime - startTime;
    }

    /**
     * Tests fix for Bug#44056 - Statement.getGeneratedKeys() retains result set instances until statement is closed.
     *
     * @throws Exception
     */
    @Test
    public void testBug44056() throws Exception {
        createTable("testBug44056", "(pk int primary key not null auto_increment)");
        Statement newStmt = this.conn.createStatement();

        try {
            newStmt.executeUpdate("INSERT INTO testBug44056 VALUES (null)", Statement.RETURN_GENERATED_KEYS);
            checkOpenResultsFor44056(newStmt);
            this.pstmt = this.conn.prepareStatement("INSERT INTO testBug44056 VALUES (null)", Statement.RETURN_GENERATED_KEYS);
            this.pstmt.executeUpdate();
            checkOpenResultsFor44056(this.pstmt);
            this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).serverPrepareStatement("INSERT INTO testBug44056 VALUES (null)",
                    Statement.RETURN_GENERATED_KEYS);
            this.pstmt.executeUpdate();
            checkOpenResultsFor44056(this.pstmt);
        } finally {
            newStmt.close();
        }
    }

    private void checkOpenResultsFor44056(Statement newStmt) throws SQLException {
        this.rs = newStmt.getGeneratedKeys();
        assertEquals(0, ((com.mysql.cj.jdbc.JdbcStatement) newStmt).getOpenResultSetCount());
        this.rs.close();
        assertEquals(0, ((com.mysql.cj.jdbc.JdbcStatement) newStmt).getOpenResultSetCount());
    }

    @Test
    public void testBug43196() throws Exception {
        createTable("`bug43196`",
                "(`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, `a` bigint(20) unsigned NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=latin1;");

        Connection conn1 = null;

        try {
            assertEquals(1, this.stmt.executeUpdate("INSERT INTO bug43196 (a) VALUES (1)", Statement.RETURN_GENERATED_KEYS));

            this.rs = this.stmt.getGeneratedKeys();

            if (this.rs.next()) {

                Object id = this.rs.getObject(1);// use long

                assertEquals(BigInteger.class, id.getClass());
            }

            this.rs.close();

            this.rs = this.stmt.executeQuery("select id from bug43196");

            if (this.rs.next()) {
                Object id = this.rs.getObject(1);// use BigInteger

                assertEquals(BigInteger.class, id.getClass());
            }

            this.rs.close();

            // insert a id > Long.MAX_VALUE(9223372036854775807)

            assertEquals(1, this.stmt.executeUpdate("insert into bug43196(id,a) values(18446744073709551200,1)", Statement.RETURN_GENERATED_KEYS));

            this.rs = this.stmt.getGeneratedKeys();

            this.rs.first();

            assertTrue(this.rs.isFirst(), "No rows returned");
            assertEquals("18446744073709551200", this.rs.getObject(1).toString());
        } finally {
            if (conn1 != null) {
                conn1.close();
            }
        }
    }

    /**
     * Bug #42253 - multiple escaped quotes cause exception from EscapeProcessor.
     *
     * @throws Exception
     */
    @Test
    public void testBug42253() throws Exception {
        this.rs = this.stmt.executeQuery("select '\\'\\'','{t\\'}'");
        this.rs.next();
        assertEquals("''", this.rs.getString(1));
        assertEquals("{t'}", this.rs.getString(2));
    }

    /**
     * Bug #41566 - Quotes within comments not correctly ignored by escape parser
     *
     * @throws Exception
     */
    @Test
    public void testBug41566() throws Exception {
        this.rs = this.stmt.executeQuery("-- this should't change the literal\n select '{1}'");
        this.rs.next();
        assertEquals("{1}", this.rs.getString(1));
    }

    /*
     * Bug #40439 - Error rewriting batched statement if table name ends with "values".
     */
    @Test
    public void testBug40439() throws Exception {
        Connection conn2 = null;
        try {
            createTable("testBug40439VALUES", "(x int)");
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
            conn2 = getConnectionWithProps(props);
            PreparedStatement ps = conn2.prepareStatement("insert into testBug40439VALUES (x) values (?)");
            ps.setInt(1, 1);
            ps.addBatch();
            ps.setInt(1, 2);
            ps.addBatch();
            ps.executeBatch();
        } finally {
            if (conn2 != null) {
                try {
                    conn2.close();
                } catch (SQLException ex) {
                }
            }
        }
    }

    public static class Bug39426Interceptor extends BaseQueryInterceptor {

        public static List<Integer> vals = new ArrayList<>();
        String prevSql;

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            if (interceptedQuery instanceof ClientPreparedStatement) {
                String asSql = interceptedQuery.toString();
                int firstColon = asSql.indexOf(":");
                asSql = asSql.substring(firstColon + 2);

                if (asSql.equals(this.prevSql)) {
                    throw new RuntimeException("Previous statement matched current: " + sql.get());
                }
                this.prevSql = asSql;
                try {
                    ParameterBindings b = ((ClientPreparedStatement) interceptedQuery).getParameterBindings();
                    vals.add(new Integer(b.getInt(1)));

                } catch (SQLException ex) {
                    throw ExceptionFactory.createException(ex.getMessage(), ex);
                }
            }
            return null;
        }

    }

    /**
     * Bug #39426 - executeBatch passes most recent PreparedStatement params to StatementInterceptor
     *
     * @throws Exception
     */
    @Test
    public void testBug39426() throws Exception {
        for (boolean useSPS : new boolean[] { false, true }) {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.queryInterceptors.getKeyName(), "testsuite.regression.StatementRegressionTest$Bug39426Interceptor");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            Connection c = null;
            try {
                createTable("testBug39426", "(x int)");
                c = getConnectionWithProps(props);
                PreparedStatement ps = c.prepareStatement("insert into testBug39426 values (?)");
                ps.setInt(1, 1);
                ps.addBatch();
                ps.setInt(1, 2);
                ps.addBatch();
                ps.setInt(1, 3);
                ps.addBatch();
                ps.executeBatch();
                List<Integer> vals = Bug39426Interceptor.vals;
                assertEquals(new Integer(1), vals.get(0));
                assertEquals(new Integer(2), vals.get(1));
                assertEquals(new Integer(3), vals.get(2));
            } finally {
                if (c != null) {
                    c.close();
                }
            }
        }
    }

    @Test
    public void testBugDupeKeySingle() throws Exception {
        createTable("testBugDupeKeySingle", "(field1 int not null primary key)");
        Connection conn2 = null;
        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
            conn2 = getConnectionWithProps(props);

            this.pstmt = conn2.prepareStatement("INSERT INTO testBugDupeKeySingle VALUES (?) ON DUPLICATE KEY UPDATE field1=VALUES(field1)");
            this.pstmt.setInt(1, 1);
            this.pstmt.addBatch();
            this.pstmt.executeBatch();

            // this should be a syntax error
            this.pstmt = conn2.prepareStatement("INSERT INTO testBugDupeKeySingle VALUES (?) ON DUPLICATE KEY UPDATE");
            this.pstmt.setInt(1, 1);
            this.pstmt.addBatch();
            try {
                this.pstmt.executeBatch();
            } catch (SQLException sqlEx) {
                assertEquals(MysqlErrorNumbers.SQLSTATE_SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION_NO_SUBCLASS, sqlEx.getSQLState());
            }

            this.pstmt = conn2.prepareStatement("INSERT INTO testBugDupeKeySingle VALUES (?)");
            this.pstmt.setInt(1, 2);
            this.pstmt.addBatch();
            this.pstmt.executeBatch();
            this.pstmt.setInt(1, 3);
            this.pstmt.setInt(1, 4);
            this.pstmt.executeBatch();
        } finally {
            if (conn2 != null) {
                conn2.close();
            }
        }
    }

    /**
     * Bug #37458 - MySQL 5.1 returns generated keys in ascending order
     *
     * @throws Exception
     */
    @Test
    public void testBug37458() throws Exception {
        int ids[] = { 13, 1, 8 };
        String vals[] = { "c", "a", "b" };
        createTable("testBug37458", "(id int not null auto_increment, val varchar(100), primary key (id), unique (val))");
        this.stmt.executeUpdate("insert into testBug37458 values (1, 'a'), (8, 'b'), (13, 'c')");
        this.pstmt = this.conn.prepareStatement("insert into testBug37458 (val) values (?) on duplicate key update id = last_insert_id(id)",
                Statement.RETURN_GENERATED_KEYS);
        for (int i = 0; i < ids.length; ++i) {
            this.pstmt.setString(1, vals[i]);
            this.pstmt.addBatch();
        }
        this.pstmt.executeBatch();
        ResultSet keys = this.pstmt.getGeneratedKeys();
        for (int i = 0; i < ids.length; ++i) {
            assertTrue(keys.next());
            assertEquals(ids[i], keys.getInt(1));
        }
    }

    @Test
    public void testBug34555() throws Exception {
        createTable("testBug34555", "(field1 int)", "INNODB");
        this.stmt.executeUpdate("INSERT INTO testBug34555 VALUES (0)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        final Connection lockerConn = getConnectionWithProps(props);
        lockerConn.setAutoCommit(false);
        lockerConn.createStatement().execute("SELECT * FROM testBug34555 WHERE field1=0 FOR UPDATE");

        this.conn.setAutoCommit(false);

        this.pstmt = this.conn.prepareStatement("UPDATE testBug34555 SET field1=1 WHERE field1=?");
        this.pstmt.setQueryTimeout(1);
        this.pstmt.setInt(1, 0);
        this.pstmt.addBatch();
        this.pstmt.setInt(1, 2);
        this.pstmt.addBatch();

        try {
            this.pstmt.executeBatch();
        } catch (BatchUpdateException batchEx) {
            assertTrue(batchEx.getMessage().startsWith("Statement cancelled"));
        } finally {
            this.conn.setAutoCommit(true);
            lockerConn.commit();
        }
    }

    @Test
    public void testBug46788() throws Exception {
        createTable("testBug46788", "(modified varchar(32), id varchar(32))");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        Connection rewriteConn = getConnectionWithProps(props);

        this.pstmt = rewriteConn.prepareStatement("insert into testBug46788 (modified,id) values (?,?) ON DUPLICATE KEY UPDATE modified=?");

        this.pstmt.setString(1, "theID");
        this.pstmt.setString(2, "Hello_world_");
        this.pstmt.setString(3, "Hello_world_");

        for (int i = 0; i < 10; i++) {
            this.pstmt.addBatch();
        }

        this.pstmt.executeBatch();
    }

    @Test
    public void testBug31193() throws Exception {
        createTable("bug31193", "(sometime datetime, junk text)");
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useCursorFetch.getKeyName(), "true");
        Connection fetchConn = getConnectionWithProps(props);
        Statement fetchStmt = fetchConn.createStatement();

        fetchStmt.setFetchSize(10000);

        assertEquals(1, fetchStmt.executeUpdate("INSERT INTO bug31193 (sometime) values ('2007-01-01 12:34:56.7')"));
        this.rs = fetchStmt.executeQuery("SELECT * FROM bug31193");
        this.rs.next();
        String badDatetime = this.rs.getString("sometime");

        this.rs = fetchStmt.executeQuery("SELECT sometime FROM bug31193");
        this.rs.next();
        String goodDatetime = this.rs.getString("sometime");
        assertEquals(goodDatetime, badDatetime);
    }

    @Test
    public void testBug51776() throws Exception {
        Properties props = getHostFreePropertiesFromTestsuiteUrl();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.socketFactory.getKeyName(), "testsuite.UnreliableSocketFactory");

        Properties parsed = getPropertiesFromTestsuiteUrl();
        String db = parsed.getProperty(PropertyKey.DBNAME.getKeyName());
        String port = parsed.getProperty(PropertyKey.PORT.getKeyName());
        String host = getPortFreeHostname(props);

        UnreliableSocketFactory.flushAllStaticData();
        UnreliableSocketFactory.mapHost("first", host);

        Connection testConn = getConnectionWithProps("jdbc:mysql://first:" + port + "/" + db, props);
        testConn.setAutoCommit(false);
        testConn.createStatement().execute("SELECT 1");
        UnreliableSocketFactory.downHost("first");
        assertThrows("Should receive SQLException on rollback().", SQLException.class, () -> {
            testConn.rollback();
            return null;
        });
    }

    @Test
    public void testBug51666() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.queryInterceptors.getKeyName(), TestBug51666QueryInterceptor.class.getName());
        Connection testConn = getConnectionWithProps(props);
        createTable("testQueryInterceptorCount", "(field1 int)");
        this.stmt.executeUpdate("INSERT INTO testQueryInterceptorCount VALUES (0)");
        ResultSet testRs = testConn.createStatement().executeQuery("SHOW SESSION STATUS LIKE 'Com_select'");
        testRs.next();
        int s = testRs.getInt(2);
        this.rs = testConn.createStatement().executeQuery("SELECT 1");
        testRs = testConn.createStatement().executeQuery("SHOW SESSION STATUS LIKE 'Com_select'");
        testRs.next();
        assertEquals(s + 1, testRs.getInt(2));
    }

    public static class TestBug51666QueryInterceptor extends BaseQueryInterceptor {

        private JdbcConnection connection;

        @Override
        public QueryInterceptor init(MysqlConnection conn, Properties props, Log log) {
            this.connection = (JdbcConnection) conn;
            return this;
        }

        @SuppressWarnings("unchecked")
        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            if (sql.get().equals("SELECT 1")) {
                try {
                    java.sql.Statement test = this.connection.createStatement();
                    return (T) test.executeQuery("/* execute this, not the original */ SELECT 1");
                } catch (SQLException ex) {
                    throw ExceptionFactory.createException(ex.getMessage(), ex);
                }
            }
            return null;
        }

        @Override
        public void destroy() {
            this.connection = null;
        }

    }

    @Test
    public void testReversalOfScanFlags() throws Exception {
        createTable("testReversalOfScanFlags", "(field1 int)");
        this.stmt.executeUpdate("INSERT INTO testReversalOfScanFlags VALUES (1),(2),(3)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.queryInterceptors.getKeyName(), ScanDetectingInterceptor.class.getName());
        Connection scanningConn = getConnectionWithProps(props);

        try {
            this.rs = scanningConn.createStatement().executeQuery("SELECT field1 FROM testReversalOfScanFlags");
            assertTrue(ScanDetectingInterceptor.hasSeenScan);
            assertFalse(ScanDetectingInterceptor.hasSeenBadIndex);
        } finally {
            scanningConn.close();
        }
    }

    public static class ScanDetectingInterceptor extends BaseQueryInterceptor {

        static boolean hasSeenScan = false;
        static boolean hasSeenBadIndex = false;

        @Override
        public <T extends Resultset> T postProcess(Supplier<String> sql, Query interceptedQuery, T originalResultSet, ServerSession serverSession) {
            if (serverSession.noIndexUsed()) {
                hasSeenScan = true;
            }

            if (serverSession.noGoodIndexUsed()) {
                hasSeenBadIndex = true;
            }

            return null;
        }

    }

    /**
     * Tests fix for Bug#51704, rewritten batched statements don't honor escape processing flag of Statement that they are created for
     *
     * @throws Exception
     */
    @Test
    public void testBug51704() throws Exception {
        createTable("testBug51704", "(field1 TIMESTAMP)");
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        Connection rewriteConn = getConnectionWithProps(props);
        Statement rewriteStmt = rewriteConn.createStatement();

        try {
            rewriteStmt.setEscapeProcessing(false);

            for (int i = 0; i < 20; i++) {
                rewriteStmt.addBatch("INSERT INTO testBug51704 VALUES ({tsp '2002-11-12 10:00:00'})");
            }

            rewriteStmt.executeBatch(); // this should pass, because mysqld doesn't validate any escape sequences,
                                       // it just strips them, where our escape processor validates them

            Statement batchStmt = this.conn.createStatement();
            batchStmt.setEscapeProcessing(false);
            batchStmt.addBatch("INSERT INTO testBug51704 VALUES ({tsp '2002-11-12 10:00:00'})");
            batchStmt.executeBatch(); // same here
        } finally {
            rewriteConn.close();
        }
    }

    @Test
    public void testBug54175() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.characterEncoding.getKeyName(), "UTF-8");
        Connection utf8conn = getConnectionWithProps(props);

        createTable("testBug54175", "(a VARCHAR(10)) CHARACTER SET utf8mb4");
        this.stmt.execute("INSERT INTO testBug54175 VALUES(0xF0AFA6B2)");
        this.rs = utf8conn.createStatement().executeQuery("SELECT * FROM testBug54175");
        assertTrue(this.rs.next());
        assertEquals(55422, this.rs.getString(1).charAt(0));
    }

    /**
     * Tests fix for Bug#58728, NPE in com.mysql.jdbc.StatementWrappe.getResultSet()
     * ((com.mysql.jdbc.ResultSetInternalMethods) rs).setWrapperStatement(this);
     * when rs is null
     *
     * @throws Exception
     */
    @Test
    public void testBug58728() throws Exception {
        createTable("testbug58728", "(Id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, txt VARCHAR(50))", "InnoDB");
        this.stmt.executeUpdate("INSERT INTO testbug58728 VALUES (NULL, 'Text 1'), (NULL, 'Text 2')");

        MysqlConnectionPoolDataSource pds = new MysqlConnectionPoolDataSource();
        pds.setUrl(dbUrl);
        pds.<SslMode>getEnumProperty(PropertyKey.sslMode).setValue(SslMode.DISABLED);
        pds.getBooleanProperty(PropertyKey.allowPublicKeyRetrieval).setValue(true);
        Statement stmt1 = pds.getPooledConnection().getConnection().createStatement();
        stmt1.executeUpdate("UPDATE testbug58728 SET txt = 'New text' WHERE Id > 0");
        ResultSet rs1 = stmt1.getResultSet();
        stmt1.close();
        if (rs1 != null) {
            rs1.close();
        }
    }

    @Test
    public void testBug61501() throws Exception {
        createTable("testBug61501", "(id int)");
        this.stmt.executeUpdate("INSERT INTO testBug61501 VALUES (1)");
        String sql = "SELECT id FROM testBug61501 where id=1";
        this.pstmt = this.conn.prepareStatement(sql);
        this.rs = this.pstmt.executeQuery();
        this.pstmt.cancel();
        this.pstmt.close();

        this.pstmt = this.conn.prepareStatement(sql);
        this.rs = this.pstmt.executeQuery();

        this.stmt.cancel();
        this.rs = this.stmt.executeQuery(sql);
        this.stmt.cancel();
        this.stmt.execute(sql);
        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).serverPrepareStatement(sql);
        this.pstmt.execute();
        this.pstmt.cancel();
        this.pstmt.execute();

        sql = "INSERT INTO testBug61501 VALUES (2)";
        this.pstmt = this.conn.prepareStatement(sql);
        this.pstmt.execute();
        assertEquals(1, this.pstmt.getUpdateCount());
        this.pstmt.cancel();
        this.pstmt.close();

        this.pstmt = this.conn.prepareStatement(sql);
        assertEquals(1, this.pstmt.executeUpdate());

        this.stmt.cancel();
        assertEquals(1, this.stmt.executeUpdate(sql));
        this.stmt.cancel();
        this.stmt.execute(sql);
        assertEquals(1, this.stmt.getUpdateCount());

        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).serverPrepareStatement(sql);
        this.pstmt.execute();
        assertEquals(1, this.pstmt.getUpdateCount());
        this.pstmt.cancel();
        this.pstmt.close();

        this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).serverPrepareStatement(sql);
        assertEquals(1, this.pstmt.executeUpdate());

        this.pstmt.cancel();
        this.pstmt.addBatch();
        this.pstmt.addBatch();
        this.pstmt.addBatch();
        int[] counts = this.pstmt.executeBatch();

        for (int i = 0; i < counts.length; i++) {
            assertEquals(1, counts[i]);
        }

        this.pstmt = this.conn.prepareStatement(sql);
        this.pstmt.cancel();
        this.pstmt.addBatch();
        this.pstmt.addBatch();
        this.pstmt.addBatch();
        counts = this.pstmt.executeBatch();

        for (int i = 0; i < counts.length; i++) {
            assertEquals(1, counts[i]);
        }

        this.stmt.cancel();
        this.stmt.addBatch(sql);
        this.stmt.addBatch(sql);
        this.stmt.addBatch(sql);

        counts = this.stmt.executeBatch();

        for (int i = 0; i < counts.length; i++) {
            assertEquals(1, counts[i]);
        }
    }

    @Test
    public void testbug61866() throws Exception {
        createProcedure("WARN_PROCEDURE", "() BEGIN	DECLARE l_done INT;	SELECT 1 	INTO l_done	FROM DUAL	WHERE 1=2; END");
        this.pstmt = this.conn.prepareCall("{CALL WARN_PROCEDURE()}");
        this.pstmt.execute();
        assertTrue(this.pstmt.getWarnings().toString().contentEquals("java.sql.SQLWarning: No data - zero rows fetched, selected, or processed"),
                "No warning when expected");
        this.pstmt.clearWarnings();
        assertNull(this.pstmt.getWarnings(), "Warning when not expected");
    }

    @Test
    public void testbug12565726() throws Exception {
        // Not putting the space between VALUES() and ON DUPLICATE KEY UPDATE
        // causes C/J a) enter rewriting the query altrhough it has ON UPDATE
        // and b) to generate the wrong query with multiple ON DUPLICATE KEY

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
        props.setProperty(PropertyKey.enablePacketDebug.getKeyName(), "true");
        this.conn = getConnectionWithProps(props);
        this.stmt = this.conn.createStatement();

        try {
            createTable("testbug12565726", "(id int primary key, txt1 varchar(32))");
            this.stmt.executeUpdate("INSERT INTO testbug12565726 (id, txt1) VALUES (1, 'something')");

            this.pstmt = this.conn.prepareStatement("INSERT INTO testbug12565726 (id, txt1) VALUES (?, ?)ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)+10");

            this.pstmt.setInt(1, 1);
            this.pstmt.setString(2, "something else");
            this.pstmt.addBatch();

            this.pstmt.setInt(1, 2);
            this.pstmt.setString(2, "hope it is not error again!");
            this.pstmt.addBatch();

            this.pstmt.executeBatch();

        } finally {
        }
    }

    @Test
    public void testBug36478() throws Exception {
        createTable("testBug36478", "(`limit` varchar(255) not null primary key, id_limit INT, limit1 INT, maxlimit2 INT)");

        this.stmt.execute("INSERT INTO testBug36478 VALUES ('bahblah',1,1,1)");
        this.stmt.execute("INSERT INTO testBug36478 VALUES ('bahblah2',2,2,2)");
        this.pstmt = this.conn.prepareStatement("select 1 FROM testBug36478", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);

        this.pstmt.setMaxRows(1);
        this.rs = this.pstmt.executeQuery();
        this.rs.first();
        assertTrue(this.rs.isFirst());
        assertTrue(this.rs.isLast());

        this.pstmt = this.conn.prepareStatement("select `limit`, id_limit, limit1, maxlimit2 FROM testBug36478", ResultSet.TYPE_SCROLL_INSENSITIVE,
                ResultSet.CONCUR_READ_ONLY);
        this.pstmt.setMaxRows(0);
        this.rs = this.pstmt.executeQuery();
        this.rs.first();
        assertTrue(this.rs.isFirst());
        assertFalse(this.rs.isLast());

        //SSPS
        Connection _conn = null;
        PreparedStatement s = null;
        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");

            _conn = getConnectionWithProps(props);
            s = _conn.prepareStatement("select 1 FROM testBug36478", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);

            s.setMaxRows(1);
            ResultSet _rs = s.executeQuery();
            _rs.first();
            assertTrue(_rs.isFirst());
            assertTrue(_rs.isLast());

            s.close();
            s = _conn.prepareStatement("select `limit`, id_limit, limit1, maxlimit2 FROM testBug36478", ResultSet.TYPE_SCROLL_INSENSITIVE,
                    ResultSet.CONCUR_READ_ONLY);
            s.setMaxRows(0);
            _rs = s.executeQuery();
            _rs.first();
            assertTrue(_rs.isFirst());
            assertFalse(_rs.isLast());

        } finally {
            if (s != null) {
                s.close();
            }
            if (_conn != null) {
                _conn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#40279 - Timestamp values get truncated when passed as prepared statement parameters
     * (and duplicate BUG#60584 - prepared statements truncate milliseconds)
     *
     * @throws Exception
     */
    @Test
    public void testBug40279() throws Exception {
        assumeTrue(versionMeetsMinimum(5, 6, 4), "MySQL 5.6.4+ is required to run this test.");

        createTable("testBug40279", "(f1 int, f2 timestamp(6))");

        Timestamp ts = new Timestamp(1300791248001L);

        Connection ps_conn = null;
        Connection ssps_conn = null;

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.connectionTimeZone.getKeyName(), "UTC");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
            ps_conn = getConnectionWithProps(props);

            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
            ssps_conn = getConnectionWithProps(props);

            this.pstmt = ps_conn.prepareStatement("INSERT INTO testBug40279(f1, f2) VALUES (?, ?)");
            this.pstmt.setInt(1, 1);
            this.pstmt.setTimestamp(2, ts);
            this.pstmt.execute();
            this.pstmt.close();

            this.pstmt = ssps_conn.prepareStatement("INSERT INTO testBug40279(f1, f2) VALUES (?, ?)");
            this.pstmt.setInt(1, 2);
            this.pstmt.setTimestamp(2, ts);
            this.pstmt.execute();
            this.pstmt.close();

            this.rs = this.stmt.executeQuery("SELECT f2 FROM testBug40279");
            while (this.rs.next()) {
                assertEquals(ts.getNanos(), this.rs.getTimestamp("f2").getNanos());
            }

        } finally {
            if (ps_conn != null) {
                ps_conn.close();
            }
            if (ssps_conn != null) {
                ssps_conn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#35653 - executeQuery() in Statement.java let "TRUNCATE" queries being executed.
     * "RENAME" is also filtered now.
     *
     * @throws Exception
     */
    @Test
    public void testBug35653() throws Exception {
        createTable("testBug35653", "(f1 int)");
        try {
            this.rs = this.stmt.executeQuery("TRUNCATE testBug35653");
            fail("executeQuery() shouldn't allow TRUNCATE");
        } catch (SQLException e) {
            assertTrue(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT == e.getSQLState());
        }

        try {
            this.rs = this.stmt.executeQuery("RENAME TABLE testBug35653 TO testBug35653_new");
            fail("executeQuery() shouldn't allow RENAME");
        } catch (SQLException e) {
            assertTrue(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT == e.getSQLState());
        } finally {
            dropTable("testBug35653_new");
        }
    }

    /**
     * Tests fix for BUG#64805 - StatementImpl$CancelTask occasionally throws NullPointerExceptions.
     *
     * @throws Exception
     */
    @Test
    public void testBug64805() throws Exception {
        try {
            this.stmt.setQueryTimeout(5);
            this.rs = this.stmt.executeQuery("select sleep(5)");
        } catch (NullPointerException e) {
            e.printStackTrace();
            fail();
        } catch (Exception e) {
            if (e instanceof MySQLTimeoutException) {
                // expected behavior in slow environment
            } else {
                throw e;
            }
        }
    }

    /**
     * WL#4897 - Add EXPLAIN INSERT/UPDATE/DELETE
     *
     * Added support for EXPLAIN INSERT/REPLACE/UPDATE/DELETE. Connector/J must issue a warning containing the execution plan for slow queries when connection
     * properties logSlowQueries=true and explainSlowQueries=true are used.
     *
     * @throws Exception
     */
    @Test
    public void testExecutionPlanForSlowQueries() throws Exception {
        // once slow query (with execution plan) warning is sent to System.err, we capture messages sent here to check proper operation.
        final class TestHandler {

            // System.err diversion handling
            PrintStream systemErrBackup = null;
            ByteArrayOutputStream systemErrDetour = null;

            // Connection handling
            Connection testConn = null;

            TestHandler() {
                this.systemErrBackup = System.err;
                this.systemErrDetour = new ByteArrayOutputStream(8192);
                System.setErr(new PrintStream(this.systemErrDetour));
            }

            boolean containsSlowQueryMsg(String lookFor) {
                String errMsg = this.systemErrDetour.toString();
                boolean found = false;

                if (errMsg.indexOf("Slow query explain results for '" + lookFor + "'") != -1) {
                    found = true;
                }
                this.systemErrDetour.reset();
                // print message in original OutputStream.
                this.systemErrBackup.print(errMsg);
                return found;
            }

            void undoSystemErrDiversion() throws IOException {
                this.systemErrBackup.print(this.systemErrDetour.toString());
                this.systemErrDetour.close();
                System.setErr(this.systemErrBackup);
                this.systemErrDetour = null;
                this.systemErrBackup = null;
            }

            Connection getNewConnectionForSlowQueries() throws SQLException {
                releaseConnectionResources();
                Properties props = new Properties();
                props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
                props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
                props.setProperty(PropertyKey.logSlowQueries.getKeyName(), "true");
                props.setProperty(PropertyKey.explainSlowQueries.getKeyName(), "true");
                this.testConn = getConnectionWithProps(props);
                Statement st = this.testConn.createStatement();
                // execute several fast queries to unlock slow query analysis and lower query execution time mean
                for (int i = 0; i < 25; i++) {
                    st.execute("SELECT 1");
                }
                return this.testConn;
            }

            void releaseConnectionResources() throws SQLException {
                if (this.testConn != null) {
                    this.testConn.close();
                    this.testConn = null;
                }
            }

        }

        TestHandler testHandler = new TestHandler();
        Statement testStatement = null;

        try {
            if (versionMeetsMinimum(5, 6, 3)) {
                createTable("testWL4897", "(f1 INT NOT NULL PRIMARY KEY, f2 CHAR(50))");

                // when executed in the following sequence, each one of these queries take approximately 1 sec.
                final String[] slowQueries = { "INSERT INTO testWL4897 VALUES (SLEEP(0.5) + 1, 'MySQL'), (SLEEP(0.5) + 2, 'Connector/J')",
                        "SELECT * FROM testWL4897 WHERE f1 + SLEEP(0.5) = f1",
                        "REPLACE INTO testWL4897 VALUES (SLEEP(0.33) + 2, 'Database'), (SLEEP(0.33) + 3, 'Connector'), (SLEEP(0.33) + 4, 'Java')",
                        "UPDATE testWL4897 SET f1 = f1 * 10 + SLEEP(0.25)", "DELETE FROM testWL4897 WHERE f1 + SLEEP(0.25) = f1" };

                for (String query : slowQueries) {
                    testStatement = testHandler.getNewConnectionForSlowQueries().createStatement();
                    testStatement.execute(query);
                    assertTrue(testHandler.containsSlowQueryMsg(query), "A slow query explain results warning should have been issued for: '" + query + "'.");
                    testStatement.close();
                }
            } else {
                // only SELECT is qualified to log slow query explain results warning
                final String query = "SELECT SLEEP(1)";

                testStatement = testHandler.getNewConnectionForSlowQueries().createStatement();
                testStatement.execute(query);
                assertTrue(testHandler.containsSlowQueryMsg(query), "A slow query explain results warning should have been issued for: '" + query + "'.");
                testStatement.close();
            }
        } finally {
            testHandler.releaseConnectionResources();
            testHandler.undoSystemErrDiversion();
        }
    }

    /**
     * Tests fix for BUG#68562 - Combination rewriteBatchedStatements and useAffectedRows not working as expected
     *
     * @throws Exception
     */
    @Test
    public void testBug68562() throws Exception {
        testBug68562BatchWithSize(1);
        testBug68562BatchWithSize(3);
    }

    private void testBug68562BatchWithSize(int batchSize) throws Exception {
        createTable("testBug68562_found", "(id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255) NOT NULL, version VARCHAR(255)) ENGINE=InnoDB;");
        createTable("testBug68562_affected", "(id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255) NOT NULL, version VARCHAR(255)) ENGINE=InnoDB;");

        // insert the records (no update)
        int[] foundRows = testBug68562ExecuteBatch(batchSize, false, false, false);
        for (int foundRow : foundRows) {
            assertEquals(1, foundRow);
        }
        int[] affectedRows = testBug68562ExecuteBatch(batchSize, true, false, false);
        for (int affectedRow : affectedRows) {
            assertEquals(1, affectedRow);
        }

        // update the inserted records with same values
        foundRows = testBug68562ExecuteBatch(batchSize, false, false, false);
        for (int foundRow : foundRows) {
            assertEquals(1, foundRow);
        }
        affectedRows = testBug68562ExecuteBatch(batchSize, true, false, false);
        for (int affectedRow : affectedRows) {
            assertEquals(0, affectedRow);
        }

        // update the inserted records with same values REWRITING THE BATCHED STATEMENTS
        foundRows = testBug68562ExecuteBatch(batchSize, false, true, false);
        for (int foundRow : foundRows) {
            assertEquals(batchSize > 1 ? Statement.SUCCESS_NO_INFO : batchSize, foundRow);
        }
        affectedRows = testBug68562ExecuteBatch(batchSize, true, true, false);
        for (int affectedRow : affectedRows) {
            assertEquals(0, affectedRow);
        }

        // update the inserted records with NEW values REWRITING THE BATCHED STATEMENTS
        foundRows = testBug68562ExecuteBatch(batchSize, false, true, true);
        for (int foundRow : foundRows) {
            assertEquals(batchSize > 1 ? Statement.SUCCESS_NO_INFO : 2 * batchSize, foundRow);
        }
        affectedRows = testBug68562ExecuteBatch(batchSize, true, true, true);
        for (int affectedRow : affectedRows) {
            assertEquals(batchSize > 1 ? Statement.SUCCESS_NO_INFO : 2 * batchSize, affectedRow);
        }
    }

    private int[] testBug68562ExecuteBatch(int batchSize, boolean useAffectedRows, boolean rewriteBatchedStatements, boolean realUpdate)
            throws ClassNotFoundException, SQLException {
        String tableName = "testBug68562";

        Properties properties = new Properties();
        properties.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        properties.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        if (useAffectedRows) {
            properties.setProperty(PropertyKey.useAffectedRows.getKeyName(), "true");
            tableName += "_affected";
        } else {
            tableName += "_found";
        }
        if (rewriteBatchedStatements) {
            properties.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        }
        Connection connection = getConnectionWithProps(properties);

        PreparedStatement statement = connection
                .prepareStatement("INSERT INTO " + tableName + "(id, name, version) VALUES(?,?,?) ON DUPLICATE KEY UPDATE version = "
                        + (realUpdate ? "CONCAT(VALUES(version),'updated'), name = CONCAT(VALUES(name),'updated')" : "VALUES(version), name = VALUES(name)"));
        for (int i = 0; i < batchSize; i++) {
            statement.setInt(1, i);
            statement.setString(2, "name" + i);
            statement.setString(3, "version" + i);
            statement.addBatch();
        }

        int[] affectedRows = statement.executeBatch();

        statement.close();
        connection.close();

        return affectedRows;
    }

    /**
     * Tests fix for BUG#55340 - initializeResultsMetadataFromCache fails on second call to stored proc
     *
     * @throws Exception
     */
    @Test
    public void testBug55340() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.cacheResultSetMetadata.getKeyName(), "true");
        Connection testConnCacheRSMD = getConnectionWithProps(props);
        ResultSetMetaData rsmd;

        createTable("testBug55340", "(col1 INT, col2 CHAR(10))");
        createProcedure("testBug55340", "() BEGIN SELECT * FROM testBug55340; END");

        assertEquals(this.stmt.executeUpdate("INSERT INTO testBug55340 (col1, col2) VALUES (1, 'one'), (2, 'two'), (3, 'three')"), 3);

        for (Connection testConn : new Connection[] { this.conn, testConnCacheRSMD }) {
            String testDesc = testConn == testConnCacheRSMD ? "Conn. with 'cacheResultSetMetadata=true'" : "Default connection";

            // bug occurs in 2nd call only
            for (int i = 1; i <= 2; i++) {
                for (PreparedStatement testStmt : new PreparedStatement[] {
                        testConn.prepareStatement("SELECT * FROM testBug55340", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY),
                        testConn.prepareCall("CALL testBug55340()", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY) }) {

                    assertTrue(testStmt.execute());
                    this.rs = testStmt.getResultSet();
                    assertResultSetLength(this.rs, 3);

                    rsmd = this.rs.getMetaData();
                    assertEquals(2, rsmd.getColumnCount(),
                            "(" + i + ") " + testDesc + " - " + testStmt.getClass().getSimpleName() + ":RSMetaData - wrong column count.");
                    assertEquals(Integer.class.getName(), rsmd.getColumnClassName(1),
                            "(" + i + ") " + testDesc + " - " + testStmt.getClass().getSimpleName() + ":RSMetaData - wrong column(1) type.");
                    assertEquals(String.class.getName(), rsmd.getColumnClassName(2),
                            "(" + i + ") " + testDesc + " - " + testStmt.getClass().getSimpleName() + ":RSMetaData - wrong column(2) type.");

                    testStmt.close();
                }
            }
        }

        testConnCacheRSMD.close();
    }

    /**
     * Tests fix for BUG#71396 - setMaxRows (SQL_SELECT_LIMIT) from one query used in later queries (sometimes)
     *
     * @throws Exception
     */
    @Test
    public void testBug71396() throws Exception {
        final String queryLimitClause = "SELECT * FROM testBug71396 LIMIT 2";
        final String queryLimitClauseInJoin = "SELECT * FROM testBug71396 A JOIN (SELECT * FROM testBug71396 LIMIT 2) B ON A.c != B.c";
        final String queryLimitInQuotes = "SELECT * FROM testBug71396 WHERE c != 'Unlimited'";
        final String queryLimitInComment = "SELECT * FROM testBug71396 -- Unlimited";
        final String queryNoLimit = "SELECT * FROM testBug71396";

        final String[] queries = new String[] { queryLimitClause, queryLimitClauseInJoin, queryLimitInQuotes, queryLimitInComment, queryNoLimit };

        Connection testConn;
        Statement testStmt;
        ResultSet testRS;
        PreparedStatement testPStmtSet[];

        createTable("testBug71396", "(c VARCHAR(5))");
        this.stmt.execute("INSERT INTO testBug71396 VALUES ('One'), ('Two'), ('Three')");

        /*
         * Case 1: Statement.executeQuery() and Statement.execute() with plain Connection.
         */
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        testConn = getConnectionWithProps(props);

        // safety check
        testBug71396StatementMultiCheck(testConn, queries, new int[] { 2, 4, 3, 3, 3 });

        // initialize Statement with a given maxRow value, keep open until end of the case
        testStmt = testBug71396StatementInit(testConn, 1);

        // check results count using the same Statement[maxRows = 1] for all queries
        testBug71396StatementMultiCheck(testStmt, queries, new int[] { 1, 1, 1, 1, 1 });

        // check results count using same Connection and one new Statement[default maxRows] per query
        testBug71396StatementMultiCheck(testConn, queries, new int[] { 2, 4, 3, 3, 3 });

        // recheck results count reusing the first Statement[maxRows = 1] for all queries - confirm maxRows wasn't lost
        testBug71396StatementMultiCheck(testStmt, queries, new int[] { 1, 1, 1, 1, 1 });

        testStmt.close();
        testConn.close();

        /*
         * Case 2: PreparedStatement.executeQuery() and PreparedStatement.execute() with plain Connection.
         */
        testConn = getConnectionWithProps(props);

        // safety check
        testBug71396PrepStatementMultiCheck(testConn, queries, new int[] { 2, 4, 3, 3, 3 });

        // initialize Statement with a given maxRow value, keep open until end of the case
        testStmt = testBug71396StatementInit(testConn, 1);

        // initialize a set of PreparedStatements with a given maxRow value, keep open until end of the case
        testPStmtSet = testBug71396PrepStatementInit(testConn, queries, 1);

        // check results count using same Connection and one PreparedStatement[maxRows = 1] per query
        testBug71396PrepStatementMultiCheck(testPStmtSet, queries, new int[] { 1, 1, 1, 1, 1 });

        // check results count using same Connection and one new PreparedStatement[default maxRows] per query
        testBug71396PrepStatementMultiCheck(testConn, queries, new int[] { 2, 4, 3, 3, 3 });

        // check results count reusing the first PreparedStatement[maxRows = 1] per query - confirm maxRows wasn't lost
        testBug71396PrepStatementMultiCheck(testPStmtSet, queries, new int[] { 1, 1, 1, 1, 1 });

        testBug71396PrepStatementClose(testPStmtSet);
        testStmt.close();
        testConn.close();

        /*
         * Case 3: PreparedStatement.executeQuery() and PreparedStatement.execute() with
         * Connection[useServerPrepStmts=true].
         */
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        testConn = getConnectionWithProps(props);

        // safety check
        testBug71396PrepStatementMultiCheck(testConn, queries, new int[] { 2, 4, 3, 3, 3 });

        // initialize Statement with a given maxRow value, keep open until end of the case.
        testStmt = testBug71396StatementInit(testConn, 1);

        // initialize a set of PreparedStatements with a given maxRow value, keep open until end of the case
        testPStmtSet = testBug71396PrepStatementInit(testConn, queries, 1);

        // check results count using same Connection and one PreparedStatement[maxRows = 1] per query
        testBug71396PrepStatementMultiCheck(testPStmtSet, queries, new int[] { 1, 1, 1, 1, 1 });

        // check results count using same Connection and one new PreparedStatement[default maxRows] per query
        testBug71396PrepStatementMultiCheck(testConn, queries, new int[] { 2, 4, 3, 3, 3 });

        // check results count reusing the first PreparedStatement[maxRows = 1] per query - confirm maxRows wasn't lost
        testBug71396PrepStatementMultiCheck(testPStmtSet, queries, new int[] { 1, 1, 1, 1, 1 });

        testBug71396PrepStatementClose(testPStmtSet);
        testStmt.close();
        testConn.close();

        /*
         * Case 4: Statement.executeQuery() and Statement.execute() with Connection[maxRows=2].
         */
        props.remove(PropertyKey.useServerPrepStmts.getKeyName());
        props.setProperty(PropertyKey.maxRows.getKeyName(), "2");
        testConn = getConnectionWithProps(props);

        // safety check
        testBug71396StatementMultiCheck(testConn, queries, new int[] { 2, 2, 2, 2, 2 });

        // initialize Statement with a given maxRow value, keep open until end of the case
        testStmt = testBug71396StatementInit(testConn, 1);

        // check results count using the same Statement[maxRows = 1] for all queries
        testBug71396StatementMultiCheck(testStmt, queries, new int[] { 1, 1, 1, 1, 1 });

        // check results count using same Connection and one new Statement[default maxRows] per query
        testBug71396StatementMultiCheck(testConn, queries, new int[] { 2, 2, 2, 2, 2 });

        // recheck results count reusing the first Statement[maxRows = 1] for all queries - confirm maxRows wasn't lost
        testBug71396StatementMultiCheck(testStmt, queries, new int[] { 1, 1, 1, 1, 1 });

        testStmt.close();
        testConn.close();

        /*
         * Case 5: PreparedStatement.executeQuery() and PreparedStatement.execute() with Connection[maxRows=2].
         */
        testConn = getConnectionWithProps(props);

        // safety check
        testBug71396PrepStatementMultiCheck(testConn, queries, new int[] { 2, 2, 2, 2, 2 });

        // initialize Statement with a given maxRow value, keep open until end of the case
        testStmt = testBug71396StatementInit(testConn, 1);

        // initialize a set of PreparedStatements with a given maxRow value, keep open until end of the case
        testPStmtSet = testBug71396PrepStatementInit(testConn, queries, 1);

        // check results count using same Connection and one PreparedStatement[maxRows = 1] per query
        testBug71396PrepStatementMultiCheck(testPStmtSet, queries, new int[] { 1, 1, 1, 1, 1 });

        // check results count using same Connection and one new PreparedStatement[default maxRows] per query
        testBug71396PrepStatementMultiCheck(testConn, queries, new int[] { 2, 2, 2, 2, 2 });

        // check results count reusing the first PreparedStatement[maxRows = 1] per query - confirm maxRows wasn't lost
        testBug71396PrepStatementMultiCheck(testPStmtSet, queries, new int[] { 1, 1, 1, 1, 1 });

        testBug71396PrepStatementClose(testPStmtSet);
        testStmt.close();
        testConn.close();

        /*
         * Case 6: PreparedStatement.executeQuery() and PreparedStatement.execute() with
         * Connection[useServerPrepStmts=true;maxRows=2].
         */
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        testConn = getConnectionWithProps(props);

        // safety check
        testBug71396PrepStatementMultiCheck(testConn, queries, new int[] { 2, 2, 2, 2, 2 });

        // initialize Statement with a given maxRow value, keep open until end of the case
        testStmt = testBug71396StatementInit(testConn, 1);

        // initialize a set of PreparedStatements with a given maxRow value, keep open until end of the case
        testPStmtSet = testBug71396PrepStatementInit(testConn, queries, 1);

        // check results count using same Connection and one PreparedStatement[maxRows = 1] per query
        testBug71396PrepStatementMultiCheck(testPStmtSet, queries, new int[] { 1, 1, 1, 1, 1 });

        // check results count using same Connection and one new PreparedStatement[default maxRows] per query
        testBug71396PrepStatementMultiCheck(testConn, queries, new int[] { 2, 2, 2, 2, 2 });

        // check results count reusing the first PreparedStatement[maxRows = 1] per query - confirm maxRows wasn't lost
        testBug71396PrepStatementMultiCheck(testPStmtSet, queries, new int[] { 1, 1, 1, 1, 1 });

        testBug71396PrepStatementClose(testPStmtSet);
        testStmt.close();
        testConn.close();

        /*
         * Case 7: Multiple combinations between maxRows connection prop, Statement.setMaxRows() and LIMIT clause.
         * Covers some cases not tested previously.
         */
        props.clear();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        testBug71396MultiSettingsCheck(props, -1, 1, 1);
        testBug71396MultiSettingsCheck(props, -1, 2, 2);
        testBug71396MultiSettingsCheck(props, 1, 1, 1);
        testBug71396MultiSettingsCheck(props, 1, 2, 1);
        testBug71396MultiSettingsCheck(props, 2, 1, 1);
        testBug71396MultiSettingsCheck(props, 2, 2, 2);

        props.setProperty(PropertyKey.maxRows.getKeyName(), "1");
        testBug71396MultiSettingsCheck(props, -1, 1, 1);
        testBug71396MultiSettingsCheck(props, -1, 2, 1);
        testBug71396MultiSettingsCheck(props, 1, 1, 1);
        testBug71396MultiSettingsCheck(props, 1, 2, 1);
        testBug71396MultiSettingsCheck(props, 2, 1, 1);
        testBug71396MultiSettingsCheck(props, 2, 2, 2);

        props.setProperty(PropertyKey.maxRows.getKeyName(), "2");
        testBug71396MultiSettingsCheck(props, -1, 1, 1);
        testBug71396MultiSettingsCheck(props, -1, 2, 2);
        testBug71396MultiSettingsCheck(props, 1, 1, 1);
        testBug71396MultiSettingsCheck(props, 1, 2, 1);
        testBug71396MultiSettingsCheck(props, 2, 1, 1);
        testBug71396MultiSettingsCheck(props, 2, 2, 2);

        // Case 8: New session due to user change
        createUser("'testBug71396User'@'%'", "IDENTIFIED BY 'testBug71396User'");
        this.stmt.execute("GRANT SELECT ON *.* TO 'testBug71396User'@'%'");

        props.clear();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        testConn = getConnectionWithProps(props);
        testStmt = testBug71396StatementInit(testConn, 5);

        ((JdbcConnection) testConn).changeUser("testBug71396User", "testBug71396User");

        Statement testStmtTmp = testConn.createStatement();
        testRS = testStmtTmp.executeQuery("SELECT CURRENT_USER(), @@SESSION.SQL_SELECT_LIMIT");
        assertTrue(testRS.next());
        assertEquals("testBug71396User@%", testRS.getString(1));
        assertTrue(testRS.getBigDecimal(2).compareTo(new BigDecimal(Integer.MAX_VALUE)) == 1,
                String.format("expected:higher than<%d> but was:<%s>", Integer.MAX_VALUE, testRS.getBigDecimal(2)));
        testRS.close();
        testStmtTmp.close();

        testRS = testStmt.executeQuery("SELECT CURRENT_USER(), @@SESSION.SQL_SELECT_LIMIT");
        assertTrue(testRS.next());
        assertEquals("testBug71396User@%", testRS.getString(1));
        assertEquals(new BigDecimal(5), testRS.getBigDecimal(2));
        testRS.close();

        testStmt.close();
        testConn.close();

        // Case 9: New session due to reconnection
        testConn = getConnectionWithProps(props);
        testStmt = testBug71396StatementInit(testConn, 5);

        ((JdbcConnection) testConn).createNewIO(true); // true or false argument is irrelevant for this test case

        testStmtTmp = testConn.createStatement();
        testRS = testStmtTmp.executeQuery("SELECT @@SESSION.SQL_SELECT_LIMIT");
        assertTrue(testRS.next());
        assertTrue(testRS.getBigDecimal(1).compareTo(new BigDecimal(Integer.MAX_VALUE)) == 1,
                String.format("expected:higher than<%d> but was:<%s>", Integer.MAX_VALUE, testRS.getBigDecimal(1)));
        testRS.close();
        testStmtTmp.close();

        testRS = testStmt.executeQuery("SELECT @@SESSION.SQL_SELECT_LIMIT");
        assertTrue(testRS.next());
        assertEquals(new BigDecimal(5), testRS.getBigDecimal(1));
        testRS.close();

        testStmt.close();
        testConn.close();
    }

    /**
     * Initializes and returns a Statement with maxRows defined. Tests the SQL_SELECT_LIMIT defined. Executing this query also forces this limit to be defined
     * at session level.
     *
     * @param testConn
     * @param maxRows
     * @return a Statement
     * @throws SQLException
     */
    private Statement testBug71396StatementInit(Connection testConn, int maxRows) throws SQLException {
        ResultSet testRS;
        Statement testStmt = testConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);

        testStmt.setMaxRows(maxRows);
        // while consulting SQL_SELECT_LIMIT setting also forces limit to be applied into current session
        testRS = testStmt.executeQuery("SELECT @@SESSION.SQL_SELECT_LIMIT");
        testRS.next();
        assertEquals(maxRows, testRS.getInt(1), "Wrong @@SESSION.SQL_SELECT_LIMIT");

        return testStmt;
    }

    /**
     * Executes a set of queries using a Statement (newly created) and tests if the results count is the expected.
     *
     * @param testConn
     * @param queries
     * @param expRowCount
     * @throws SQLException
     */
    private void testBug71396StatementMultiCheck(Connection testConn, String[] queries, int[] expRowCount) throws SQLException {
        assertTrue(queries.length == expRowCount.length, "Bad arguments!");
        Statement testStmt = testConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
        testBug71396StatementMultiCheck(testStmt, queries, expRowCount);
        testStmt.close();
    }

    /**
     * Executes a set of queries using a Statement and tests if the results count is the expected.
     *
     * @param testStmt
     * @param queries
     * @param expRowCount
     * @throws SQLException
     */
    private void testBug71396StatementMultiCheck(Statement testStmt, String[] queries, int[] expRowCount) throws SQLException {
        assertTrue(queries.length == expRowCount.length, "Bad arguments!");
        for (int i = 0; i < queries.length; i++) {
            testBug71396StatementCheck(testStmt, queries[i], expRowCount[i]);
        }
    }

    /**
     * Executes one query using a Statement and tests if the results count is the expected.
     *
     * @param testStmt
     * @param query
     * @param expRowCount
     * @throws SQLException
     */
    private void testBug71396StatementCheck(Statement testStmt, String query, int expRowCount) throws SQLException {
        ResultSet testRS;

        testRS = testStmt.executeQuery(query);
        assertTrue(testRS.last());
        assertEquals(expRowCount, testRS.getRow(), String.format("Wrong number of rows for query '%s'", query));
        testRS.close();

        testStmt.execute(query);
        testRS = testStmt.getResultSet();
        assertTrue(testRS.last());
        assertEquals(expRowCount, testRS.getRow(), String.format("Wrong number of rows for query '%s'", query));
        testRS.close();
    }

    /**
     * Initializes and returns an array of PreparedStatements, with maxRows defined, for a set of queries.
     *
     * @param testConn
     * @param queries
     * @param maxRows
     * @return a PreparedStatement array
     * @throws SQLException
     */
    private PreparedStatement[] testBug71396PrepStatementInit(Connection testConn, String[] queries, int maxRows) throws SQLException {
        PreparedStatement[] testPStmt = new PreparedStatement[queries.length];

        for (int i = 0; i < queries.length; i++) {
            testPStmt[i] = testConn.prepareStatement(queries[i], ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            if (maxRows > 0) {
                testPStmt[i].setMaxRows(maxRows);
            }
        }
        return testPStmt;
    }

    /**
     * Closes all PreparedStatements in the array.
     *
     * @param testPStmt
     * @throws SQLException
     */
    private void testBug71396PrepStatementClose(PreparedStatement[] testPStmt) throws SQLException {
        for (Statement testStmt : testPStmt) {
            testStmt.close();
        }
    }

    /**
     * Executes a set of queries using newly created PreparedStatements and tests if the results count is the expected.
     *
     * @param testConn
     * @param queries
     * @param expRowCount
     * @throws SQLException
     */
    private void testBug71396PrepStatementMultiCheck(Connection testConn, String[] queries, int[] expRowCount) throws SQLException {
        assertTrue(queries.length == expRowCount.length, "Bad arguments!");
        for (int i = 0; i < queries.length; i++) {
            testBug71396PrepStatementCheck(testConn, queries[i], expRowCount[i], -1);
        }
    }

    /**
     * Executes a set of queries using the given PreparedStatements and tests if the results count is the expected.
     *
     * @param testPStmt
     * @param queries
     * @param expRowCount
     * @throws SQLException
     */
    private void testBug71396PrepStatementMultiCheck(PreparedStatement[] testPStmt, String[] queries, int[] expRowCount) throws SQLException {
        assertFalse(testPStmt.length != queries.length || testPStmt.length != expRowCount.length, "Bad arguments!");
        for (int i = 0; i < queries.length; i++) {
            testBug71396PrepStatementCheck(testPStmt[i], queries[i], expRowCount[i]);
        }
    }

    /**
     * Executes one query using a newly created PreparedStatement, setting its maxRows limit, and tests if the results count is the expected.
     *
     * @param testConn
     * @param query
     * @param expRowCount
     * @param maxRows
     * @throws SQLException
     */
    private void testBug71396PrepStatementCheck(Connection testConn, String query, int expRowCount, int maxRows) throws SQLException {
        PreparedStatement chkPStmt;

        chkPStmt = testConn.prepareStatement(query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
        if (maxRows > 0) {
            chkPStmt.setMaxRows(maxRows);
        }
        testBug71396PrepStatementCheck(chkPStmt, query, expRowCount);
        chkPStmt.close();
    }

    /**
     * Executes one query using a PreparedStatement and tests if the results count is the expected.
     *
     * @param testPStmt
     * @param query
     * @param expRowCount
     * @throws SQLException
     */
    private void testBug71396PrepStatementCheck(PreparedStatement testPStmt, String query, int expRowCount) throws SQLException {
        ResultSet testRS;

        testRS = testPStmt.executeQuery();
        assertTrue(testRS.last());
        assertEquals(expRowCount, testRS.getRow(), String.format("Wrong number of rows for query '%s'", query));
        testRS.close();

        testPStmt.execute();
        testRS = testPStmt.getResultSet();
        assertTrue(testRS.last());
        assertEquals(expRowCount, testRS.getRow(), String.format("Wrong number of rows for query '%s'", query));
        testRS.close();
    }

    /**
     * Executes a query containing the clause LIMIT with a Statement and a PreparedStatement, using a combination of Connection properties, maxRows value and
     * limit clause value, and tests if the results count is the expected.
     *
     * @param connProps
     * @param maxRows
     * @param limitClause
     * @param expRowCount
     * @throws SQLException
     */
    private void testBug71396MultiSettingsCheck(Properties connProps, int maxRows, int limitClause, int expRowCount) throws SQLException {
        Connection testConn = getConnectionWithProps(connProps);

        Statement testStmt = testConn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
        if (maxRows > 0) {
            testStmt.setMaxRows(maxRows);
        }
        testStmt.execute("SELECT 1"); // force limit to be applied into current session

        testBug71396StatementCheck(testStmt, String.format("SELECT * FROM testBug71396 LIMIT %d", limitClause), expRowCount);
        testBug71396PrepStatementCheck(testConn, String.format("SELECT * FROM testBug71396 LIMIT %d", limitClause), expRowCount, maxRows);

        testStmt.close();
        testConn.close();
    }

    /**
     * Tests fix for 18091639 - STRINGINDEXOUTOFBOUNDSEXCEPTION IN PREPAREDSTATEMENT.SETTIMESTAMP WITH 5.6.15
     *
     * @throws SQLException
     */
    @Test
    public void testBug18091639() throws SQLException {
        String str = TimeUtil.formatNanos(900000000, 1);
        assertEquals("9", str);

        str = TimeUtil.formatNanos(90000000, 1);
        assertEquals("0", str);

        str = TimeUtil.formatNanos(900000000, 0);
        assertEquals("0", str);

        assertThrows(WrongArgumentException.class, "fsp value must be in 0 to 6 range but was 9", () -> {
            TimeUtil.formatNanos(1, 9);
            return null;
        });

        str = TimeUtil.formatNanos(1, 6);
        assertEquals("0", str);

        str = TimeUtil.formatNanos(1999, 6);
        assertEquals("000001", str);

        str = TimeUtil.formatNanos(123999, 3);
        assertEquals("0", str);

        str = TimeUtil.formatNanos(123999, 4);
        assertEquals("0001", str);

        assertThrows(WrongArgumentException.class, "nanos value must be in 0 to 999999999 range but was 1000000010", () -> {
            TimeUtil.formatNanos(1000000010, 6);
            return null;
        });

        str = TimeUtil.formatNanos(100000000, 6);
        assertEquals("1", str);
    }

    /**
     * Tests fix for Bug#66947 (16004987) - Calling ServerPreparedStatement.close() twice corrupts cached statements
     *
     * @throws Exception
     */
    @Test
    public void testBug66947() throws Exception {
        Connection con = null;
        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
            props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
            props.setProperty(PropertyKey.prepStmtCacheSize.getKeyName(), "2");

            con = getConnectionWithProps(props);

            PreparedStatement ps1_1;
            PreparedStatement ps1_2;

            String query = "Select 'a' from dual";

            ps1_1 = con.prepareStatement(query);
            ps1_1.execute();
            ps1_1.close();

            ps1_2 = con.prepareStatement(query);
            assertSame(ps1_1, ps1_2, "SSPS should be taken from cache but is not the same.");
            ps1_2.execute();
            ps1_2.close();
            ps1_2.close(); // doesn't matter how many times close() is called.
            ps1_2.close();

            ps1_1 = con.prepareStatement(query);
            assertSame(ps1_2, ps1_1, "SSPS should be taken from cache but is not the same.");
            ps1_1.execute();
            ps1_1.close();
            ps1_1.close();

            // check that removeEldestEntry doesn't remove elements twice
            PreparedStatement ps2_1;
            PreparedStatement ps2_2;
            PreparedStatement ps3_1;
            PreparedStatement ps3_2;

            ps1_1 = con.prepareStatement("Select 'b' from dual");
            ps1_1.execute();
            ps1_1.close();
            ps2_1 = con.prepareStatement("Select 'c' from dual");
            ps2_1.execute();
            ps2_1.close();
            ps3_1 = con.prepareStatement("Select 'd' from dual");
            ps3_1.execute();
            ps3_1.close();

            ps1_2 = con.prepareStatement("Select 'b' from dual");
            assertNotSame(ps1_1, ps1_2, "SSPS should not be taken from cache but is the same.");

            ps2_2 = con.prepareStatement("Select 'c' from dual");
            assertSame(ps2_1, ps2_2, "SSPS should be taken from cache but is not the same.");

            ps3_2 = con.prepareStatement("Select 'd' from dual");
            assertSame(ps3_1, ps3_2, "SSPS should be taken from cache but is not the same.");

        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for BUG#68916 - closeOnCompletion doesn't work.
     *
     * This test requires help and timezone tables in mysql database to be initialized, see http://dev.mysql.com/doc/refman/5.7/en/time-zone-support.html and
     * http://dev.mysql.com/doc/refman/5.7/en/server-side-help-support.html
     *
     * @throws Exception
     */
    @Test
    public void testBug68916() throws Exception {
        assumeTrue(supportsTimeZoneNames(this.stmt), "This test requies the server with populated time zone tables.");

        // Prepare common test objects
        createProcedure("testBug68916_proc", "() BEGIN SELECT 1; SELECT 2; SELECT 3; END");
        createTable("testBug68916_tbl", "(fld1 INT NOT NULL AUTO_INCREMENT, fld2 INT, PRIMARY KEY(fld1))");

        // STEP 1: Test using standard connection (no properties)
        subTestBug68916ForStandardConnection();

        // STEP 2: Test using connection property holdResultsOpenOverStatementClose=true
        subTestBug68916ForHoldResultsOpenOverStatementClose();

        // STEP 3: Test using connection property dontTrackOpenResources=true
        subTestBug68916ForDontTrackOpenResources();

        // STEP 4: Test using connection property allowMultiQueries=true
        subTestBug68916ForAllowMultiQueries();

        // STEP 5: Test concurrent Statement/ResultSet sharing same Connection
        subTestBug68916ForConcurrency();
    }

    private void subTestBug68916ForStandardConnection() throws Exception {
        Connection testConnection = this.conn;
        String testStep;
        ResultSet testResultSet1, testResultSet2, testResultSet3;

        Statement testStatement = null;
        PreparedStatement testPrepStatement = null;
        CallableStatement testCallStatement = null;

        /*
         * Testing with standard connection (no properties)
         */
        testStep = "Standard Connection";

        /*
         * SUB-STEP 0: The basics (connection without properties)
         */
        // **testing Statement**
        // ResultSets should be closed when owning Statement is closed
        testStatement = testConnection.createStatement();

        assertFalse(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): false by default.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.closeOnCompletion();

        assertTrue(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): true after Statement.closeOnCompletion().");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.closeOnCompletion();

        assertTrue(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): true after 2nd Statement.closeOnCompletion().");

        // test Statement.close()
        testResultSet1 = testStatement.executeQuery("SELECT 1");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:0. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.close();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:0. ResultSet.isClosed(): true after Statement.Close().");
        assertTrue(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): true after Statement.Close().");

        // **testing PreparedStatement**
        // ResultSets should be closed when owning PreparedStatement is closed
        testPrepStatement = testConnection.prepareStatement("SELECT 1");

        assertFalse(testPrepStatement.isCloseOnCompletion(), testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): false by default.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.closeOnCompletion();

        assertTrue(testPrepStatement.isCloseOnCompletion(),
                testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): true after Statement.closeOnCompletion().");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.closeOnCompletion();

        assertTrue(testPrepStatement.isCloseOnCompletion(),
                testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): true after 2nd Statement.closeOnCompletion().");

        // test PreparedStatement.close()
        testPrepStatement.execute();
        testResultSet1 = testPrepStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:0. ResultSet.isClosed(): false.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.close();

        assertTrue(testResultSet1.isClosed(), testStep + ".PS:0. ResultSet.isClosed(): true after PreparedStatement.close().");
        assertTrue(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): true after PreparedStatement.close().");

        /*
         * SUB-STEP 1: One ResultSet (connection without properties)
         */
        // **testing Statement**
        // Statement using closeOnCompletion should be closed when last ResultSet is closed
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        while (testResultSet1.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // test implicit resultset close, keeping statement open, when following with an executeBatch()
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testStatement.addBatch("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)");
        testStatement.executeBatch();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true after executeBatch() in same Statement.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1 = testStatement.getGeneratedKeys();
        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // test implicit resultset close keeping statement open, when following with an executeUpdate()
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)", Statement.RETURN_GENERATED_KEYS);

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true after executeUpdate() in same Statement.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1 = testStatement.getGeneratedKeys();
        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // **testing PreparedStatement**
        // PreparedStatement using closeOnCompletion should be closed when last ResultSet is closed
        testPrepStatement = testConnection.prepareStatement("SELECT 1");
        testPrepStatement.closeOnCompletion();

        testResultSet1 = testPrepStatement.executeQuery();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): false.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false.");

        while (testResultSet1.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): true.");
        assertTrue(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 2: Multiple ResultSets, sequentially (connection without properties)
         */
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testResultSet2 = testStatement.executeQuery("SELECT 2"); // closes testResultSet1

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true after 2nd Statement.executeQuery().");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        while (testResultSet2.next()) {
        }

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet3 = testStatement.executeQuery("SELECT 3"); // closes testResultSet2

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true after 3rd Statement.executeQuery().");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet3.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 3: Multiple ResultSets, returned at once (connection without properties)
         */
        // **testing Statement**
        // Statement using closeOnCompletion should be closed when last ResultSet is closed
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        assertTrue(testStatement.execute("CALL testBug68916_proc"), testStep + ".ST:3. There should be some ResultSets.");
        testResultSet1 = testStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT), testStep + ".ST:3. There should be more ResultSets.");
        testResultSet2 = testStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.KEEP_CURRENT_RESULT).");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS), testStep + ".ST:3. There should be more ResultSets.");
        testResultSet3 = testStatement.getResultSet();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        // no more ResultSets, must close Statement
        assertFalse(testStatement.getMoreResults(), testStep + ".ST:3. There should be no more ResultSets.");

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after last Satement.getMoreResults().");
        assertTrue(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): true when last ResultSet is closed.");

        // **testing CallableStatement**
        // CallableStatement using closeOnCompletion should be closed when last ResultSet is closed
        testCallStatement = testConnection.prepareCall("CALL testBug68916_proc");
        testCallStatement.closeOnCompletion();

        assertTrue(testCallStatement.execute(), testStep + ".CS:3. There should be some ResultSets.");
        testResultSet1 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertTrue(testCallStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT), testStep + ".CS:3. There should be more ResultSets.");
        testResultSet2 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.KEEP_CURRENT_RESULT).");
        assertFalse(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertTrue(testCallStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS), testStep + ".CS:3. There should be more ResultSets.");
        testResultSet3 = testCallStatement.getResultSet();

        assertTrue(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertTrue(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        // no more ResultSets, must close Statement
        assertFalse(testCallStatement.getMoreResults(), testStep + ".CS:3. There should be no more ResultSets.");

        assertTrue(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after last Satement.getMoreResults().");
        assertTrue(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 4: Generated Keys ResultSet (connection without properties)
         */
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)", Statement.RETURN_GENERATED_KEYS);

        testResultSet1 = testStatement.getGeneratedKeys();
        assertTrue(testResultSet1.next(), testStep + ".ST:4. Statement.getGeneratedKeys(): should return some values.");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): true when last ResultSet is closed.");

        // test again and combine with simple query
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (4), (5), (6)", Statement.RETURN_GENERATED_KEYS);

        testResultSet1 = testStatement.getGeneratedKeys();
        assertTrue(testResultSet1.next(), testStep + ".ST:4. Statement.getGeneratedKeys(): should return some values.");

        testResultSet2 = testStatement.executeQuery("SELECT 2");

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true after executeQuery() in same Statement.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false.");

        testResultSet2.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): true when last ResultSet is closed.");
    }

    private void subTestBug68916ForHoldResultsOpenOverStatementClose() throws Exception {
        Connection testConnection;
        String testStep;
        ResultSet testResultSet1, testResultSet2, testResultSet3;

        Statement testStatement = null;
        PreparedStatement testPrepStatement = null;
        CallableStatement testCallStatement = null;

        /*
         * Testing with connection property holdResultsOpenOverStatementClose=true
         */
        testStep = "Conn. Prop. 'holdResultsOpenOverStatementClose'";
        testConnection = getConnectionWithProps("holdResultsOpenOverStatementClose=true");

        /*
         * SUB-STEP 0: The basics (holdResultsOpenOverStatementClose=true)
         */
        // **testing Statement**
        // ResultSets should stay open when owning Statement is closed
        testStatement = testConnection.createStatement();

        assertFalse(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): false dy default.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.closeOnCompletion();

        assertTrue(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): true after Statement.closeOnCompletion().");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.closeOnCompletion();

        assertTrue(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): true after 2nd Statement.closeOnCompletion().");

        // test Statement.close()
        testResultSet1 = testStatement.executeQuery("SELECT 1");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:0. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.close();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:0. ResultSet.isClosed(): false after Statement.Close().");
        assertTrue(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): true after Statement.Close().");

        // **testing PreparedStatement**
        // ResultSets should stay open when owning PreparedStatement is closed
        testPrepStatement = testConnection.prepareStatement("SELECT 1");

        assertFalse(testPrepStatement.isCloseOnCompletion(), testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): false by default.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.closeOnCompletion();

        assertTrue(testPrepStatement.isCloseOnCompletion(),
                testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): true after Statement.closeOnCompletion().");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.closeOnCompletion();

        assertTrue(testPrepStatement.isCloseOnCompletion(),
                testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): true after 2nd Statement.closeOnCompletion().");

        // test PreparedStatement.close()
        testPrepStatement.execute();
        testResultSet1 = testPrepStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:0. ResultSet.isClosed(): false.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.close();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:0. ResultSet.isClosed(): false after PreparedStatement.close().");
        assertTrue(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): true after PreparedStatement.close().");

        /*
         * SUB-STEP 1: One ResultSet (holdResultsOpenOverStatementClose=true)
         */
        // **testing Statement**
        // Statement using closeOnCompletion should be closed when last ResultSet is closed
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        while (testResultSet1.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // test implicit resultset close keeping statement open, when following with an executeBatch()
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testStatement.addBatch("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)");
        testStatement.executeBatch();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false after executeBatch() in same Statement.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1 = testStatement.getGeneratedKeys();
        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // test implicit resultset close keeping statement open, when following with an executeUpdate()
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)", Statement.RETURN_GENERATED_KEYS);

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false after executeUpdate() in same Statement.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1 = testStatement.getGeneratedKeys();
        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // **testing PreparedStatement**
        // PreparedStatement using closeOnCompletion should be closed when last ResultSet is closed
        testPrepStatement = testConnection.prepareStatement("SELECT 1");
        testPrepStatement.closeOnCompletion();

        testResultSet1 = testPrepStatement.executeQuery();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): false.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false.");

        while (testResultSet1.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): true.");
        assertTrue(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 2: Multiple ResultSets, sequentially (holdResultsOpenOverStatementClose=true)
         */
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testResultSet2 = testStatement.executeQuery("SELECT 2"); // mustn't close testResultSet1

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false after 2nd Statement.executeQuery().");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        while (testResultSet2.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet3 = testStatement.executeQuery("SELECT 3"); // mustn't close testResultSet1 nor testResultSet2

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false after 3rd Statement.executeQuery().");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet2.close();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet1.close();
        testResultSet3.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 3: Multiple ResultSets, returned at once (holdResultsOpenOverStatementClose=true)
         */
        // **testing Statement**
        // Statement using closeOnCompletion should be closed when last ResultSet is closed
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        assertTrue(testStatement.execute("CALL testBug68916_proc"), testStep + ".ST:3. There should be some ResultSets.");
        testResultSet1 = testStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT), testStep + ".ST:3. There should be more ResultSets.");
        testResultSet2 = testStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.KEEP_CURRENT_RESULT).");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS), testStep + ".ST:3. There should be more ResultSets.");
        testResultSet3 = testStatement.getResultSet();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        // no more ResultSets, must close Statement
        assertFalse(testStatement.getMoreResults(), testStep + ".ST:3. There should be no more ResultSets.");

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after last Satement.getMoreResults().");
        assertTrue(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): true when last ResultSet is closed.");

        // **testing CallableStatement**
        // CallableStatement using closeOnCompletion should be closed when last ResultSet is closed
        testCallStatement = testConnection.prepareCall("CALL testBug68916_proc");
        testCallStatement.closeOnCompletion();

        assertTrue(testCallStatement.execute(), testStep + ".CS:3. There should be some ResultSets.");
        testResultSet1 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertTrue(testCallStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT), testStep + ".CS:3. There should be more ResultSets.");
        testResultSet2 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.KEEP_CURRENT_RESULT).");
        assertFalse(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertTrue(testCallStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS), testStep + ".CS:3. There should be more ResultSets.");
        testResultSet3 = testCallStatement.getResultSet();

        assertTrue(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertTrue(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        // no more ResultSets, must close Statement
        assertFalse(testCallStatement.getMoreResults(), testStep + ".CS:3. There should be no more ResultSets.");

        assertTrue(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after last Satement.getMoreResults().");
        assertTrue(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 4: Generated Keys ResultSet (holdResultsOpenOverStatementClose=true)
         */
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)", Statement.RETURN_GENERATED_KEYS);

        testResultSet1 = testStatement.getGeneratedKeys();
        assertTrue(testResultSet1.next(), testStep + ".ST:4. Statement.getGeneratedKeys(): should return some values.");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): true when last ResultSet is closed.");

        // test again and combine with simple query
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (4), (5), (6)", Statement.RETURN_GENERATED_KEYS);

        testResultSet1 = testStatement.getGeneratedKeys();
        assertTrue(testResultSet1.next(), testStep + ".ST:4. Statement.getGeneratedKeys(): should return some values.");

        testResultSet2 = testStatement.executeQuery("SELECT 2");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false after executeQuery() in same Statement.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false.");

        testResultSet2.close();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false when last ResultSet is closed (still one open).");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): true when last ResultSet is closed.");

        testConnection.close();
    }

    private void subTestBug68916ForDontTrackOpenResources() throws Exception {
        Connection testConnection;
        String testStep;
        ResultSet testResultSet1, testResultSet2, testResultSet3;

        Statement testStatement = null;
        PreparedStatement testPrepStatement = null;
        CallableStatement testCallStatement = null;

        /*
         * Testing with connection property dontTrackOpenResources=true
         */
        testStep = "Conn. Prop. 'dontTrackOpenResources'";
        testConnection = getConnectionWithProps("dontTrackOpenResources=true");

        /*
         * SUB-STEP 0: The basics (dontTrackOpenResources=true)
         */
        // **testing Statement**
        // ResultSets should stay open when owning Statement is closed
        testStatement = testConnection.createStatement();

        assertFalse(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): false by default.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.closeOnCompletion();

        assertTrue(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): true after Statement.closeOnCompletion().");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.closeOnCompletion();

        assertTrue(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): true after 2nd Statement.closeOnCompletion().");

        // test Statement.close()
        testResultSet1 = testStatement.executeQuery("SELECT 1");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:0. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.close();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:0. ResultSet.isClosed(): false after Statement.Close().");
        assertTrue(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): true after Statement.Close().");

        // **testing PreparedStatement**
        // ResultSets should stay open when owning PreparedStatement is closed
        testPrepStatement = testConnection.prepareStatement("SELECT 1");

        assertFalse(testPrepStatement.isCloseOnCompletion(), testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): false by default.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.closeOnCompletion();

        assertTrue(testPrepStatement.isCloseOnCompletion(),
                testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): true after Statement.closeOnCompletion().");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.closeOnCompletion();

        assertTrue(testPrepStatement.isCloseOnCompletion(),
                testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): true after 2nd Statement.closeOnCompletion().");

        // test PreparedStatement.close()
        testPrepStatement.execute();
        testResultSet1 = testPrepStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:0. ResultSet.isClosed(): false.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.close();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:0. ResultSet.isClosed(): false after PreparedStatement.close().");
        assertTrue(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): true after PreparedStatement.close().");

        /*
         * SUB-STEP 1: One ResultSet (dontTrackOpenResources=true)
         */
        // **testing Statement**
        // Statement, although using closeOnCompletion, shouldn't be closed when last ResultSet is closed
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        while (testResultSet1.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1.close(); // although it's last open ResultSet, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false when last ResultSet is closed.");

        // test implicit resultset (not) close, keeping statement open, when following with an executeBatch()
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testStatement.addBatch("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)");
        testStatement.executeBatch();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false after executeBatch() in same Statement.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1 = testStatement.getGeneratedKeys();
        testResultSet1.close(); // although it's last open ResultSet, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false when last ResultSet is closed.");

        // test implicit resultset (not) close keeping statement open, when following with an executeUpdate()
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)", Statement.RETURN_GENERATED_KEYS);

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false after executeUpdate() in same Statement.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1 = testStatement.getGeneratedKeys();
        testResultSet1.close(); // although it's last open ResultSet, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false when last ResultSet is closed.");

        // **testing PreparedStatement**
        // PreparedStatement, although using closeOnCompletion, shouldn't be closed when last ResultSet is closed
        testPrepStatement = testConnection.prepareStatement("SELECT 1");
        testPrepStatement.closeOnCompletion();

        testResultSet1 = testPrepStatement.executeQuery();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): false.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false.");

        while (testResultSet1.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false.");

        testResultSet1.close(); // although it's last open ResultSet, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): true.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false when last ResultSet is closed.");

        /*
         * SUB-STEP 2: Multiple ResultSets, sequentially (dontTrackOpenResources=true)
         */
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testResultSet2 = testStatement.executeQuery("SELECT 2"); // mustn't close testResultSet1

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false after 2nd Statement.executeQuery().");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        while (testResultSet2.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet3 = testStatement.executeQuery("SELECT 3"); // mustn't close testResultSet1 nor testResultSet2

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false after 3rd Statement.executeQuery().");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet2.close();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet1.close();
        testResultSet3.close(); // although it's last open ResultSet, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false when last ResultSet is closed.");

        /*
         * SUB-STEP 3: Multiple ResultSets, returned at once (dontTrackOpenResources=true)
         */
        // **testing Statement**
        // Statement, although using closeOnCompletion, shouldn't be closed when last ResultSet is closed
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        assertTrue(testStatement.execute("CALL testBug68916_proc"), testStep + ".ST:3. There should be some ResultSets.");
        testResultSet1 = testStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT), testStep + ".ST:3. There should be more ResultSets.");
        testResultSet2 = testStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.KEEP_CURRENT_RESULT).");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS), testStep + ".ST:3. There should be more ResultSets.");
        testResultSet3 = testStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertFalse(testStatement.getMoreResults(), testStep + ".ST:3. There should be no more ResultSets.");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false after last Satement.getMoreResults().");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false after last Satement.getMoreResults().");

        // since open ResultSets aren't tracked, we need to close all manually
        testResultSet1.close();
        testResultSet2.close();
        testResultSet3.close();
        // although there are no more ResultSets, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false when last ResultSet is closed.");

        // **testing CallableStatement**
        // CallableStatement, although using closeOnCompletion, shouldn't be closed when last ResultSet is closed
        testCallStatement = testConnection.prepareCall("CALL testBug68916_proc");
        testCallStatement.closeOnCompletion();

        assertTrue(testCallStatement.execute(), testStep + ".CS:3. There should be some ResultSets.");
        testResultSet1 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertTrue(testCallStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT), testStep + ".CS:3. There should be more ResultSets.");
        testResultSet2 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.KEEP_CURRENT_RESULT).");
        assertFalse(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertTrue(testCallStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS), testStep + ".CS:3. There should be more ResultSets.");
        testResultSet3 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertFalse(testCallStatement.getMoreResults(), testStep + ".CS:3. There should be no more ResultSets.");

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false after last Satement.getMoreResults().");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false after last Satement.getMoreResults().");

        // since open ResultSets aren't tracked, we need to close all manually
        testResultSet1.close();
        testResultSet2.close();
        testResultSet3.close();
        // although there are no more ResultSets, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false when last ResultSet is closed.");

        /*
         * SUB-STEP 4: Generated Keys ResultSet (dontTrackOpenResources=true)
         */
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)", Statement.RETURN_GENERATED_KEYS);

        testResultSet1 = testStatement.getGeneratedKeys();
        assertTrue(testResultSet1.next(), testStep + ".ST:4. Statement.getGeneratedKeys(): should return some values.");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false.");

        testResultSet1.close(); // although it's last open ResultSet, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false when last ResultSet is closed.");

        // test again and combine with simple query
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (4), (5), (6)", Statement.RETURN_GENERATED_KEYS);

        testResultSet1 = testStatement.getGeneratedKeys();
        assertTrue(testResultSet1.next(), testStep + ".ST:4. Statement.getGeneratedKeys(): should return some values.");

        testResultSet2 = testStatement.executeQuery("SELECT 2");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false after executeQuery() in same Statement.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false.");

        testResultSet2.close();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false when last ResultSet is closed (still one open).");

        testResultSet1.close(); // although it's last open ResultSet, Statement mustn't be closed

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false when last ResultSet is closed.");

        testConnection.close();
    }

    private void subTestBug68916ForAllowMultiQueries() throws Exception {
        Connection testConnection;
        String testStep;
        ResultSet testResultSet1, testResultSet2, testResultSet3;

        Statement testStatement = null;
        PreparedStatement testPrepStatement = null;
        CallableStatement testCallStatement = null;

        /*
         * Testing with connection property allowMultiQueries=true
         */
        testStep = "Conn. Prop. 'allowMultiQueries'";
        testConnection = getConnectionWithProps("allowMultiQueries=true");

        /*
         * SUB-STEP 0: The basics (allowMultiQueries=true)
         */
        // **testing Statement**
        // ResultSets should be closed when owning Statement is closed
        testStatement = testConnection.createStatement();

        assertFalse(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): false by default.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.closeOnCompletion();

        assertTrue(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): true after Statement.closeOnCompletion().");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.closeOnCompletion();

        assertTrue(testStatement.isCloseOnCompletion(), testStep + ".ST:0. Statement.isCloseOnCompletion(): true after 2nd Statement.closeOnCompletion().");

        // test Statement.close()
        testResultSet1 = testStatement.executeQuery("SELECT 1");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:0. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): false.");

        testStatement.close();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:0. ResultSet.isClosed(): true after Statement.Close().");
        assertTrue(testStatement.isClosed(), testStep + ".ST:0. Statement.isClosed(): true after Statement.Close().");

        // **testing PreparedStatement**
        // ResultSets should be closed when owning PreparedStatement is closed
        testPrepStatement = testConnection.prepareStatement("SELECT 1");

        assertFalse(testPrepStatement.isCloseOnCompletion(), testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): false by default.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.closeOnCompletion();

        assertTrue(testPrepStatement.isCloseOnCompletion(),
                testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): true after Statement.closeOnCompletion().");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.closeOnCompletion();

        assertTrue(testPrepStatement.isCloseOnCompletion(),
                testStep + ".PS:0. PreparedStatement.isCloseOnCompletion(): true after 2nd Statement.closeOnCompletion().");

        // test PreparedStatement.close()
        testPrepStatement.execute();
        testResultSet1 = testPrepStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:0. ResultSet.isClosed(): false.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): false.");

        testPrepStatement.close();

        assertTrue(testResultSet1.isClosed(), testStep + ".PS:0. ResultSet.isClosed(): true after PreparedStatement.close().");
        assertTrue(testPrepStatement.isClosed(), testStep + ".PS:0. PreparedStatement.isClosed(): true after PreparedStatement.close().");

        /*
         * SUB-STEP 1: One ResultSet (allowMultiQueries=true)
         */
        // **testing Statement**
        // Statement using closeOnCompletion should be closed when last ResultSet is closed
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        while (testResultSet1.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // test implicit resultset close, keeping statement open, when following with an executeBatch()
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testStatement.addBatch("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)");
        testStatement.executeBatch();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true after executeBatch() in same Statement.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1 = testStatement.getGeneratedKeys();
        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // test implicit resultset close keeping statement open, when following with an executeUpdate()
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3)", Statement.RETURN_GENERATED_KEYS);

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true after executeUpdate() in same Statement.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): false.");

        testResultSet1 = testStatement.getGeneratedKeys();
        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:1. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:1. Statement.isClosed(): true when last ResultSet is closed.");

        // **testing PreparedStatement**
        // PreparedStatement using closeOnCompletion should be closed when last ResultSet is closed
        testPrepStatement = testConnection.prepareStatement("SELECT 1");
        testPrepStatement.closeOnCompletion();

        testResultSet1 = testPrepStatement.executeQuery();

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): false.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false.");

        while (testResultSet1.next()) {
        }

        assertFalse(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".PS:1. ResultSet.isClosed(): true.");
        assertTrue(testPrepStatement.isClosed(), testStep + ".PS:1. PreparedStatement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 2: Multiple ResultSets, sequentially (allowMultiQueries=true)
         */
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1");
        testResultSet2 = testStatement.executeQuery("SELECT 2; SELECT 3"); // closes testResultSet1

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true after 2nd Statement.executeQuery().");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        while (testResultSet2.next()) {
        }

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false after ResultSet have reached the end.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(), testStep + ".ST:3. There should be more ResultSets."); // closes
                                                                                                         // testResultSet2
        testResultSet3 = testStatement.getResultSet();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true after Statement.getMoreResults().");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): false.");

        testResultSet3.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".ST:2. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:2. Statement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 3: Multiple ResultSets, returned at once (allowMultiQueries=true)
         */
        // **testing Statement**
        // Statement using closeOnCompletion should be closed when last ResultSet is closed
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testResultSet1 = testStatement.executeQuery("SELECT 1; SELECT 2; SELECT 3");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT), testStep + ".ST:3. There should be more ResultSets.");
        testResultSet2 = testStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.KEEP_CURRENT_RESULT).");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        assertTrue(testStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS), testStep + ".ST:3. There should be more ResultSets.");
        testResultSet3 = testStatement.getResultSet();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): false.");

        // no more ResultSets, must close Statement
        assertFalse(testStatement.getMoreResults(), testStep + ".ST:3. There should be no more ResultSets.");

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".ST:3. ResultSet.isClosed(): true after last Satement.getMoreResults().");
        assertTrue(testStatement.isClosed(), testStep + ".ST:3. Statement.isClosed(): true when last ResultSet is closed.");

        // **testing CallableStatement**
        // CallableStatement using closeOnCompletion should be closed when last ResultSet is closed
        testCallStatement = testConnection.prepareCall("CALL testBug68916_proc");
        testCallStatement.closeOnCompletion();

        assertTrue(testCallStatement.execute(), testStep + ".CS:3. There should be some ResultSets.");
        testResultSet1 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertTrue(testCallStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT), testStep + ".CS:3. There should be more ResultSets.");
        testResultSet2 = testCallStatement.getResultSet();

        assertFalse(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false after Statement.getMoreResults(Statement.KEEP_CURRENT_RESULT).");
        assertFalse(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        assertTrue(testCallStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS), testStep + ".CS:3. There should be more ResultSets.");
        testResultSet3 = testCallStatement.getResultSet();

        assertTrue(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertTrue(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after Statement.getMoreResults(Statement.CLOSE_ALL_RESULTS).");
        assertFalse(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): false.");
        assertFalse(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): false.");

        // no more ResultSets, must close Statement
        assertFalse(testCallStatement.getMoreResults(), testStep + ".CS:3. There should be no more ResultSets.");

        assertTrue(testResultSet1.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true.");
        assertTrue(testResultSet3.isClosed(), testStep + ".CS:3. ResultSet.isClosed(): true after last Satement.getMoreResults().");
        assertTrue(testCallStatement.isClosed(), testStep + ".CS:3. CallableStatement.isClosed(): true when last ResultSet is closed.");

        /*
         * SUB-STEP 4: Generated Keys ResultSet (allowMultiQueries=true)
         */
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (1), (2), (3); INSERT INTO testBug68916_tbl (fld2) VALUES (4), (5), (6)",
                Statement.RETURN_GENERATED_KEYS);

        testResultSet1 = testStatement.getGeneratedKeys();
        assertTrue(testResultSet1.next(), testStep + ".ST:4. Statement.getGeneratedKeys(): should return some values.");

        assertFalse(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false.");

        testResultSet1.close(); // last open ResultSet, must close Statement

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): true when last ResultSet is closed.");

        // test again and combine with simple query
        testStatement = testConnection.createStatement();
        testStatement.closeOnCompletion();

        testStatement.executeUpdate("INSERT INTO testBug68916_tbl (fld2) VALUES (4), (5), (6)", Statement.RETURN_GENERATED_KEYS);

        testResultSet1 = testStatement.getGeneratedKeys();
        assertTrue(testResultSet1.next(), testStep + ".ST:4. Statement.getGeneratedKeys(): should return some values.");

        testResultSet2 = testStatement.executeQuery("SELECT 2; SELECT 3");

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true after executeQuery() in same Statement.");
        assertFalse(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): false.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): false.");

        // last open ResultSet won't close the Statement
        // because we didn't fetch the next one (SELECT 3)
        testResultSet2.close();

        assertTrue(testResultSet1.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertTrue(testResultSet2.isClosed(), testStep + ".ST:4. ResultSet.isClosed(): true.");
        assertFalse(testStatement.isClosed(), testStep + ".ST:4. Statement.isClosed(): true when last ResultSet is closed.");
        testStatement.close();

        testConnection.close();
    }

    private void subTestBug68916ForConcurrency() throws Exception {
        ExecutorService executor = Executors.newCachedThreadPool();
        CompletionService<String> complService = new ExecutorCompletionService<>(executor);

        String[] connectionProperties = new String[] { "", "holdResultsOpenOverStatementClose=true", "dontTrackOpenResources=true" };
        // overridesCloseOnCompletion[n] refers to the effect of connectionProperties[n] on Statement.closeOnCompletion()
        boolean[] overridesCloseOnCompletion = new boolean[] { false, false, true };
        String[] sampleQueries = new String[] { "SELECT * FROM mysql.help_topic", "SELECT SLEEP(1)",
                "SELECT * FROM mysql.time_zone tz INNER JOIN mysql.time_zone_name tzn ON tz.time_zone_id = tzn.time_zone_id "
                        + "INNER JOIN mysql.time_zone_transition tzt ON tz.time_zone_id = tzt.time_zone_id "
                        + "INNER JOIN mysql.time_zone_transition_type tztt ON tzt.time_zone_id = tztt.time_zone_id "
                        + "AND tzt.transition_type_id = tztt.transition_type_id ORDER BY tzn.name , tztt.abbreviation , tzt.transition_time",
                "SELECT 1" };
        int threadCount = sampleQueries.length;

        for (int c = 0; c < connectionProperties.length; c++) {
            System.out.println("Test Connection with property '" + connectionProperties[c] + "'");
            Connection testConnection = getConnectionWithProps(connectionProperties[c]);

            for (int t = 0; t < threadCount; t++) {
                complService.submit(new subTestBug68916ConcurrentTask(testConnection, sampleQueries[t], overridesCloseOnCompletion[c]));
            }

            for (int t = 0; t < threadCount; t++) {
                try {
                    System.out.println("   " + complService.take().get());
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                } catch (ExecutionException ex) {
                    if (ex.getCause() instanceof Error) {
                        // let JUnit try to report as Failure instead of Error
                        throw (Error) ex.getCause();
                    }
                }
            }

            testConnection.close();
        }
    }

    private class subTestBug68916ConcurrentTask implements Callable<String> {

        Connection testConnection = null;
        String query = null;
        boolean closeOnCompletionIsOverriden = false;

        subTestBug68916ConcurrentTask(Connection testConnection, String query, boolean closeOnCompletionIsOverriden) {
            this.testConnection = testConnection;
            this.query = query;
            this.closeOnCompletionIsOverriden = closeOnCompletionIsOverriden;
        }

        @Override
        public String call() throws Exception {
            String threadName = Thread.currentThread().getName();
            long startTime = System.currentTimeMillis();
            long stopTime = startTime;
            StatementImpl testStatement = null;
            int count1 = 0;

            try {
                testStatement = (StatementImpl) this.testConnection.createStatement();
                testStatement.closeOnCompletion();

                System.out.println(threadName + " is executing: " + this.query);
                ResultSet testResultSet = testStatement.executeQuery(this.query);
                while (testResultSet.next()) {
                    count1++;
                }
                assertTrue(count1 > 0, threadName + ": Query should return some values.");
                assertFalse(testStatement.isClosed(), threadName + ": Statement shouldn't be closed.");

                testResultSet.close(); // should close statement if not closeOnCompletionIsOverriden
                if (this.closeOnCompletionIsOverriden) {
                    assertFalse(testStatement.isClosed(), threadName + ": Statement shouldn't be closed.");
                } else {
                    assertTrue(testStatement.isClosed(), threadName + ": Statement should be closed.");
                }

            } catch (SQLException e) {
                e.printStackTrace();
                fail(threadName + ": Something went wrong, maybe Connection or Statement was closed before its time.");

            } finally {
                if (testStatement != null) {
                    try {
                        testStatement.close();
                    } catch (SQLException e) {
                    }
                }
                stopTime = System.currentTimeMillis();
            }
            return threadName + ": processed " + count1 + " rows in " + (stopTime - startTime) + " milliseconds.";
        }

    }

    /**
     * Tests fix for Bug#71672 - Every SQL statement is checked if it contains "ON DUPLICATE KEY UPDATE" or not
     *
     * @throws SQLException
     */
    @Test
    public void testBug71672() throws SQLException {
        boolean lastTest = false;
        int testStep = 0;

        Connection testConn = null;
        Statement testStmt = null;
        PreparedStatement testPStmt = null;
        ResultSet testRS = null;
        int[] res = null;

        int[] expectedUpdCount = null;
        int[][] expectedGenKeys = null;
        int[][] expectedGenKeysBatchStmt = null;
        int[] expectedGenKeysMultiQueries = null;
        int[] expectedUpdCountBatchPStmt = null;
        int[] expectedGenKeysBatchPStmt = null;

        final String tableDDL = "(id INT AUTO_INCREMENT PRIMARY KEY, ch CHAR(1) UNIQUE KEY, ct INT)";

        // *** CONTROL DATA SET 1: queries for both Statement and PreparedStatement
        final String[] queries = new String[] { "INSERT INTO testBug71672 (ch, ct) VALUES ('A', 100), ('C', 100), ('D', 100)",
                "INSERT INTO testBug71672 (ch, ct) VALUES ('B', 2), ('C', 3), ('D', 4), ('E', 5) ON DUPLICATE KEY UPDATE ct = -1 * (ABS(ct) + VALUES(ct))",
                "INSERT INTO testBug71672 (ch, ct) VALUES ('F', 100) ON DUPLICATE KEY UPDATE ct = -1 * (ABS(ct) + VALUES(ct))",
                "INSERT INTO testBug71672 (ch, ct) VALUES ('B', 2), ('F', 6) ON DUPLICATE KEY UPDATE ct = -1 * (ABS(ct) + VALUES(ct))",
                "INSERT INTO testBug71672 (ch, ct) VALUES ('G', 100)" }; // rewriteBatchedStatements needs > 4 queries

        // expected update counts per query:
        final int[] expectedUpdCountDef = new int[] { 3, 6, 1, 4, 1 };
        // expected generated keys per query:
        final int[][] expectedGenKeysForChkODKU = new int[][] { { 1, 2, 3 }, { 4 }, { 8 }, { 8 }, { 11 } };
        final int[][] expectedGenKeysForNoChkODKU = new int[][] { { 1, 2, 3 }, { 4, 5, 6, 7, 8, 9 }, { 8 }, { 8, 9, 10, 11 }, { 11 } };
        final int[][] expectedGenKeysForBatchStmtRW = new int[][] { { 1 }, { 4 }, { 8 }, { 8 }, { 11 } };

        // *** CONTROL DATA SET 2: query and params for batch PrepatedStatement
        final String queryBatchPStmt = "INSERT INTO testBug71672 (ch, ct) VALUES (?, ?) ON DUPLICATE KEY UPDATE ct = -1 * (ABS(ct) + VALUES(ct))";
        final String[] paramsBatchPStmt = new String[] { "A100", "C100", "D100", "B2", "C3", "D4", "E5", "F100", "B2", "F6", "G100" };

        // expected update counts per param:
        final int[] expectedUpdCountBatchPStmtNoRW = new int[] { 1, 1, 1, 1, 2, 2, 1, 1, 2, 2, 1 };
        final int sni = Statement.SUCCESS_NO_INFO;
        final int[] expectedUpdCountBatchPStmtRW = new int[] { sni, sni, sni, sni, sni, sni, sni, sni, sni, sni, sni };
        // expected generated keys:
        final int[] expectedGenKeysForBatchPStmtChkODKU = new int[] { 1, 2, 3, 4, 2, 3, 7, 8, 4, 8, 11 };
        final int[] expectedGenKeysForBatchPStmtNoChkODKU = new int[] { 1, 2, 3, 4, 2, 3, 3, 4, 7, 8, 4, 5, 8, 9, 11 };
        final int[] expectedGenKeysForBatchPStmtRW = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };

        // Test multiple connection props
        Properties props = new Properties();
        do {
            props.clear();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            switch (++testStep) {
                case 1:
                    testConn = getConnectionWithProps(props);
                    expectedUpdCount = expectedUpdCountDef;
                    expectedGenKeys = expectedGenKeysForChkODKU;
                    expectedGenKeysBatchStmt = expectedGenKeys;
                    expectedUpdCountBatchPStmt = expectedUpdCountBatchPStmtNoRW;
                    expectedGenKeysBatchPStmt = expectedGenKeysForBatchPStmtChkODKU;
                    break;
                case 2:
                    props.setProperty(PropertyKey.dontCheckOnDuplicateKeyUpdateInSQL.getKeyName(), "true");
                    testConn = getConnectionWithProps(props);
                    expectedUpdCount = expectedUpdCountDef;
                    expectedGenKeys = expectedGenKeysForNoChkODKU;
                    expectedGenKeysBatchStmt = expectedGenKeys;
                    expectedUpdCountBatchPStmt = expectedUpdCountBatchPStmtNoRW;
                    expectedGenKeysBatchPStmt = expectedGenKeysForBatchPStmtNoChkODKU;
                    break;
                case 3:
                    props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
                    testConn = getConnectionWithProps(props);
                    expectedUpdCount = expectedUpdCountDef;
                    expectedGenKeys = expectedGenKeysForChkODKU;
                    expectedGenKeysBatchStmt = expectedGenKeysForBatchStmtRW;
                    expectedUpdCountBatchPStmt = expectedUpdCountBatchPStmtRW;
                    expectedGenKeysBatchPStmt = expectedGenKeysForBatchPStmtRW;
                    break;
                case 4:
                    // dontCheckOnDuplicateKeyUpdateInSQL=true is canceled by rewriteBatchedStatements=true
                    props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
                    props.setProperty(PropertyKey.dontCheckOnDuplicateKeyUpdateInSQL.getKeyName(), "true");
                    testConn = getConnectionWithProps(props);
                    expectedUpdCount = expectedUpdCountDef;
                    expectedGenKeys = expectedGenKeysForChkODKU;
                    expectedGenKeysBatchStmt = expectedGenKeysForBatchStmtRW;
                    expectedUpdCountBatchPStmt = expectedUpdCountBatchPStmtRW;
                    expectedGenKeysBatchPStmt = expectedGenKeysForBatchPStmtRW;
                    lastTest = true;
                    break;
            }

            // A. Test Statement.execute() results
            createTable("testBug71672", tableDDL);
            for (int i = 0; i < queries.length; i++) {
                checkStatementForTestBug71672(testStep, testConn, queries[i], -1, expectedGenKeys[i]);
            }
            dropTable("testBug71672");

            // B. Test Statement.executeUpdate() results
            createTable("testBug71672", tableDDL);
            for (int i = 0; i < queries.length; i++) {
                checkStatementForTestBug71672(testStep, testConn, queries[i], expectedUpdCount[i], expectedGenKeys[i]);
            }
            dropTable("testBug71672");

            // C. Test Statement.executeBatch() results
            createTable("testBug71672", tableDDL);
            testStmt = testConn.createStatement();
            for (String query : queries) {
                testStmt.addBatch(query);
            }
            res = testStmt.executeBatch();
            assertEquals(expectedUpdCount.length, res.length, testStep + ". Satement.executeBatch() result");
            for (int i = 0; i < expectedUpdCount.length; i++) {
                assertEquals(expectedUpdCount[i], res[i], testStep + "." + i + ". Satement.executeBatch() result");
            }
            testRS = testStmt.getGeneratedKeys();
            for (int i = 0; i < expectedGenKeysBatchStmt.length; i++) {
                for (int j = 0; j < expectedGenKeysBatchStmt[i].length; j++) {
                    assertTrue(testRS.next(), testStep + ". Row expected in generated keys ResultSet");
                    assertEquals(expectedGenKeysBatchStmt[i][j], testRS.getInt(1), testStep + ".[" + i + "][" + j + "]. Wrong generated key");
                }
            }
            assertFalse(testRS.next(), testStep + ". No more rows expected in generated keys ResultSet");
            testRS.close();
            testStmt.close();
            dropTable("testBug71672");

            // D. Test PreparedStatement.execute() results
            createTable("testBug71672", tableDDL);
            for (int i = 0; i < queries.length; i++) {
                checkPreparedStatementForTestBug71672(testStep, testConn, queries[i], -1, expectedGenKeys[i]);
            }
            dropTable("testBug71672");

            // E. Test PreparedStatement.executeUpdate() results
            createTable("testBug71672", tableDDL);
            for (int i = 0; i < queries.length; i++) {
                checkPreparedStatementForTestBug71672(testStep, testConn, queries[i], expectedUpdCount[i], expectedGenKeys[i]);
            }
            dropTable("testBug71672");

            // F. Test PreparedStatement.executeBatch() results
            createTable("testBug71672", tableDDL);
            testPStmt = testConn.prepareStatement(queryBatchPStmt, Statement.RETURN_GENERATED_KEYS);
            for (String param : paramsBatchPStmt) {
                testPStmt.setString(1, param.substring(0, 1));
                testPStmt.setInt(2, Integer.parseInt(param.substring(1)));
                testPStmt.addBatch();
            }
            res = testPStmt.executeBatch();
            assertEquals(expectedUpdCountBatchPStmt.length, res.length, testStep + ". PreparedSatement.executeBatch() result");
            for (int i = 0; i < expectedUpdCountBatchPStmt.length; i++) {
                assertEquals(expectedUpdCountBatchPStmt[i], res[i], testStep + "." + i + ". PreparedSatement.executeBatch() result");
            }
            testRS = testPStmt.getGeneratedKeys();
            for (int i = 0; i < expectedGenKeysBatchPStmt.length; i++) {
                assertTrue(testRS.next(), testStep + ". Row expected in generated keys ResultSet");
                assertEquals(expectedGenKeysBatchPStmt[i], testRS.getInt(1), testStep + ".[" + i + "]. Wrong generated key");
            }
            assertFalse(testRS.next(), testStep + ". No more rows expected in generated keys ResultSet");
            testRS.close();
            testPStmt.close();
            dropTable("testBug71672");

            testConn.close();
        } while (!lastTest);

        // Test connection prop allowMultiQueries=true
        // (behaves as if only first query has been executed)
        lastTest = false;
        String allQueries = "";
        for (String q : queries) {
            allQueries += q + ";";
        }
        do {
            props.clear();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            switch (++testStep) {
                case 5:
                    props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), "true");
                    testConn = getConnectionWithProps(props);
                    expectedGenKeysMultiQueries = new int[] { 1 };
                    break;
                case 6:
                    props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), "true");
                    props.setProperty(PropertyKey.dontCheckOnDuplicateKeyUpdateInSQL.getKeyName(), "true");
                    testConn = getConnectionWithProps(props);
                    expectedGenKeysMultiQueries = new int[] { 1, 2, 3 };
                    lastTest = true;
                    break;
            }

            // A. Test Statement.execute() results
            createTable("testBug71672", tableDDL);
            checkStatementForTestBug71672(testStep, testConn, allQueries, -1, expectedGenKeysMultiQueries);
            dropTable("testBug71672");

            // B. Test Statement.executeUpdate() results
            createTable("testBug71672", tableDDL);
            checkStatementForTestBug71672(testStep, testConn, allQueries, 3, expectedGenKeysMultiQueries);
            dropTable("testBug71672");

            // C. Test PreparedStatement.execute() results
            createTable("testBug71672", tableDDL);
            checkPreparedStatementForTestBug71672(testStep, testConn, allQueries, -1, expectedGenKeysMultiQueries);
            dropTable("testBug71672");

            // D. Test PreparedStatement.executeUpdate() results
            createTable("testBug71672", tableDDL);
            checkPreparedStatementForTestBug71672(testStep, testConn, allQueries, 3, expectedGenKeysMultiQueries);
            dropTable("testBug71672");

            testConn.close();
        } while (!lastTest);
    }

    /**
     * Check the update count and returned keys for an INSERT query using a Statement object. If expectedUpdateCount < 0 then runs Statement.execute() otherwise
     * Statement.executeUpdate().
     *
     * @param testStep
     * @param testConn
     * @param query
     * @param expectedUpdateCount
     * @param expectedKeys
     * @throws SQLException
     */
    private void checkStatementForTestBug71672(int testStep, Connection testConn, String query, int expectedUpdateCount, int[] expectedKeys)
            throws SQLException {
        Statement testStmt = testConn.createStatement();

        if (expectedUpdateCount < 0) {
            assertFalse(testStmt.execute(query, Statement.RETURN_GENERATED_KEYS), testStep + ". Stmt.execute() result");
        } else {
            assertEquals(expectedUpdateCount, testStmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS), testStep + ". Stmt.executeUpdate() result");
        }

        ResultSet testRS = testStmt.getGeneratedKeys();
        for (int k : expectedKeys) {
            assertTrue(testRS.next(), testStep + ". Row expected in generated keys ResultSet");
            assertEquals(k, testRS.getInt(1), testStep + ". Wrong generated key");
        }
        assertFalse(testRS.next(), testStep + ". No more rows expected in generated keys ResultSet");
        testRS.close();
        testStmt.close();
    }

    /**
     * Check the update count and returned keys for an INSERT query using a PreparedStatement object. If expectedUpdateCount < 0 then runs
     * PreparedStatement.execute() otherwise PreparedStatement.executeUpdate().
     *
     * @param testStep
     * @param testConn
     * @param query
     * @param expectedUpdateCount
     * @param expectedKeys
     * @throws SQLException
     */
    private void checkPreparedStatementForTestBug71672(int testStep, Connection testConn, String query, int expectedUpdateCount, int[] expectedKeys)
            throws SQLException {
        PreparedStatement testPStmt = testConn.prepareStatement(query);

        if (expectedUpdateCount < 0) {
            assertFalse(testPStmt.execute(query, Statement.RETURN_GENERATED_KEYS), testStep + ". PrepStmt.execute() result");
        } else {
            assertEquals(expectedUpdateCount, testPStmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS), testStep + ". PrepStmt.executeUpdate() result");
        }

        ResultSet testRS = testPStmt.getGeneratedKeys();
        for (int k : expectedKeys) {
            assertTrue(testRS.next(), testStep + ". Row expected in generated keys ResultSet");
            assertEquals(k, testRS.getInt(1), testStep + ". Wrong generated key");
        }
        assertFalse(testRS.next(), testStep + ". No more rows expected in generated keys ResultSet");
        testRS.close();
        testPStmt.close();
    }

    /**
     * Tests fix for BUG#71923 - Incorrect generated keys if ON DUPLICATE KEY UPDATE not exact
     *
     * @throws Exception
     */
    @Test
    public void testBug71923() throws Exception {
        final String tableDDL = "(id INT AUTO_INCREMENT PRIMARY KEY, ch CHAR(1) UNIQUE KEY, ct INT, dt VARCHAR(100))";
        final String defaultQuery = "Insert into testBug71923 (ch, ct) values ('A', 1), ('B', 2)";
        final String[] testQueriesPositiveMatches = new String[] {
                "INSERT INTO testBug71923 (ch, ct) VALUES ('B', 2), ('C', 3) ON DUPLICATE KEY UPDATE ct = ABS(ct) + VALUES(ct)",
                "INSERT INTO testBug71923 (ch, ct) VALUES ('B', 2), ('C', 3) ON  DUPLICATE  KEY  UPDATE ct = ABS(ct) + VALUES(ct)",
                "INSERT INTO testBug71923 (ch, ct) VALUES ('B', 2), ('C', 3) /*! ON   DUPLICATE */ KEY /*!UPDATE*/ ct = ABS(ct) + VALUES(ct)",
                "INSERT INTO testBug71923 (ch, ct) VALUES ('B', 2), ('C', 3) ON/* ON */DUPLICATE /* DUPLICATE */KEY/* KEY *//* KEY */ UPDATE /* UPDATE */ ct = ABS(ct) + VALUES(ct)",
                "INSERT INTO testBug71923 (ch, ct) VALUES ('B', 2), ('C', 3) ON -- new line\n DUPLICATE KEY UPDATE ct = ABS(ct) + VALUES(ct)",
                "INSERT INTO testBug71923 (ch, ct) VALUES ('B', 2), ('C', 3) ON DUPLICATE # new line\n KEY UPDATE ct = ABS(ct) + VALUES(ct)",
                "INSERT INTO testBug71923 (ch, ct) VALUES ('B', 2), ('C', 3) ON/* comment */DUPLICATE# new line\nKEY-- new line\nUPDATE ct = ABS(ct) + VALUES(ct)" };
        final String[] testQueriesNegativeMatches = new String[] {
                "INSERT INTO testBug71923 (ch, ct, dt) VALUES ('C', 3, NULL), ('D', 4, NULL) /* ON DUPLICATE KEY UPDATE */",
                "INSERT INTO testBug71923 (ch, ct, dt) VALUES ('C', 3, NULL), ('D', 4, NULL) -- ON DUPLICATE KEY UPDATE",
                "INSERT INTO testBug71923 (ch, ct, dt) VALUES ('C', 3, NULL), ('D', 4, NULL) # ON DUPLICATE KEY UPDATE",
                "INSERT INTO testBug71923 (ch, ct, dt) VALUES ('C', 3, NULL), ('D', 4, 'ON DUPLICATE KEY UPDATE')" };

        int c = 0;
        for (String query : testQueriesPositiveMatches) {
            c++;

            // A. test Statement.execute()
            createTable("testBug71923", tableDDL);
            assertEquals(2, this.stmt.executeUpdate(defaultQuery));

            assertFalse(this.stmt.execute(query, Statement.RETURN_GENERATED_KEYS));
            this.rs = this.stmt.getGeneratedKeys();
            assertTrue(this.rs.next(), c + ".A Statement.execute() - generated keys row expected");
            assertEquals(3, this.rs.getInt(1), c + ".A Statement.execute() - wrong generated key value");
            assertFalse(this.rs.next(), c + ".A Statement.execute() - no more generated keys rows expected");
            this.rs.close();

            dropTable("testBug71923");

            // B. test Statement.executeUpdate
            createTable("testBug71923", tableDDL);
            assertEquals(2, this.stmt.executeUpdate(defaultQuery));

            assertEquals(3, this.stmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS));
            this.rs = this.stmt.getGeneratedKeys();
            assertTrue(this.rs.next(), c + ".B Statement.executeUpdate() - generated keys row expected");
            assertEquals(3, this.rs.getInt(1), c + ".B Statement.executeUpdate() - wrong generated key value");
            assertFalse(this.rs.next(), c + ".B Statement.executeUpdate() - no more generated keys rows expected");
            this.rs.close();

            // prepare statement for next tet cases
            this.pstmt = this.conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);

            // C. test PreparedStatment.execute()
            createTable("testBug71923", tableDDL);
            assertEquals(2, this.stmt.executeUpdate(defaultQuery));

            assertFalse(this.pstmt.execute(query, Statement.RETURN_GENERATED_KEYS));
            this.rs = this.pstmt.getGeneratedKeys();
            assertTrue(this.rs.next(), c + ".C PreparedStatment.execute() - generated keys row expected");
            assertEquals(3, this.rs.getInt(1), c + ".C PreparedStatment.execute() - wrong generated key value");
            assertFalse(this.rs.next(), c + ".C PreparedStatment.execute() - no more generated keys rows expected");
            this.rs.close();

            dropTable("testBug71923");

            // D. test PreparedStatment.executeUpdate
            createTable("testBug71923", tableDDL);
            assertEquals(2, this.stmt.executeUpdate(defaultQuery));

            assertEquals(3, this.pstmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS));
            this.rs = this.pstmt.getGeneratedKeys();
            assertTrue(this.rs.next(), c + ".D PreparedStatment.executeUpdate() - generated keys row expected");
            assertEquals(3, this.rs.getInt(1), c + ".D PreparedStatment.executeUpdate() - wrong generated key value");
            assertFalse(this.rs.next(), c + ".D PreparedStatment.executeUpdate() - no more generated keys rows expected");
            this.rs.close();

            dropTable("testBug71923");
        }

        c = 0;
        for (String query : testQueriesNegativeMatches) {
            c++;

            // E. test Statement.execute()
            createTable("testBug71923", tableDDL);
            assertEquals(2, this.stmt.executeUpdate(defaultQuery));

            assertFalse(this.stmt.execute(query, Statement.RETURN_GENERATED_KEYS));
            this.rs = this.stmt.getGeneratedKeys();
            assertTrue(this.rs.next(), c + ".E Statement.execute() - generated keys 1st row expected");
            assertEquals(3, this.rs.getInt(1), c + ".E Statement.execute() - wrong 1st generated key value");
            assertTrue(this.rs.next(), c + ".E Statement.execute() - generated keys 2nd row expected");
            assertEquals(4, this.rs.getInt(1), c + ".E Statement.execute() - wrong 2nd generated key value");
            assertFalse(this.rs.next(), c + ".E Statement.execute() - no more generated keys rows expected");
            this.rs.close();

            dropTable("testBug71923");

            // F. test Statement.executeUpdate
            createTable("testBug71923", tableDDL);
            assertEquals(2, this.stmt.executeUpdate(defaultQuery));

            assertEquals(2, this.stmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS));
            this.rs = this.stmt.getGeneratedKeys();
            assertTrue(this.rs.next(), c + ".F Statement.execute() - generated keys 1st row expected");
            assertEquals(3, this.rs.getInt(1), c + ".F Statement.execute() - wrong 1st generated key value");
            assertTrue(this.rs.next(), c + ".F Statement.execute() - generated keys 2nd row expected");
            assertEquals(4, this.rs.getInt(1), c + ".F Statement.execute() - wrong 2nd generated key value");
            assertFalse(this.rs.next(), c + ".F Statement.execute() - no more generated keys rows expected");
            this.rs.close();

            // prepare statement for next tet cases
            this.pstmt = this.conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);

            // G. test PreparedStatment.execute()
            createTable("testBug71923", tableDDL);
            assertEquals(2, this.stmt.executeUpdate(defaultQuery));

            assertFalse(this.pstmt.execute(query, Statement.RETURN_GENERATED_KEYS));
            this.rs = this.pstmt.getGeneratedKeys();
            assertTrue(this.rs.next(), c + ".G PreparedStatment.execute() - generated keys 1st row expected");
            assertEquals(3, this.rs.getInt(1), c + ".G PreparedStatment.execute() - wrong 1st generated key value");
            assertTrue(this.rs.next(), c + ".G PreparedStatment.execute() - generated keys 2nd row expected");
            assertEquals(4, this.rs.getInt(1), c + ".G PreparedStatment.execute() - wrong 2nd generated key value");
            assertFalse(this.rs.next(), c + ".G PreparedStatment.execute() - no more generated keys rows expected");
            this.rs.close();

            dropTable("testBug71923");

            // H. test PreparedStatment.executeUpdate
            createTable("testBug71923", tableDDL);
            assertEquals(2, this.stmt.executeUpdate(defaultQuery));

            assertEquals(2, this.pstmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS));
            this.rs = this.pstmt.getGeneratedKeys();
            assertTrue(this.rs.next(), c + ".H PreparedStatment.executeUpdate() - generated keys 1st row expected");
            assertEquals(3, this.rs.getInt(1), c + ".H PreparedStatment.executeUpdate() - wrong 1st generated key value");
            assertTrue(this.rs.next(), c + ".H PreparedStatment.executeUpdate() - generated keys 2nd row expected");
            assertEquals(4, this.rs.getInt(1), c + ".H PreparedStatment.executeUpdate() - wrong 2nd generated key value");
            assertFalse(this.rs.next(), c + ".H PreparedStatment.executeUpdate() - no more generated keys rows expected");
            this.rs.close();

            dropTable("testBug71923");
        }
    }

    /**
     * Tests fix for BUG#73163 - IndexOutOfBoundsException thrown preparing statement.
     *
     * This bug occurs only if running with Java6+.
     *
     * @throws Exception
     */
    @Test
    public void testBug73163() throws Exception {
        try {
            this.stmt = this.conn.prepareStatement("LOAD DATA INFILE ? INTO TABLE testBug73163");
        } catch (SQLException e) {
            assertFalse(e.getCause() instanceof IndexOutOfBoundsException, "IOOBE thrown in Java6+ while preparing a LOAD DATA statement with placeholders.");
        }
    }

    /**
     * Tests fix for BUG#74998 - readRemainingMultiPackets not computed correctly for rows larger than 16 MB.
     *
     * This bug is observed only when a multipacket uses packets 127 and 128. It happens due to the transition from positive to negative values in a signed byte
     * numeric value (127 + 1 == -128).
     *
     * The test case forces a multipacket to use packets 127, 128 and 129, where packet 129 is 0-length, this being another boundary case.
     * Query (*1) generates the following MySQL protocol packets from the server:
     * - Packets 1 to 4 contain protocol control data and results metadata info. (*2)
     * - Packets 5 to 126 contain each row "X". (*3)
     * - Packets 127 to 129 contain row "Y..." as a multipacket (size("Y...") = 32*1024*1024-15 requires 3 packets). (*4)
     * - Packet 130 contains row "Z". (*5)
     *
     * @throws Exception
     */
    @Test
    public void testBug74998() throws Exception {
        int maxAllowedPacketAtServer = Integer.parseInt(((JdbcConnection) this.conn).getSession().getServerSession().getServerVariable("max_allowed_packet"));
        int maxAllowedPacketMinimumForTest = 32 * 1024 * 1024;
        boolean changeMaxAllowedPacket = maxAllowedPacketAtServer < maxAllowedPacketMinimumForTest;

        if (!versionMeetsMinimum(5, 7)) {
            this.rs = this.stmt.executeQuery("SHOW VARIABLES LIKE 'innodb_log_file_size'");
            this.rs.next();
            long defaultInnodbLogFileSize = this.rs.getInt(2);
            assumeFalse(defaultInnodbLogFileSize < maxAllowedPacketMinimumForTest * 10,
                    "This test requires innodb_log_file_size > " + maxAllowedPacketMinimumForTest * 10);
        }

        createTable("testBug74998", "(id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, data LONGBLOB)"); // (*2)

        Connection con1 = null;
        try {
            if (changeMaxAllowedPacket) {
                this.stmt.executeUpdate("SET GLOBAL max_allowed_packet=" + maxAllowedPacketMinimumForTest);
            }

            StringBuilder query = new StringBuilder("INSERT INTO testBug74998 (data) VALUES ('X')");
            for (int i = 0; i < 121; i++) {
                query.append(",('X')");
            }

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            con1 = getConnectionWithProps(props);
            Statement st = con1.createStatement();

            assertEquals(122, st.executeUpdate(query.toString())); // (*3)

            int lengthOfRowForMultiPacket = maxAllowedPacketMinimumForTest - 15; // 32MB - 15Bytes causes an empty packet at the end of the multipacket sequence

            st.executeUpdate("INSERT INTO testBug74998 (data) VALUES (REPEAT('Y', " + lengthOfRowForMultiPacket + "))"); // (*4)
            st.executeUpdate("INSERT INTO testBug74998 (data) VALUES ('Z')"); // (*5)

            try {
                this.rs = st.executeQuery("SELECT id, data FROM testBug74998 ORDER BY id"); // (*1)
            } catch (CJCommunicationsException | CommunicationsException e) {
                assertFalse(e.getCause() instanceof IOException && "Packets received out of order".compareTo(e.getCause().getMessage()) == 0,
                        "Failed to correctly fetch all data from communications layer due to wrong processing of muli-packet number.");
            }

            // safety check
            for (int i = 1; i <= 122; i++) {
                assertTrue(this.rs.next());
                assertEquals(i, this.rs.getInt(1));
                assertEquals("X", this.rs.getString(2));
            }
            assertTrue(this.rs.next());
            assertEquals(123, this.rs.getInt(1));
            assertEquals("YYYYY", this.rs.getString(2).substring(0, 5));
            assertEquals("YYYYY", this.rs.getString(2).substring(lengthOfRowForMultiPacket - 5));
            assertTrue(this.rs.next());
            assertEquals(124, this.rs.getInt(1));
            assertEquals("Z", this.rs.getString(2));
            assertFalse(this.rs.next());
        } finally {
            if (changeMaxAllowedPacket) {
                this.stmt.executeUpdate("SET GLOBAL max_allowed_packet=" + maxAllowedPacketAtServer);
            }
            if (con1 != null) {
                con1.close();
            }
        }
    }

    /**
     * Tests fix for BUG#50348 - mysql connector/j 5.1.10 render the wrong value for dateTime column in GMT DB.
     *
     * With the right time zone settings in server and client, and using the property 'useTimezone=true', time shifts are computed in the opposite direction of
     * those that are computed otherwise.
     *
     * This issue is observed when the server is configured with time zone 'GMT' and the client other than 'GMT'. However, if the server's time zone is one
     * equivalent to 'GMT' but under a different identifier, say "UTC" or "GMT+00", the wrong behavior isn't observed anymore.
     *
     * @throws Exception
     */
    @Test
    public void testBug50348() throws Exception {
        final TimeZone defaultTZ = TimeZone.getDefault();

        final Properties testConnProps = new Properties();
        testConnProps.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        testConnProps.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        testConnProps.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");
        testConnProps.setProperty(PropertyKey.cacheDefaultTimeZone.getKeyName(), "false");
        testConnProps.setProperty(PropertyKey.connectionTimeZone.getKeyName(), "SERVER");
        testConnProps.setProperty(PropertyKey.preserveInstants.getKeyName(), "true");

        Connection testConn = null;

        try {
            TimeZone.setDefault(TimeZone.getTimeZone("America/Chicago")); // ~~ CST (UTC-06)
            final SimpleDateFormat tsFormat = TimeUtil.getSimpleDateFormat(null, "yyyy-MM-dd HH:mm:ss", null);
            final Timestamp timestamp = new Timestamp(tsFormat.parse("2015-01-01 10:00:00").getTime());
            final SimpleDateFormat tFormat = TimeUtil.getSimpleDateFormat(null, "HH:mm:ss", null);
            final Time time = new Time(tFormat.parse("10:00:00").getTime());

            // Test a number of time zones that coincide with 'GMT' on the some specific point in time.
            for (String tz : new String[] { "Europe/Lisbon", "UTC", "GMT+00", "GMT" }) {
                //  Europe/Lisbon ~~ WET (UTC) on 2015-01-01; ~~ CET (UTC+01) on 1970-01-01
                System.out.println("\nServer time zone: " + tz);
                System.out.println("---------------------------------------------------");

                testConnProps.setProperty(PropertyKey.connectionTimeZone.getKeyName(), tz);
                testConn = getConnectionWithProps(testConnProps);

                checkResultSetForTestBug50348(testConn, "2015-01-01 04:00:00.0", "10:00:00");
                checkPreparedStatementForTestBug50348(testConn, timestamp, time, "2015-01-01 16:00:00", "10:00:00");

                testConn.close();
            }

            // Cycle through a wide range of generic 'GMT+/-hh:mm' and assert the expected time shift for a specific point in time.
            for (int tzOffset = -15; tzOffset <= 15; tzOffset++) { // cover a wider range than standard
                for (int tzSubOffset : new int[] { 0, 30 }) {
                    final StringBuilder tz = new StringBuilder("GMT");
                    tz.append(tzOffset < 0 ? "-" : "+").append(String.format("%02d", Math.abs(tzOffset)));
                    tz.append(String.format(":%02d", tzSubOffset));

                    System.out.println("\nServer time zone: " + tz.toString());
                    System.out.println("---------------------------------------------------");
                    testConnProps.setProperty(PropertyKey.connectionTimeZone.getKeyName(), tz.toString());
                    testConn = getConnectionWithProps(testConnProps);

                    final int diffTzOffset = tzOffset + 6; // CST offset = -6 hours
                    final Calendar cal = Calendar.getInstance();

                    cal.setTime(tsFormat.parse("2015-01-01 10:00:00"));
                    cal.add(Calendar.HOUR, -diffTzOffset);
                    cal.add(Calendar.MINUTE, tzOffset < 0 ? tzSubOffset : -tzSubOffset);
                    String expectedTimestampFromRS = tsFormat.format(cal.getTime()) + ".0";
                    cal.setTime(tFormat.parse("10:00:00"));
                    cal.add(Calendar.HOUR, -diffTzOffset);
                    cal.add(Calendar.MINUTE, tzOffset < 0 ? tzSubOffset : -tzSubOffset);
                    checkResultSetForTestBug50348(testConn, expectedTimestampFromRS, "10:00:00");

                    cal.setTime(tsFormat.parse("2015-01-01 10:00:00"));
                    cal.add(Calendar.HOUR, diffTzOffset);
                    cal.add(Calendar.MINUTE, tzOffset < 0 ? -tzSubOffset : tzSubOffset);
                    String expectedTimestampFromPS = tsFormat.format(cal.getTime());
                    cal.setTime(tFormat.parse("10:00:00"));
                    cal.add(Calendar.HOUR, diffTzOffset);
                    cal.add(Calendar.MINUTE, tzOffset < 0 ? -tzSubOffset : tzSubOffset);
                    checkPreparedStatementForTestBug50348(testConn, timestamp, time, expectedTimestampFromPS, "10:00:00");

                    testConn.close();
                }
            }
        } finally {
            TimeZone.setDefault(defaultTZ);

            if (testConn != null) {
                testConn.close();
            }
        }
    }

    private void checkResultSetForTestBug50348(Connection testConn, String expectedTimestamp, String expectedTime) throws SQLException {
        this.rs = testConn.createStatement().executeQuery("SELECT '2015-01-01 10:00:00', '10:00:00'");
        this.rs.next();
        String timestampAsString = this.rs.getTimestamp(1).toString();
        String timeAsString = this.rs.getTime(2).toString();
        String alert = expectedTimestamp.equals(timestampAsString) && expectedTime.equals(timeAsString) ? "" : " <-- (!)";
        System.out.printf("[RS] expected: '%s' | '%s'%n", expectedTimestamp, expectedTime);
        System.out.printf("       actual: '%s' | '%s' %s%n", timestampAsString, timeAsString, alert);
        assertEquals(expectedTimestamp, timestampAsString);
        assertEquals(expectedTime, timeAsString);
    }

    private void checkPreparedStatementForTestBug50348(Connection testConn, Timestamp timestamp, Time time, String expectedTimestamp, String expectedTime)
            throws SQLException {
        PreparedStatement testPstmt = testConn.prepareStatement("SELECT ?, ?");
        testPstmt.setTimestamp(1, timestamp);
        testPstmt.setTime(2, time);

        this.rs = testPstmt.executeQuery();
        this.rs.next();
        String timestampAsString = new String(this.rs.getBytes(1));
        String timeAsString = new String(this.rs.getBytes(2));
        String alert = expectedTimestamp.equals(timestampAsString) && expectedTime.equals(timeAsString) ? "" : " <-- (!)";
        System.out.printf("[PS] expected: '%s' | '%s'%n", expectedTimestamp, expectedTime);
        System.out.printf("       actual: '%s' | '%s' %s%n", timestampAsString, timeAsString, alert);
        assertEquals(expectedTimestamp, timestampAsString);
        assertEquals(expectedTime, timeAsString);
    }

    /**
     * Tests fix for Bug#77449 - Add 'truncateFractionalSeconds=true|false' property (contribution).
     *
     * The property actually added was 'sendFractionalSeconds' and works as the opposite of the proposed one.
     *
     * @throws Exception
     */
    @Test
    public void testBug77449() throws Exception {
        assumeTrue(versionMeetsMinimum(5, 6, 4), "MySQL 5.6.4+ is required to run this test.");

        Timestamp originalTs = new Timestamp(TimeUtil.getSimpleDateFormat(null, "yyyy-MM-dd HH:mm:ss.SSS", null).parse("2014-12-31 23:59:59.999").getTime());
        Timestamp roundedTs = new Timestamp(originalTs.getTime() + 1);
        Timestamp truncatedTs = new Timestamp(originalTs.getTime() - 999);
        LocalDateTime originalLdt = LocalDateTime.of(2014, 12, 31, 23, 59, 59, 999000000);
        LocalDateTime roundedLdt = LocalDateTime.of(2015, 1, 1, 0, 0);
        LocalDateTime truncatedLdt = LocalDateTime.of(2014, 12, 31, 23, 59, 59);
        // MySQL can store 24:00:00, but LocalTime cannot.
        // ResultSet#getObject(i, LocalTime.class) might need some adjustment.
        LocalTime originalLt = LocalTime.of(22, 59, 59, 999000000);
        LocalTime roundedLt = LocalTime.of(23, 0, 0);
        LocalTime truncatedLt = LocalTime.of(22, 59, 59);

        assertEquals("2014-12-31 23:59:59.999", originalTs.toString());
        assertEquals("2014-12-31 23:59:59.0", TimeUtil.truncateFractionalSeconds(originalTs).toString());

        createTable("testBug77449", "(id INT PRIMARY KEY, ts_short TIMESTAMP, ts_long TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6))");
        createProcedure("testBug77449", "(ts_short TIMESTAMP, ts_long TIMESTAMP(6)) BEGIN SELECT ts_short, ts_long; END");
        createTable("testBug77449_time", "(id INT PRIMARY KEY, t_short TIME, t_long TIME(6))");
        createProcedure("testBug77449_time", "(t_short TIME, t_long TIME(6)) BEGIN SELECT t_short, t_long; END");

        boolean useSPS = false;
        boolean sendFS = false;
        do {
            String testCase = String.format("Case [ useSSPS; %s,  sendFracSecs: %s ]", useSPS ? "Y" : "N", sendFS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.queryInterceptors.getKeyName(), TestBug77449QueryInterceptor.class.getName());
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.sendFractionalSeconds.getKeyName(), Boolean.toString(sendFS));
            props.setProperty(PropertyKey.preserveInstants.getKeyName(), "false");

            Connection testConn = getConnectionWithProps(props);

            // Send timestamps as Strings, using Statement -> no truncation occurs.
            Statement testStmt = testConn.createStatement();
            testStmt.executeUpdate("INSERT INTO testBug77449 VALUES (1, '2014-12-31 23:59:59.999', '2014-12-31 23:59:59.999')/* no_ts_trunk */");
            testStmt.close();

            // Send timestamps using PreparedStatement -> truncation occurs according to 'sendFractionalSeconds' value.
            PreparedStatement testPStmt = testConn.prepareStatement("INSERT INTO testBug77449 VALUES (2, ?, ?)");
            testPStmt.setTimestamp(1, originalTs);
            testPStmt.setTimestamp(2, originalTs);
            assertEquals(1, testPStmt.executeUpdate(), testCase);
            testPStmt.close();

            // Send timestamps using UpdatableResultSet -> truncation occurs according to 'sendFractionalSeconds' value.
            testStmt = testConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
            testStmt.executeUpdate("INSERT INTO testBug77449 VALUES (3, NOW(), NOW())/* no_ts_trunk */"); // insert dummy row
            this.rs = testStmt.executeQuery("SELECT * FROM testBug77449 WHERE id = 3");
            assertTrue(this.rs.next(), testCase);
            this.rs.updateTimestamp("ts_short", originalTs);
            this.rs.updateTimestamp("ts_long", originalTs);
            this.rs.updateRow();
            this.rs.moveToInsertRow();
            this.rs.updateInt("id", 4);
            this.rs.updateTimestamp("ts_short", originalTs);
            this.rs.updateTimestamp("ts_long", originalTs);
            this.rs.insertRow();

            // Send LocalDateTime using PreparedStatement -> truncation occurs according to 'sendFractionalSeconds' value.
            testPStmt = testConn.prepareStatement("INSERT INTO testBug77449 VALUES (5, ?, ?)");
            testPStmt.setObject(1, originalLdt);
            testPStmt.setObject(2, originalLdt);
            assertEquals(1, testPStmt.executeUpdate(), testCase);
            testPStmt.close();

            // Send LocalDateTime using UpdatableResultSet -> truncation occurs according to 'sendFractionalSeconds' value.
            testStmt = testConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
            testStmt.executeUpdate("INSERT INTO testBug77449 VALUES (6, NOW(), NOW())/* no_ts_trunk */"); // insert dummy row
            this.rs = testStmt.executeQuery("SELECT * FROM testBug77449 WHERE id = 6");
            assertTrue(this.rs.next(), testCase);
            this.rs.updateObject("ts_short", originalLdt);
            this.rs.updateObject("ts_long", originalLdt);
            this.rs.updateRow();
            this.rs.moveToInsertRow();
            this.rs.updateInt("id", 7);
            this.rs.updateObject("ts_short", originalLdt);
            this.rs.updateObject("ts_long", originalLdt);
            this.rs.insertRow();

            // Assert values from previous inserts/updates.
            // 1st row: from Statement sent as String, no subject to TZ conversions.
            this.rs = testStmt.executeQuery("SELECT * FROM testBug77449 WHERE id = 1");
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertEquals(roundedTs, this.rs.getTimestamp(2), testCase);
            assertEquals(originalTs, this.rs.getTimestamp(3), testCase);
            // 2nd row: from PreparedStatement; 3rd row: from UpdatableResultSet.updateRow(); 4th row: from UpdatableResultSet.insertRow()
            this.rs = testStmt.executeQuery("SELECT * FROM testBug77449 WHERE id >= 2");
            for (int i = 2; i <= 4; i++) {
                assertTrue(this.rs.next(), testCase);
                assertEquals(i, this.rs.getInt(1), testCase);
                assertEquals(sendFS ? roundedTs : truncatedTs, this.rs.getTimestamp(2), testCase);
                assertEquals(sendFS ? originalTs : truncatedTs, this.rs.getTimestamp(3), testCase);
            }
            // 5th row: from PreparedStatement; 6th row: from UpdatableResultSet.updateRow(); 7th row: from UpdatableResultSet.insertRow()
            for (int i = 5; i <= 7; i++) {
                assertTrue(this.rs.next(), testCase);
                assertEquals(i, this.rs.getInt(1), testCase);
                assertEquals(sendFS ? roundedLdt : truncatedLdt, this.rs.getObject(2, LocalDateTime.class), testCase);
                assertEquals(sendFS ? originalLdt : truncatedLdt, this.rs.getObject(3, LocalDateTime.class), testCase);
            }

            this.stmt.execute("DELETE FROM testBug77449");

            // Send LocalTime using PreparedStatement -> truncation occurs according to 'sendFractionalSeconds' value.
            testPStmt = testConn.prepareStatement("INSERT INTO testBug77449_time VALUES (1, ?, ?)");
            testPStmt.setObject(1, originalLt);
            testPStmt.setObject(2, originalLt);
            assertEquals(1, testPStmt.executeUpdate(), testCase);
            testPStmt.close();
            // Send LocalTime using UpdatableResultSet -> truncation occurs according to 'sendFractionalSeconds' value.
            testStmt = testConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
            testStmt.executeUpdate("INSERT INTO testBug77449_time VALUES (2, NOW(), NOW())/* no_ts_trunk */"); // insert dummy row
            this.rs = testStmt.executeQuery("SELECT * FROM testBug77449_time WHERE id = 2");
            assertTrue(this.rs.next(), testCase);
            this.rs.updateObject("t_short", originalLt);
            this.rs.updateObject("t_long", originalLt);
            this.rs.updateRow();
            this.rs.moveToInsertRow();
            this.rs.updateInt("id", 3);
            this.rs.updateObject("t_short", originalLt);
            this.rs.updateObject("t_long", originalLt);
            this.rs.insertRow();
            // Assert previous LocalTime insertions
            this.rs = this.stmt.executeQuery("SELECT * FROM testBug77449_time");
            for (int i = 1; i <= 3; i++) {
                assertTrue(this.rs.next(), testCase);
                assertEquals(i, this.rs.getInt(1), testCase);
                assertEquals(sendFS ? roundedLt : truncatedLt, this.rs.getObject(2, LocalTime.class), testCase);
                assertEquals(sendFS ? originalLt : truncatedLt, this.rs.getObject(3, LocalTime.class), testCase);
            }

            this.stmt.execute("DELETE FROM testBug77449_time");

            // Compare Connector/J with client truncation -> truncation occurs according to 'sendFractionalSeconds' value.
            testPStmt = testConn.prepareStatement("SELECT ? = ?");
            testPStmt.setTimestamp(1, originalTs);
            testPStmt.setTimestamp(2, truncatedTs);
            this.rs = testPStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            if (sendFS) {
                assertFalse(this.rs.getBoolean(1), testCase);
            } else {
                assertTrue(this.rs.getBoolean(1), testCase);
            }
            testPStmt.close();
            // Same test with LocalDateTime
            testPStmt = testConn.prepareStatement("SELECT ? = ?");
            testPStmt.setObject(1, originalLdt);
            testPStmt.setObject(2, truncatedLdt);
            this.rs = testPStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            if (sendFS) {
                assertFalse(this.rs.getBoolean(1), testCase);
            } else {
                assertTrue(this.rs.getBoolean(1), testCase);
            }
            testPStmt.close();
            // Same test with LocalTime
            testPStmt = testConn.prepareStatement("SELECT ? = ?");
            testPStmt.setObject(1, originalLt);
            testPStmt.setObject(2, truncatedLt);
            this.rs = testPStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            if (sendFS) {
                assertFalse(this.rs.getBoolean(1), testCase);
            } else {
                assertTrue(this.rs.getBoolean(1), testCase);
            }
            testPStmt.close();

            // Send timestamps using CallableStatement -> truncation occurs according to 'sendFractionalSeconds' value.
            CallableStatement cstmt = testConn.prepareCall("{call testBug77449(?, ?)}");
            cstmt.setTimestamp("ts_short", originalTs);
            cstmt.setTimestamp("ts_long", originalTs);
            cstmt.execute();
            this.rs = cstmt.getResultSet();
            assertTrue(this.rs.next(), testCase);
            assertEquals(sendFS ? roundedTs : truncatedTs, this.rs.getTimestamp(1), testCase);
            assertEquals(sendFS ? originalTs : truncatedTs, this.rs.getTimestamp(2), testCase);
            cstmt.close();
            // Same test with LocalDateTime
            cstmt = testConn.prepareCall("{call testBug77449(?, ?)}");
            cstmt.setObject("ts_short", originalLdt);
            cstmt.setObject("ts_long", originalLdt);
            cstmt.execute();
            this.rs = cstmt.getResultSet();
            assertTrue(this.rs.next(), testCase);
            assertEquals(sendFS ? roundedLdt : truncatedLdt, this.rs.getObject(1, LocalDateTime.class), testCase);
            assertEquals(sendFS ? originalLdt : truncatedLdt, this.rs.getObject(2, LocalDateTime.class), testCase);
            cstmt.close();
            // Same test with LocalTime
            cstmt = testConn.prepareCall("{call testBug77449_time(?, ?)}");
            cstmt.setObject("t_short", originalLt);
            cstmt.setObject("t_long", originalLt);
            cstmt.execute();
            this.rs = cstmt.getResultSet();
            assertTrue(this.rs.next(), testCase);
            assertEquals(sendFS ? roundedLt : truncatedLt, this.rs.getObject(1, LocalTime.class), testCase);
            assertEquals(sendFS ? originalLt : truncatedLt, this.rs.getObject(2, LocalTime.class), testCase);
            cstmt.close();

            testConn.close();
        } while ((useSPS = !useSPS) || (sendFS = !sendFS));
    }

    public static class TestBug77449QueryInterceptor extends BaseQueryInterceptor {

        private boolean sendFracSecs = false;

        @Override
        public QueryInterceptor init(MysqlConnection conn, Properties props, Log log) {
            this.sendFracSecs = Boolean.parseBoolean(props.getProperty(PropertyKey.sendFractionalSeconds.getKeyName()));
            return this;
        }

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            if (!(interceptedQuery instanceof ServerPreparedStatement || interceptedQuery instanceof ServerPreparedQuery)) {
                String query = sql.get();
                if (query == null && (interceptedQuery instanceof ClientPreparedStatement || interceptedQuery instanceof ClientPreparedQuery)) {
                    query = interceptedQuery.toString();
                    query = query.substring(query.indexOf(':') + 2);
                }

                if (query != null && (query.startsWith("INSERT") || query.startsWith("UPDATE") || query.startsWith("CALL")) && !query.contains("no_ts_trunk")) {
                    assertFalse(this.sendFracSecs ^ query.contains(".999"), "Wrong TIMESTAMP trunctation in query [" + query + "]");
                }
            }
            return super.preProcess(sql, interceptedQuery);
        }

    }

    /**
     * Tests fix for BUG#77681 - rewrite replace sql like insert when rewriteBatchedStatements=true (contribution)
     *
     * When using 'rewriteBatchedStatements=true' we rewrite several batched statements into one single query by extending its VALUES clause. Although INSERT
     * REPLACE have the same syntax, this wasn't happening for REPLACE statements.
     *
     * This tests the number of queries actually sent to server when rewriteBatchedStatements is used and not by using a QueryInterceptor. The test is
     * repeated for server side prepared statements. Without the fix, this test fails while checking the number of expected REPLACE queries.
     *
     * @throws Exception
     */
    @Test
    public void testBug77681() throws Exception {
        createTable("testBug77681", "(id INT, txt VARCHAR(50), PRIMARY KEY (id))");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.queryInterceptors.getKeyName(), TestBug77681QueryInterceptor.class.getName());

        for (int tst = 0; tst < 4; tst++) {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString((tst & 0x1) != 0));
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString((tst & 0x2) != 0));
            Connection testConn = getConnectionWithProps(props);

            PreparedStatement testPstmt = testConn.prepareStatement("INSERT INTO testBug77681 VALUES (?, ?)");
            testPstmt.setInt(1, 1);
            testPstmt.setString(2, "one");
            testPstmt.addBatch();
            testPstmt.setInt(1, 2);
            testPstmt.setString(2, "two");
            testPstmt.addBatch();
            testPstmt.setInt(1, 3);
            testPstmt.setString(2, "three");
            testPstmt.addBatch();
            testPstmt.setInt(1, 4);
            testPstmt.setString(2, "four");
            testPstmt.addBatch();
            testPstmt.setInt(1, 5);
            testPstmt.setString(2, "five");
            testPstmt.addBatch();
            testPstmt.executeBatch();
            testPstmt.close();

            testPstmt = testConn.prepareStatement("REPLACE INTO testBug77681 VALUES (?, ?)");
            testPstmt.setInt(1, 2);
            testPstmt.setString(2, "TWO");
            testPstmt.addBatch();
            testPstmt.setInt(1, 4);
            testPstmt.setString(2, "FOUR");
            testPstmt.addBatch();
            testPstmt.setInt(1, 6);
            testPstmt.setString(2, "SIX");
            testPstmt.addBatch();
            testPstmt.executeBatch();
            testPstmt.close();

            Statement testStmt = testConn.createStatement();
            testStmt.clearBatch();
            testStmt.addBatch("INSERT INTO testBug77681 VALUES (7, 'seven')");
            testStmt.addBatch("INSERT INTO testBug77681 VALUES (8, 'eight')");
            testStmt.addBatch("INSERT INTO testBug77681 VALUES (9, 'nine')");
            testStmt.addBatch("INSERT INTO testBug77681 VALUES (10, 'ten')");
            testStmt.addBatch("INSERT INTO testBug77681 VALUES (11, 'eleven')");
            testStmt.executeBatch();

            testStmt.clearBatch();
            testStmt.addBatch("REPLACE INTO testBug77681 VALUES (8, 'EIGHT')");
            testStmt.addBatch("REPLACE INTO testBug77681 VALUES (10, 'TEN')");
            testStmt.addBatch("REPLACE INTO testBug77681 VALUES (12, 'TWELVE')");
            testStmt.addBatch("REPLACE INTO testBug77681 VALUES (14, 'FOURTEEN')");
            testStmt.addBatch("REPLACE INTO testBug77681 VALUES (16, 'SIXTEEN')");
            testStmt.executeBatch();

            this.stmt.executeUpdate("DELETE FROM testBug77681");
        }
    }

    public static class TestBug77681QueryInterceptor extends BaseQueryInterceptor {

        private static final char[] expectedNonRWBS = new char[] { 'I', 'I', 'I', 'I', 'I', 'R', 'R', 'R', 'I', 'I', 'I', 'I', 'I', 'R', 'R', 'R', 'R', 'R' };
        private static final char[] expectedRWBS = new char[] { 'I', 'R', 'I', 'R' };

        private char[] expected;
        private int execCounter = 0;

        @Override
        public QueryInterceptor init(MysqlConnection conn, Properties props, Log log) {
            super.init(conn, props, log);
            System.out.println("\nuseServerPrepStmts: " + props.getProperty(PropertyKey.useServerPrepStmts.getKeyName()) + " | rewriteBatchedStatements: "
                    + props.getProperty(PropertyKey.rewriteBatchedStatements.getKeyName()));
            System.out.println("--------------------------------------------------------------------------------");
            this.expected = Boolean.parseBoolean(props.getProperty(PropertyKey.rewriteBatchedStatements.getKeyName())) ? expectedRWBS : expectedNonRWBS;
            return this;
        }

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            String query = sql.get();
            if (query == null && interceptedQuery instanceof ClientPreparedStatement) {
                query = interceptedQuery.toString();
                query = query.substring(query.indexOf(':') + 2);
            }
            if (query != null && query.indexOf("testBug77681") != -1) {
                System.out.println(this.execCounter + " --> " + query);
                assertFalse(this.execCounter > this.expected.length, "Failed to rewrite statements");
                assertEquals(this.expected[this.execCounter++], query.charAt(0), "Wrong statement at execution number " + this.execCounter);
            }
            return super.preProcess(sql, interceptedQuery);
        }

    }

    /**
     * Tests fix for Bug#21876798 - CONNECTOR/J WITH MYSQL FABRIC AND SPRING PRODUCES PROXY ERROR.
     *
     * Although this is a Fabric related bug we are able reproduce it using a couple of multi-host connections.
     *
     * @throws Exception
     */
    @Test
    public void testBug21876798() throws Exception {
        createTable("testBug21876798", "(tst INT, val INT)");

        for (int tst = 0; tst < 4; tst++) {
            boolean useServerPrepStmts = (tst & 0x1) != 0;
            boolean rewriteBatchedStatements = (tst & 0x2) != 0;

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useServerPrepStmts));
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rewriteBatchedStatements));

            String testCase = String.format("Case: %d [ %s | %s ]", tst, useServerPrepStmts ? "useSPS" : "-",
                    rewriteBatchedStatements ? "rwBatchedStmts" : "-");

            Connection highLevelConn = getLoadBalancedConnection(props);
            assertTrue(highLevelConn.getClass().getName().startsWith("com.sun.proxy") || highLevelConn.getClass().getName().startsWith("$Proxy"), testCase);

            Connection lowLevelConn = getSourceReplicaReplicationConnection(props);
            // This simulates the behavior from Fabric connections that are causing the problem.
            ((ReplicationConnection) lowLevelConn).setProxy((JdbcConnection) highLevelConn);

            // Insert data. We need at least 4 rows to force rewriting batch statements.
            this.pstmt = lowLevelConn.prepareStatement("INSERT INTO testBug21876798 VALUES (?, ?)");
            for (int i = 1; i <= 4; i++) {
                this.pstmt.setInt(1, tst);
                this.pstmt.setInt(2, i);
                this.pstmt.addBatch();
            }
            this.pstmt.executeBatch();

            // Check if data was inserted correctly.
            this.rs = this.stmt.executeQuery("SELECT val FROM testBug21876798 WHERE tst = " + tst);
            for (int i = 1; i <= 4; i++) {
                assertTrue(this.rs.next(), testCase + "/Row#" + i);
                assertEquals(i, this.rs.getInt(1), testCase + "/Row#" + i);
            }
            assertFalse(this.rs.next(), testCase);

            // Update data. We need at least 4 rows to force rewriting batch statements.
            this.pstmt = lowLevelConn.prepareStatement("UPDATE testBug21876798 SET val = ? WHERE tst = ? AND val = ?");
            for (int i = 1; i <= 4; i++) {
                this.pstmt.setInt(1, -i);
                this.pstmt.setInt(2, tst);
                this.pstmt.setInt(3, i);
                this.pstmt.addBatch();
            }
            this.pstmt.executeBatch();

            // Check if data was updated correctly.
            this.rs = this.stmt.executeQuery("SELECT val FROM testBug21876798 WHERE tst = " + tst);
            for (int i = 1; i <= 4; i++) {
                assertTrue(this.rs.next(), testCase + "/Row#" + i);
                assertEquals(-i, this.rs.getInt(1), testCase + "/Row#" + i);
            }
            assertFalse(this.rs.next(), testCase);

            lowLevelConn.close();
            highLevelConn.close();
        }
    }

    /**
     * Tests fix for Bug#78961 - Can't call MySQL procedure with InOut parameters in Fabric environment.
     *
     * Although this is a Fabric related bug we are able reproduce it using a couple of multi-host connections.
     *
     * @throws Exception
     */
    @Test
    public void testBug78961() throws Exception {
        createProcedure("testBug78961", "(IN c1 FLOAT, IN c2 FLOAT, OUT h FLOAT, INOUT t FLOAT) BEGIN SET h = SQRT(c1 * c1 + c2 * c2); SET t = t + h; END;");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        Connection highLevelConn = getLoadBalancedConnection(props);
        assertTrue(highLevelConn.getClass().getName().startsWith("com.sun.proxy") || highLevelConn.getClass().getName().startsWith("$Proxy"));

        Connection lowLevelConn = getSourceReplicaReplicationConnection(props);
        // This simulates the behavior from Fabric connections that are causing the problem.
        ((ReplicationConnection) lowLevelConn).setProxy((JdbcConnection) highLevelConn);

        CallableStatement cstmt = lowLevelConn.prepareCall("{CALL testBug78961 (?, ?, ?, ?)}");
        cstmt.setFloat(1, 3.0f);
        cstmt.setFloat(2, 4.0f);
        cstmt.setFloat(4, 5.0f);
        cstmt.registerOutParameter(3, Types.FLOAT);
        cstmt.registerOutParameter(4, Types.FLOAT);
        cstmt.execute();

        assertEquals(5.0f, cstmt.getFloat(3));
        assertEquals(10.0f, cstmt.getFloat(4));
    }

    /**
     * Test Bug#75956 - Inserting timestamps using a server PreparedStatement and useLegacyDatetimeCode=false
     *
     * @throws Exception
     */
    @Test
    public void testBug75956() throws Exception {
        createTable("bug75956", "(id int not null primary key auto_increment, dt1 datetime, dt2 datetime)");
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useCursorFetch.getKeyName(), "true");
        // props.setProperty("useLegacyDatetimeCode", "false");
        Connection sspsConn = getConnectionWithProps(props);
        this.pstmt = sspsConn.prepareStatement("insert into bug75956 (dt1, dt2) values (?, ?)");
        this.pstmt.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
        this.pstmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
        this.pstmt.addBatch();
        this.pstmt.clearParameters();
        this.pstmt.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
        this.pstmt.setTimestamp(2, null);
        this.pstmt.addBatch();
        this.pstmt.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
        this.pstmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
        this.pstmt.addBatch();
        this.pstmt.executeBatch();
        this.pstmt.close();
        this.rs = sspsConn.createStatement().executeQuery("select count(*) from bug75956 where dt2 is NULL");
        this.rs.next();
        assertEquals(1, this.rs.getInt(1));
        sspsConn.close();
    }

    /**
     * Tests fix for Bug#71131 - Poor error message in CallableStatement.java.
     *
     * @throws Exception
     */
    @Test
    public void testBug71131() throws Exception {
        createProcedure("testBug71131", "(IN r DOUBLE, OUT p DOUBLE) BEGIN SET p = 2 * r * PI(); END");
        final CallableStatement cstmt = this.conn.prepareCall("{ CALL testBug71131 (?, 5) }");
        assertThrows(SQLException.class, "Parameter p is not registered as an output parameter", () -> {
            cstmt.execute();
            return null;
        });
        cstmt.close();
    }

    /**
     * Tests fix for Bug#23188498 - CLIENT HANG WHILE USING SERVERPREPSTMT WHEN PROFILESQL=TRUE AND USEIS=TRUE.
     *
     * @throws Exception
     */
    @Test
    public void testBug23188498() throws Exception {
        createTable("testBug23188498", "(id INT)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        props.setProperty(PropertyKey.profileSQL.getKeyName(), "true");
        JdbcConnection testConn = (JdbcConnection) getConnectionWithProps(props);
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // Insert data:
        this.pstmt = testConn.prepareStatement("INSERT INTO testBug23188498 (id) VALUES (?)");
        this.pstmt.setInt(1, 10);
        final PreparedStatement localPStmt1 = this.pstmt;
        Future<Void> future1 = executor.submit(() -> {
            localPStmt1.executeUpdate();
            return null;
        });
        try {
            future1.get(5, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            // The connection hung, forcibly closing it releases resources.
            this.stmt.execute("KILL CONNECTION " + testConn.getSession().getThreadId());
            fail("Connection hung after executeUpdate().");
        }
        this.pstmt.close();

        // Fetch data:
        this.pstmt = testConn.prepareStatement("SELECT * FROM testBug23188498 WHERE id > ?");
        this.pstmt.setInt(1, 1);
        final PreparedStatement localPStmt2 = this.pstmt;
        Future<ResultSet> future2 = executor.submit(() -> localPStmt2.executeQuery());
        try {
            this.rs = future2.get(5, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            // The connection hung, forcibly closing it releases resources.
            this.stmt.execute("KILL CONNECTION " + testConn.getSession().getThreadId());
            fail("Connection hung after executeQuery().");
        }
        assertTrue(this.rs.next());
        assertEquals(10, this.rs.getInt(1));
        assertFalse(this.rs.next());
        this.pstmt.close();

        executor.shutdownNow();
        testConn.close();
    }

    /**
     * Tests fix for Bug#23201930 - CLIENT HANG WHEN RSLT CUNCURRENCY=CONCUR_UPDATABLE AND RSLTSET TYPE=FORWARD_ONLY.
     *
     * @throws Exception
     */
    @Test
    public void testBug23201930() throws Exception {
        assumeTrue((((MysqlConnection) this.conn).getSession().getServerSession().getCapabilities().getCapabilityFlags() & NativeServerSession.CLIENT_SSL) != 0,
                "This test requires server with SSL support.");
        assumeTrue(supportsTLSv1_2(((MysqlConnection) this.conn).getSession().getServerSession().getServerVersion()),
                "This test requires server with TLSv1.2+ support.");
        assumeTrue(supportsTestCertificates(this.stmt),
                "This test requires the server configured with SSL certificates from ConnectorJ/src/test/config/ssl-test-certs");

        boolean useSSL = false;
        boolean useSPS = false;
        boolean useCursor = false;
        boolean useCompr = false;

        final char[] chars = new char[32 * 1024];
        Arrays.fill(chars, 'x');
        final String longData = String.valueOf(chars); // Using large data makes SSL connections hang sometimes.

        do {
            final String testCase = String.format("Case [SSL: %s, SPS: %s, Cursor: %s, Compr: %s]", useSSL ? "Y" : "N", useSPS ? "Y" : "N",
                    useCursor ? "Y" : "N", useCompr ? "Y" : "N");

            createTable("testBug23201930", "(id TINYINT AUTO_INCREMENT PRIMARY KEY, f1 INT DEFAULT 1, f2 INT DEFAULT 1, f3 INT DEFAULT 1, "
                    + "f4 INT DEFAULT 1, f5 INT DEFAULT 1, fl LONGBLOB)");

            final Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), useSSL ? SslMode.REQUIRED.name() : SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.useCursorFetch.getKeyName(), Boolean.toString(useCursor));
            if (useCursor) {
                props.setProperty(PropertyKey.defaultFetchSize.getKeyName(), "1");
            }
            props.setProperty(PropertyKey.useCompression.getKeyName(), Boolean.toString(useCompr));

            final JdbcConnection testConn = (JdbcConnection) getConnectionWithProps(props);

            final ExecutorService executor = Executors.newSingleThreadExecutor();
            final Future<Void> future = executor.submit(() -> {
                final Statement testStmt = testConn.createStatement();
                testStmt.execute("INSERT INTO testBug23201930 (id) VALUES (100)");

                PreparedStatement testPstmt = testConn.prepareStatement("INSERT INTO testBug23201930 (id, fl) VALUES (?, ?)", ResultSet.TYPE_FORWARD_ONLY,
                        ResultSet.CONCUR_UPDATABLE);
                testPstmt.setObject(1, 101, java.sql.Types.INTEGER);
                testPstmt.setObject(2, longData, java.sql.Types.VARCHAR);
                testPstmt.execute();
                testPstmt.setObject(1, 102, java.sql.Types.INTEGER);
                testPstmt.execute();
                testPstmt.close();

                testPstmt = testConn.prepareStatement("SELECT * FROM testBug23201930 WHERE id >= ? ORDER BY id ASC", ResultSet.TYPE_FORWARD_ONLY,
                        ResultSet.CONCUR_UPDATABLE);
                testPstmt.setObject(1, 100, java.sql.Types.INTEGER);
                final ResultSet testRs = testPstmt.executeQuery();
                assertTrue(testRs.next());
                assertEquals(100, testRs.getInt(1));
                assertTrue(testRs.next());
                assertEquals(101, testRs.getInt(1));
                assertTrue(testRs.next());
                assertEquals(102, testRs.getInt(1));
                assertFalse(testRs.next());
                testPstmt.close();
                return null;
            });

            try {
                future.get(10, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                // The connection hung, forcibly closing it releases resources.
                this.stmt.executeQuery("KILL CONNECTION " + testConn.getSession().getThreadId());
                fail(testCase + ": Connection hung!");
            }
            executor.shutdownNow();

            testConn.close();
        } while ((useSSL = !useSSL) || (useSPS = !useSPS) || (useCursor = !useCursor) || (useCompr = !useCompr)); // Cycle through all possible combinations.
    }

    /**
     * Tests fix for Bug#80615 - prepared statement leak when rewriteBatchedStatements=true and useServerPrepStmt.
     *
     * There are two bugs here:
     * 1. A server prepared statement leakage by not actually closing the statement on server when .close() is called in the client side. This occurs when
     * setting 'cachePrepStmts=true&useServerPrepStmts=true' and a prepared statement is set as non-poolable ('setPoolable(false)'). By itself this doesn't
     * cause any visible issue because the connector has a fail-safe mechanism that uses client-side prepared statements when server-side prepared statements
     * fail to be prepared. So, the connector ends up using client-side prepared statements after the number of open prepared statements on server hits the
     * value of 'max_prepared_stmt_count'.
     * 2. A prepared statement fails to be prepared when there are too many open prepared statements on server. By setting the options
     * 'rewriteBatchedStatements=true&useServerPrepStmts=true' when a query happens to be rewritten a new (server-side) prepared statement is required but the
     * fail-safe mechanism isn't implemented in this spot, so, since the leakage described above already consumed all available prepared statements on server,
     * this ends up throwing the exception.
     *
     * This test combines three elements:
     * 1. Call .close() on a server prepared statement. This promotes a prepared statement for caching if prepared statements cache is enabled.
     * 2. cachePrepStmts=true|false. Turns on/off the prepared statements cache.
     * 3. Call .setPoolable(true|false) on the prepared statement. This allows canceling the prepared statement caching, on a per statement basis. It has no
     * effect if the prepared statements cache if turned off for the current connection.
     *
     * Expected behavior:
     * - If .close() is not called on server prepared statements then they also can't be promoted for caching. This causes a server prepared statements leak in
     * all remaining combinations.
     * - If .close() is called on server prepared statements and the prepared statements cache is disabled by any form (either per connection or per statement),
     * then the statements is immediately closed on server side too.
     * - If .close() is called on server prepared statements and the prepared statements cache is enabled (both in the connection and in the statement) then the
     * statement is cached and only effectively closed in the server side if and when removed from the cache.
     *
     * @throws Exception
     */
    @Test
    public void testBug80615() throws Exception {
        final int prepStmtCacheSize = 5;
        final int maxPrepStmtCount = 25;
        final int testRepetitions = maxPrepStmtCount + 5;
        int maxPrepStmtCountOri = -1;

        try {
            // Check if it is possible to create a server prepared statement with the current max_prepared_stmt_count.
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
            Connection checkConn = getConnectionWithProps(props);
            PreparedStatement checkPstmt = checkConn.prepareStatement("SELECT 1");
            assertTrue(checkPstmt instanceof ServerPreparedStatement,
                    "Failed to create a server prepared statement possibly because there are too many active prepared statements on server already.");
            checkPstmt.close();

            this.rs = this.stmt.executeQuery("SELECT @@GLOBAL.max_prepared_stmt_count");
            this.rs.next();
            maxPrepStmtCountOri = this.rs.getInt(1);

            this.stmt.execute("SET GLOBAL max_prepared_stmt_count = " + maxPrepStmtCount);
            this.stmt.execute("FLUSH STATUS");

            // Check if it is still possible to prepare new statements after setting the new max. This test requires at least prepStmtCacheSize + 2.
            // The extra two statements are:
            // 1 - The first statement that only gets cached in the end (when calling .close() on it).
            // 2 - The statement that triggers the expelling of the oldest element of the cache to get room for itself.
            for (int i = 1; i <= prepStmtCacheSize + 2; i++) {
                checkPstmt = checkConn.prepareStatement("SELECT " + i);
                assertTrue(checkPstmt instanceof ServerPreparedStatement,
                        "Test ABORTED because the server doesn't allow preparing at least " + (prepStmtCacheSize + 2) + " more statements.");
            }
            checkConn.close(); // Also closes all prepared statements.

            // Good to go, start the test.
            boolean closeStmt = false;
            boolean useCache = false;
            boolean poolable = false;
            do {
                final String testCase = String.format("Case: [Close STMTs: %s, Use cache: %s, Poolable: %s ]", closeStmt ? "Y" : "N", useCache ? "Y" : "N",
                        poolable ? "Y" : "N");

                createTable("testBug80615", "(id INT)");

                props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
                props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), Boolean.toString(useCache));
                if (useCache) {
                    props.setProperty(PropertyKey.prepStmtCacheSize.getKeyName(), String.valueOf(prepStmtCacheSize));
                }

                final Connection testConn = getConnectionWithProps(props);
                final Statement checkStmt = testConn.createStatement();

                // Prepare a statement to be executed later. This is prepare #1.
                PreparedStatement testPstmt1 = testConn.prepareStatement("INSERT INTO testBug80615 VALUES (?)");
                assertTrue(testPstmt1 instanceof ServerPreparedStatement, testCase);
                ((StatementImpl) testPstmt1).setPoolable(poolable); // Need to cast, this is a JDBC 4.0 feature.
                testPstmt1.setInt(1, 100);
                testPstmt1.addBatch();
                testPstmt1.setInt(1, 200);
                testPstmt1.addBatch();

                int prepCount = 1; // One server-side prepared statement already prepared.
                int expectedPrepCount = prepCount;
                int expectedExecCount = 0;
                int expectedCloseCount = 0;

                testBug80615CheckComStmtStatus(prepCount, true, testCase, checkStmt, expectedPrepCount, expectedExecCount, expectedCloseCount);

                // Prepare a number of statements higher than the limit set on server. There are at most (*) maxPrepStmtCount - 1 prepares available.
                // This should exhaust the number of allowed prepared statements, forcing the connector to use client-side prepared statements from that point
                // forward unless statements are closed correctly.
                // Under the tested circumstances there where some unexpected server prepared statements leaks (1st bug).
                // (*) There's no canonical way of knowing exactly how many preparing statement slots are available because other sessions may be using them.
                boolean isSPS = true;
                do {
                    PreparedStatement testPstmt2 = testConn.prepareStatement("INSERT INTO testBug80615 VALUES (" + prepCount + " + ?)");
                    prepCount++;

                    isSPS = testPstmt2 instanceof ServerPreparedStatement;
                    if (closeStmt) {
                        // Statements are being correctly closed so there is room to create new ones every time.
                        assertTrue(isSPS, testCase);
                    } else if (prepCount > maxPrepStmtCount) {
                        // Not closing statements causes a server prepared statements leak on server.
                        // In this iteration (if not before) it should have started failing-over to a client-side prepared statement.
                        assertFalse(isSPS, testCase);
                    } else if (prepCount <= prepStmtCacheSize + 2) {
                        // There should be enough room to prepare server-side prepared statements. (This was checked in the beginning.)
                        assertTrue(isSPS, testCase);
                    } // prepStmtCacheSize + 1 < prepCount <= maxPrepStmtCount --> can't assert anything as there can statements prepared externally.

                    ((StatementImpl) testPstmt2).setPoolable(poolable); // Need to cast, this is a JDBC 4.0 feature.
                    testPstmt2.setInt(1, 0);
                    testPstmt2.execute();
                    if (isSPS) {
                        expectedPrepCount++;
                        expectedExecCount++;
                    }
                    if (closeStmt) {
                        testPstmt2.close();
                        if (isSPS) {
                            if (useCache && poolable && prepCount - 1 > prepStmtCacheSize) { // The first statement isn't cached yet.
                                // A statement (oldest in cache) is effectively closed on server side only after local statements cache is full.
                                expectedCloseCount++;
                            } else if (!useCache || !poolable) {
                                // The statement is closed immediately on server side.
                                expectedCloseCount++;
                            }
                        }
                    }

                    testBug80615CheckComStmtStatus(prepCount, isSPS, testCase, checkStmt, expectedPrepCount, expectedExecCount, expectedCloseCount);
                } while (prepCount < testRepetitions && isSPS);

                if (closeStmt) {
                    assertEquals(testRepetitions, prepCount, testCase);
                } else {
                    assertTrue(prepCount > prepStmtCacheSize + 2, testCase);
                    assertTrue(prepCount <= maxPrepStmtCount + 1, testCase);
                }

                // Batched statements are being rewritten so this will prepare another statement underneath.
                // It was failing before if the the number of stmt prepares on server was exhausted at this point (2nd Bug).
                testPstmt1.executeBatch();
                testPstmt1.close();

                testConn.close();
            } while ((closeStmt = !closeStmt) || (useCache = !useCache) || (poolable = !poolable));
        } finally {
            if (maxPrepStmtCountOri >= 0) {
                this.stmt.execute("SET GLOBAL max_prepared_stmt_count = " + maxPrepStmtCountOri);
                this.stmt.execute("FLUSH STATUS");
            }
        }
    }

    private void testBug80615CheckComStmtStatus(int prepCount, boolean isSPS, String testCase, Statement testStmt, int expectedPrepCount, int expectedExecCount,
            int expectedCloseCount) throws Exception {
        System.out.print(prepCount + ". ");
        System.out.print(isSPS ? "[SPS]" : "[CPS]");

        testCase += "\nIteration: " + prepCount;

        int actualPrepCount = 0;
        int actualExecCount = 0;
        int actualCloseCount = 0;
        this.rs = testStmt.executeQuery("SHOW SESSION STATUS WHERE Variable_name IN ('Com_stmt_prepare', 'Com_stmt_execute', 'Com_stmt_close')");
        while (this.rs.next()) {
            System.out.print(" (" + this.rs.getString(1).replace("Com_stmt_", "") + " " + this.rs.getInt(2) + ")");
            if (this.rs.getString(1).equalsIgnoreCase("Com_stmt_prepare")) {
                actualPrepCount = this.rs.getInt(2);
            } else if (this.rs.getString(1).equalsIgnoreCase("Com_stmt_execute")) {
                actualExecCount = this.rs.getInt(2);
            } else if (this.rs.getString(1).equalsIgnoreCase("Com_stmt_close")) {
                actualCloseCount = this.rs.getInt(2);
            }
        }
        System.out.println();

        assertEquals(expectedPrepCount, actualPrepCount, testCase);
        assertEquals(expectedExecCount, actualExecCount, testCase);
        assertEquals(expectedCloseCount, actualCloseCount, testCase);
    }

    /**
     * Tests fix for Bug#81706 - NullPointerException in driver.
     *
     * @throws Exception
     */
    @Test
    public void testBug81706() throws Exception {
        boolean useSPS = false;
        boolean cacheRsMd = false;
        boolean readOnly = false;

        do {
            final String testCase = String.format("Case [SPS: %s, CacheRsMd: %s, Read-only: %s]", useSPS ? "Y" : "N", cacheRsMd ? "Y" : "N",
                    readOnly ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.cacheResultSetMetadata.getKeyName(), Boolean.toString(cacheRsMd));
            props.setProperty(PropertyKey.queryInterceptors.getKeyName(), TestBug81706QueryInterceptor.class.getName());

            Connection testConn = getConnectionWithProps(props);
            testConn.setReadOnly(readOnly);
            Statement testStmt;
            PreparedStatement testPstmt;

            TestBug81706QueryInterceptor.isActive = true;
            TestBug81706QueryInterceptor.testCase = testCase;

            // Statement.executeQuery();
            testStmt = testConn.createStatement();
            testStmt.setFetchSize(Integer.MIN_VALUE);
            testStmt.executeQuery("/* ping */");
            testStmt.close();

            // Statemente.execute();
            testStmt = testConn.createStatement();
            testStmt.setFetchSize(Integer.MIN_VALUE);
            testStmt.execute("/* ping */");
            testStmt.close();

            // PreparedStatement.executeQuery();
            testPstmt = testConn.prepareStatement("/* ping */");
            assertFalse(testPstmt instanceof ServerPreparedStatement, testCase + ": Not the right Statement type.");
            testPstmt.setFetchSize(Integer.MIN_VALUE);
            testPstmt.executeQuery();
            testPstmt.close();

            // PreparedStatement.execute();
            testPstmt = testConn.prepareStatement("/* ping */");
            assertFalse(testPstmt instanceof ServerPreparedStatement, testCase + ": Not the right Statement type.");
            testPstmt.setFetchSize(Integer.MIN_VALUE);
            testPstmt.execute();
            testPstmt.close();

            TestBug81706QueryInterceptor.isActive = false;
            testConn.close();

        } while ((useSPS = !useSPS) || (cacheRsMd = !cacheRsMd) || (readOnly = !readOnly)); // Cycle through all possible combinations.
    }

    public static class TestBug81706QueryInterceptor extends BaseQueryInterceptor {

        public static boolean isActive = false;
        public static String testCase = "";

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            if (isActive) {
                String query = sql.get();
                if (query == null && interceptedQuery instanceof ClientPreparedStatement) {
                    query = interceptedQuery.toString();
                    query = query.substring(query.indexOf(':') + 2);
                }
                fail(testCase + ": Unexpected query executed - " + query);
            }
            return super.preProcess(sql, interceptedQuery);
        }

    }

    /**
     * Tests fix for Bug#66430 - setCatalog on connection leaves ServerPreparedStatement cache for old catalog.
     *
     * @throws Exception
     */
    @Test
    public void testBug66430() throws Exception {
        createDatabase("testBug66430DB1");
        createTable("testBug66430DB1.testBug66430", "(id INT)");
        this.stmt.executeUpdate("INSERT INTO testBug66430DB1.testBug66430 VALUES (1)");

        createDatabase("testBug66430DB2");
        createTable("testBug66430DB2.testBug66430", "(id INT)");
        this.stmt.executeUpdate("INSERT INTO testBug66430DB2.testBug66430 VALUES (2)");

        boolean useSPS = false;
        boolean cachePS = false;
        do {
            final String testCase = String.format("Case: [useSPS: %s, cachePS: %s ]", useSPS ? "Y" : "N", cachePS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), Boolean.toString(cachePS));
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            Connection testConn = getConnectionWithProps(props);

            if (((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA) {
                testConn.setSchema("testBug66430DB1");
            } else {
                testConn.setCatalog("testBug66430DB1");
            }
            PreparedStatement testPStmt = testConn.prepareStatement("SELECT * FROM testBug66430 WHERE id > ?");
            testPStmt.setInt(1, 0);
            this.rs = testPStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertFalse(this.rs.next(), testCase);
            testPStmt.close();

            if (((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA) {
                testConn.setSchema("testBug66430DB2");
            } else {
                testConn.setCatalog("testBug66430DB2");
            }
            testPStmt = testConn.prepareStatement("SELECT * FROM testBug66430 WHERE id > ?");
            testPStmt.setInt(1, 0);
            this.rs = testPStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            assertEquals(2, this.rs.getInt(1), testCase);
            assertFalse(this.rs.next(), testCase);
            testPStmt.close();

            // Do it again to make sure cached prepared statements behave correctly.
            if (((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA) {
                testConn.setSchema("testBug66430DB1");
            } else {
                testConn.setCatalog("testBug66430DB1");
            }
            testPStmt = testConn.prepareStatement("SELECT * FROM testBug66430 WHERE id > ?");
            testPStmt.setInt(1, 0);
            this.rs = testPStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertFalse(this.rs.next(), testCase);
            testPStmt.close();

            if (((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA) {
                testConn.setSchema("testBug66430DB2");
            } else {
                testConn.setCatalog("testBug66430DB2");
            }
            testPStmt = testConn.prepareStatement("SELECT * FROM testBug66430 WHERE id > ?");
            testPStmt.setInt(1, 0);
            this.rs = testPStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            assertEquals(2, this.rs.getInt(1), testCase);
            assertFalse(this.rs.next(), testCase);
            testPStmt.close();

            testConn.close();
        } while ((useSPS = !useSPS) || (cachePS = !cachePS));
    }

    /**
     * Tests fix for Bug#84783 - query timeout is not working(thread hang).
     *
     * @throws Exception
     */
    @Test
    public void testBug84783() throws Exception {
        // Test using a standard connection.
        final Statement testStmt = this.conn.createStatement();
        testStmt.setQueryTimeout(1);
        assertThrows(SQLException.class, "Statement cancelled due to timeout or client request", () -> {
            testStmt.executeQuery("SELECT SLEEP(3)");
            return null;
        });
        testStmt.close();

        boolean useSPS = false;
        do {
            final Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            final String testCase = String.format("Case [SPS: %s]", useSPS ? "Y" : "N");

            Connection testConn;

            // Test using a failover connection.
            testConn = getUnreliableFailoverConnection(new String[] { "host1", "host2" }, props);
            final Statement testStmtFO = testConn.createStatement();
            testStmtFO.setQueryTimeout(1);
            assertThrows(testCase, SQLException.class, "Statement cancelled due to timeout or client request", () -> {
                testStmtFO.executeQuery("SELECT SLEEP(3)");
                return null;
            });
            final PreparedStatement testPstmtFO = testConn.prepareStatement("SELECT SLEEP(3)");
            testPstmtFO.setQueryTimeout(1);
            assertThrows(testCase, SQLException.class, "Statement cancelled due to timeout or client request", () -> {
                testPstmtFO.executeQuery();
                return null;
            });
            testConn.close();

            // Test using a load-balanced connection.
            testConn = getUnreliableLoadBalancedConnection(new String[] { "host1", "host2" }, props);
            final Statement testStmtLB = testConn.createStatement();
            testStmtLB.setQueryTimeout(1);
            assertThrows(testCase, SQLException.class, "Statement cancelled due to timeout or client request", () -> {
                testStmtLB.executeQuery("SELECT SLEEP(3)");
                return null;
            });
            final PreparedStatement testPstmtLB = testConn.prepareStatement("SELECT SLEEP(3)");
            testPstmtLB.setQueryTimeout(1);
            assertThrows(testCase, SQLException.class, "Statement cancelled due to timeout or client request", () -> {
                testPstmtLB.executeQuery();
                return null;
            });
            testConn.close();

            // Test using a replication connection.
            testConn = getUnreliableReplicationConnection(new String[] { "host1", "host2" }, props);
            final Statement testStmtR = testConn.createStatement();
            testStmtR.setQueryTimeout(1);
            assertThrows(testCase, SQLException.class, "Statement cancelled due to timeout or client request", () -> {
                testStmtR.executeQuery("SELECT SLEEP(3)");
                return null;
            });
            final PreparedStatement testPstmtR = testConn.prepareStatement("SELECT SLEEP(3)");
            testPstmtR.setQueryTimeout(1);
            assertThrows(testCase, SQLException.class, "Statement cancelled due to timeout or client request", () -> {
                testPstmtR.executeQuery();
                return null;
            });
            testConn.close();
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#74932 - ConnectionImp Doesn't Close Server Prepared Statement (PreparedStatement Leak).
     *
     * @throws Exception
     */
    @Test
    public void testBug74932() throws Exception {
        createTable("testBug74932", "(c1 INT, c2 INT)");
        this.stmt.executeUpdate("INSERT INTO testBug74932 VALUES (1, 1), (1, 2), (2, 1), (2, 2)");

        String sql1 = "SELECT * FROM testBug74932 WHERE c1 = ?";
        String sql2 = "SELECT * FROM testBug74932 WHERE c2 = ?";

        final Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.prepStmtCacheSize.getKeyName(), "10");
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");

        // Prepare different statements.
        Connection testConn = getConnectionWithProps(props);
        this.rs = testConn.createStatement().executeQuery("SHOW STATUS LIKE 'Prepared_stmt_count'");
        assertTrue(this.rs.next());
        int currPrepCount = this.rs.getInt(2);
        for (int i = 0; i < 10; i++) {
            testBug74932ExecuteStmts(testConn, sql1, sql2);

            this.rs = testConn.createStatement().executeQuery("SHOW STATUS LIKE 'Prepared_stmt_count'");
            assertTrue(this.rs.next());
            assertEquals(2, this.rs.getInt(2) - currPrepCount);
        }
        testConn.close();

        // Prepare same statement.
        testConn = getConnectionWithProps(props);
        this.rs = testConn.createStatement().executeQuery("SHOW STATUS LIKE 'Prepared_stmt_count'");
        assertTrue(this.rs.next());
        currPrepCount = this.rs.getInt(2);
        for (int i = 0; i < 10; i++) {
            testBug74932ExecuteStmts(testConn, sql1, sql1);
            this.rs = testConn.createStatement().executeQuery("SHOW STATUS LIKE 'Prepared_stmt_count'");
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(2) - currPrepCount);
        }
        testConn.close();
    }

    private void testBug74932ExecuteStmts(final Connection testConn, final String sqlOuter, final String sqlInner) throws SQLException {
        PreparedStatement psOuter = null;
        PreparedStatement psInner = null;
        ResultSet rsOuter = null;
        ResultSet rsInner = null;

        psOuter = testConn.prepareStatement(sqlOuter);
        psOuter.setInt(1, 1);
        rsOuter = psOuter.executeQuery();
        for (int i = 0; i < 2; i++) {
            assertTrue(rsOuter.next());
            try {
                psInner = testConn.prepareStatement(sqlInner);
                psInner.setInt(1, 2);
                rsInner = psInner.executeQuery();
                assertTrue(rsInner.next());
                assertTrue(rsInner.next());
                assertFalse(rsInner.next());
            } finally {
                if (rsInner != null) {
                    rsInner.close();
                }
                psInner.close();
            }
        }
        assertFalse(rsOuter.next());
        rsOuter.close();
        rsInner.close();
        psOuter.close();
    }

    /**
     * Tests fix for Bug#78313 - proxies not handling Object.equals(Object) calls correctly.
     *
     * @throws Exception
     */
    @Test
    public void testBug78313() throws Exception {
        Connection testConn;

        // Plain connection.
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        testConn = getConnectionWithProps(props);
        assertFalse(testConn.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(testConn.equals(testConn));
        this.stmt = testConn.createStatement();
        assertFalse(this.stmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.stmt.equals(this.stmt));
        this.rs = this.stmt.executeQuery("SELECT 'testBug78313'");
        assertFalse(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        this.pstmt = testConn.prepareStatement("SELECT 'testBug78313'");
        assertFalse(this.pstmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.pstmt.equals(this.pstmt));
        assertFalse(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        testConn.close();

        // Plain connection with proxied result sets.
        props.setProperty(PropertyKey.queryInterceptors.getKeyName(), ResultSetScannerInterceptor.class.getName());
        props.setProperty(ResultSetScannerInterceptor.PNAME_resultSetScannerRegex, ".*");
        testConn = getConnectionWithProps(props);
        assertFalse(testConn.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(testConn.equals(testConn));
        this.stmt = testConn.createStatement();
        assertFalse(this.stmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.stmt.equals(this.stmt));
        this.rs = this.stmt.executeQuery("SELECT 'testBug78313'");
        assertTrue(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        this.pstmt = testConn.prepareStatement("SELECT 'testBug78313'");
        assertFalse(this.pstmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.pstmt.equals(this.pstmt));
        this.rs = this.pstmt.executeQuery();
        assertTrue(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        testConn.close();

        // Fail-over connection; all JDBC objects are proxied.
        props.clear();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        testConn = getFailoverConnection(props);
        assertTrue(testConn.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(testConn.equals(testConn));
        this.stmt = testConn.createStatement();
        assertTrue(this.stmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.stmt.equals(this.stmt));
        this.rs = this.stmt.executeQuery("SELECT 'testBug78313'");
        assertTrue(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        this.pstmt = testConn.prepareStatement("SELECT 'testBug78313'");
        assertTrue(this.pstmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.pstmt.equals(this.pstmt));
        this.rs = this.pstmt.executeQuery();
        assertTrue(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        testConn.close();

        // Load-balanced connection; all JDBC objects are proxied.
        testConn = getLoadBalancedConnection(props);
        assertTrue(testConn.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(testConn.equals(testConn));
        this.stmt = testConn.createStatement();
        assertTrue(this.stmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.stmt.equals(this.stmt));
        this.rs = this.stmt.executeQuery("SELECT 'testBug78313'");
        assertTrue(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        this.pstmt = testConn.prepareStatement("SELECT 'testBug78313'");
        assertTrue(this.pstmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.pstmt.equals(this.pstmt));
        this.rs = this.pstmt.executeQuery();
        assertTrue(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        testConn.close();

        // Replication connection; all JDBC objects are proxied.
        testConn = getSourceReplicaReplicationConnection(props);
        assertTrue(testConn.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(testConn.equals(testConn));
        this.stmt = testConn.createStatement();
        assertTrue(this.stmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.stmt.equals(this.stmt));
        this.rs = this.stmt.executeQuery("SELECT 'testBug78313'");
        assertTrue(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        this.pstmt = testConn.prepareStatement("SELECT 'testBug78313'");
        assertTrue(this.pstmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.pstmt.equals(this.pstmt));
        this.rs = this.pstmt.executeQuery();
        assertTrue(this.rs.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(this.rs.equals(this.rs));
        testConn.close();

        // XA Connection; unwrapped connections and statements are proxied.
        MysqlXADataSource xaDs = new MysqlXADataSource();
        xaDs.setUrl(BaseTestCase.dbUrl);
        xaDs.<SslMode>getEnumProperty(PropertyKey.sslMode).setValue(SslMode.DISABLED);
        xaDs.getBooleanProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName()).setValue(true);
        XAConnection xaTestConn = xaDs.getXAConnection();
        testConn = xaTestConn.getConnection();
        Connection unwrappedTestConn = testConn.unwrap(JdbcConnection.class);
        assertTrue(unwrappedTestConn.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(unwrappedTestConn.equals(unwrappedTestConn));
        this.stmt = testConn.createStatement();
        Statement unwrappedStmt = this.stmt.unwrap(com.mysql.cj.jdbc.JdbcStatement.class);
        assertTrue(unwrappedStmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(unwrappedStmt.equals(unwrappedStmt));
        this.pstmt = testConn.prepareStatement("SELECT 'testBug78313'");
        Statement unwrappedPstmt = this.pstmt.unwrap(com.mysql.cj.jdbc.JdbcStatement.class);
        assertTrue(unwrappedPstmt.getClass().getName().matches("^(?:com\\.sun\\.proxy\\.)?\\$Proxy\\d*"));
        assertTrue(unwrappedPstmt.equals(unwrappedPstmt));
        testConn.close();
        xaTestConn.close();
    }

    /**
     * Tests fix for Bug#87429 - repeated close of ServerPreparedStatement causes memory leak.
     *
     * Original de-cache on double close() behavior modified by:
     * WL#11101 - Remove de-cache and close of SSPSs on double call to close().
     *
     * @throws Exception
     */
    @Test
    public void testBug87429() throws Exception {
        Field stmtsCacheField = ConnectionImpl.class.getDeclaredField("serverSideStatementCache");
        stmtsCacheField.setAccessible(true);
        ToIntFunction<Connection> getStmtsCacheSize = c -> {
            try {
                LRUCache<?, ?> stmtsCacheObj = (LRUCache<?, ?>) stmtsCacheField.get(c);
                return stmtsCacheObj == null ? -1 : stmtsCacheObj.size();
            } catch (IllegalArgumentException | IllegalAccessException e) {
                fail("Fail getting the statemets cache size.");
                return -1;
            }
        };

        final String sql1 = "SELECT 1, ?";
        final String sql2 = "SELECT 2, ?";

        boolean useSPS = false;
        boolean cachePS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), Boolean.toString(cachePS));
            props.setProperty(PropertyKey.prepStmtCacheSize.getKeyName(), "5");

            boolean cachedSPS = useSPS && cachePS;

            JdbcConnection testConn = (JdbcConnection) getConnectionWithProps(props);
            // Single PreparedStatement, closed multiple times.
            for (int i = 0; i < 100; i++) {
                this.pstmt = testConn.prepareStatement(sql1);
                assertTrue(this.pstmt.isPoolable());
                assertEquals(1, testConn.getActiveStatementCount());
                assertEquals(cachedSPS ? 0 : -1, getStmtsCacheSize.applyAsInt(testConn));
                this.pstmt.close(); // Close & cache.
                assertEquals(cachedSPS ? 1 : 0, testConn.getActiveStatementCount());
                assertEquals(cachedSPS ? 1 : -1, getStmtsCacheSize.applyAsInt(testConn));
                this.pstmt.close();  // No-op.
                assertEquals(cachedSPS ? 1 : 0, testConn.getActiveStatementCount());
                assertEquals(cachedSPS ? 1 : -1, getStmtsCacheSize.applyAsInt(testConn));
                this.pstmt.close();  // No-op.
                assertEquals(cachedSPS ? 1 : 0, testConn.getActiveStatementCount());
                assertEquals(cachedSPS ? 1 : -1, getStmtsCacheSize.applyAsInt(testConn));

                try {
                    this.pstmt.setPoolable(false); // De-caches the statement or no-op if not cached.
                } catch (SQLException e) {
                    assertFalse(cachedSPS, "Exception [" + e.getMessage() + "] not expected.");
                }
                this.pstmt.close(); // No-op.
                assertEquals(0, testConn.getActiveStatementCount());
                assertEquals(cachedSPS ? 0 : -1, getStmtsCacheSize.applyAsInt(testConn));
            }
            testConn.close();
            assertEquals(0, testConn.getActiveStatementCount());
            assertEquals(cachedSPS ? 0 : -1, getStmtsCacheSize.applyAsInt(testConn));

            testConn = (JdbcConnection) getConnectionWithProps(props);
            // Multiple PreparedStatements interchanged, two queries, closed multiple times.
            for (int i = 0; i < 100; i++) {
                for (int j = 1; j <= 4; j++) {
                    PreparedStatement pstmt1 = testConn.prepareStatement(j == 1 ? sql2 : sql1);
                    PreparedStatement pstmt2 = testConn.prepareStatement(j == 2 ? sql2 : sql1);
                    PreparedStatement pstmt3 = testConn.prepareStatement(j == 3 ? sql2 : sql1);
                    PreparedStatement pstmt4 = testConn.prepareStatement(j == 4 ? sql2 : sql1);
                    assertEquals(4, testConn.getActiveStatementCount());

                    // Close and cache statements successively.
                    pstmt4.close();
                    pstmt4.close(); // No-op.
                    assertEquals(cachedSPS ? 4 : 3, testConn.getActiveStatementCount());
                    pstmt3.close();
                    pstmt3.close(); // No-op.
                    assertEquals(cachedSPS ? j >= 3 ? 4 : 3 : 2, testConn.getActiveStatementCount());
                    pstmt2.close();
                    pstmt2.close(); // No-op.
                    assertEquals(cachedSPS ? j >= 2 ? 3 : 2 : 1, testConn.getActiveStatementCount());
                    pstmt1.close();
                    pstmt1.close(); // No-op.
                    assertEquals(cachedSPS ? 2 : 0, testConn.getActiveStatementCount());

                    // No-ops.
                    pstmt4.close();
                    pstmt4.close();
                    pstmt3.close();
                    pstmt3.close();
                    pstmt2.close();
                    pstmt2.close();
                    pstmt1.close();
                    pstmt1.close();
                    assertEquals(cachedSPS ? 2 : 0, testConn.getActiveStatementCount());

                    // De-cache statements successively.
                    try {
                        pstmt4.setPoolable(false); // De-caches the statement or no-op if not cached.
                    } catch (SQLException e) {
                        assertFalse(cachedSPS && j == 4, "Exception [" + e.getMessage() + "] not expected.");
                    }
                    pstmt4.close(); // No-op.
                    assertEquals(cachedSPS ? j < 4 ? 2 : 1 : 0, testConn.getActiveStatementCount());
                    try {
                        pstmt3.setPoolable(false); // De-caches the statement or no-op if not cached.
                    } catch (SQLException e) {
                        assertFalse(cachedSPS && j == 3, "Exception [" + e.getMessage() + "] not expected.");
                    }
                    pstmt3.close(); // No-op.
                    assertEquals(cachedSPS ? j < 3 ? 2 : 1 : 0, testConn.getActiveStatementCount());
                    try {
                        pstmt2.setPoolable(false); // De-caches the statement or no-op if not cached.
                    } catch (SQLException e) {
                        assertFalse(cachedSPS && j == 2, "Exception [" + e.getMessage() + "] not expected.");
                    }
                    pstmt2.close(); // No-op.
                    assertEquals(cachedSPS ? 1 : 0, testConn.getActiveStatementCount());
                    try {
                        pstmt1.setPoolable(false); // De-caches the statement or no-op if not cached.
                    } catch (SQLException e) {
                        assertFalse(cachedSPS, "Exception [" + e.getMessage() + "] not expected.");
                    }
                    pstmt1.close(); // No-op.
                    assertEquals(0, testConn.getActiveStatementCount());
                }
            }
            testConn.close();
            assertEquals(0, testConn.getActiveStatementCount());
        } while ((useSPS = !useSPS) || (cachePS = !cachePS));
    }

    /**
     * Tests fix for Bug#26995710 - WL#11161 : NULL POINTER EXCEPTION IN EXECUTEBATCH() AND CLOSE().
     *
     * @throws Exception
     */
    @Test
    public void testBug26995710() throws Exception {
        createTable("testBug26995710", "(c1 char(20),c2 char(20))");

        boolean useSPS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
            props.setProperty(PropertyKey.autoGenerateTestcaseScript.getKeyName(), "true");
            props.setProperty(PropertyKey.sessionVariables.getKeyName(), "sql_mode=STRICT_TRANS_TABLES");

            Connection con = null;
            try {
                this.stmt.executeUpdate("truncate table testBug26995710");
                this.stmt.executeUpdate("insert into testBug26995710 values('a','b')");

                int colCnt = 0, i = 0;
                ResultSetMetaData rsmd = null;

                con = getConnectionWithProps(props);

                PreparedStatement ps = con.prepareStatement("Insert into testBug26995710 values(?,?) ", ResultSet.TYPE_SCROLL_SENSITIVE,
                        ResultSet.CONCUR_UPDATABLE);
                ps.setQueryTimeout(2);
                assertEquals(2, ps.getQueryTimeout());
                ps.setString(1, "abc1");
                ps.setString(2, "xyz1");
                ps.addBatch();
                ps.setString(1, "abc2");
                ps.setString(2, "xyz2");
                ps.addBatch();
                ps.setNull(1, java.sql.Types.VARCHAR);
                ps.setString(2, "xyz4");
                ps.addBatch();
                ps.setString(1, "abc4");
                ps.setNull(2, java.sql.Types.VARCHAR);
                ps.addBatch();
                ps.setNull(1, java.sql.Types.VARCHAR);
                ps.setNull(2, java.sql.Types.VARCHAR);
                ps.addBatch();
                ps.executeBatch();
                ps.close();

                ResultSet rset = this.stmt.executeQuery("select * from testBug26995710");
                rsmd = rset.getMetaData();
                colCnt = rsmd.getColumnCount();
                System.out.println(" Column Cnt = " + colCnt);
                while (rset.next()) {
                    for (i = 1; i <= colCnt; i++) {
                        System.out.print(" [" + rset.getString(i) + "]");
                    }
                    System.out.print("\n");
                }
                rset.close();
                ps.close();

                ps = con.prepareStatement("delete from testBug26995710 where c1=? ", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
                ps.setQueryTimeout(2);
                ps.setString(1, "abc1");
                ps.addBatch();
                ps.setNull(1, java.sql.Types.VARCHAR);
                ps.addBatch();
                ps.setString(1, "abc4");
                ps.addBatch();
                ps.setString(1, "abc4");
                ps.addBatch();
                ps.executeBatch();
                ps.close();

                ps = con.prepareStatement("select count(*) from testBug26995710 ", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
                rset = ps.executeQuery();
                rset.next();
                assertEquals(4, rset.getInt(1));
                rset.close();

                ps = con.prepareStatement("select * from testBug26995710 ", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
                rset = ps.executeQuery();
                rsmd = rset.getMetaData();
                colCnt = rsmd.getColumnCount();
                System.out.println(" Column Cnt = " + colCnt);
                while (rset.next()) {
                    for (i = 1; i <= colCnt; i++) {
                        System.out.print(" [" + rset.getString(i) + "]");
                    }
                    System.out.print("\n");
                }
                rset.close();
                ps.close();

                ps = con.prepareStatement("insert into testBug26995710 select sleep(?)+1,sleep(?)+2 ", ResultSet.TYPE_SCROLL_SENSITIVE,
                        ResultSet.CONCUR_UPDATABLE);
                ps.setQueryTimeout(2);
                ps.setInt(1, 2);
                ps.setInt(2, 3);
                ps.addBatch();
                ps.setInt(1, 0);
                ps.setInt(2, 1);
                ps.addBatch();
                ps.setNull(1, java.sql.Types.INTEGER);
                ps.setInt(2, 1);
                ps.addBatch();
                ps.setInt(1, 4);
                ps.setNull(2, java.sql.Types.INTEGER);
                ps.addBatch();
                ps.setNull(1, java.sql.Types.INTEGER);
                ps.setNull(2, java.sql.Types.INTEGER);
                ps.addBatch();
                try {
                    ps.executeBatch();
                    fail("Expected Timeout Error ");
                } catch (SQLException ex) {
                    System.out.print("Error " + ex.getMessage());
                    assertTrue(ex.getMessage().contains("Statement cancelled due to timeout or client request"));
                }
                ps.close();

                ps = con.prepareStatement("select count(*) from testBug26995710 ", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
                rset = ps.executeQuery();
                rset.next();
                System.out.println(" Rec Cnt " + rset.getInt(1));
                //  assertEquals(4,rs.getInt(1));

                rset.close();
                ps.close();

                ps = con.prepareStatement("select * from testBug26995710 ", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
                rset = ps.executeQuery();
                rsmd = rset.getMetaData();
                colCnt = rsmd.getColumnCount();
                System.out.println(" Column Cnt = " + colCnt);
                while (rset.next()) {
                    for (i = 1; i <= colCnt; i++) {
                        System.out.print(" [" + rset.getString(i) + "]");
                    }
                    System.out.print("\n");
                }
                rset.close();
                ps.close();
            } finally {
                if (con != null) {
                    con.close();
                }
            }
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#26748909, NO OPERATIONS ALLOWED AFTER STATEMENT CLOSED FOR TOSTRING()
     *
     * @throws Exception
     */
    @Test
    public void testBug26748909() throws Exception {
        createTable("testBug26748909", "(id int)");

        Connection con = null;

        PreparedStatement ps1 = null;
        PreparedStatement ps2 = null;
        PreparedStatement ps3 = null;

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        boolean useSPS = false;

        do {

            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            try {
                con = getConnectionWithProps(props);
                ps1 = con.prepareStatement("Select 'aaaaaaaaa' from dual");
                ps2 = con.prepareStatement("insert into testBug26748909 values(?)");
                ps3 = con.prepareStatement("select * from testBug26748909 where id=?");

                ps1.execute();
                ps1.close();

                ps2.setInt(1, 10);
                ps2.execute();
                ps2.close();

                ps3.setInt(1, 10);
                ResultSet r = ps3.executeQuery();
                assertTrue(r.next());
                assertEquals(10, r.getInt(1));
                ps3.close();

                System.out.println("'" + ps1 + "'");
                System.out.println("'" + ps2 + "'");
                System.out.println("'" + ps3 + "'");

                if (useSPS) {
                    assertEquals(ps1.toString(), "com.mysql.cj.jdbc.ServerPreparedStatement[1]: Select 'aaaaaaaaa' from dual");
                    assertEquals(ps2.toString(), "com.mysql.cj.jdbc.ServerPreparedStatement[2]: insert into testBug26748909 values(** NOT SPECIFIED **)");
                    assertEquals(ps3.toString(), "com.mysql.cj.jdbc.ServerPreparedStatement[3]: select * from testBug26748909 where id=** NOT SPECIFIED **");
                } else {
                    assertEquals(ps1.toString(), "com.mysql.cj.jdbc.ClientPreparedStatement: Select 'aaaaaaaaa' from dual");
                    assertEquals(ps2.toString(), "com.mysql.cj.jdbc.ClientPreparedStatement: insert into testBug26748909 values(** NOT SPECIFIED **)");
                    assertEquals(ps3.toString(), "com.mysql.cj.jdbc.ClientPreparedStatement: select * from testBug26748909 where id=** NOT SPECIFIED **");
                }

            } catch (Exception e) {
                e.printStackTrace();
                throw e;
            } finally {
                if (con != null) {
                    con.close();
                }
            }
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#87534 - UNION ALL query fails when useServerPrepStmts=true on database connection.
     * Base Bug#27422376 - NEWDATE TYPE IS LEAKING OUT, fixed in MySQL 5.7.22.
     *
     * @throws Exception
     */
    @Test
    public void testBug87534() throws Exception {
        assumeFalse(versionMeetsMinimum(5, 7) && !versionMeetsMinimum(5, 7, 22),
                "This test doesn't work with MySQL 5.7.0-5.7.21 because of server Bug#27422376.");

        System.out.println("running");

        boolean useSPS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSPS);
            Connection testConn = getConnectionWithProps(props);

            this.pstmt = testConn.prepareStatement("SELECT CAST(NOW() AS DATE) UNION SELECT CAST(NOW() AS DATE)");
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertFalse(this.rs.next());
            this.pstmt.close();

            this.pstmt = testConn.prepareStatement("SELECT CAST(NOW() AS DATE) UNION ALL SELECT CAST(NOW() AS DATE)");
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertTrue(this.rs.next());
            assertFalse(this.rs.next());
            this.pstmt.close();

            createTable("testBug87534", "(dt DATE DEFAULT NULL)");
            this.stmt.execute("INSERT INTO testBug87534 VALUES ('2018-01-01')");

            this.pstmt = testConn.prepareStatement("SELECT dt FROM testBug87534 UNION SELECT dt FROM testBug87534");
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertFalse(this.rs.next());
            this.pstmt.close();

            this.pstmt = testConn.prepareStatement("SELECT dt FROM testBug87534 UNION ALL SELECT dt FROM testBug87534");
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertTrue(this.rs.next());
            assertFalse(this.rs.next());
            this.pstmt.close();

            testConn.close();
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#84813 (25501750), rewriteBatchedStatements fails in INSERT.
     *
     * @throws Exception
     */
    @Test
    public void testBug84813() throws Exception {
        createTable("testBug84813", "(id INT AUTO_INCREMENT PRIMARY KEY, z INT, n INT)");

        boolean rwBS = false;
        boolean useSPS = false;

        do {
            final String testCase = String.format("Case [rwBS: %s, useSPS: %s]", rwBS ? "Y" : "N", useSPS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.emulateUnsupportedPstmts.getKeyName(), "false");

            Connection testConn = getConnectionWithProps(props);

            for (int r = 1; r <= 5; r++) {
                for (String odku : new String[] { "", " ON DUPLICATE KEY UPDATE id = -id" }) {
                    final String testCaseExtra = odku.length() > 0 ? "/ODKU" : "/non-ODKU";
                    this.pstmt = testConn.prepareStatement("INSERT INTO testBug84813 VALUES (NULL, 0, 0) /* Comment (?) */" + odku);
                    for (int i = 0; i < r; i++) {
                        this.pstmt.addBatch();
                    }
                    this.pstmt.executeBatch();
                    testBug84813CheckAndReset(testCase + testCaseExtra, r, true);
                    this.pstmt.close();

                    this.pstmt = testConn.prepareStatement("INSERT INTO testBug84813 VALUES (NULL, ?, ?) /* Comment (?) */ " + odku);
                    for (int i = 0; i < r; i++) {
                        this.pstmt.setInt(1, 0);
                        this.pstmt.setInt(2, i);
                        this.pstmt.addBatch();
                    }
                    this.pstmt.executeBatch();
                    testBug84813CheckAndReset(testCase + testCaseExtra, r, false);
                    this.pstmt.close();
                }
            }

            testConn.close();
        } while ((rwBS = !rwBS) || (useSPS = !useSPS));
    }

    private void testBug84813CheckAndReset(String testCase, int repetitions, boolean allZero) throws Exception {
        this.rs = this.stmt.executeQuery("SELECT * FROM testBug84813");
        for (int i = 0; i < repetitions; i++) {
            assertTrue(this.rs.next());
            assertEquals(i + 1, this.rs.getInt(1), testCase);
            assertEquals(0, this.rs.getInt(2), testCase);
            assertEquals(allZero ? 0 : i, this.rs.getInt(3), testCase);
        }
        assertFalse(this.rs.next());
        this.stmt.execute("TRUNCATE TABLE testBug84813");
    }

    /**
     * Tests fix for Bug#81063 (23098159), w/ rewriteBatchedStatements, when 2 tables involved, the rewriting not correct.
     *
     * @throws Exception
     */
    @Test
    public void testBug81063() throws Exception {
        createTable("testBug81063a", "(c1 INT, c2 INT, c3 INT DEFAULT 0, c4 INT DEFAULT 0)");
        createTable("testBug81063b", "(c1 INT, c2 INT)");

        boolean rwBS = false;
        boolean useSPS = false;

        do {
            final String testCase = String.format("Case [rwBS: %s, useSPS: %s]", rwBS ? "Y" : "N", useSPS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.emulateUnsupportedPstmts.getKeyName(), "true");
            props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), "true");
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            Connection testConn = getConnectionWithProps(props);

            for (String sql : new String[] { "INSERT INTO testBug81063a VALUES (?, ?, ?, ?)",
                    "INSERT INTO testBug81063a (c1, c2) VALUES (?, ?); INSERT INTO testBug81063b VALUES (?, ?)" }) {
                int valsTbl1 = 4;
                int valsTbl2 = 0;
                if (sql.indexOf(';') != -1) {
                    valsTbl1 = 2;
                    valsTbl2 = 2;
                }
                for (int r = 1; r <= 5; r++) {
                    this.pstmt = testConn.prepareStatement(sql);
                    for (int i = 0; i < r; i++) {
                        this.pstmt.setLong(1, 4 * i + 1);
                        this.pstmt.setLong(2, 4 * i + 2);
                        this.pstmt.setLong(3, 4 * i + 3);
                        this.pstmt.setLong(4, 4 * i + 4);
                        this.pstmt.addBatch();
                    }
                    this.pstmt.executeBatch();
                    this.pstmt.close();
                    testBug81063CheckAndReset(testCase, valsTbl1, valsTbl2, r);
                }
            }
            testConn.close();
        } while ((rwBS = !rwBS) || (useSPS = !useSPS));
    }

    private void testBug81063CheckAndReset(String testCase, int t1Vals, int t2Vals, int repetitions) throws Exception {
        testCase += " (" + repetitions + " x " + t1Vals + "/" + t2Vals + ")";

        if (t1Vals > 0) {
            this.rs = this.stmt.executeQuery("SELECT * FROM testBug81063a");
            for (int r = 1, e = 1; r <= repetitions; r++, e += 4 - t1Vals) {
                assertTrue(this.rs.next(), testCase);
                for (int c = 1; c <= t1Vals; c++, e++) {
                    assertEquals(e, this.rs.getInt(c), testCase);
                }
            }
            assertFalse(this.rs.next());
        }
        this.stmt.execute("TRUNCATE TABLE testBug81063a");

        if (t2Vals > 0) {
            this.rs = this.stmt.executeQuery("SELECT * FROM testBug81063b");
            for (int r = 1, e = 3; r <= repetitions; r++, e += 4 - t2Vals) {
                assertTrue(this.rs.next(), testCase);
                for (int c = 1; c <= t2Vals; c++, e++) {
                    assertEquals(e, this.rs.getInt(c), testCase);
                }
            }
            assertFalse(this.rs.next());
        }
        this.stmt.execute("TRUNCATE TABLE testBug81063b");
    }

    /**
     * Tests fix for Bug#22931700, BINDINGS.GETBOOLEAN() ALWAYS RETURNS FALSE.
     *
     * @throws Exception
     */
    @Test
    public void testBug22931700() throws Exception {
        createTable("testBug22931700", "(c1 int,c2 bool)");

        for (boolean useSPS : new boolean[] { false, true }) {
            this.stmt.executeUpdate("truncate table testBug22931700");
            this.stmt.executeUpdate("INSERT INTO testBug22931700 values(100,false)");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            Connection testConn = getConnectionWithProps(props);

            this.pstmt = testConn.prepareStatement("update testBug22931700 set c1=?,c2=? ");
            this.pstmt.setNull(1, java.sql.Types.INTEGER);
            this.pstmt.setBoolean(2, true);

            ParameterBindings bindings = ((JdbcPreparedStatement) this.pstmt).getParameterBindings();
            assertTrue(bindings.isNull(1));
            assertFalse(bindings.isNull(2));
            assertTrue(bindings.getBoolean(2));

            this.pstmt.executeUpdate();
        }
    }

    /**
     * Tests fix for Bug#27453692, CHARACTERS GET GARBLED IN CONCAT() IN PS WHEN USECURSORFETCH=TRUE.
     *
     * @throws Exception
     */
    @Test
    public void testBug27453692() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.characterEncoding.getKeyName(), "utf8");

        String str = "\u30c6\u30b9\u30c8:";

        boolean useSPS = false;
        boolean useCursorFetch = false;
        do {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.useCursorFetch.getKeyName(), Boolean.toString(useCursorFetch));
            Connection con1 = getConnectionWithProps(props);

            PreparedStatement ps1 = con1.prepareStatement("SELECT CONCAT('" + str + "', ?) AS res");
            ps1.setInt(1, 123);
            ResultSet rs1 = ps1.executeQuery();
            assertTrue(rs1.next());
            System.out.println(rs1.getString(1));
            assertEquals(str + "123", rs1.getString(1));

            con1.close();

        } while ((useSPS = !useSPS) || (useCursorFetch = !useCursorFetch));
    }

    /**
     * Tests fix for Bug#96442 (30151808), INCORRECT DATE ERROR WHEN CALLING GETMETADATA ON PREPARED STATEMENT.
     *
     * @throws Exception
     */
    @Test
    public void testBug96442() throws Exception {
        createTable("testBug96442", "(id INT UNSIGNED NOT NULL, rdate DATE NOT NULL, ts TIMESTAMP NOT NULL)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        boolean useSPS = false;
        do {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            Connection con = getConnectionWithProps(props);
            try {
                con.prepareStatement("SELECT id FROM testBug96442 WHERE rdate = ? AND ts = ?").getMetaData();
                con.prepareStatement("SELECT DISTINCT id FROM testBug96442 WHERE rdate = ? AND ts = ?").getMetaData();
                con.prepareStatement("SELECT * FROM testBug96442 WHERE id = ? AND ts = ?").getMetaData();
                con.prepareStatement("SELECT * FROM testBug96442 WHERE rdate = ? AND ts = ?").getMetaData();
                con.prepareStatement("SELECT id,rdate FROM testBug96442 WHERE rdate = ? AND ts = ?").getMetaData();
                con.prepareStatement("SELECT * FROM testBug96442 WHERE rdate = '2000-01-01' AND ts = ?").getMetaData();
                con.prepareStatement("SELECT count(id) FROM testBug96442 WHERE rdate = ? AND ts = ?").getMetaData();
                con.prepareStatement("SELECT * FROM testBug96442 HAVING rdate = ? AND ts = ?").getMetaData();
            } finally {
                con.close();
            }
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#Bug#91112 (28125069), AGAIN WRONG JAVA.SQL.DATE.
     *
     * @throws Exception
     */
    @Test
    public void testBug91112() throws Exception {
        createTable("testBug91112", "(d1 DATE, d2 DATE, d3 DATE)");
        String query = "select d1, CONVERT(d1, CHAR(10)) as d1c, d2, CONVERT(d2, CHAR(10)) as d2c, d3, CONVERT(d3, CHAR(10)) as d3c from testBug91112";

        TimeZone defaultTimeZone = TimeZone.getDefault();
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.put(PropertyKey.connectionTimeZone, "UTC");
        try {
            // setting a custom calendar time zone later than the client one to detect day switch
            Calendar cal_Los_Angeles = Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"), Locale.US);
            SimpleDateFormat sdf_Los_Angeles = TimeUtil.getSimpleDateFormat("yyyy-MM-dd HH:mm:ss", cal_Los_Angeles);

            for (boolean useSSPS : new boolean[] { false, true }) {
                for (boolean cacheDefaultTimeZone : new boolean[] { true, false }) {
                    String errMsg = "Using useSSPS=" + useSSPS + ", cacheDefaultTimeZone=" + cacheDefaultTimeZone + ":";
                    System.out.println(errMsg);
                    props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSSPS);
                    props.put(PropertyKey.cacheDefaultTimeZone.getKeyName(), "" + cacheDefaultTimeZone);

                    // setting a client time zone ahead of the server one to detect day switch
                    TimeZone.setDefault(TimeZone.getTimeZone("Europe/Moscow"));

                    Connection con = getConnectionWithProps(props);

                    PreparedStatement pst1 = con.prepareStatement("insert into testBug91112 values (?, ?, ?)");
                    java.sql.Date d_1982_04_01_MSK = java.sql.Date.valueOf("1982-04-01");
                    java.sql.Date d_1990_10_20_MSK = java.sql.Date.valueOf("1990-10-20");
                    LocalDate ld_1990_10_20 = LocalDate.of(1990, 10, 20);

                    pst1.setDate(1, d_1982_04_01_MSK);
                    pst1.setDate(2, d_1982_04_01_MSK, cal_Los_Angeles);
                    pst1.setObject(3, ld_1990_10_20);
                    pst1.execute();

                    this.rs = con.createStatement().executeQuery(query);
                    testBug91112CheckResultMSK(d_1982_04_01_MSK, d_1990_10_20_MSK, cal_Los_Angeles, sdf_Los_Angeles);

                    PreparedStatement pst2 = con.prepareStatement(query);
                    this.rs = pst2.executeQuery();
                    testBug91112CheckResultMSK(d_1982_04_01_MSK, d_1990_10_20_MSK, cal_Los_Angeles, sdf_Los_Angeles);

                    // check the cacheDefaultTimeZone behaviour, changing time zone in runtime
                    TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin"));

                    this.stmt.execute("TRUNCATE TABLE testBug91112");

                    pst1.setDate(1, d_1982_04_01_MSK);
                    pst1.setDate(2, d_1982_04_01_MSK, cal_Los_Angeles);
                    pst1.setObject(3, ld_1990_10_20);
                    pst1.execute();

                    this.rs = con.createStatement().executeQuery(query);
                    testBug91112CheckResultBerlin(d_1982_04_01_MSK, d_1990_10_20_MSK, cal_Los_Angeles, sdf_Los_Angeles, cacheDefaultTimeZone);

                    this.rs = pst2.executeQuery();
                    testBug91112CheckResultBerlin(d_1982_04_01_MSK, d_1990_10_20_MSK, cal_Los_Angeles, sdf_Los_Angeles, cacheDefaultTimeZone);

                    PreparedStatement pst3 = con.prepareStatement(query);
                    this.rs = pst3.executeQuery();
                    testBug91112CheckResultBerlin(d_1982_04_01_MSK, d_1990_10_20_MSK, cal_Los_Angeles, sdf_Los_Angeles, cacheDefaultTimeZone);

                    this.stmt.execute("TRUNCATE TABLE testBug91112");
                }
            }

        } finally {
            TimeZone.setDefault(defaultTimeZone);
        }
    }

    public void testBug91112CheckResultMSK(java.sql.Date d_1982_04_01_MSK, java.sql.Date d_1990_10_20_MSK, Calendar cal_Los_Angeles,
            SimpleDateFormat sdf_Los_Angeles) throws Exception {
        assertTrue(this.rs.next());
        assertEquals("1982-04-01", this.rs.getString(1));
        assertEquals("1982-04-01", this.rs.getString(2));
        assertEquals("1982-03-31", this.rs.getString(3));
        assertEquals("1982-03-31", this.rs.getString(4));
        assertEquals("1990-10-20", this.rs.getString(5));
        assertEquals("1990-10-20", this.rs.getString(6));
        assertEquals(d_1982_04_01_MSK, this.rs.getDate(1));
        assertEquals(d_1982_04_01_MSK, this.rs.getDate(1, Calendar.getInstance()));
        assertEquals("1982-03-30 13:00:00", sdf_Los_Angeles.format(this.rs.getDate(3)));
        assertEquals("1982-03-31 00:00:00", sdf_Los_Angeles.format(this.rs.getDate(3, cal_Los_Angeles)));
        assertEquals(d_1990_10_20_MSK, this.rs.getDate(5));
        assertEquals(d_1990_10_20_MSK, this.rs.getDate(5, Calendar.getInstance()));
    }

    public void testBug91112CheckResultBerlin(java.sql.Date d_1982_04_01_MSK, java.sql.Date d_1990_10_20_MSK, Calendar cal_Los_Angeles,
            SimpleDateFormat sdf_Los_Angeles, boolean cacheDefaultTimeZone) throws Exception {
        assertTrue(this.rs.next());
        if (cacheDefaultTimeZone) {
            assertEquals("1982-04-01", this.rs.getString(1));
            assertEquals("1982-04-01", this.rs.getString(2));
            assertEquals("1982-03-30 13:00:00", sdf_Los_Angeles.format(this.rs.getDate(3)));
        } else {
            assertEquals("1982-03-31", this.rs.getString(1));
            assertEquals("1982-03-31", this.rs.getString(2));
            assertEquals("1982-03-30 14:00:00", sdf_Los_Angeles.format(this.rs.getDate(3)));
        }
        assertEquals("1982-03-31", this.rs.getString(4));
        assertEquals("1990-10-20", this.rs.getString(5));
        assertEquals("1990-10-20", this.rs.getString(6));

        java.sql.Date d_1982_03_31_Berlin = java.sql.Date.valueOf("1982-03-31");
        java.sql.Date d_1990_10_20_Berlin = java.sql.Date.valueOf("1990-10-20");

        if (cacheDefaultTimeZone) {
            assertEquals(d_1982_04_01_MSK, this.rs.getDate(1));
            assertEquals(d_1982_04_01_MSK, this.rs.getDate(1, Calendar.getInstance(TimeZone.getTimeZone("Europe/Moscow"))));
        } else {
            assertEquals(d_1982_03_31_Berlin, this.rs.getDate(1));
            assertEquals(d_1982_03_31_Berlin, this.rs.getDate(1, Calendar.getInstance(TimeZone.getTimeZone("Europe/Berlin"))));
        }

        assertEquals("1982-03-31", this.rs.getString(3));
        assertEquals("1982-03-31 00:00:00", sdf_Los_Angeles.format(this.rs.getDate(3, cal_Los_Angeles)));

        if (cacheDefaultTimeZone) {
            assertEquals(d_1990_10_20_MSK, this.rs.getDate(5));
            assertEquals(d_1990_10_20_MSK, this.rs.getDate(5, Calendar.getInstance(TimeZone.getTimeZone("Europe/Moscow"))));
        } else {
            assertEquals(d_1990_10_20_Berlin, this.rs.getDate(5));
            assertEquals(d_1990_10_20_Berlin, this.rs.getDate(5, Calendar.getInstance(TimeZone.getTimeZone("Europe/Berlin"))));
        }
    }

    /**
     * Tests fix for Bug#98536 (30877755), SIMPLEDATEFORMAT COULD CACHE A WRONG CALENDAR.
     *
     * @throws Exception
     */
    @Test
    public void testBug98536() throws Exception {
        GregorianCalendar prolepticGc = new GregorianCalendar();
        prolepticGc.setGregorianChange(new Date(Long.MIN_VALUE));
        prolepticGc.clear();
        prolepticGc.set(Calendar.DAY_OF_MONTH, 8);
        prolepticGc.set(Calendar.MONTH, Calendar.OCTOBER);
        prolepticGc.set(Calendar.YEAR, 1582);

        GregorianCalendar gc = new GregorianCalendar();
        gc.clear();
        gc.setTimeInMillis(prolepticGc.getTimeInMillis());

        assertEquals(1582, gc.get(Calendar.YEAR));
        assertEquals(8, gc.get(Calendar.MONTH));
        assertEquals(28, gc.get(Calendar.DAY_OF_MONTH));

        createTable("testBug98536", "(d1 date, pd date, d2 date)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        boolean useServerPrepStmts = false;

        do {
            final String testCase = String.format("Case: [useServerPrepStmts=%s]", useServerPrepStmts ? "Y" : "N");
            System.out.println(testCase);

            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useServerPrepStmts);

            Connection c1 = getConnectionWithProps(props);
            Statement st1 = c1.createStatement();

            st1.execute("truncate table testBug98536");

            java.sql.Date d1 = new java.sql.Date(prolepticGc.getTime().getTime());

            this.pstmt = c1.prepareStatement("insert into testBug98536 values(?,?,?)");
            this.pstmt.setDate(1, d1);
            this.pstmt.setDate(2, d1, prolepticGc);
            this.pstmt.setDate(3, d1);
            this.pstmt.execute();

            /*
             * Checking stored values by retrieving them as strings to avoid conversions on c/J side.
             */
            this.rs = st1.executeQuery(
                    "select DATE_FORMAT(d1, '%Y-%m-%d') as d1, DATE_FORMAT(pd, '%Y-%m-%d') as pd, DATE_FORMAT(d2, '%Y-%m-%d') as d2 from testBug98536");
            this.rs.next();
            System.out.println(this.rs.getString(1) + ", " + this.rs.getString(2) + ", " + this.rs.getString(3));

            assertEquals("1582-09-28", this.rs.getString(1), testCase); // according to Julian calendar
            assertEquals("1582-10-08", this.rs.getString(2), testCase); // according to proleptic Gregorian calendar
            assertEquals("1582-09-28", this.rs.getString(3), testCase); // according to Julian calendar

            c1.close();

        } while (useServerPrepStmts = !useServerPrepStmts);
    }

    /**
     * Tests fix for Bug#98237 (30911870), PREPAREDSTATEMENT.SETOBJECT(I, "FALSE", TYPES.BOOLEAN) ALWAYS SETS TRUE OR 1.
     *
     * @throws Exception
     */
    @Test
    public void testBug98237() throws Exception {
        createTable("testBug98237", "(b tinyint)");

        String[] falses = new String[] { "False", "n", "0", "-0", "0.00", "-0.0" };
        String[] trues = new String[] { "true", "y", "1", "-1", "1.0", "-1.0", "0.01", "-0.01" };

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        boolean useSPS = false;
        do {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            Connection con = getConnectionWithProps(props);
            try {
                PreparedStatement ps = con.prepareStatement("insert into testBug98237 values(?)");
                Statement st = con.createStatement();

                con.createStatement().execute("truncate table testBug98237");
                for (String val : falses) {
                    ps.clearParameters();
                    ps.setObject(1, val, Types.BOOLEAN);
                    ps.execute();
                }
                this.rs = st.executeQuery("select * from testBug98237");
                for (String val : falses) {
                    assertTrue(this.rs.next());
                    assertEquals(0, this.rs.getInt(1), "'false' was expected for " + val);
                }

                con.createStatement().execute("truncate table testBug98237");
                for (String val : trues) {
                    ps.clearParameters();
                    ps.setObject(1, val, Types.BOOLEAN);
                    ps.execute();
                }
                this.rs = st.executeQuery("select * from testBug98237");
                for (String val : trues) {
                    assertTrue(this.rs.next());
                    assertEquals(1, this.rs.getInt(1), "'true' was expected for " + val);
                }

                ps.clearParameters();
                assertThrows(SQLException.class, "No conversion from abc to Types.BOOLEAN possible.+", () -> {
                    ps.setObject(1, "abc", Types.BOOLEAN);
                    ps.execute();
                    return null;
                });

            } finally {
                con.close();
            }
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#99713 (31418928), NPE DURING COM.MYSQL.CJ.SERVERPREPAREDQUERYBINDVALUE.STOREDATE().
     *
     * @throws Exception
     */
    @Test
    public void testBug99713() throws Exception {
        createTable("testBug99713", "(d1 DATE)");
        Connection con = null;
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        try {
            for (boolean useSSPS : new boolean[] { false, true }) {
                for (boolean cacheDefaultTimeZone : new boolean[] { true, false }) {
                    props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSSPS);
                    props.put(PropertyKey.cacheDefaultTimeZone.getKeyName(), "" + cacheDefaultTimeZone);
                    con = getConnectionWithProps(props);
                    this.pstmt = con.prepareStatement("INSERT into testBug99713 VALUES (?)");
                    this.pstmt.setDate(1, java.sql.Date.valueOf("1982-04-01"));
                    this.pstmt.addBatch();
                    assertEquals(1, this.pstmt.executeBatch()[0]);
                }
            }
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#101242 (32046007), CANNOT USE BYTEARRAYINPUTSTREAM AS ARGUMENTS IN PREPARED STATEMENTS ANYMORE.
     *
     * @throws Exception
     */
    @Test
    public void testBug101242() throws Exception {
        createTable("testBug101242", "(my_column blob NOT NULL)");

        byte[] value = "TEST".getBytes();

        Connection con = null;
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        try {
            for (boolean useSSPS : new boolean[] { false, true }) {
                props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSSPS);
                con = getConnectionWithProps(props);

                this.stmt.executeUpdate("TRUNCATE TABLE testBug101242");

                this.pstmt = con.prepareStatement("INSERT INTO testBug101242(my_column) VALUES (?)");
                this.pstmt.setObject(1, new ByteArrayInputStream(value));
                this.pstmt.execute();

                this.rs = this.stmt.executeQuery("SELECT * FROM testBug101242");
                this.rs.next();

                assertByteArrayEquals("Different value retrieved than inserted", value, this.rs.getBytes("my_column"));
            }
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#98695 (30962953), EXECUTION OF "LOAD DATA LOCAL INFILE" COMMAND THROUGH JDBC FOR DATETIME COLUMN.
     *
     * @throws Exception
     */
    @Test
    public void testBug98695() throws Exception {
        assumeTrue(supportsLoadLocalInfile(this.stmt), "This test requires the server started with --local-infile=ON");

        createTable("testBug98695",
                "(id bigint(20) NOT NULL AUTO_INCREMENT, ts timestamp NULL DEFAULT NULL, dt datetime DEFAULT NULL,date_input_str varchar(45) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB");

        File tempFile = File.createTempFile("data", ".csv");
        Writer out = new FileWriter(tempFile);
        out.write("2019-03-09 23:01:54,2019-03-09 23:01:54,2019-03-09 23:01:54\n" + //
                "2019-03-10 03:00:00,2019-03-10 03:00:00,2019-03-10 03:00:00\n" + //
                "2019-03-10 23:01:54,2019-03-10 23:01:54,2019-03-10 23:01:54\n" + //
                "2019-03-11 23:01:54,2019-03-11 23:01:54,2019-03-11 23:01:54\n" + //
                "2019-04-13 23:01:54,2019-04-13 23:01:54,2019-04-13 23:01:54\n" + //
                "2019-11-02 23:01:54,2019-11-02 23:01:54,2019-11-02 23:01:54\n" + //
                "2019-11-03 02:00:00,2019-11-03 02:00:00,2019-11-03 02:00:00\n" + //
                "2019-11-03 01:00:00,2019-11-03 01:00:00,2019-11-03 01:00:00\n" + //
                "2019-11-03 03:00:00,2019-11-03 03:00:00,2019-11-03 03:00:00\n" + //
                "2019-11-03 04:00:00,2019-11-03 04:00:00,2019-11-03 04:00:00\n" + //
                "2019-11-03 23:01:54,2019-11-03 23:01:54,2019-11-03 23:01:54\n" + //
                "2019-11-21 23:01:54,2019-11-21 23:01:54,2019-11-21 23:01:54");
        out.close();

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.connectionTimeZone.getKeyName(), "LOCAL");
        props.setProperty(PropertyKey.allowLoadLocalInfile.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        String fileName = tempFile.getAbsolutePath();

        if (File.separatorChar == '\\') {
            StringBuilder fileNameBuf = new StringBuilder();
            int fileNameLength = fileName.length();
            for (int i = 0; i < fileNameLength; i++) {
                char c = fileName.charAt(i);
                fileNameBuf.append(c == '\\' ? "/" : c);
            }
            fileName = fileNameBuf.toString();
        }

        PreparedStatement preparedStmt = testConn.prepareStatement(
                "LOAD DATA LOCAL INFILE '" + fileName + "' INTO TABLE testBug98695 FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' (ts,dt,date_input_str)");
        preparedStmt.execute();
        preparedStmt.close();

        preparedStmt = testConn.prepareStatement("select ts, dt, date_input_str from testBug98695");
        ResultSet resultSet = preparedStmt.executeQuery();

        while (resultSet.next()) {
            Object ts = resultSet.getObject("ts");
            Object dt = resultSet.getObject("dt");
            String name = resultSet.getString("date_input_str");

            assertTrue(ts instanceof Timestamp);
            assertTrue(dt instanceof LocalDateTime);

            System.out.print("ts:" + ts + " ,");
            System.out.print("dt:" + ((LocalDateTime) dt).format(TimeUtil.DATETIME_FORMATTER_NO_FRACT_NO_OFFSET) + ", ");
            System.out.println("name:" + name);

            assertEquals(name, ((Timestamp) ts).toLocalDateTime().format(TimeUtil.DATETIME_FORMATTER_NO_FRACT_NO_OFFSET));
            assertEquals(name, ((LocalDateTime) dt).format(TimeUtil.DATETIME_FORMATTER_NO_FRACT_NO_OFFSET));

        }
    }

    /**
     * Tests fix for Bug#101558 (32141210), NULLPOINTEREXCEPTION WHEN EXECUTING INVALID QUERY WITH USEUSAGEADVISOR ENABLED.
     *
     * @throws Exception
     */
    @Test
    public void testBug101558() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.useUsageAdvisor.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        // The query would work after setting allowMultiQueries=true, but when it is set the original bug is not reproducible.
        assertThrows(SQLException.class,
                "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SELECT 1 FROM DUAL' at line 1",
                () -> {
                    PreparedStatement statement = testConn.prepareStatement("SELECT 1 FROM DUAL; SELECT 1 FROM DUAL");
                    statement.execute();
                    return null;
                });

        testConn.close();
    }

    /**
     * Tests fix for Bug#20453773, EXECUTEUPDATE() FAILS FOR SERVER SIDE STMT WHEN LOGSLOWQUERIES=TRUE.
     *
     * @throws Exception
     */
    @Test
    public void testBug20453773() throws Exception {
        createTable("testBug20453773", "(c1 longblob)");
        this.stmt.execute("insert into testBug20453773 values('xyz')");

        Connection con = null;
        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
            props.setProperty(PropertyKey.useNanosForElapsedTime.getKeyName(), "true");
            props.setProperty(PropertyKey.logSlowQueries.getKeyName(), "true");
            con = getConnectionWithProps(props);

            this.rs = this.stmt.executeQuery("select * from testBug20453773");
            this.rs.next();
            Clob clob1 = this.rs.getClob(1);
            this.rs.close();

            PreparedStatement ps = con.prepareStatement("insert into testBug20453773 (c1) values(?) ", ResultSet.TYPE_SCROLL_SENSITIVE,
                    ResultSet.CONCUR_READ_ONLY);
            ps.setClob(1, clob1);
            ps.executeUpdate();
            ps.close();
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#21132376, PSTMT->CLOSE() AFTER PSTMT->EXECUTEBATCH() CALL RETURNS ERROR.
     *
     * @throws Exception
     */
    @Test
    public void testBug21132376() throws Exception {
        createTable("testBug21132376", "(c1 longblob,c2 longblob)");

        this.rs = this.stmt.executeQuery("show variables like 'max_allowed_packet'");
        this.rs.next();
        int initialPLen = this.rs.getInt(2);

        try {
            this.stmt.executeUpdate("set global max_allowed_packet=1024*1024");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
            Connection con = getConnectionWithProps(props);
            Statement st = con.createStatement();
            con = getConnectionWithProps(props);
            st = con.createStatement();
            this.rs = st.executeQuery("show variables like 'max_allowed_packet'");
            this.rs.next();
            int pLen = this.rs.getInt(2);
            this.rs.close();
            assertEquals(1024 * 1024, pLen);
            byte[] blobData = new byte[pLen + 1];
            Arrays.fill(blobData, (byte) 'S');

            PreparedStatement ps = con.prepareStatement("insert into testBug21132376 values(?,?) ", ResultSet.TYPE_SCROLL_SENSITIVE,
                    ResultSet.CONCUR_UPDATABLE);
            ps.setInt(1, 1);
            ps.setBytes(2, blobData);
            ps.addBatch();
            ps.setInt(1, 2);
            ps.setString(2, "2");
            ps.addBatch();
            ps.setInt(1, 3);
            ps.setBytes(2, blobData);
            ps.addBatch();
            assertThrows(SQLException.class, "Packet for query is too large \\(1.048.6\\d\\d > 1.048.576\\).*", () -> ps.executeBatch());
            ps.close(); // was failing

            this.rs = st.executeQuery("select c1 from testBug21132376 ");
            assertTrue(this.rs.next());
            assertEquals("2", this.rs.getString(1));
            assertFalse(this.rs.next());

        } finally {
            this.stmt.executeUpdate("set global max_allowed_packet=" + initialPLen);
        }
    }

    /**
     * Tests fix for Bug#20391550, SETSTRING() CALL AFTER CONNECTION CLOSE RESULTS IN NULLPOINTEREXCEPTION.
     *
     * @throws Exception
     */
    @Test
    public void testBug20391550() throws Exception {
        createTable("testBug20391550", "(c1 int,c2 varchar(100))");
        this.stmt.execute("insert into testBug20391550 values(100, 'strval')");

        Connection con = null;
        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.autoReconnect.getKeyName(), "true");
            props.setProperty(PropertyKey.enableQueryTimeouts.getKeyName(), "true");
            props.setProperty(PropertyKey.queryTimeoutKillsConnection.getKeyName(), "true");
            con = getConnectionWithProps(props);

            Statement st = con.createStatement();
            st.setQueryTimeout(2);

            PreparedStatement ps = con.prepareStatement("update testBug20391550 set c2=? where c1=?");
            assertThrows(SQLException.class, "Statement cancelled due to timeout or client request", () -> st.executeQuery("select sleep(180)"));

            TimeUnit.SECONDS.sleep(2); // Make sure the connection gets time to be closed since the cancellation is happening on another thread.

            assertThrows(SQLException.class, "No operations allowed after statement closed.", () -> {
                ps.setInt(2, 100);
                return null;
            });
            assertThrows(SQLException.class, "No operations allowed after statement closed.", () -> {
                ps.setString(1, "NewData");
                return null;
            });
            assertThrows(SQLException.class, "No operations allowed after statement closed.", () -> {
                ps.executeUpdate();
                return null;
            });
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#103303 (32766143), JAVA.LANG.CLASSCASTEXCEPTION WHEN INSERTING BLOB WITH SERVER PREPARED STATEMENT.
     *
     * @throws Exception
     */
    @Test
    public void testBug103303() throws Exception {
        createTable("testBug103303", "(id INT AUTO_INCREMENT PRIMARY KEY, blob1 MEDIUMBLOB)");

        Connection con = null;
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        for (boolean rewriteBS : new boolean[] { true, false }) {
            for (boolean useSSPS : new boolean[] { true, false }) {
                props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "" + rewriteBS);
                props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSSPS);

                try {
                    this.stmt.executeUpdate("truncate table testBug103303");

                    con = getConnectionWithProps(props);

                    PreparedStatement statement = con.prepareStatement("INSERT INTO testBug103303(blob1) VALUES(?)", Statement.RETURN_GENERATED_KEYS);
                    statement.setBlob(1, new SerialBlob("test1".getBytes()));
                    statement.addBatch();
                    statement.setBlob(1, new SerialBlob("test2".getBytes()));
                    statement.addBatch();
                    statement.executeBatch();

                    this.rs = this.stmt.executeQuery("select blob1 from testBug103303 order by id");
                    assertTrue(this.rs.next());
                    assertEquals("test1", this.rs.getString(1));
                    assertTrue(this.rs.next());
                    assertEquals("test2", this.rs.getString(1));
                    assertFalse(this.rs.next());

                } finally {
                    if (con != null) {
                        con.close();
                    }
                }
            }
        }
    }

    /**
     * Tests fix for Bug#103796 (32922715), CONNECTOR/J 8 STMT SETQUERYTIMEOUT CAN NOT WORK.
     *
     * @throws Exception
     */
    @Test
    public void testBug103796() throws Exception {
        Connection con = null;
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useCursorFetch.getKeyName(), "true");

        try {
            con = getConnectionWithProps(props);
            Statement timeoutStmt = con.createStatement();
            timeoutStmt.setFetchSize(10);
            timeoutStmt.setQueryTimeout(2);

            long begin = System.currentTimeMillis();

            try {
                timeoutStmt.execute("SELECT SLEEP(5)");
                fail("Query didn't time out");
            } catch (MySQLTimeoutException timeoutEx) {
                long duration = System.currentTimeMillis() - begin;
                assertTrue(duration > 1000 && duration < 3000);
            }
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#103878 (32954449), CONNECTOR/J 8 : QUERY WITH 'SHOW XXX' WILL GET EXCEPTION WHEN USE CURSOR.
     *
     * @throws Exception
     */
    @Test
    public void testBug103878() throws Exception {
        createTable("testBug103878", "(`fid` bigint(20) DEFAULT NULL, `ftext` longtext COLLATE utf8mb4_unicode_ci)"
                + " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
        createView("testBug103878_view", "AS SELECT * FROM testBug103878 WHERE fid > 1");

        Connection con = null;
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useCursorFetch.getKeyName(), "true");
        props.setProperty(PropertyKey.defaultFetchSize.getKeyName(), "100");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");

        try {
            con = getConnectionWithProps(props);
            PreparedStatement ps = con.prepareStatement("SHOW CREATE TABLE testBug103878");
            this.rs = ps.executeQuery();
            assertTrue(this.rs.next());
            assertTrue(StringUtils.startsWithIgnoreCase(this.rs.getString(2), "CREATE TABLE"));
            ps.close();

            ps = con.prepareStatement("SHOW CREATE VIEW testBug103878_view");
            this.rs = ps.executeQuery();
            assertTrue(this.rs.next());
            assertTrue(StringUtils.startsWithIgnoreCase(this.rs.getString(2), "CREATE"));
            assertTrue(StringUtils.indexOfIgnoreCase(this.rs.getString(2), "testBug103878_view") >= 0);
            ps.close();

            ps = con.prepareStatement("SHOW PROCESSLIST");
            this.rs = ps.executeQuery();
            assertTrue(this.rs.next());
            ps.close();

            if (versionMeetsMinimum(5, 7)) {
                createUser("'testBug103878User'@'%'", "IDENTIFIED BY 'abc'");

                ps = con.prepareStatement("SHOW CREATE USER testBug103878User");
                this.rs = ps.executeQuery();
                assertTrue(this.rs.next());
                assertTrue(StringUtils.startsWithIgnoreCase(this.rs.getString(1), "CREATE USER"));
                ps.close();
            }
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#23204652, CURSOR POSITIONING API'S DOESNOT CHECK THE VALIDITY OF RESULTSET.
     *
     * @throws Exception
     *
     * @see StatementsTest#testResultSetProducingQueries()
     */
    @Test
    public void testBug23204652() throws Exception {
        assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.",
                () -> this.stmt.executeQuery("DO 1 + 2"));
    }

    /**
     * Tests fix for Bug#71929 (18346501), Prefixing query with double comments cancels query DML validation.
     *
     * @throws Exception
     */
    @Test
    public void testBug71929() throws Exception {
        String[] queries = new String[] { "CREATE TABLE testBug71929 (id INT)", "/* comments */CREATE TABLE testBug71929 (id INT)",
                "/* comments *//* more comments */CREATE TABLE testBug71929 (id INT)" };
        for (String query : queries) {
            assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.",
                    () -> this.stmt.executeQuery(query));
        }

        queries = new String[] { "SELECT 1", "/* comments */SELECT 1", "/* comments *//* more comments */SELECT 1" };
        for (String query : queries) {
            assertThrows(SQLException.class,
                    "Statement\\.executeUpdate\\(\\) or Statement\\.executeLargeUpdate\\(\\) cannot issue statements that produce result sets\\.",
                    () -> this.stmt.executeUpdate(query));
        }
    }

    /**
     * Tests fix for Bug#103612 (32902019), Incorrectly identified WITH...SELECT as unsafe for read-only connections.
     *
     * @throws Exception
     *
     * @see StatementsTest#testReadOnlySafeStatements()
     */
    @Test
    public void testBug103612() throws Exception {
        assumeTrue(versionMeetsMinimum(8, 0, 1), "MySQL 8.0.1+ is required to run this test.");

        createTable("testBug103612", "(id INT)");
        String query = "WITH cte AS (SELECT * FROM testBug103612) SELECT * FROM cte";

        boolean useSPS = false;
        boolean readOnly = false;
        do {
            final String testCase = String.format("Case [SPS: %s, ReadOnly: %s]", useSPS ? "Y" : "N", readOnly ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            Connection testConn = getConnectionWithProps(props);
            testConn.setReadOnly(readOnly);

            try (Statement testStmt = testConn.createStatement()) {
                assertTrue(testStmt.execute(query), testCase);
            }
            try (PreparedStatement testPstmt = testConn.prepareStatement(query)) {
                assertTrue(testPstmt.execute(), testCase);
            }

            testConn.close();
        } while ((useSPS = !useSPS) || (readOnly = !readOnly));
    }

    /**
     * Tests fix for Bug#101389 (32089018), GETWARNINGS SHOULD CHECK WARNING COUNT BEFORE SENDING SHOW.
     *
     * @throws Exception
     */
    @Test
    public void testBug101389() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.queryInterceptors.getKeyName(), Bug101389QueryInterceptor.class.getName());

        boolean useSPS = false;
        do {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            Bug101389QueryInterceptor.enabled = false; // some warnings are expected here when running against old server versions
            java.sql.Connection testConn = getConnectionWithProps(props);

            Bug101389QueryInterceptor.enabled = true;
            Statement st = testConn.createStatement();
            st.executeQuery("SELECT 1 FROM dual;");
            st.getWarnings();

            PreparedStatement ps = testConn.prepareStatement("SELECT 1 FROM dual;");
            ps.execute();
            ps.getWarnings();

            testConn.close();
        } while (useSPS = !useSPS);
    }

    public static class Bug101389QueryInterceptor extends BaseQueryInterceptor {

        public static boolean enabled = false;

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            if (enabled) {
                assertFalse(sql.get().contains("SHOW WARNINGS"), "Unexpected [SHOW WARNINGS] was issued");
            }
            return super.preProcess(sql, interceptedQuery);
        }

        @Override
        public <M extends Message> M preProcess(M queryPacket) {
            if (enabled) {
                String sql = StringUtils.toString(queryPacket.getByteBuffer(), 1, queryPacket.getPosition() - 1);
                assertFalse(sql.contains("SHOW WARNINGS"), "Unexpected [SHOW WARNINGS] was issued");
            }
            return super.preProcess(queryPacket);
        }

    }

    @Test
    public void testBlobWithSJIS() throws Exception {
        createTable("testBlobWithSJIS", "(a SERIAL, b BLOB)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        String sqlMode = getMysqlVariable("sql_mode");
        sqlMode = removeSqlMode("ANSI_QUOTES", sqlMode);
        sqlMode = removeSqlMode("NO_BACKSLASH_ESCAPES", sqlMode);
        if (sqlMode.length() > 0) {
            sqlMode += ",";
        }

        byte[] data1 = new byte[20];
        data1[0] = -21;
        data1[1] = '\'';
        data1[2] = '"';
        data1[3] = '\b';
        data1[4] = '\n';
        data1[5] = '\r';
        data1[6] = '\t';
        data1[7] = 26; // \Z ASCII 26 (Control+Z)
        data1[8] = '\\';
        data1[9] = '%'; // \% A % character; see note following the table
        data1[10] = '_'; // \_ A _ character; see note following the table
        data1[11] = 39; // \'

        for (boolean useSSPS : new boolean[] { false, true }) {
            for (String mode : new String[] { "", "ANSI_QUOTES", "NO_BACKSLASH_ESCAPES", "ANSI_QUOTES,NO_BACKSLASH_ESCAPES" }) {
                for (String enc : new String[] { "SJIS", "UTF-8" }) {
                    props.setProperty(PropertyKey.characterEncoding.getKeyName(), enc);
                    props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSSPS);
                    props.setProperty(PropertyKey.sessionVariables.getKeyName(), "sql_mode='" + sqlMode + mode + "'");

                    Connection con = getConnectionWithProps(props);
                    Statement st = con.createStatement();

                    st.executeUpdate("truncate table testBlobWithSJIS");

                    PreparedStatement ps1 = con.prepareStatement("INSERT INTO testBlobWithSJIS (b) VALUES(?)");
                    Blob blob = con.createBlob();
                    blob.setBytes(1, data1);
                    ps1.setBlob(1, blob);
                    ps1.executeUpdate();

                    this.rs = st.executeQuery("SELECT b FROM testBlobWithSJIS");
                    assertTrue(this.rs.next());
                    byte[] data2 = this.rs.getBytes(1);
                    assertTrue(Arrays.equals(data1, data2));
                    assertFalse(this.rs.next());

                    con.close();
                }
            }
        }
    }

    /**
     * Tests fix for Bug#105211 (33468860), class java.time.LocalDate cannot be cast to class java.sql.Date.
     *
     * @throws Exception
     */
    @Test
    public void testBug105211() throws Exception {
        createTable("testBug105211", "(dt date)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");

        boolean useSPS = false;
        do {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            Connection con = getConnectionWithProps(props);
            this.pstmt = con.prepareStatement("insert into testBug105211(dt) values(?)");
            con.setAutoCommit(false);

            for (int i = 0; i <= 1000; i++) {
                this.pstmt.setObject(1, LocalDate.now());
                this.pstmt.addBatch();
                if (i % 100 == 0) {
                    this.pstmt.executeBatch();
                    this.pstmt.clearBatch();
                }
            }
            con.commit();

            con.close();
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#84365 (33425867), INSERT..VALUE with VALUES function lead to a StringIndexOutOfBoundsException.
     *
     * @throws Exception
     */
    @Test
    public void testBug84365() throws Exception {
        createTable("testBug84365", "(id int(11) NOT NULL, name varchar(25) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=latin1");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");

        for (boolean useSSPS : new boolean[] { false, true }) {

            this.stmt.executeUpdate("truncate table testBug84365");

            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSSPS);
            Connection testConn = getConnectionWithProps(props);

            PreparedStatement st = testConn.prepareStatement("insert into testBug84365(id, name) VALUES(?,?) on duplicate key update id = values(id) + 1");
            st.setInt(1, 1);
            st.setString(2, "Name1");
            st.execute();
            st.close();

            st = testConn.prepareStatement("insert into testBug84365(id, name) VALUE(?,?) on duplicate key update id = values(id) + 1");
            st.setInt(1, 2);
            st.setString(2, "Name2");
            st.execute();
            st.close();

            st = testConn.prepareStatement("insert into testBug84365 set id = 2 on duplicate key update id = values(id) + 1");
            st.execute();

            this.rs = testConn.createStatement().executeQuery("select * from testBug84365 order by id");
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt("id"));
            assertEquals("Name1", this.rs.getString("name"));
            assertTrue(this.rs.next());
            assertEquals(3, this.rs.getInt("id"));
            assertEquals("Name2", this.rs.getString("name"));
            assertFalse(this.rs.next());
        }
    }

    /**
     * Tests fix for Bug#85223 (25656020), MYSQLSQLXML SETSTRING CRASH.
     *
     * @throws Exception
     */
    @Test
    public void testBug85223() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        String elementString = "<Person ID=\"1\"><PersonType>M</PersonType><FirstName>FName</FirstName><LastName>LName</LastName></Person>";
        Connection con = getConnectionWithProps(props);
        PreparedStatement pst = con.prepareStatement("SELECT ExtractValue(?,?) as val1");
        SQLXML xml = con.createSQLXML();
        xml.setString(elementString);
        pst.setSQLXML(1, xml);
        pst.setString(2, "/Person/LastName");
        pst.execute();
        this.rs = pst.getResultSet();
        this.rs.next();
        String value = this.rs.getString(1);
        assertEquals("LName", value);
    }

    /**
     * Tests fix for Bug#96900 (30355150), STATEMENT.CANCEL()CREATE A DATABASE CONNECTION BUT DOES NOT CLOSE THE CONNECTION.
     *
     * @throws Exception
     */
    @Test
    public void testBug96900() throws Exception {
        assumeTrue("ON".equalsIgnoreCase(getMysqlVariable("performance_schema")), "PERFORMANCE_SCHEMA must be enabled to run this this.");
        assumeTrue(versionMeetsMinimum(5, 7), "MySQL 5.7+ is required to run this test.");

        Supplier<Integer> sessionCount = () -> {
            try {
                this.rs = this.stmt.executeQuery("SELECT COUNT(*) FROM performance_schema.threads WHERE TYPE='FOREGROUND' AND CONNECTION_TYPE='TCP/IP'");
                this.rs.next();
                return this.rs.getInt(1);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        };

        int initialSessionCount = sessionCount.get();

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());

        Connection testConn = getConnectionWithProps(props);
        Statement testStmt = testConn.createStatement();

        new Thread(() -> {
            try {
                testStmt.executeQuery("SELECT SLEEP(60)");
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }).start();

        // The difference between the `initialSessionCount` and the current session count would be greater than one if connections external to this test are
        // created in between. Chances for this to happen in a controlled or development environment are very low and can be neglected.
        assertEquals(1, sessionCount.get() - initialSessionCount);

        testStmt.cancel();
        assertEquals(1, sessionCount.get() - initialSessionCount);

        testConn.close();
        assertEquals(0, sessionCount.get() - initialSessionCount);
    }

    /**
     * Tests fix for Bug#99260 (31189960), statement.setQueryTimeout,creates a database connection and does not close.
     *
     * @throws Exception
     */
    @Test
    public void testBug99260() throws Exception {
        assumeTrue("ON".equalsIgnoreCase(getMysqlVariable("performance_schema")), "PERFORMANCE_SCHEMA must be enabled to run this this.");
        assumeTrue(versionMeetsMinimum(5, 7), "MySQL 5.7+ is required to run this test.");

        Supplier<Integer> sessionCount = () -> {
            try {
                this.rs = this.stmt.executeQuery("SELECT COUNT(*) FROM performance_schema.threads WHERE TYPE='FOREGROUND' AND CONNECTION_TYPE='TCP/IP'");
                this.rs.next();
                return this.rs.getInt(1);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        };

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        Connection testConn = getConnectionWithProps(props);
        Statement testStmt = testConn.createStatement();
        testStmt.setQueryTimeout(1);
        int initialSessionCount = sessionCount.get();
        for (int i = 0; i < 5; i++) {
            assertThrows(MySQLTimeoutException.class, "Statement cancelled due to timeout or client request", () -> {
                testStmt.executeQuery("SELECT SLEEP(30)");
                return null;
            });
            // The difference between the `initialSessionCount` and the current session count would be greater than one if connections external to this test are
            // created in between. Chances for this to happen in a controlled or development environment are very low and can be neglected.
            assertTrue(sessionCount.get() - initialSessionCount < 1);
        }

        testConn.close();
    }

    /**
     * Tests fix for Bug#85317 (25672958), EXECUTE BATCH WILL THROW NULL POINTER EXCEPTION WHERE THE COLUMN IS BLOB!
     *
     * @throws Exception
     */
    @Test
    public void testBug85317() throws Exception {
        createTable("testBug85317", "(k varchar(127) not null, v blob not null, primary key(k)) default charset utf8");

        Properties props = new Properties();
        props.setProperty(PropertyKey.useSSL.getKeyName(), "false");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        byte[] ba = "abcdefg".getBytes();

        boolean useSPS = false;
        boolean rewriteBS = false;
        do {
            this.stmt.executeUpdate("truncate table testBug85317");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSPS);
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "" + rewriteBS);

            Connection con = getConnectionWithProps(props);
            con.setAutoCommit(false);
            PreparedStatement ps = con.prepareStatement("replace into testBug85317(k,v) values(?,?)");
            for (int i = 1; i < 10000; i++) {
                ps.setString(1, String.valueOf(i));
                ps.setBlob(2, new ByteArrayInputStream(ba));
                ps.addBatch();
            }
            ps.executeBatch(); // was throwing NPE when useServerPrepStmts=false
            ps.close();
            con.commit();

            int cnt = 0;
            this.rs = this.stmt.executeQuery("select * from testBug85317");
            while (this.rs.next()) {
                cnt++;
                assertTrue(Arrays.equals(ba, this.rs.getBytes(2)));
            }
            assertEquals(9999, cnt);

        } while ((useSPS = !useSPS) || (rewriteBS = !rewriteBS));
    }

    /**
     * Tests fix for Bug#104349 (33563548), com.mysql.cj NPE.
     *
     * @throws Exception
     */
    @Test
    public void testBug104349() throws Exception {
        createTable("testBug104349", "(col1 INT, col2 VARCHAR(100))");

        this.stmt.executeUpdate("INSERT INTO testBug104349 VALUES (1, 'key 1')");
        this.stmt.executeUpdate("INSERT INTO testBug104349 VALUES (2, 'key 2')");
        this.stmt.executeUpdate("INSERT INTO testBug104349 VALUES (3, 'key 3')");

        Properties props = new Properties();
        props.setProperty(PropertyKey.useSSL.getKeyName(), "false");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");

        boolean useSPS = false;
        do {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSPS);
            Connection testConn = getConnectionWithProps(props);

            PreparedStatement ps = testConn.prepareStatement("SELECT * FROM testBug104349 WHERE col1 = ?");
            assertTrue(ps.toString().endsWith("SELECT * FROM testBug104349 WHERE col1 = ** NOT SPECIFIED **"));
            ps.setInt(1, 1);
            assertTrue(ps.toString().endsWith("SELECT * FROM testBug104349 WHERE col1 = 1"));
            this.rs = ps.executeQuery();
            assertTrue(ps.toString().endsWith("SELECT * FROM testBug104349 WHERE col1 = 1"));
            while (this.rs.next()) {
                assertEquals(1, this.rs.getInt(1));
                assertEquals("key 1", this.rs.getString(2));
            }
            ps.close();
            ps = testConn.prepareStatement("SELECT * FROM testBug104349 WHERE col1 = ?");
            assertTrue(ps.toString().endsWith("SELECT * FROM testBug104349 WHERE col1 = ** NOT SPECIFIED **"));
            ps.setInt(1, 2);
            assertTrue(ps.toString().endsWith("SELECT * FROM testBug104349 WHERE col1 = 2"));
            this.rs = ps.executeQuery();
            assertTrue(ps.toString().endsWith("SELECT * FROM testBug104349 WHERE col1 = 2"));
            while (this.rs.next()) {
                assertEquals(2, this.rs.getInt(1));
                assertEquals("key 2", this.rs.getString(2));
            }
            testConn.close();

        } while (useSPS = !useSPS);
    }

    /**
     * Tests for Bug#62006 (16714956), JAVA.IO.NOTSERIALIZABLEEXCEPTION: JAVA.IO.STRINGREADER WHEN PROFILESQL=TRUE.
     *
     * @throws Exception
     */
    @Test
    public void testBug62006() throws Exception {
        createTable("testBug62006", "(f1 longtext)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.profileSQL.getKeyName(), "true");

        boolean useSPS = false;
        do {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSPS);
            Connection testConn = getConnectionWithProps(props);
            this.pstmt = testConn.prepareStatement("insert into testBug62006 values(?)");
            this.pstmt.setObject(1, new StringReader("test"), java.sql.Types.LONGVARCHAR, 0);
            this.pstmt.execute();
            this.pstmt.setObject(1, new StringReader("test"), java.sql.Types.LONGVARCHAR, 1);
            this.pstmt.execute();
            this.pstmt.setObject(1, new StringReader("test"));
            this.pstmt.execute();
            testConn.close();
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#106240 (33781440), StringIndexOutOfBoundsException when VALUE is at the end of the query.
     *
     * @throws Exception
     */
    @Test
    public void testBug106240() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");

        boolean useSPS = false;
        do {
            String testCase = String.format("Case: [ useSPS=%s ]", useSPS ? "Y" : "N");

            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            Connection testConn = getConnectionWithProps(props);

            assertThrows(testCase, SQLException.class,
                    "You have an error in your SQL syntax; check the manual that corresponds to your "
                            + "MySQL server version for the right syntax to use near '' at line 1",
                    () -> testConn.prepareStatement("INSERT INTO testBug106240 VALUES").execute());
            assertThrows(testCase, SQLException.class,
                    "You have an error in your SQL syntax; check the manual that corresponds to your "
                            + "MySQL server version for the right syntax to use near '' at line 1",
                    () -> testConn.prepareStatement("INSERT INTO testBug106240 VALUES   ").execute());
            assertThrows(testCase, SQLException.class,
                    "You have an error in your SQL syntax; check the manual that corresponds to your "
                            + "MySQL server version for the right syntax to use near '' at line 1",
                    () -> testConn.prepareStatement("INSERT INTO testBug106240 VALUES\n").execute());
            assertThrows(testCase, SQLException.class,
                    "You have an error in your SQL syntax; check the manual that corresponds to your "
                            + "MySQL server version for the right syntax to use near '' at line 1",
                    () -> testConn.prepareStatement("INSERT INTO testBug106240 VALUES/* (?, ?) */").execute());
            assertThrows(testCase, SQLException.class,
                    "You have an error in your SQL syntax; check the manual that corresponds to your "
                            + "MySQL server version for the right syntax to use near '' at line 1",
                    () -> testConn.prepareStatement("INSERT INTO testBug106240 VALUES -- (?, ?) ON DUPLICATE KEY UPDATE").execute());
            assertThrows(testCase, SQLException.class,
                    "You have an error in your SQL syntax; check the manual that corresponds to your "
                            + "MySQL server version for the right syntax to use near 'AS tb106240\\(a, b\\)' at line 1",
                    () -> testConn.prepareStatement("INSERT INTO testBug106240 VALUES/* (?, ?) */AS tb106240(a, b)").execute());
            assertThrows(testCase, SQLException.class,
                    "You have an error in your SQL syntax; check the manual that corresponds to your "
                            + "MySQL server version for the right syntax to use near 'ON DUPLICATE KEY UPDATE' at line 1",
                    () -> testConn.prepareStatement("INSERT INTO testBug106240 VALUES/* (?, ?) */ON DUPLICATE KEY UPDATE").execute());
        } while (useSPS = !useSPS);
    }

    /**
     * Tests for Bug#81468 (23312764), MySQL server fails to rewrite batch insert when column name contains word select.
     * Resolved by fix for Bug#106240 (33781440), StringIndexOutOfBoundsException when VALUE is at the end of the query.
     *
     * @throws Exception
     */
    @Test
    public void testBug81468() throws Exception {
        createTable("testBug81468", "(selection INT)"); // Column has to contain "select" in its name.

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.queryInterceptors.getKeyName(), TestBug81468QueryInterceptor.class.getName());
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        props.setProperty(PropertyKey.continueBatchOnError.getKeyName(), "false");
        props.setProperty(PropertyKey.emulateUnsupportedPstmts.getKeyName(), "true");

        boolean useSPS = false;

        do {
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            Connection testConn = getConnectionWithProps(props);

            // Re-writes in a single query with three values.
            TestBug81468QueryInterceptor.resetExpectedValues(1, 1, 3);
            PreparedStatement testPStmt = testConn.prepareStatement("INSERT INTO testBug81468 (selection) VALUES (?)");
            testPStmt.setInt(1, 11);
            testPStmt.addBatch();
            testPStmt.setInt(1, 12);
            testPStmt.addBatch();
            testPStmt.setInt(1, 13);
            testPStmt.addBatch();
            assertArrayEquals(new int[] { Statement.SUCCESS_NO_INFO, Statement.SUCCESS_NO_INFO, Statement.SUCCESS_NO_INFO }, testPStmt.executeBatch());
            testPStmt.close();

            // Re-writes in a single query with five values.
            TestBug81468QueryInterceptor.resetExpectedValues(1, 1, 5);
            testPStmt = testConn.prepareStatement("INSERT INTO testBug81468 (selection) VALUES (?)");
            testPStmt.setInt(1, 21);
            testPStmt.addBatch();
            testPStmt.setInt(1, 22);
            testPStmt.addBatch();
            testPStmt.setInt(1, 23);
            testPStmt.addBatch();
            testPStmt.setInt(1, 24);
            testPStmt.addBatch();
            testPStmt.setInt(1, 25);
            testPStmt.addBatch();
            assertArrayEquals(new int[] { Statement.SUCCESS_NO_INFO, Statement.SUCCESS_NO_INFO, Statement.SUCCESS_NO_INFO, Statement.SUCCESS_NO_INFO,
                    Statement.SUCCESS_NO_INFO }, testPStmt.executeBatch());
            testPStmt.close();

            // Not enough queries to trigger the query re-write feature.
            TestBug81468QueryInterceptor.resetExpectedValues(3, 1, 1);
            Statement testStmt = testConn.createStatement();
            testStmt.addBatch("INSERT INTO testBug81468 (selection) VALUES (31)");
            testStmt.addBatch("INSERT INTO testBug81468 (selection) VALUES (32)");
            testStmt.addBatch("INSERT INTO testBug81468 (selection) VALUES (33)");
            assertArrayEquals(new int[] { 1, 1, 1 }, testStmt.executeBatch());
            testStmt.close();

            // Five or more queries triggers the query re-write feature.
            TestBug81468QueryInterceptor.resetExpectedValues(1, 5, 5);
            testStmt = testConn.createStatement();
            testStmt.addBatch("INSERT INTO testBug81468 (selection) VALUES (41)");
            testStmt.addBatch("INSERT INTO testBug81468 (selection) VALUES (42)");
            testStmt.addBatch("INSERT INTO testBug81468 (selection) VALUES (43)");
            testStmt.addBatch("INSERT INTO testBug81468 (selection) VALUES (44)");
            testStmt.addBatch("INSERT INTO testBug81468 (selection) VALUES (45)");
            assertArrayEquals(new int[] { 1, 1, 1, 1, 1 }, testStmt.executeBatch());
            testStmt.close();

            testConn.close();
        } while (useSPS = !useSPS);
    }

    public static class TestBug81468QueryInterceptor extends BaseQueryInterceptor {

        private static int expectedNumberOfExecutions = 0;
        private static int expectedNumberOfQueries = 0;
        private static int expectedNumberOfValues = 0;
        private static int executionsCount = 0;

        static void resetExpectedValues(int expExecs, int expQueries, int expVals) {
            expectedNumberOfExecutions = expExecs;
            expectedNumberOfQueries = expQueries;
            expectedNumberOfValues = expVals;
            executionsCount = 0;
        }

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            String query = sql.get();
            if (query.contains("testBug81468")) {
                assertTrue(++executionsCount <= expectedNumberOfExecutions,
                        "Too many statement executions. The query [" + query + "] should have been re-written");
                assertEquals(expectedNumberOfQueries, countQueries(query), "Wrong number of queries in [" + query + "]");
                assertEquals(expectedNumberOfValues, countValues(query));
            }
            return super.preProcess(sql, interceptedQuery);
        }

        private int countQueries(String query) {
            return query.split(";").length;
        }

        private int countValues(String query) {
            query += "/* junk */"; // Add some junk to the end of the query so that the following split allows counting the values correctly.
            if (query.contains("?")) {
                return query.split("\\(\\?\\)").length - 1;
            }
            return query.split("\\(\\d{2}\\)").length - 1;
        }

    }

    /**
     * Tests for Bug#21978230, COMMENT PARSING NOT PROPER IN PREPSTMT.EXECUTEBATCH().
     *
     * @throws Exception
     */
    @Test
    public void testBug21978230() throws Exception {
        createTable("testBug21978230", "(c1 INT, c2 INT, t VARCHAR(100))");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        props.setProperty(PropertyKey.continueBatchOnError.getKeyName(), "false");
        props.setProperty(PropertyKey.emulateUnsupportedPstmts.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        Consumer<String> runQueryAndAssertResults = query -> {
            try {
                this.pstmt = testConn.prepareStatement(query);
                this.pstmt.setInt(1, 1);
                this.pstmt.setInt(2, 10);
                this.pstmt.setString(3, "A");
                this.pstmt.setString(4, "A");
                this.pstmt.addBatch();
                this.pstmt.setInt(1, 2);
                this.pstmt.setInt(2, 20);
                this.pstmt.setString(3, "B");
                this.pstmt.setString(4, "B");
                this.pstmt.addBatch();
                this.pstmt.setInt(1, 3);
                this.pstmt.setInt(2, 30);
                this.pstmt.setString(3, "C");
                this.pstmt.setString(4, "C");
                this.pstmt.addBatch();
                this.pstmt.setInt(1, 4);
                this.pstmt.setInt(2, 40);
                this.pstmt.setString(3, "D");
                this.pstmt.setString(4, "D");
                this.pstmt.addBatch();
                this.pstmt.executeBatch();

                this.rs = this.stmt.executeQuery("SELECT * FROM testBug21978230");
                for (int i = 1; i <= 4; i++) {
                    assertTrue(this.rs.next());
                    assertEquals(i, this.rs.getInt(1));
                    assertEquals(i * 10, this.rs.getInt(2));
                    char chr = (char) ('A' + i - 1);
                    assertEquals(chr + "^^^^" + chr, this.rs.getString(3));
                }
                assertFalse(this.rs.next());
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        };

        /*
         * Expected: "Parameter index out of range".
         */
        this.stmt.execute("TRUNCATE TABLE testBug21978230");
        Exception ex = assertThrows(RuntimeException.class, ".*Parameter index out of range \\(3 > number of parameters, which is 2\\)\\.", () -> {
            runQueryAndAssertResults.accept("REPLACE INTO testBug21978230 VALUES (?, ?, /*/ CONCAT(?, '^^^^', ?)) /*ON DUPLICATE KEY UPDATE v=concat(v,?)*/");
            return null;
        });
        assertEquals(ex.getCause().getClass(), SQLException.class);

        this.stmt.execute("TRUNCATE TABLE testBug21978230");
        ex = assertThrows(RuntimeException.class, ".*Parameter index out of range \\(3 > number of parameters, which is 2\\)\\.", () -> {
            runQueryAndAssertResults.accept("INSERT INTO testBug21978230 VALUES (?, ?, /*/ CONCAT(?, '^^^^', ?)) /*ON DUPLICATE KEY UPDATE v=concat(v,?)*/");
            return null;
        });
        assertEquals(ex.getCause().getClass(), SQLException.class);

        /*
         * Expected: 4 records inserted in each query
         */
        this.stmt.execute("TRUNCATE TABLE testBug21978230");
        runQueryAndAssertResults.accept("REPLACE INTO testBug21978230 VALUES (?, ?, /**/ CONCAT(?, '^^^^', ?)) /*ON DUPLICATE KEY UPDATE v=concat(v,?)*/");

        this.stmt.execute("TRUNCATE TABLE testBug21978230");
        runQueryAndAssertResults.accept("INSERT INTO testBug21978230 VALUES (?, ?, /**/ CONCAT(?, '^^^^', ?)) /*ON DUPLICATE KEY UPDATE v=concat(v,?)*/");

        this.stmt.execute("TRUNCATE TABLE testBug21978230");
        runQueryAndAssertResults.accept("REPLACE INTO testBug21978230 VALUES (?, ?, concat(?, '^^^^', ?)) /*ON DUPLICATE KEY UPDaTE v=concat(v,?)*/");

        this.stmt.execute("TRUNCATE TABLE testBug21978230");
        runQueryAndAssertResults.accept("INSERT INTO testBug21978230 VALUES (?, ?, concat(?, '^^^^', ?)) /*ON DUPLICATE KEY UPDaTE v=concat(v,?)*/");

        this.stmt.execute("TRUNCATE TABLE testBug21978230");
        runQueryAndAssertResults.accept("REPLACE INTO testBug21978230 VALUES(?, ?, /* comment */CONCAT(?, '^^^^', ?))");

        this.stmt.execute("TRUNCATE TABLE testBug21978230");
        runQueryAndAssertResults.accept("INSERT INTO testBug21978230 VALUES(?, ?, /* comment */CONCAT(?, '^^^^', ?))");
    }

    /**
     * Tests for Bug#76623 (20856749), JDBC driver misinterprets "--" as a comment, instead of "-- ".
     *
     * Fixed in Connector/J 8.0.27 under fix for Bug#71929 (18346501), Prefixing query with double comments cancels query DML validation.
     *
     * @throws Exception
     */
    @Test
    public void testBug76623() throws Exception {
        this.rs = this.stmt.executeQuery("SELECT 1--2");
        assertTrue(this.rs.next());
        assertEquals(3, this.rs.getInt(1));

        this.rs = this.stmt.executeQuery("SELECT 1 --2");
        assertTrue(this.rs.next());
        assertEquals(3, this.rs.getInt(1));

        this.rs = this.stmt.executeQuery("SELECT 1 - -2");
        assertTrue(this.rs.next());
        assertEquals(3, this.rs.getInt(1));

        this.rs = this.stmt.executeQuery("SELECT 1-- 2");
        assertTrue(this.rs.next());
        assertEquals(1, this.rs.getInt(1));

        this.rs = this.stmt.executeQuery("SELECT 1 -- 2");
        assertTrue(this.rs.next());
        assertEquals(1, this.rs.getInt(1));

        boolean useSPS = false;
        do {
            String expectedErrMsg = useSPS ? "No parameters specified during prepareStatement\\(\\) or prepareCall\\(\\)"
                    : "Parameter index out of range \\(1 > number of parameters, which is 0\\)\\.";

            Properties props = new Properties();
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            Connection testConn = getConnectionWithProps(props);

            this.pstmt = testConn.prepareStatement("SELECT 1--2 + ?");
            this.pstmt.setInt(1, 3);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(6, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 --2 + ?");
            this.pstmt.setInt(1, 3);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(6, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 - -2 + ?");
            this.pstmt.setInt(1, 3);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(6, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1-- 2 + ?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 -- 2 + ?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 -- 2--?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 -- 2 --?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 -- 2 - -?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 -- 2-- ?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 -- 2 -- ?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 - -2--?");
            this.pstmt.setInt(1, 3);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(6, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 - -2 --?");
            this.pstmt.setInt(1, 3);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(6, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 - -2 - -?");
            this.pstmt.setInt(1, 3);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(6, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 - -2-- ?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(3, this.rs.getInt(1));

            this.pstmt = testConn.prepareStatement("SELECT 1 - -2 -- ?");
            assertThrows(SQLException.class, expectedErrMsg, () -> {
                this.pstmt.setInt(1, 3);
                return null;
            });
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(3, this.rs.getInt(1));

            testConn.close();
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#104753 (Bug#33286177), PreparedStatement.setFetchSize(0) causes ArrayIndexOutOfBoundsException.
     *
     * @throws Exception
     */
    @Test
    public void testBug104753() throws Exception {
        createTable("testBug104753", "(id BIGINT NOT NULL, PRIMARY KEY (id))");

        Consumer<Integer> runQuery = f -> { // fetchSize
            try {
                this.pstmt.setFetchSize(f);
                this.pstmt.execute();
                this.rs = this.pstmt.getResultSet();
                assertTrue(this.rs.next());
                assertEquals(1, this.rs.getInt(1));
            } catch (Throwable e) {
                fail("Exception not expected");
            }
        };

        this.stmt.executeUpdate("INSERT INTO testBug104753 VALUES (1)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.useCursorFetch.getKeyName(), "true");

        // With prepared statements cache, COM_STMT_RESET sent.
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        this.pstmt = testConn.prepareStatement("SELECT id FROM testBug104753");
        runQuery.accept(0);
        this.pstmt.close();
        this.pstmt = testConn.prepareStatement("SELECT id FROM testBug104753");
        runQuery.accept(1);
        this.pstmt.close();
        this.pstmt = testConn.prepareStatement("SELECT id FROM testBug104753");
        runQuery.accept(0);
        this.pstmt.close();
        this.pstmt = testConn.prepareStatement("SELECT id FROM testBug104753");
        runQuery.accept(0);
        this.pstmt.close();
        this.pstmt = testConn.prepareStatement("SELECT id FROM testBug104753");
        runQuery.accept(1);
        this.pstmt.close();

        testConn.close();

        // No prepared statements cache, no COM_STMT_RESET sent.
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "false");
        testConn = getConnectionWithProps(props);

        this.pstmt = testConn.prepareStatement("SELECT id FROM testBug104753");
        runQuery.accept(0);
        runQuery.accept(1);
        runQuery.accept(0);
        runQuery.accept(0);
        runQuery.accept(1);

        testConn.close();
    }

    /**
     * Tests fix for Bug#107222 (Bug#34150112), ClientPreparedStatement.toString() no longer interpolates byte arrays.
     *
     * @throws Exception
     */
    @Test
    public void testBug107222() throws Exception {
        boolean useSPS = false;
        boolean setMax = false;

        createTable("testBug107222", "(b1 VARBINARY(100), b2 VARBINARY(100), i1 INT)");
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            if (setMax) {
                props.setProperty(PropertyKey.maxByteArrayAsHex.getKeyName(), "10");
            }
            Connection testConn = getConnectionWithProps(props);

            final String testCase = String.format("Case [SPS: %s, SetMax: %s]", useSPS ? "Y" : "N", setMax ? "Y" : "N");
            String expectedInterpolation = setMax ? "** BYTE ARRAY DATA **" : "x'4d7953514c20436f6e6e6563746f722f4a'";

            this.pstmt = testConn.prepareStatement("INSERT INTO testBug107222 VALUES (?, ?, ?)");
            this.pstmt.setBytes(1, "MySQL Connector/J".getBytes());
            this.pstmt.setString(2, null);
            this.pstmt.setNull(3, Types.INTEGER);
            this.pstmt.execute();

            String sql = this.pstmt.unwrap(JdbcPreparedStatement.class).toString();
            int startPos = sql.indexOf("testBug107222");
            assertTrue(startPos > 0, testCase);
            int pos = sql.indexOf(expectedInterpolation, startPos);
            assertTrue(pos > 0, testCase);
            startPos = pos + expectedInterpolation.length();
            pos = sql.indexOf("NULL", startPos);
            assertTrue(pos > 0, testCase);
            startPos = pos + "NULL".length();
            pos = sql.indexOf("NULL", startPos);
            assertTrue(pos > 0, testCase);
            startPos = pos + "NULL".length();
            assertEquals(-1, sql.indexOf("NULL", startPos), testCase);

            this.pstmt = testConn.prepareStatement("SELECT ?, ?, ?");
            this.pstmt.setString(1, null);
            this.pstmt.setBytes(2, "MySQL Connector/J".getBytes());
            this.pstmt.setNull(3, Types.BOOLEAN);
            this.pstmt.executeQuery();

            sql = this.pstmt.unwrap(JdbcPreparedStatement.class).toString();
            startPos = sql.indexOf("SELECT");
            assertTrue(startPos > 0, testCase);
            pos = sql.indexOf("NULL", startPos);
            assertTrue(pos > 0, testCase);
            startPos = pos + "NULL".length();
            pos = sql.indexOf(expectedInterpolation, startPos);
            assertTrue(pos > 0, testCase);
            startPos = pos + expectedInterpolation.length();
            pos = sql.indexOf("NULL", startPos);
            assertTrue(pos > 0, testCase);
            startPos = pos + "NULL".length();
            assertEquals(-1, sql.indexOf("NULL", startPos), testCase);
        } while ((useSPS = !useSPS) && (setMax = !setMax));
    }

    /**
     * Tests fix for Bug#108419 (Bug#34578010), Contribution: Recognize "ON DUPLICATE KEY UPDATE" in "INSERT SET" Statement.
     *
     * @throws Exception
     */
    @Test
    public void testBug108419() throws Exception {
        createTable("testBug108419", "(id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, data1 VARCHAR(255) NOT NULL, data2 VARCHAR(255))");

        boolean useSPS = false;
        boolean rwBS = false;
        boolean noODKU = false;

        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));
            props.setProperty(PropertyKey.dontCheckOnDuplicateKeyUpdateInSQL.getKeyName(), Boolean.toString(noODKU));
            Connection testConn = getConnectionWithProps(props);

            final String testCase = String.format("Case [useSPS: %s, rwBS: %s, noODKU: %s]", useSPS ? "Y" : "N", rwBS ? "Y" : "N", noODKU ? "Y" : "N");

            this.pstmt = testConn.prepareStatement("INSERT INTO testBug108419 (id, data1) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS);
            this.pstmt.setInt(1, 1);
            this.pstmt.setString(2, "MySQL");
            this.pstmt.execute();
            this.rs = this.pstmt.getGeneratedKeys();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertFalse(this.rs.next(), testCase);

            this.pstmt = testConn.prepareStatement("INSERT INTO testBug108419 (id, data1) VALUES (?, ?) ON DUPLICATE KEY UPDATE data2 = VALUES(data1)",
                    Statement.RETURN_GENERATED_KEYS);
            this.pstmt.setInt(1, 1);
            this.pstmt.setString(2, "Connector/J");
            this.pstmt.execute();
            this.rs = this.pstmt.getGeneratedKeys();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            if (!rwBS && noODKU) { // Assume two keys were generated only when no query re-write and don't check for ODKU.
                assertTrue(this.rs.next(), testCase);
                assertEquals(2, this.rs.getInt(1), testCase);
            }
            assertFalse(this.rs.next(), testCase);

            this.pstmt = testConn.prepareStatement("INSERT INTO testBug108419 SET id = ?, data1 = ? ON DUPLICATE KEY UPDATE data2 = VALUES(data1)",
                    Statement.RETURN_GENERATED_KEYS);
            this.pstmt.setInt(1, 1);
            this.pstmt.setString(2, "MySQL Connector/J");
            this.pstmt.execute();
            this.rs = this.pstmt.getGeneratedKeys();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            if (!rwBS && noODKU) { // Assume two keys were generated only when no query re-write and don't check for ODKU.
                assertTrue(this.rs.next(), testCase);
                assertEquals(2, this.rs.getInt(1), testCase);
            }
            assertFalse(this.rs.next(), testCase);

            this.stmt.execute("TRUNCATE TABLE testBug108419");
        } while ((useSPS = !useSPS) || (rwBS = !rwBS) || (noODKU = !noODKU));
    }

    /**
     * Tests fix for Bug#106252 (Bug#33968169), Connector/J client hangs after prepare & execute process with old version server.
     *
     * @throws Exception
     */
    @Test
    public void testBug106252() throws Exception {
        boolean useSPS = false;

        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            Connection testConn = getConnectionWithProps(props);

            this.pstmt = testConn.prepareStatement("SELECT 1 + ? AS c1, 'MySQL' AS c2, ? AS c3");

            ResultSetMetaData md = this.pstmt.getMetaData();
            assertEquals(3, md.getColumnCount());
            assertEquals("c1", md.getColumnName(1));
            assertEquals("c2", md.getColumnName(2));
            assertEquals("c3", md.getColumnName(3));

            ParameterMetaData pmd = this.pstmt.getParameterMetaData();
            assertEquals(2, pmd.getParameterCount());
            if (useSPS) {
                // Prior to this fix one of the following was throwing a NullPointerException.
                // The values they return are not important for this test because different server versions return different metadata.
                pmd.getParameterClassName(1);
                pmd.getParameterTypeName(1);
                pmd.getParameterClassName(2);
                pmd.getParameterTypeName(2);
            } else {
                assertThrows(SQLException.class, () -> pmd.getParameterType(1));
                assertThrows(SQLException.class, () -> pmd.getParameterType(2));
            }

            this.pstmt.setInt(1, 5);
            this.pstmt.setString(2, "Connector/J");
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals(6, this.rs.getInt(1));
            assertEquals("MySQL", this.rs.getString(2));
            assertEquals("Connector/J", this.rs.getString(3));
            assertFalse(this.rs.next());

            testConn.close();
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#108414 (Bug#34578541), Malformed packet generation for `COM_STMT_EXECUTE`.
     *
     * @throws Exception
     */
    @Test
    public void testBug108414() throws Exception {
        boolean useSPS = false;
        boolean bitIsBool = false;
        boolean value = false;

        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), PropertyDefinitions.SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.transformedBitIsBoolean.getKeyName(), Boolean.toString(bitIsBool));

            final String testCase = String.format("Case [SPS: %s, BitIsBool: %s, Expected: %s]", useSPS ? "Y" : "N", bitIsBool ? "Y" : "N", value ? "Y" : "N");

            Connection testConn = getConnectionWithProps(props);
            this.pstmt = testConn.prepareStatement("SELECT ?, ?, ?, ?, ?");

            this.pstmt.setBoolean(1, value);
            this.pstmt.setObject(2, value);
            this.pstmt.setObject(3, value, MysqlType.BOOLEAN);
            this.pstmt.setObject(4, value, MysqlType.TINYINT);
            this.pstmt.setObject(5, value, MysqlType.BIT);

            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());

            assertEquals(value, this.rs.getBoolean(1), testCase);
            assertEquals(value, this.rs.getBoolean(2), testCase);
            assertEquals(value, this.rs.getBoolean(3), testCase);
            assertEquals(value, this.rs.getBoolean(4), testCase);
            assertEquals(value, this.rs.getBoolean(5), testCase);

            testConn.close();
        } while ((useSPS = !useSPS) || (bitIsBool = !bitIsBool) || (value = !value));
    }

    /**
     * Tests fix for Bug#108195 (Bug#34512212), Connector/J rejects UNION with CTE.
     *
     * @throws Exception
     */
    @Test
    public void testBug108195() throws Exception {
        assumeTrue(versionMeetsMinimum(8, 0), "MySQL 8.0+ is required to run this test.");

        this.rs = this.stmt.executeQuery("WITH cte AS (SELECT 1) (SELECT * FROM cte) UNION (SELECT 2)");
        assertTrue(this.rs.next());
        assertEquals(1, this.rs.getInt(1));
        assertTrue(this.rs.next());
        assertEquals(2, this.rs.getInt(1));
        assertFalse(this.rs.next());
    }

    /**
     * Tests fix for Bug#99604 (Bug#31612628), Add support to row alias on INSERT... ON DUPLICATE KEY UPDATE on batch mode.
     * Resolved by fix for Bug#106240 (33781440), StringIndexOutOfBoundsException when VALUE is at the end of the query.
     *
     * @throws Exception
     */
    @Test
    public void testBug99604() throws Exception {
        assumeTrue(versionMeetsMinimum(8, 0, 19), "MySQL 8.0.19+ is required to run this test.");

        createTable("testBug99604", "(id INT NOT NULL PRIMARY KEY, data VARCHAR(100))");

        boolean useSPS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            final String testCase = String.format("Case [SPS: %s]", useSPS ? "Y" : "N");

            Connection testConn = getConnectionWithProps(props);

            PreparedStatement testStmt = testConn.prepareStatement(
                    "INSERT INTO testBug99604 (id, data) VALUES (?, ?) AS new ON DUPLICATE KEY UPDATE testBug99604.data = CONCAT(testBug99604.data, new.data)");
            testStmt.setInt(1, 1);
            testStmt.setString(2, "MySQL ");
            testStmt.addBatch();
            testStmt.setInt(1, 1);
            testStmt.setString(2, "Connector/J ");
            testStmt.addBatch();
            testStmt.executeBatch();

            this.rs = this.stmt.executeQuery("SELECT * FROM testBug99604");
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertEquals("MySQL Connector/J ", this.rs.getString(2), testCase);
            assertFalse(this.rs.next(), testCase);

            this.stmt.execute("TRUNCATE TABLE testBug99604");

            testConn.close();
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#102520 (Bug#32476663), serverSideStatementCache ignores resultSetType.
     *
     * @throws Exception
     */
    @Test
    void testBug102520() throws Exception {
        createTable("testBug102520", "(id INT)");
        this.stmt.executeUpdate("INSERT INTO testBug102520 VALUES (1), (2), (3), (4), (5)");

        boolean useSPS = false;
        boolean cachePS = false;

        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), Boolean.toString(cachePS));

            final String testCase = String.format("Case [SPS: %s, CachePS: %s]", useSPS ? "Y" : "N", cachePS ? "Y" : "N");

            Connection testConn = getConnectionWithProps(props);

            PreparedStatement testStmt = testConn.prepareStatement("SELECT * FROM testBug102520"); // ResultSet.TYPE_FORWARD_ONLY by default.
            this.rs = testStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertThrows(testCase, SQLException.class, "Operation not allowed for a result set of type ResultSet.TYPE_FORWARD_ONLY\\.", () -> {
                this.rs.first();
                return null;
            });
            testStmt.close();

            testStmt = testConn.prepareStatement("SELECT * FROM testBug102520", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            this.rs = testStmt.executeQuery();
            assertTrue(this.rs.absolute(3), testCase);
            assertEquals(3, this.rs.getInt(1), testCase);
            testStmt.close();

            testStmt = testConn.prepareStatement("SELECT * FROM testBug102520");
            this.rs = testStmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertThrows(testCase, SQLException.class, "Operation not allowed for a result set of type ResultSet.TYPE_FORWARD_ONLY\\.", () -> {
                assertTrue(this.rs.absolute(3), testCase);
                return null;
            });
            testStmt.close();

            testConn.close();
        } while ((useSPS = !useSPS) || (cachePS = !cachePS));
    }

    /**
     * Tests fix for Bug#109243 (Bug#34852047), Judge whether the returned result set of the sql statement is incorrect.
     *
     * @throws Exception
     */
    @Test
    public void testBug109243() throws Exception {
        assumeTrue(versionMeetsMinimum(8, 0), "MySQL 8.0+ is required to run this test.");

        this.rs = this.stmt.executeQuery("WITH q1 AS (SELECT 1 v1),q2 AS (SELECT 2 v2) SELECT v1,v2 FROM q1,q2");
        assertTrue(this.rs.next());
        assertEquals(1, this.rs.getInt(1));
        assertEquals(2, this.rs.getInt(2));
        assertFalse(this.rs.next());
    }

    /**
     * Tests fix for Bug#77368 (Bug#21321849), "LOAD DATA LOCAL INFILE" doesn't work properly with relative paths.
     *
     * Testing this would require changing the value of "user.dir" which could cause other tests to fail. As such, only basic testing to verify that the default
     * relative paths work fine is done here.
     *
     * @throws Exception
     */
    @Test
    public void testBug77368() throws Exception {
        createTable("testBug77368", "(id INT, txt VARCHAR(100))");

        final String data = "1\tMySQL\n2\tConnector/J";
        final String fileName = "TestBug77368.tsv";

        Files.write(Paths.get(fileName), data.getBytes());

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.allowLoadLocalInfile.getKeyName(), "true");
            Connection testConn = getConnectionWithProps(props);
            Statement testStmt = testConn.createStatement();
            testStmt.execute("LOAD DATA LOCAL INFILE './" + fileName + "' INTO TABLE testBug77368");

            this.rs = this.stmt.executeQuery("SELECT * FROM testBug77368");
            assertTrue(this.rs.next());
            assertEquals(1, this.rs.getInt(1));
            assertEquals("MySQL", this.rs.getString(2));
            assertTrue(this.rs.next());
            assertEquals(2, this.rs.getInt(1));
            assertEquals("Connector/J", this.rs.getString(2));
            assertFalse(this.rs.next());
            testConn.close();
        } finally {
            try {
                Files.delete(Paths.get(fileName));
            } catch (FileNotFoundException e) {
            }
        }
    }

    /**
     * Tests fix for Bug#109864 (Bug#35034666), Connector/J 8.0.32 hangs on MySQL 5.5 with prepared statements.
     *
     * @throws Exception
     */
    @Test
    public void testBug109864() throws Exception {
        createTable("testBug109864", "(id INT)");

        boolean useSPS = false;

        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.socketTimeout.getKeyName(), "5000");

            Connection testConn = getConnectionWithProps(props);

            this.pstmt = testConn.prepareStatement("SELECT ?");  // Both column and parameter definition blocks in Prepare response.
            this.pstmt.setInt(1, 1);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertFalse(this.rs.next());
            this.pstmt.close();

            this.pstmt = testConn.prepareStatement("SELECT 1");  // No parameter definition block in Stmt Prepare response.
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next());
            assertFalse(this.rs.next());

            // Prior to this fix preparing this statement would hang indefinitely with server-prepared statements.
            this.pstmt = testConn.prepareStatement("INSERT INTO testBug109864 VALUES (?)"); // No column definition block in Prepare response.
            this.pstmt.setInt(1, 1);
            assertEquals(1, this.pstmt.executeUpdate());
            this.pstmt.close();

            this.pstmt = testConn.prepareStatement("INSERT INTO testBug109864 VALUES (2)");  // No column or parameter definition blocks in Prepare response.
            assertEquals(1, this.pstmt.executeUpdate());
            this.pstmt.close();

            testConn.close();
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#109377 (Bug#34900156), rewriteBatchedStatements doesn't work when parenthesis are found in values.
     *
     * @throws Exception
     */
    @Test
    void testBug109377() throws Exception {
        createTable("testBug109377", "(d DOUBLE, i INT)");

        boolean useSPS = false;
        do {
            final String testCase = String.format("Case [useSPS: %s]", useSPS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.queryInterceptors.getKeyName(), QueryInfoQueryInterceptor.class.getName());
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            try (Connection testConn = getConnectionWithProps(props)) {
                QueryInfoQueryInterceptor.startCapturing();
                this.pstmt = testConn.prepareStatement("INSERT INTO testBug109377 (d, i) VALUES (RAND(?), ?)");
                this.pstmt.setInt(1, 10);
                this.pstmt.setInt(2, 1);
                this.pstmt.addBatch();
                this.pstmt.setInt(1, 10);
                this.pstmt.setInt(2, 2);
                this.pstmt.addBatch();
                this.pstmt.executeBatch();
                if (useSPS) {
                    QueryInfoQueryInterceptor.assertCapturedSql(testCase, "INSERT INTO testBug109377 (d, i) VALUES (RAND(?), ?),(RAND(?), ?)");
                } else {
                    QueryInfoQueryInterceptor.assertCapturedSql(testCase, "INSERT INTO testBug109377 (d, i) VALUES (RAND(10), 1),(RAND(10), 2)");
                }
            }
        } while (useSPS = !useSPS);
    }

    /**
     * Tests for Bug#107577 (Bug#34325361), rewriteBatchedStatements not work when table column name contains 'value' string.
     *
     * Duplicate of Bug#40439 (Bug#11750096), Error rewriting batched statement if table name ends with "values", that re-surfaced while fixing Bug#109377
     * (Bug#34900156).
     *
     * @throws Exception
     */
    @Test
    void testBug107577() throws Exception {
        createTable("VALUES_testBug_VALUE_107577_VALUES", "(value INT, VALUE_foo_VALUES_bar_VALUE INT)");

        boolean useSPS = false;
        do {
            final String testCase = String.format("Case [useSPS: %s]", useSPS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.queryInterceptors.getKeyName(), QueryInfoQueryInterceptor.class.getName());
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            try (Connection testConn = getConnectionWithProps(props)) {
                QueryInfoQueryInterceptor.startCapturing();
                this.pstmt = testConn.prepareStatement("INSERT INTO VALUES_testBug_VALUE_107577_VALUES (value, VALUE_foo_VALUES_bar_VALUE) VALUES (?, ?)");
                this.pstmt.setInt(1, 1);
                this.pstmt.setInt(2, 1);
                this.pstmt.addBatch();
                this.pstmt.setInt(1, 2);
                this.pstmt.setInt(2, 2);
                this.pstmt.addBatch();
                this.pstmt.executeBatch();
                if (useSPS) {
                    QueryInfoQueryInterceptor.assertCapturedSql(testCase,
                            "INSERT INTO VALUES_testBug_VALUE_107577_VALUES (value, VALUE_foo_VALUES_bar_VALUE) VALUES (?, ?),(?, ?)");
                } else {
                    QueryInfoQueryInterceptor.assertCapturedSql(testCase,
                            "INSERT INTO VALUES_testBug_VALUE_107577_VALUES (value, VALUE_foo_VALUES_bar_VALUE) VALUES (1, 1),(2, 2)");
                }
            }
        } while (useSPS = !useSPS);
    }

    public static class QueryInfoQueryInterceptor extends BaseQueryInterceptor {

        private static boolean enabled = false;
        private static List<String> capturedSql = new ArrayList<>();

        public static void startCapturing() {
            enabled = true;
            capturedSql.clear();
        }

        public static void assertCapturedSql(String testCase, String... expectedSql) {
            enabled = false;
            assertEquals(expectedSql.length, capturedSql.size(), testCase);
            for (int i = 0; i < expectedSql.length; i++) {
                assertEquals(expectedSql[i], capturedSql.get(i), testCase);
            }
        }

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            if (enabled && interceptedQuery != null) {
                capturedSql.add(sql.get());
            }
            return super.preProcess(sql, interceptedQuery);
        }

    }

    /**
     * Tests fix for Bug#34558945, PS using setCharacterStream() fails with "Incorrect string value" if the Java program encoding is not UTF-8 after 8.0.29.
     *
     * @throws Exception
     */
    @Test
    void testBug34558945() throws Exception {
        createTable("testBug34558945Utf8", "(txt VARCHAR(100) COLLATE utf8mb4_general_ci)");
        createTable("testBug34558945SJis", "(txt VARCHAR(100) COLLATE sjis_japanese_ci)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        final String utf8 = "UTF-8";
        final String sjis = "Shift_JIS";
        final String[] charEncs = { null, utf8, sjis };
        final String value = "\u7ADC";

        for (String ce : charEncs) {
            for (String cce : charEncs) {

                if (ce == null) {
                    props.remove(PropertyKey.characterEncoding.getKeyName());
                } else {
                    props.setProperty(PropertyKey.characterEncoding.getKeyName(), ce);
                }
                if (cce == null) {
                    props.remove(PropertyKey.clobCharacterEncoding.getKeyName());
                } else {
                    props.setProperty(PropertyKey.clobCharacterEncoding.getKeyName(), cce);
                }

                boolean useSPS = false;
                do {
                    props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
                    try (Connection testConn = getConnectionWithProps(props)) {
                        int records;
                        PreparedStatement testPStmt;
                        Statement testStmt;

                        // Table with UTF-8 column.
                        records = 1;
                        testPStmt = testConn.prepareStatement("INSERT INTO testBug34558945Utf8 VALUES (?)");
                        testPStmt.setString(1, value);
                        assertEquals(1, testPStmt.executeUpdate());
                        testPStmt.setCharacterStream(1, new StringReader(value), value.length());
                        if (sjis.equals(cce) || cce == null && sjis.equals(ce)) {
                            assertThrows(SQLException.class, ".*Incorrect string value:.*", testPStmt::executeUpdate);
                        } else {
                            assertEquals(1, testPStmt.executeUpdate());
                            records = 2;
                        }

                        testStmt = testConn.createStatement();
                        this.rs = testStmt.executeQuery("SELECT * FROM testBug34558945Utf8");
                        while (records-- > 0) {
                            assertTrue(this.rs.next());
                            assertEquals(value, this.rs.getString(1));
                        }
                        assertFalse(this.rs.next());
                        testStmt.executeUpdate("TRUNCATE TABLE testBug34558945Utf8");

                        // Table with Shift_JIS column.
                        records = 1;
                        testPStmt = testConn.prepareStatement("INSERT INTO testBug34558945SJis VALUES (?)");
                        testPStmt.setString(1, value);
                        assertEquals(1, testPStmt.executeUpdate());
                        testPStmt.setCharacterStream(1, new StringReader(value), value.length());
                        if (sjis.equals(cce) || cce == null && sjis.equals(ce)) {
                            assertEquals(1, testPStmt.executeUpdate());
                            records = 2;
                        } else {
                            assertThrows(SQLException.class, ".*Incorrect string value:.*", testPStmt::executeUpdate);
                        }

                        testStmt = testConn.createStatement();
                        this.rs = testStmt.executeQuery("SELECT * FROM testBug34558945SJis");
                        while (records-- > 0) {
                            assertTrue(this.rs.next());
                            assertEquals(value, this.rs.getString(1));
                        }
                        assertFalse(this.rs.next());
                        testStmt.executeUpdate("TRUNCATE TABLE testBug34558945SJis");
                    }
                } while (useSPS = !useSPS);
            }
        }
    }

    /**
     * Tests fix for Bug#110321 (Bug#35167701), Issue in JDBC PreparedStatement on adding NO_BACKSLASH_ESCAPES in sql_mode.
     *
     * @throws Exception
     */
    @Test
    void testBug110321() throws Exception {
        String sqlMode = getMysqlVariable("sql_mode");
        if (!sqlMode.contains("NO_BACKSLASH_ESCAPES")) {
            if (sqlMode.length() > 0) {
                sqlMode += ",";
            }
            sqlMode += "NO_BACKSLASH_ESCAPES";
        }

        String version = getMysqlVariable("version");
        boolean useSPS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.sessionVariables.getKeyName(), "sql_mode='" + sqlMode + "'");

            try (Connection testConn = getConnectionWithProps(props)) {
                assertTrue(((JdbcConnection) testConn).getSession().getServerSession().isNoBackslashEscapesSet());

                Statement testStmt = testConn.createStatement();
                this.rs = testStmt.executeQuery("SHOW VARIABLES LIKE 'version'");
                assertTrue(this.rs.next());
                assertEquals(version, this.rs.getString(2));
                assertFalse(this.rs.next());

                PreparedStatement testPStmt = testConn.prepareStatement("SHOW VARIABLES LIKE ?");
                testPStmt.setString(1, "version");
                this.rs = testPStmt.executeQuery();
                assertTrue(this.rs.next());
                assertEquals(version, this.rs.getString(2));
                assertFalse(this.rs.next());

            }
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#112195 (Bug#35749998), getWarnings() of StatementImpl contains all warnings.
     *
     * @throws Exception
     */
    @Test
    void testBug112195() throws Exception {
        String sql = "DROP TABLE IF EXISTS testBug112195";

        // This creates a single warning.
        this.stmt.execute(sql);

        SQLWarning warningToLog = this.stmt.getWarnings();
        int warningCounter = 0;
        while (warningToLog != null) {
            warningCounter++;
            warningToLog = warningToLog.getNextWarning();
        }
        assertEquals(1, warningCounter);

        // This must clear the previous warning and generate a new one.
        this.stmt.execute(sql);

        warningToLog = this.stmt.getWarnings();
        warningCounter = 0;
        while (warningToLog != null) {
            warningCounter++;
            warningToLog = warningToLog.getNextWarning();
        }
        assertEquals(1, warningCounter);
    }

    /**
     * Tests fix for Bug#109546 (Bug#34958912), executeUpdate throws SQLException on queries that are only comments.
     *
     * @throws Exception
     */
    @Test
    void testBug109546() throws Exception {
        assertDoesNotThrow(() -> {
            this.stmt.executeUpdate("#comment");
        });
        assertDoesNotThrow(() -> {
            this.stmt.executeUpdate("-- comment");
        });
        assertDoesNotThrow(() -> {
            this.stmt.executeUpdate("/* comment */");
        });
        assertDoesNotThrow(() -> {
            this.stmt.executeUpdate("/* com\nment */");
        });
    }

    /**
     * Tests fix for Bug#112884 (Bug#36043166), Setting a large timeout leads to errors when executing SQL.
     *
     * @throws Exception
     */
    @Test
    void testBug112884() throws Exception {
        assertDoesNotThrow(() -> {
            this.stmt.setQueryTimeout(Integer.MAX_VALUE / 1000 + 1);
        });
    }

    /**
     * Tests fix for Bug#107107 (Bug#34101635), Redundant "Reset stmt" when setting useServerPrepStmts&cachePrepStmts to true.
     *
     * @throws Exception
     */
    @Test
    void testBug107107() throws Exception {
        String prevGeneralLog = this.getMysqlVariable("general_log");
        String prevLogOutput = this.getMysqlVariable("log_output");

        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
            props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
            Connection testConn = getConnectionWithProps(props);

            this.stmt.execute("SET GLOBAL general_log = 'OFF'");
            this.stmt.execute("SET GLOBAL log_output = 'TABLE'");
            this.stmt.execute("SET GLOBAL general_log = 'ON'");

            createTable("testBug107107", "(id INT)");

            PreparedStatement ps = null;
            String sql = "INSERT INTO testBug107107 (id) VALUES (?)";
            for (int i = 1; i <= 3; i++) {
                ps = testConn.prepareStatement(sql);
                ps.setInt(1, i);
                ps.execute();
                ps.close();
            }

            boolean foundTestTable = false;
            this.rs = this.stmt.executeQuery("SELECT command_type, CONVERT(argument USING utf8mb4) FROM mysql.general_log ORDER BY event_time DESC LIMIT 10");
            while (this.rs.next()) {
                String commandType = this.rs.getString(1);
                String argument = this.rs.getString(2);
                if (!foundTestTable && argument.contains("testBug107107")) {
                    foundTestTable = true;
                }
                assertFalse(commandType.contains("Reset stmt"), "Unexpected Reset stmt command found.");
            }
            assertTrue(foundTestTable, "Expected general log events were not recorded in mysql.general_log.");

            testConn.close();
        } finally {
            this.stmt.execute("SET GLOBAL general_log = 'OFF'");
            this.stmt.execute("SET GLOBAL log_output = '" + prevLogOutput + "'");
            this.stmt.execute("SET GLOBAL general_log = '" + prevGeneralLog + "'");
        }
    }

    /**
     * Tests fix for Bug#77183 (Bug#21181501), INSERT..VALUE..lead to invalidation of batch insert.
     *
     * @throws Exception
     */
    @Test
    public void testBug77183() throws Exception {
        createTable("testBug77183", "(c1 INT, c2 INT)");

        boolean useSPS = false;
        boolean rwBS = false;
        boolean useRep = false;
        boolean useValue = false;

        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));
            props.setProperty(PropertyKey.queryInterceptors.getKeyName(), TestBug77183StatementInterceptor.class.getName());

            Connection testConn = getConnectionWithProps(props);

            this.pstmt = testConn.prepareStatement((useRep ? "REPLACE" : "INSERT") + " INTO testBug77183 " + (useValue ? "VALUE" : "VALUES") + " (?, ?)");
            for (int i = 1; i <= 3; i++) {
                this.pstmt.setInt(1, i);
                this.pstmt.setInt(2, i);
                this.pstmt.addBatch();
            }
            this.pstmt.executeBatch();

            testConn.close();
        } while ((useSPS = !useSPS) || (rwBS = !rwBS) || (useRep = !useRep) || (useValue = !useValue));

        assertEquals(32, TestBug77183StatementInterceptor.countInterceptions);
    }

    public static class TestBug77183StatementInterceptor extends BaseQueryInterceptor {

        public static int countInterceptions = 0;

        @Override
        public <T extends Resultset> T preProcess(java.util.function.Supplier<String> sql, Query interceptedQuery) {
            String query = sql.get();
            if (query == null && interceptedQuery instanceof JdbcPreparedStatement) {
                query = interceptedQuery.toString();
                query = query.substring(query.indexOf(':') + 2);
            }

            if (interceptedQuery != null) {
                countInterceptions++;

                PropertySet pset = interceptedQuery.getSession().getPropertySet();

                final boolean useSPS = pset.getBooleanProperty(PropertyKey.useServerPrepStmts).getValue();
                final boolean rwBS = pset.getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue();
                final String testCase = String.format("Case [SPS: %s, RwBS: %s, Query: %s]", useSPS ? "Y" : "N", rwBS ? "Y" : "N", sql);
                final int numParamSets = query.length() - query.replace("(", "").length();
                final int numPlaceholders = query.length() - query.replace("?", "").length();

                assertEquals(rwBS ? 3 : 1, numParamSets, testCase);
                assertEquals(useSPS ? rwBS ? 6 : 2 : 0, numPlaceholders, testCase);
            }
            return super.preProcess(sql, interceptedQuery);
        }

    }

    /**
     * Tests fix for Bug#22931632, GETPARAMETERBINDINGS() ON A PS RETURNS NPE WHEN NOT ALL PARAMETERS ARE BOUND.
     *
     * @throws Exception
     */
    @Test
    public void testBug22931632() throws Exception {
        this.pstmt = this.conn.prepareStatement("SELECT ?");
        assertDoesNotThrow(((JdbcPreparedStatement) this.pstmt)::getParameterBindings);
    }

    /**
     * Test fix for Bug#109418 (Bug#36043556), batch insert threw an unexpected exception.
     *
     * @throws Exception
     */
    @Test
    public void testBug109418() throws Exception {
        boolean useRBS = false;
        boolean cachePS = false;
        boolean useSPS = false;
        boolean useRGK = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(useRBS));
            props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), Boolean.toString(cachePS));
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            try (Connection testConn = getConnectionWithProps(props)) {
                createTable("testBug109418", "(id INT PRIMARY KEY)");

                this.pstmt = testConn.prepareStatement("INSERT INTO testBug109418(id) VALUES(?)",
                        useRGK ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);

                this.pstmt.setInt(1, 1);
                this.pstmt.addBatch();
                this.pstmt.setInt(1, 2);
                this.pstmt.addBatch();
                this.pstmt.executeBatch();
                this.pstmt.close();

                this.pstmt = testConn.prepareStatement("INSERT INTO testBug109418(id) VALUES(?)",
                        useRGK ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);
                this.pstmt.setInt(1, 3);
                this.pstmt.addBatch();
                this.pstmt.setInt(1, 1);
                this.pstmt.addBatch();

                final String testCase = String.format("Case: [useRBS=%s, cachePS=%s, useSPS=%s, useRGK=%s]", useRBS ? "Y" : "N", cachePS ? "Y" : "N",
                        useSPS ? "Y" : "N", useRGK ? "Y" : "N");
                assertThrows(testCase, BatchUpdateException.class, this.pstmt::executeBatch);
            } finally {
                dropTable("testBug109418");
                this.pstmt.close();
            }

        } while ((useRBS = !useRBS) || (cachePS = !cachePS) || (useSPS = !useSPS) || (useRGK = !useRGK));
    }

    /**
     * Tests fix for Bug#113509 (Bug#36154975), closeOnCompletion cause no statement reuse and server memory leak.
     *
     * @throws Exception
     */
    @Test
    void testBug113509() throws Exception {
        boolean useSPS = false;
        boolean cachePS = false;
        boolean closeOnCompl = false;

        do {
            final String testCase = String.format("Case [SPS: %s, CachePS: %s, CloseOnCompletion: %s]", useSPS ? "Y" : "N", cachePS ? "Y" : "N",
                    closeOnCompl ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), Boolean.toString(cachePS));

            try (Connection testConn = getConnectionWithProps(props)) {
                this.rs = testConn.createStatement().executeQuery("SHOW STATUS LIKE 'Prepared_stmt_count'");
                assertTrue(this.rs.next());
                int currPrepCount = this.rs.getInt(2);

                for (int i = 0; i < 3; i++) {
                    PreparedStatement testPstmt = testConn.prepareStatement("SELECT ?");
                    if (closeOnCompl) {
                        testPstmt.closeOnCompletion();
                    }

                    testPstmt.setString(1, "Connector/J");
                    ResultSet testRs = testPstmt.executeQuery();
                    assertTrue(testRs.next(), testCase);
                    testRs.close(); // Triggers statement close if closeOnCompletion has been set.

                    assertEquals(closeOnCompl, testPstmt.isClosed(), testCase);
                    testPstmt.close(); // Does nothing if closeOnCompletion has been set.
                    assertTrue(testPstmt.isClosed(), testCase);
                }
                this.rs = testConn.createStatement().executeQuery("SHOW STATUS LIKE 'Prepared_stmt_count'");
                assertTrue(this.rs.next(), testCase);
                assertEquals(useSPS && cachePS ? 1 : 0, this.rs.getInt(2) - currPrepCount, testCase);
            }
        } while ((useSPS = !useSPS) || (cachePS = !cachePS) || (closeOnCompl = !closeOnCompl));
    }

    /**
     * Tests fix for Bug#112790 (Bug#35936477), Statement.getGeneratedKeys() returns unexpected value.
     *
     * @throws Exception
     */
    @Test
    public void testBug112790() throws Exception {
        createTable("testBug112790", "(id INT AUTO_INCREMENT PRIMARY KEY, value INT)");

        this.stmt.addBatch("INSERT INTO testBug112790 VALUES (1, 1)");
        this.stmt.executeBatch();

        this.stmt.executeUpdate("INSERT INTO testBug112790 (value) VALUES (2)", Statement.RETURN_GENERATED_KEYS);
        this.rs = this.stmt.getGeneratedKeys();
        assertTrue(this.rs.next());
        assertEquals(2, this.rs.getInt(1));
        assertFalse(this.rs.next());

        this.stmt.addBatch("INSERT INTO testBug112790 VALUES (3, 3)");
        this.stmt.executeBatch();

        this.stmt.executeUpdate("INSERT INTO testBug112790 (value) VALUES (4)", Statement.NO_GENERATED_KEYS);
        assertThrows(SQLException.class, this.stmt::getGeneratedKeys);
    }

    /**
     * Tests fix for Bug#108415 (Bug#34579258), NullPointerException in AbstractQuery::stopQueryTimer.
     *
     * @throws Exception
     */
    @Test
    void testBug108415() throws Exception {
        Connection testConn = getNewConnection();
        final long sessionToKill = ((JdbcConnection) testConn).getId();

        ExecutorService execServ = Executors.newSingleThreadExecutor();
        execServ.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
            }
            try {
                System.out.println("Killing process id " + sessionToKill);
                this.stmt.execute("KILL " + sessionToKill);
            } catch (SQLException e) {
            }
        });

        Statement testStmt = testConn.createStatement();
        testStmt.setQueryTimeout(15);
        assertThrows(SQLException.class, () -> {
            testStmt.executeQuery("SELECT SLEEP(15)");
            return null;
        });

        execServ.shutdown();
    }

    /**
     * Tests fix for Bug#101054 (Bug#32544786), Batched Query > maxAllowedPacket size causes an ArrayIndexOutOfBoundsException.
     *
     * @throws Exception
     */
    @Test
    public void testBug101054() throws Exception {
        boolean allowMQ = false;
        createTable("testBug101054", "(c0 VARCHAR(2048) NOT NULL)");
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), Boolean.toString(allowMQ));
            props.setProperty(PropertyKey.characterEncoding.getKeyName(), "utf8");
            props.setProperty(PropertyKey.maxAllowedPacket.getKeyName(), "1024");

            try (Connection testConn = getConnectionWithProps(props)) {
                this.stmt = testConn.createStatement();

                final String valueToInsert = new String(new char[512]).replace('\0', 'X');
                final String sql = String.format("INSERT INTO testBug101054 VALUES ('%s')", valueToInsert);
                final String testCase = String.format("Case [allowMQ: %s]", allowMQ ? "Y" : "N");

                this.stmt.addBatch(sql);
                assertDoesNotThrow(this.stmt::executeBatch, testCase);

                this.rs = this.stmt.executeQuery("SELECT * FROM testBug101054");
                while (this.rs.next()) {
                    assertEquals(valueToInsert, this.rs.getString(1), testCase);
                }
            }
        } while (allowMQ = !allowMQ);
    }

    /**
     * Tests fix for Bug#84117 (Bug#25247468), includeThreadNamesAsStatementComment ignored when using prepared statement.
     *
     * @throws Exception
     */
    @Test
    public void testBug84117() throws Exception {
        String threadName = Thread.currentThread().getName();
        try {
            Thread.currentThread().setName("TestBug84117.thread");

            boolean incThrNm = false;
            do {
                TestBug84117QueryInterceptor.expectsThreadName = incThrNm;
                TestBug84117QueryInterceptor.occurrences = 0;

                final Properties props = new Properties();
                props.setProperty(PropertyKey.queryInterceptors.getKeyName(), TestBug84117QueryInterceptor.class.getName());
                props.setProperty(PropertyKey.includeThreadNamesAsStatementComment.getKeyName(), Boolean.toString(incThrNm));

                final String testQuery = "/* TestBug84117.comment */ SELECT 'MySQL Connector/J'";

                // Statement:
                try (Connection testConn = getConnectionWithProps(props); Statement testStmt = testConn.createStatement()) {
                    testStmt.executeQuery(testQuery);
                }

                // Client PreparedStatement:
                props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
                try (Connection testConn = getConnectionWithProps(props); PreparedStatement testPstmt = testConn.prepareStatement(testQuery)) {
                    testPstmt.executeQuery();
                }

                // Server PreparedStatement:
                props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
                try (Connection testConn = getConnectionWithProps(props); PreparedStatement testPstmt = testConn.prepareStatement(testQuery)) {
                    testPstmt.executeQuery();
                }

                assertEquals(3, TestBug84117QueryInterceptor.occurrences);
            } while (incThrNm = !incThrNm);
        } finally {
            Thread.currentThread().setName(threadName);
        }
    }

    public static class TestBug84117QueryInterceptor extends BaseQueryInterceptor {

        static boolean expectsThreadName = false;
        static int occurrences = 0;

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
            String sqlString = sql.get();
            if (sqlString.contains("SELECT 'MySQL Connector/J'")) {
                if (expectsThreadName) {
                    assertTrue(sqlString.contains("/* Java thread: TestBug84117.thread */"));
                } else {
                    assertFalse(sqlString.contains("/* Java thread: TestBug84117.thread */"));
                }
                occurrences++;
            }
            return super.preProcess(sql, interceptedQuery);
        }

    }

    /**
     * Tests fix for Bug#96623 (Bug#30221117), batch update with rewriteBatchedStatements&useServerPrepStmts send fail request.
     *
     * @throws Exception
     */
    @Test
    public void testBug96623() throws Exception {
        boolean allowMQ = false;
        boolean rwBS = false;
        boolean useSPS = false;
        boolean cachePS = false;

        createTable("testBug96623", "(c0 int)");
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), Boolean.toString(allowMQ));
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), Boolean.toString(cachePS));
            props.setProperty(PropertyKey.emulateUnsupportedPstmts.getKeyName(), "true");
            props.setProperty(PropertyKey.exceptionInterceptors.getKeyName(), TestBug96623ExceptionInterceptor.class.getName());

            TestBug96623ExceptionInterceptor.testCase = String.format("Case [allowMQ: %s, rwBS: %s, useSPS: %s, cachePS: %s]", allowMQ ? "Y" : "N",
                    rwBS ? "Y" : "N", useSPS ? "Y" : "N", cachePS ? "Y" : "N");

            try (Connection testConn = getConnectionWithProps(props); PreparedStatement ps = testConn.prepareStatement("UPDATE testBug96623 SET c0=?")) {
                for (int i = 0; i < 4; i++) {
                    ps.setInt(1, i);
                    ps.addBatch();
                }

                ps.executeBatch();
            }
        } while ((allowMQ = !allowMQ) || (rwBS = !rwBS) || (useSPS = !useSPS) || (cachePS = !cachePS));
    }

    public static class TestBug96623ExceptionInterceptor implements ExceptionInterceptor {

        static String testCase = null;

        @Override
        public ExceptionInterceptor init(Properties props, Log log) {
            return this;
        }

        @Override
        public void destroy() {
        }

        @Override
        public SQLException interceptException(Exception sqlEx) {
            assertFalse(sqlEx instanceof SQLSyntaxErrorException, testCase);
            return null;
        }

    }

    /**
     * Tests fix for Bug#116114 (Bug#37067812), Connector/J is writing incorrect values when passed negative dates.
     *
     * @throws Exception
     */
    @Test
    public void testBug116114() throws Exception {
        createTable("testBug116114", "(d DATE, dt DATETIME)");

        LocalDate negativeDate = LocalDate.of(-1, 1, 1);
        this.pstmt = this.conn.prepareStatement("INSERT INTO testBug116114 (d) VALUES (?)");
        this.pstmt.setObject(1, negativeDate);
        assertThrows(MysqlDataTruncation.class, this.pstmt::executeUpdate);
        assertThrows(SQLSyntaxErrorException.class, () -> this.stmt.executeUpdate("INSERT INTO testBug116114 (d) VALUES '-0001-01-01'"));

        LocalDateTime negativeDateTime = LocalDateTime.of(-1, 1, 1, 1, 1, 1);
        this.pstmt = this.conn.prepareStatement("INSERT INTO testBug116114 (dt) VALUES (?)");
        this.pstmt.setObject(1, negativeDateTime);
        assertThrows(MysqlDataTruncation.class, this.pstmt::executeUpdate);
        assertThrows(SQLSyntaxErrorException.class, () -> this.stmt.executeUpdate("INSERT INTO testBug116114 (dt) VALUES '-0001-01-01 01:01:01'"));
    }

    /**
     * Tests fix for Bug#21983318, QUERYTIMEOUT VALUE RESETTING TO 0 AFTER PREPSTMT.EXECUTEBATCH() EXECUTION.
     *
     * @throws Exception
     */
    @Test
    public void testBug21983318() throws Exception {
        createTable("testBug21983318", "(id INT)");

        Statement queryTimeoutStmt = this.conn.createStatement();
        queryTimeoutStmt.setQueryTimeout(10);
        assertEquals(10, queryTimeoutStmt.getQueryTimeout());
        this.stmt.executeUpdate("INSERT INTO testBug21983318 VALUES(2)");
        assertEquals(10, queryTimeoutStmt.getQueryTimeout());
        queryTimeoutStmt.close();

        this.pstmt = this.conn.prepareStatement("INSERT INTO testBug21983318 values(?)");
        this.pstmt.setQueryTimeout(10);
        assertEquals(10, this.pstmt.getQueryTimeout());
        this.pstmt.setInt(1, 1);
        this.pstmt.addBatch();
        this.pstmt.executeBatch();
        assertEquals(10, this.pstmt.getQueryTimeout());
    }

    /**
     * Tests fix for Bug#96786 (Bug#30280035), Retrieving streaming result set fails when closeOnCompletion is enabled.
     *
     * @throws Exception
     */
    @Test
    public void testBug96786() throws Exception {
        createTable("testBug96786", "(id INT)");
        this.stmt = this.conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY);
        this.stmt.setFetchSize(Integer.MIN_VALUE);
        this.stmt.execute("INSERT INTO testBug96786 VALUES (1)");
        this.stmt.closeOnCompletion();
        this.rs = this.stmt.executeQuery("SELECT * FROM testBug96786");
        assertTrue(this.rs.next());
        assertEquals(1, this.rs.getInt(1));
        this.rs.close();
        assertTrue(this.stmt.isClosed());
    }

    /**
     * Tests fix for Bug#117873 (Bug#37785888), Time instances created from negative milliseconds are encoded without fractional seconds.
     *
     * @throws Exception
     */
    @Test
    void testBug117873() throws Exception {
        boolean useSPS = false;
        do {
            final String testCase = String.format("Case [SPS: %s]", useSPS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            try (Connection testConn = getConnectionWithProps(props)) {
                this.stmt = testConn.createStatement();
                this.stmt.execute("CREATE TEMPORARY TABLE testBug117873 (t TIME(3))");

                this.pstmt = testConn.prepareStatement("INSERT INTO testBug117873 VALUES (?);");
                this.pstmt.setTime(1, new Time(-1500));
                this.pstmt.executeUpdate();
                this.pstmt.setTime(1, new Time(1500));
                this.pstmt.executeUpdate();

                this.rs = this.stmt.executeQuery("SELECT * FROM testBug117873");
                while (this.rs.next()) {
                    assertTrue(this.rs.getString(1).endsWith(".500"), testCase);
                    assertEquals(500, Math.abs(this.rs.getTime(1).getTime() % 1000), testCase);
                    assertEquals(500000000, this.rs.getObject(1, LocalTime.class).getNano(), testCase);
                }
            }
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#109339 (Bug#34898663), Failed to insert bignums due to overflow.
     *
     * @throws Exception
     */
    @Test
    public void testBug109339() throws Exception {
        createTable("testBug109339", "(value BIGINT UNSIGNED)");

        BigInteger value = new BigInteger("18446744073709551615"); // 2^64-1 - maximum value supported by BIGINT UNSIGNED
        assertEquals(1, this.stmt.executeUpdate("INSERT INTO testBug109339 VALUES (" + value + ")"));

        boolean useSPS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            try (Connection testConn = getConnectionWithProps(props)) {
                PreparedStatement ps = testConn.prepareStatement("INSERT INTO testBug109339 VALUES (?)");
                ps.setObject(1, value);
                ps.executeUpdate();
            }
        } while (useSPS = !useSPS);

        this.rs = this.stmt.executeQuery("SELECT * FROM testBug109339");
        while (this.rs.next()) {
            assertEquals(value, this.rs.getObject(1));
        }
        assertTrue(this.rs.last());
        assertEquals(3, this.rs.getRow());
    }

    /**
     * Tests fix for Bug#118079 (Bug#37888527), An output message error.
     *
     * @throws Exception
     */
    @Test
    public void testBug118079() throws Exception {
        assertThrows(SQLException.class, "setMaxRows\\(\\) out of range. -1 < 0.", () -> {
            this.conn.createStatement().setMaxRows(-1);
            return null;
        });
    }

    /**
     * Tests fix for Bug#77658 (Bug#21946136), Incorrect java.sql.Statement.cancel() behavior.
     *
     * @throws Exception
     */
    @Test
    public void testBug77658() throws Exception {
        createTable("testBug77658", "(id INT)");
        this.stmt.executeUpdate("INSERT INTO testBug77658 VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)");

        ScheduledExecutorService interruptorExecService = Executors.newSingleThreadScheduledExecutor();

        boolean clobStRes = false;
        boolean dontTrOpRes = false;
        do {
            final String testCase = String.format("Case [ClobberStreamRes: %s, DontTrackRes: %s]", clobStRes ? "Y" : "N", dontTrOpRes ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.clobberStreamingResults.getKeyName(), Boolean.toString(clobStRes));
            props.setProperty(PropertyKey.dontTrackOpenResources.getKeyName(), Boolean.toString(dontTrOpRes));
            Connection testConn = getConnectionWithProps(props);

            /*
             * Query cancellation detected while reading all rows.
             */
            Statement testStmt1 = testConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
            testStmt1.setFetchSize(Integer.MIN_VALUE); // Enable streaming results.
            interruptorExecService.schedule(() -> {
                testStmt1.cancel();
                return null;
            }, 2000, TimeUnit.MILLISECONDS);

            this.rs = testStmt1.executeQuery("SELECT SLEEP(1) FROM testBug77658");
            do {
                assertTrue(this.rs.next(), testCase);
            } while (this.rs.getInt(1) == 0); // A number of rows should have been sent before the query was canceled.
            assertEquals(1, this.rs.getInt(1), testCase); // Row where the query was canceled.
            assertThrows(testCase, MySQLQueryInterruptedException.class, () -> {
                this.rs.next();
                return null;
            });
            this.rs.close();

            assertTrue(this.rs.isClosed(), testCase);
            this.rs = testStmt1.executeQuery("SELECT 1");
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            this.rs.close();

            /*
             * Query cancellation detected while closing the ResultSet, if 'clobberStreamingResults=false'.
             */
            Statement testStmt2 = testConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
            testStmt2.setFetchSize(Integer.MIN_VALUE); // Enable streaming results.
            interruptorExecService.schedule(() -> {
                testStmt2.cancel();
                return null;
            }, 2000, TimeUnit.MILLISECONDS);

            this.rs = testStmt2.executeQuery("SELECT SLEEP(1) FROM testBug77658");
            assertTrue(this.rs.next(), testCase); // At least one row should have been sent before the query was canceled.
            if (clobStRes) {
                this.rs.close();
            } else {
                assertThrows(testCase, MySQLQueryInterruptedException.class, () -> {
                    this.rs.close();
                    return null;
                });
            }

            assertTrue(this.rs.isClosed(), testCase);
            this.rs = testStmt2.executeQuery("SELECT 1");
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            this.rs.close();

            /*
             * Query cancellation detected while implicitly closing the ResultSet, if 'clobberStreamingResults=false'.
             */
            Statement testStmt3 = testConn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
            testStmt3.setFetchSize(Integer.MIN_VALUE); // Enable streaming results.
            interruptorExecService.schedule(() -> {
                testStmt3.cancel();
                return null;
            }, 2000, TimeUnit.MILLISECONDS);

            this.rs = testStmt3.executeQuery("SELECT SLEEP(1) FROM testBug77658");
            assertTrue(this.rs.next(), testCase); // At least one row should have been sent before the query was canceled.

            assertFalse(this.rs.isClosed(), testCase);
            if (clobStRes) {
                this.rs = testStmt3.executeQuery("SELECT 1");
                assertTrue(this.rs.next(), testCase);
                assertEquals(1, this.rs.getInt(1), testCase);
                this.rs.close();
            } else if (dontTrOpRes) {
                assertThrows(testCase, SQLException.class,
                        ".*No statements may be issued when any streaming result sets are open and in use on a given connection.*", () -> {
                            testStmt3.executeQuery("SELECT 1");
                            return null;
                        });
            } else {
                assertThrows(testCase, MySQLQueryInterruptedException.class, "Query execution was interrupted", () -> {
                    testStmt3.executeQuery("SELECT 1");
                    return null;
                });
            }
        } while ((clobStRes = !clobStRes) || (dontTrOpRes = !dontTrOpRes));

        interruptorExecService.shutdown();
    }

    /**
     * Tests fix for Bug#118201 (Bug#37971552), A potential bug in Mysql Connector/J.
     *
     * @throws Exception
     */
    @Test
    void testBug118201() throws Exception {
        createTable("testBug118201", "(id INT PRIMARY KEY)");

        boolean mltQry = false;
        boolean rwBS = false;
        boolean contBE = false;
        boolean useSPS = false;

        Properties props = new Properties();
        do {
            props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), Boolean.toString(mltQry));
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));
            props.setProperty(PropertyKey.continueBatchOnError.getKeyName(), Boolean.toString(contBE));

            this.stmt.execute("TRUNCATE TABLE testBug118201");
            this.stmt.executeUpdate("INSERT INTO testBug118201 VALUES (3)");

            // Test Statement.
            String testCase = String.format("Case [mulitQueries: %s, rwBS: %s, contBE: %s]", mltQry ? "Y" : "N", rwBS ? "Y" : "N", contBE ? "Y" : "N");
            try (Connection testConn = getConnectionWithProps(props)) {
                Statement testStmt = testConn.createStatement();
                testStmt.addBatch("INSERT INTO testBug118201 VALUES (1)");
                testStmt.addBatch("INSERT INTO testBug118201 VALUES (2)");
                testStmt.addBatch("INSERT INTO testBug118201 VALUES (3)");
                testStmt.addBatch("INSERT INTO testBug118201 VALUES (4)");
                testStmt.addBatch("INSERT INTO testBug118201 VALUES (5)");
                assertThrows(BatchUpdateException.class, "(?i)Duplicate entry '3' for key 'testBug118201.PRIMARY'", testStmt::executeBatch);
            }

            this.rs = this.stmt.executeQuery("SELECT * FROM testBug118201");
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertTrue(this.rs.next(), testCase);
            assertEquals(2, this.rs.getInt(1), testCase);
            assertTrue(this.rs.next(), testCase);
            assertEquals(3, this.rs.getInt(1), testCase);
            if (!rwBS && contBE) {
                assertTrue(this.rs.next(), testCase);
                assertEquals(4, this.rs.getInt(1), testCase);
                assertTrue(this.rs.next(), testCase);
                assertEquals(5, this.rs.getInt(1), testCase);
            }
            assertFalse(this.rs.next(), testCase);

            do {
                this.stmt.execute("TRUNCATE TABLE testBug118201");
                this.stmt.executeUpdate("INSERT INTO testBug118201 VALUES (3)");

                // Test client and server PreparedStatement.
                testCase = String.format("Case [mulitQueries: %s, rwBS: %s, contBE: %s, useSPS: %s]", mltQry ? "Y" : "N", rwBS ? "Y" : "N", contBE ? "Y" : "N",
                        useSPS ? "Y" : "N");
                props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
                try (Connection testConn = getConnectionWithProps(props)) {
                    PreparedStatement testPstmt = testConn.prepareStatement("INSERT INTO testBug118201 VALUES (?)");
                    testPstmt.setInt(1, 1);
                    testPstmt.addBatch();
                    testPstmt.setInt(1, 2);
                    testPstmt.addBatch();
                    testPstmt.setInt(1, 3);
                    testPstmt.addBatch();
                    testPstmt.setInt(1, 4);
                    testPstmt.addBatch();
                    testPstmt.setInt(1, 5);
                    testPstmt.addBatch();
                    assertThrows(BatchUpdateException.class, "(?i)Duplicate entry '3' for key 'testBug118201.PRIMARY'", testPstmt::executeBatch);
                }

                this.rs = this.stmt.executeQuery("SELECT * FROM testBug118201");
                assertTrue(this.rs.next(), testCase);
                if (rwBS) {
                    assertEquals(3, this.rs.getInt(1), testCase);
                } else {
                    assertEquals(1, this.rs.getInt(1), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(2, this.rs.getInt(1), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(3, this.rs.getInt(1), testCase);
                    if (contBE) {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(4, this.rs.getInt(1), testCase);
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(5, this.rs.getInt(1), testCase);
                    }
                }
                assertFalse(this.rs.next(), testCase);
            } while (useSPS = !useSPS);
        } while ((mltQry = !mltQry) || (rwBS = !rwBS) || (contBE = !contBE));
    }

    /**
     * Tests fix for Bug#112090 (Bug#35716608), SHOW ENGINE command runs forever when using cursor fetch.
     *
     * @throws Exception
     */
    @Test
    public void testBug112090() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.defaultFetchSize.getKeyName(), "5000");
        props.setProperty(PropertyKey.useCursorFetch.getKeyName(), "true");
        props.setProperty(PropertyKey.socketTimeout.getKeyName(), "1000");
        Connection testConn = getConnectionWithProps(props);

        this.pstmt = testConn.prepareStatement("SHOW ENGINE `MEMORY` STATUS");
        this.pstmt.execute();
        this.rs = this.pstmt.getResultSet();
        assertFalse(this.rs.next());
    }

    /**
     * Tests fix for Bug#118688 (Bug#38222681), com.mysql.cj.protocol.a.StringValueEncoder#getString does not handle string escaping.
     *
     * @throws Exception
     */
    @Test
    void testBug118688() throws Exception {
        this.pstmt = this.conn.prepareStatement("SELECT * FROM testBug118688 WHERE a = ? AND b = ?");
        this.pstmt.setString(1, "'aa'");
        this.pstmt.setString(2, "b'b");
        PreparedQuery pquery = (PreparedQuery) ((JdbcStatement) this.pstmt).getQuery();
        assertEquals("SELECT * FROM testBug118688 WHERE a = '''aa''' AND b = 'b''b'", pquery.asSql());
    }

    /**
     * Tests fix for Bug#114974 (Bug#36614381), the SQL in batch will not clear after statement close.
     *
     * @throws Exception
     */
    @Test
    public void testBug114974() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);
        createTable("testBug114974", "(id INT)");
        final String sql = "INSERT INTO testBug114974 VALUES (?)";

        PreparedStatement testPStmt = testConn.prepareStatement(sql);
        testPStmt.setInt(1, 1);
        testPStmt.addBatch();
        testPStmt.close();

        testPStmt = testConn.prepareStatement(sql);
        testPStmt.setInt(1, 2);
        testPStmt.addBatch();
        testPStmt.executeBatch();

        this.rs = testConn.createStatement().executeQuery("SELECT * FROM testBug114974");
        assertTrue(this.rs.next());
        assertEquals(2, this.rs.getInt(1));
        assertFalse(this.rs.next());
    }

    /**
     * Tests fix for Bug#45554 (Bug#11754018), Connector/J does not encode binary data if useServerPrepStatements=false.
     *
     * @throws Exception
     */
    @Test
    public void testBug45554() throws Exception {
        final String testString = "€\\\\";
        final byte[] testStringBytes = testString.getBytes();

        Path filePath = Files.createTempFile("testBug45554", ".dat");
        OutputStream os = Files.newOutputStream(filePath);
        os.write(testString.getBytes());
        os.close();

        boolean useSPS = false;
        String[] charSets = new String[] { "UTF8", "GBK", "SJIS" };

        do {
            for (String cs : charSets) {
                String testCase = String.format("Case [useSPS: %s,  charset: %s]", useSPS ? "Y" : "N", cs);

                Properties props = new Properties();
                props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
                props.setProperty(PropertyKey.characterEncoding.getKeyName(), cs);

                try (Connection testConn = getConnectionWithProps(props)) {
                    this.rs = this.stmt.executeQuery("SELECT '" + testString.replace("\\", "\\\\") + "'");
                    assertTrue(this.rs.next(), testCase);
                    assertArrayEquals(testStringBytes, this.rs.getBytes(1), testCase);

                    InputStream is = Files.newInputStream(filePath);
                    this.pstmt = testConn.prepareStatement("SELECT ?");
                    this.pstmt.setBinaryStream(1, is);
                    this.rs = this.pstmt.executeQuery();
                    assertTrue(this.rs.next(), testCase);
                    assertArrayEquals(testStringBytes, this.rs.getBytes(1), testCase);
                    is.close();
                }
            }
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#17881458, BEHAVIOR OF SETBINARYSTREAM() METHOD IS DIFFERENT WHEN USESERVERPREPSTMTS=TRUE.
     *
     * @throws Exception
     */
    @Test
    public void testBug17881458() throws Exception {
        testBug17881458Blob();
        testBug17881458Clob();
    }

    private void testBug17881458Blob() throws Exception {
        String defaultMaxAllowedPacket = getMysqlVariable("max_allowed_packet");
        createTable("testBug17881458_blob", "(id INT AUTO_INCREMENT PRIMARY KEY, data LONGBLOB)");

        try {
            final int MAX_PACKET = 1024 * 10;
            this.stmt.execute("SET GLOBAL max_allowed_packet = " + MAX_PACKET);

            boolean useSPS = false;
            boolean useSrmLen = false;
            boolean underLimit = false;
            do {
                this.stmt.execute("TRUNCATE TABLE testBug17881458_blob");

                Properties props = new Properties();
                props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
                props.setProperty(PropertyKey.useStreamLengthsInPrepStmts.getKeyName(), Boolean.toString(useSrmLen));
                props.setProperty(PropertyKey.maxAllowedPacket.getKeyName(), Integer.toString(MAX_PACKET));
                props.setProperty(PropertyKey.blobSendChunkSize.getKeyName(), Integer.toString(MAX_PACKET));

                try (Connection testConn = getConnectionWithProps(props)) {
                    assertEquals(MAX_PACKET,
                            ((JdbcConnection) testConn).getPropertySet().getIntegerProperty(PropertyKey.blobSendChunkSize.getKeyName()).getValue());
                    assertEquals(MAX_PACKET,
                            ((JdbcConnection) testConn).getPropertySet().getIntegerProperty(PropertyKey.maxAllowedPacket.getKeyName()).getValue());

                    byte[] data = new byte[MAX_PACKET - (underLimit ? 7 /* (7 = length of COM_STMT_SEND_LONG_DATA internal header) */ : 6)];
                    Arrays.fill(data, (byte) 0);
                    InputStream in = new ByteArrayInputStream(data);

                    PreparedStatement testPstmt = testConn.prepareStatement("INSERT INTO testBug17881458_blob (data) VALUES (?)");
                    testPstmt.setBlob(1, in, 1000);

                    if (useSrmLen || useSPS && underLimit) {
                        testPstmt.execute();
                        testPstmt.execute();
                        testPstmt.execute();
                        testPstmt.close();

                        int expectedLen = useSrmLen ? 1000 : MAX_PACKET - 7;
                        this.rs = this.stmt.executeQuery("SELECT id, OCTET_LENGTH(data) FROM testBug17881458_blob");
                        assertTrue(this.rs.next());
                        assertEquals(1, this.rs.getInt(1));
                        assertEquals(expectedLen, this.rs.getInt(2));
                        assertTrue(this.rs.next());
                        assertEquals(2, this.rs.getInt(1));
                        assertEquals(expectedLen, this.rs.getInt(2));
                        assertTrue(this.rs.next());
                        assertEquals(3, this.rs.getInt(1));
                        assertEquals(expectedLen, this.rs.getInt(2));
                        assertFalse(this.rs.next());
                    } else {
                        assertThrows(SQLException.class, ".*Packet for query is too large .*", testPstmt::execute);
                    }
                }
            } while ((useSPS = !useSPS) || (useSrmLen = !useSrmLen) || (underLimit = !underLimit));
        } finally {
            this.stmt.execute("SET GLOBAL max_allowed_packet = " + defaultMaxAllowedPacket);
        }
    }

    private void testBug17881458Clob() throws Exception {
        String defaultMaxAllowedPacket = getMysqlVariable("max_allowed_packet");
        createTable("testBug17881458_clob", "(id INT AUTO_INCREMENT PRIMARY KEY, data TEXT)");

        try {
            final int MAX_PACKET = 1024 * 10;
            this.stmt.execute("SET GLOBAL max_allowed_packet = " + MAX_PACKET);

            boolean useSPS = false;
            boolean useSrmLen = false;
            boolean underLimit = false;
            do {
                this.stmt.execute("TRUNCATE TABLE testBug17881458_clob");

                Properties props = new Properties();
                props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
                props.setProperty(PropertyKey.useStreamLengthsInPrepStmts.getKeyName(), Boolean.toString(useSrmLen));
                props.setProperty(PropertyKey.maxAllowedPacket.getKeyName(), Integer.toString(MAX_PACKET));
                props.setProperty(PropertyKey.blobSendChunkSize.getKeyName(), Integer.toString(MAX_PACKET));

                try (Connection testConn = getConnectionWithProps(props)) {
                    assertEquals(MAX_PACKET,
                            ((JdbcConnection) testConn).getPropertySet().getIntegerProperty(PropertyKey.blobSendChunkSize.getKeyName()).getValue());
                    assertEquals(MAX_PACKET,
                            ((JdbcConnection) testConn).getPropertySet().getIntegerProperty(PropertyKey.maxAllowedPacket.getKeyName()).getValue());

                    char[] data = new char[MAX_PACKET - (underLimit ? 7 /* (7 = length of COM_STMT_SEND_LONG_DATA internal header) */ : 6)];
                    Arrays.fill(data, '-');
                    Reader reader = new CharArrayReader(data);

                    PreparedStatement testPstmt = testConn.prepareStatement("INSERT INTO testBug17881458_clob (data) VALUES (?)");
                    testPstmt.setClob(1, reader, 1000);

                    if (useSrmLen || useSPS && underLimit) {
                        testPstmt.execute();
                        testPstmt.execute();
                        testPstmt.execute();
                        testPstmt.close();

                        int expectedLen = useSrmLen ? 1000 : MAX_PACKET - 7;
                        this.rs = this.stmt.executeQuery("SELECT id, LENGTH(data) FROM testBug17881458_clob");
                        assertTrue(this.rs.next());
                        assertEquals(1, this.rs.getInt(1));
                        assertEquals(expectedLen, this.rs.getInt(2));
                        assertTrue(this.rs.next());
                        assertEquals(2, this.rs.getInt(1));
                        assertEquals(expectedLen, this.rs.getInt(2));
                        assertTrue(this.rs.next());
                        assertEquals(3, this.rs.getInt(1));
                        assertEquals(expectedLen, this.rs.getInt(2));
                        assertFalse(this.rs.next());
                    } else {
                        assertThrows(SQLException.class, ".*Packet for query is too large .*", testPstmt::execute);
                    }
                }
            } while ((useSPS = !useSPS) || (useSrmLen = !useSrmLen) || (underLimit = !underLimit));
        } finally {
            this.stmt.execute("SET GLOBAL max_allowed_packet = " + defaultMaxAllowedPacket);
        }
    }

    /**
     * Tests fix for Bug#107543 (Bug#34464351), Cannot execute a SELECT statement that writes to an OUTFILE.
     * (INTO variable variant)
     *
     * @throws Exception
     */
    @Test
    public void testBug107543_IntoVar() throws Exception {
        // Test 1: execute() with INTO @var
        this.stmt.execute("SELECT 'testBug107543_1' INTO @testbug107543_a");
        assertEquals("testBug107543_1", getSingleValueWithQuery("SELECT @testbug107543_a"));

        // Test 2: executeUpdate() with INTO @var
        this.stmt.executeUpdate("SELECT 'testBug107543_2' INTO @testbug107543_b");
        assertEquals("testBug107543_2", getSingleValueWithQuery("SELECT @testbug107543_b"));

        // Test 3: executeQuery() with INTO @var
        assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.",
                () -> this.stmt.executeQuery("SELECT 'testBug107543_3' INTO @testbug107543_c"));
    }

    /**
     * Tests fix for Bug#107543 (Bug#34464351), Cannot execute a SELECT statement that writes to an OUTFILE.
     * (INTO file variant)
     *
     * @throws Exception
     */
    @Test
    public void testBug107543_IntoFile() throws Exception {
        String filePrivDir = getMysqlVariable("secure_file_priv");
        assumeTrue(filePrivDir != null && !"NULL".equalsIgnoreCase(filePrivDir),
                "To run this test the server needs to be started with the option\"--secure-file-priv=\"");
        filePrivDir = filePrivDir.isEmpty() ? Paths.get("").toAbsolutePath().toString() : Paths.get(filePrivDir).toRealPath().toString();
        filePrivDir = filePrivDir.endsWith(File.separator) ? filePrivDir : filePrivDir + File.separator;

        createTable("testBug107543", "(txt VARCHAR(100))");
        this.stmt.executeUpdate("INSERT INTO testBug107543 VALUES ('MySQL Connector/J')");

        Path dataFilePath = Paths.get(filePrivDir, "testbug107543.dat");
        for (String statement : new String[] { "SELECT * FROM", "TABLE" }) {
            for (String outType : new String[] { "OUTFILE", "DUMPFILE" }) {
                final String sql = statement + " testBug107543 INTO " + outType + " '" + dataFilePath.toString().replace("\\", "\\\\") + "'";

                // Test 1: execute() with INTO [OUTFILE | DUMPFILE]
                try {
                    this.stmt.execute(sql);
                    List<String> lines = Files.readAllLines(dataFilePath);
                    assertEquals(1, lines.size());
                    assertEquals("MySQL Connector/J", lines.get(0));
                } finally {
                    try {
                        Files.delete(dataFilePath);
                    } catch (Exception e) {
                        // Ignore.
                    }
                }

                // Test 2: executeUpdate() with INTO [OUTFILE | DUMPFILE]
                try {
                    this.stmt.executeUpdate(sql);
                    List<String> lines = Files.readAllLines(dataFilePath);
                    assertEquals(1, lines.size());
                    assertEquals("MySQL Connector/J", lines.get(0));
                } finally {
                    try {
                        Files.delete(dataFilePath);
                    } catch (Exception e) {
                        // Ignore.
                    }
                }

                // Test 3: executeQuery() with INTO [OUTFILE | DUMPFILE]
                try {
                    assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.",
                            () -> this.stmt.executeQuery(sql));
                } finally {
                    try {
                        Files.delete(dataFilePath);
                    } catch (Exception e) {
                        // Ignore.
                    }
                }
            }
        }
    }

    /**
     * Tests for Bug#119245 (Bug#38599240), Select into fix breaks queries with 'into' in them.
     *
     * @throws Exception
     */
    @Test
    void testBug119245() throws Exception {
        createTable("testBug119245", "(preINTO INT, preINTOpos INT, INTOpos INT, pre_INTO INT, pre_INTO_pos INT, INTO_pos INT, _INTO INT, "
                + "_INTO_ INT, INTO_ INT, pre$INTO INT, pre$INTO$ INT, INTO$ INT, `INTO` INT, \uD83D\uDC2C INT)");

        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT preINTO, preINTOpos, INTOpos, pre_INTO FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT preINTO FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT preINTOpos FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT INTOpos FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT pre_INTO FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT INTO_pos FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT pre_INTO_pos FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT _INTO FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT _INTO_ FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT INTO_ FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT pre$INTO FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT pre$INTO$ FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT INTO$ FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT `INTO` FROM testBug119245").close());
        assertDoesNotThrow(() -> this.stmt.executeQuery("SELECT \uD83D\uDC2C FROM testBug119245").close());
    }

    /**
     * Tests fix for Bug#118234 (Bug#37975837), A potential bugs in Mysql Connector/J.
     *
     * @throws Exception
     */
    @Test
    public void testBug118234() throws Exception {
        boolean rwBS = false;

        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));

            for (int numQueries = 1; numQueries <= 5; numQueries++) { // Batch is rewritten as multi-query when batch size is above 4 queries.
                String testCase = String.format("Case [rwBS: %s,  NumSelects: %d]", rwBS ? "Y" : "N", numQueries);

                try (Connection testConn = getConnectionWithProps(props)) {
                    Statement testStmt = testConn.createStatement();
                    testStmt.addBatch("SET @testBug118234 = 'Connector/J'");
                    for (int i = 0; i < numQueries; i++) {
                        testStmt.addBatch("SELECT 1");
                    }
                    assertThrows(testCase, BatchUpdateException.class,
                            "Statement\\.executeUpdate\\(\\) or Statement\\.executeLargeUpdate\\(\\) cannot issue statements that produce result sets\\.",
                            testStmt::executeBatch);

                    this.rs = testStmt.executeQuery("SELECT @testBug118234");
                    assertTrue(this.rs.next(), testCase);
                    if (rwBS && numQueries >= 4) { // Entire rewritten batch fails.
                        assertNull(this.rs.getString(1), testCase);
                    } else {
                        assertEquals("Connector/J", this.rs.getString(1), testCase);
                    }
                    assertFalse(this.rs.next(), testCase);
                }
            }
        } while (rwBS = !rwBS);
    }

    /**
     * Tests fix for Bug#113336 (Bug#36080226), Inconsistent getUpdateCount() Behavior with allowMultiQueries.
     *
     * @throws Exception
     */
    @Test
    public void testBug113336() throws Exception {
        createTable("testBug113336", "(id INT PRIMARY KEY, txt VARCHAR(100))");

        boolean rwBS = false;
        do {
            String testCase = String.format("Case [rwBS: %s]", rwBS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), "true");
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));
            try (Connection testConn = getConnectionWithProps(props)) {
                int updCount;
                int[] updCounts;

                Statement testStmt = testConn.createStatement();
                testStmt.addBatch("INSERT INTO testBug113336 VALUES (1, 'Data1')");
                testStmt.addBatch("INSERT INTO testBug113336 VALUES (2, 'Data2'), (3, 'Data3')");
                updCounts = testStmt.executeBatch();
                assertArrayEquals(new int[] { 1, 2 }, updCounts, testCase);
                assertEquals(2, testStmt.getUpdateCount(), testCase);

                updCount = testStmt.executeUpdate("UPDATE testBug113336 SET txt=UPPER(txt) WHERE id=1; DELETE FROM testBug113336 WHERE id = 0; "
                        + "UPDATE testBug113336 SET txt=LOWER(txt) WHERE id >= 2"); // UpdateCount: {1, 0, 2}
                // It might be that the right updateCount should be 2 (the update count from the last query), but this is not how it currently works.
                assertEquals(1, updCount, testCase);
                assertEquals(1, testStmt.getUpdateCount(), testCase);

                testStmt.addBatch("UPDATE testBug113336 SET txt=UPPER(txt) WHERE id <= 2");
                testStmt.addBatch("DELETE FROM testBug113336 WHERE id = 1");
                testStmt.addBatch("INSERT INTO testBug113336 VALUES (4, 'Data4')");
                testStmt.addBatch("DELETE FROM testBug113336 WHERE id <= 3");
                testStmt.addBatch("INSERT INTO testBug113336 VALUES (5, 'Data5'), (6, 'Data6'), (7, 'Data7')");
                updCounts = testStmt.executeBatch();
                assertArrayEquals(new int[] { 2, 1, 1, 2, 3 }, updCounts, testCase);
                assertEquals(3, testStmt.getUpdateCount(), testCase);

                updCount = testStmt.executeUpdate("TRUNCATE TABLE testBug113336");
                assertEquals(0, updCount, testCase);
                assertEquals(0, testStmt.getUpdateCount(), testCase);
            }
        } while (rwBS = !rwBS);
    }

    /**
     * Tests fix for Bug#113130 (Bug#36043125), getGeneratedKeys() returns a zero resultset with non-key-generating statements.
     *
     * @throws SQLException
     */
    @Test
    public void testBug113130() throws SQLException {
        createTable("testBug113130NoAI", "(id INT PRIMARY KEY, data VARCHAR(100))");
        createTable("testBug113130AI", "(id INT AUTO_INCREMENT PRIMARY KEY, data VARCHAR(100))");

        boolean allowMQ = false;
        boolean rwBS = false;
        do {
            String testCase = String.format("Case [allowMQ: %s, rwBS: %s]", allowMQ ? "Y" : "N", rwBS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), Boolean.toString(allowMQ));
            props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), Boolean.toString(rwBS));

            for (int q = 1; q <= 5; q++) {
                try (Connection testConn = getConnectionWithProps(props)) {
                    Statement testStmt = testConn.createStatement();
                    for (int i = 1; i <= q; i++) {
                        testStmt.addBatch(String.format("INSERT INTO testBug113130NoAI VALUES (%d, 'Data%d')", i, i));
                    }
                    int updCounts[] = testStmt.executeBatch();
                    assertEquals(q, updCounts.length, testCase);
                    for (int i = 0; i < q; i++) {
                        assertEquals(1, updCounts[i], testCase);
                    }
                    this.rs = testStmt.getGeneratedKeys();
                    assertFalse(this.rs.next(), testCase);
                }

                try (Connection testConn = getConnectionWithProps(props)) {
                    Statement testStmt = testConn.createStatement();
                    for (int i = 1; i <= q; i++) {
                        testStmt.addBatch(String.format("INSERT INTO testBug113130AI (data) VALUES ('Data%d')", i));
                    }
                    int updCounts[] = testStmt.executeBatch();
                    assertEquals(q, updCounts.length, testCase);
                    for (int i = 0; i < q; i++) {
                        assertEquals(1, updCounts[i], testCase);
                    }
                    this.rs = testStmt.getGeneratedKeys();
                    for (int i = 1; i <= q; i++) {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(i, this.rs.getInt(1), testCase);
                    }
                    assertFalse(this.rs.next(), testCase);
                }

                this.stmt.execute("TRUNCATE TABLE testBug113130NoAI");
                this.stmt.execute("TRUNCATE TABLE testBug113130AI");
                this.stmt.execute("ALTER TABLE testBug113130AI AUTO_INCREMENT = 1");
            }
        } while ((allowMQ = !allowMQ) || (rwBS = !rwBS));
    }

    /**
     * Tests fix for Bug#118002 (Bug#37843004), The setFetchSize() method in the Statement class may have a potential bug.
     *
     * @throws Exception
     */
    @Test
    void testBug118002() throws Exception {
        assumeTrue(versionMeetsMinimum(8, 1), "MySQL 8.1+ is required to run this test.");

        createTable("testBug118002", "(id INT)");
        assertEquals(100, this.stmt.executeUpdate(
                "INSERT INTO testBug118002 (id) WITH RECURSIVE nums AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM nums WHERE n < 100) SELECT n FROM nums"));

        boolean useCF = false;
        boolean useSPS = false;
        do {
            String testCase = String.format("Case [useCF: %s, useSPS: %s]", useCF ? "Y" : "N", useSPS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.useCursorFetch.getKeyName(), Boolean.toString(useCF));
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));

            try (Connection testConn = getConnectionWithProps(props)) {
                Statement testStmt = testConn.createStatement();
                testStmt.setMaxRows(10);
                assertEquals(10, testStmt.getMaxRows(), testCase);
                testStmt.setFetchSize(50);
                assertEquals(50, testStmt.getFetchSize(), testCase);
                this.rs = testStmt.executeQuery("SELECT * FROM testBug118002");
                for (int i = 1; i <= 10; i++) {
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(i, this.rs.getInt(1), testCase);
                }
                assertFalse(this.rs.next(), testCase);

                PreparedStatement testPStmt = testConn.prepareStatement("SELECT * FROM testBug118002");
                testPStmt.setMaxRows(12);
                testPStmt.setFetchSize(60);
                this.rs = testPStmt.executeQuery();
                for (int i = 1; i <= 12; i++) {
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(i, this.rs.getInt(1), testCase);
                }
                assertFalse(this.rs.next(), testCase);
            }
        } while ((useCF = !useCF) || (useSPS = !useSPS));
    }

    /**
     * Tests fix for Bug#119659 (Bug#38916595), BinaryResultsetReader fails to consume EOF packet after column definitions when EOF is not deprecated.
     *
     * @throws Exception
     */
    @Test
    void testBug119659() throws Exception {
        createTable("testBug119659", "(id INT)");
        this.stmt.executeUpdate("INSERT INTO testBug119659 VALUES (1), (2), (3)");

        boolean useCF = false;
        boolean useSPS = false;
        do {
            String testCase = String.format("Case [useCF: %s, useSPS: %s]", useCF ? "Y" : "N", useSPS ? "Y" : "N");

            Properties props = new Properties();
            props.setProperty(PropertyKey.useCursorFetch.getKeyName(), Boolean.toString(useCF));
            props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), Boolean.toString(useSPS));
            try (Connection testConn = getConnectionWithProps(props)) {
                PreparedStatement testPstmt = testConn.prepareStatement("SELECT * FROM testBug119659");
                testPstmt.setFetchSize(3);
                this.rs = testPstmt.executeQuery();
                assertTrue(this.rs.next(), testCase);
                assertEquals(1, this.rs.getInt(1));
                assertTrue(this.rs.next(), testCase);
                assertEquals(2, this.rs.getInt(1));
                assertTrue(this.rs.next(), testCase);
                assertEquals(3, this.rs.getInt(1));
                assertFalse(this.rs.next(), testCase);

                this.rs = testConn.createStatement().executeQuery("SELECT * FROM testBug119659");
                assertTrue(this.rs.next(), testCase);
                assertEquals(1, this.rs.getInt(1));
                assertTrue(this.rs.next(), testCase);
                assertEquals(2, this.rs.getInt(1));
                assertTrue(this.rs.next(), testCase);
                assertEquals(3, this.rs.getInt(1));
                assertFalse(this.rs.next(), testCase);
            }
        } while ((useCF = !useCF) || (useSPS = !useSPS));
    }

}
