/*
 * Copyright (c) 2002, 2024, 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.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.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Supplier;

import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;

import com.mysql.cj.CharsetMappingWrapper;
import com.mysql.cj.Constants;
import com.mysql.cj.MysqlConnection;
import com.mysql.cj.MysqlType;
import com.mysql.cj.NativeCharsetSettings;
import com.mysql.cj.Query;
import com.mysql.cj.conf.PropertyDefinitions.DatabaseTerm;
import com.mysql.cj.conf.PropertyDefinitions.SslMode;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.exceptions.MysqlErrorNumbers;
import com.mysql.cj.jdbc.ClientPreparedStatement;
import com.mysql.cj.jdbc.Driver;
import com.mysql.cj.jdbc.JdbcConnection;
import com.mysql.cj.jdbc.NonRegisteringDriver;
import com.mysql.cj.protocol.Resultset;
import com.mysql.cj.util.StringUtils;

import testsuite.BaseQueryInterceptor;
import testsuite.BaseTestCase;

/**
 * Regression tests for DatabaseMetaData
 */
public class MetaDataRegressionTest extends BaseTestCase {

    @Test
    public void testBug2607() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug2607");
            this.stmt.executeUpdate("CREATE TABLE testBug2607 (field1 INT PRIMARY KEY)");

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

            ResultSetMetaData rsmd = this.rs.getMetaData();

            assertTrue(!rsmd.isAutoIncrement(1));
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug2607");
        }
    }

    /**
     * Tests fix for BUG#2852, where RSMD is not returning correct (or matching) types for TINYINT and SMALLINT.
     *
     * @throws Exception
     */
    @Test
    public void testBug2852() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug2852");
            this.stmt.executeUpdate("CREATE TABLE testBug2852 (field1 TINYINT, field2 SMALLINT)");
            this.stmt.executeUpdate("INSERT INTO testBug2852 VALUES (1,1)");

            this.rs = this.stmt.executeQuery("SELECT * from testBug2852");

            assertTrue(this.rs.next());

            ResultSetMetaData rsmd = this.rs.getMetaData();

            assertTrue(rsmd.getColumnClassName(1).equals(this.rs.getObject(1).getClass().getName()));
            assertTrue("java.lang.Integer".equals(rsmd.getColumnClassName(1)));

            assertTrue(rsmd.getColumnClassName(2).equals(this.rs.getObject(2).getClass().getName()));
            assertTrue("java.lang.Integer".equals(rsmd.getColumnClassName(2)));
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug2852");
        }
    }

    /**
     * Tests fix for BUG#2855, where RSMD is not returning correct (or matching) types for FLOAT.
     *
     * @throws Exception
     */
    @Test
    public void testBug2855() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug2855");
            this.stmt.executeUpdate("CREATE TABLE testBug2855 (field1 FLOAT)");
            this.stmt.executeUpdate("INSERT INTO testBug2855 VALUES (1)");

            this.rs = this.stmt.executeQuery("SELECT * from testBug2855");

            assertTrue(this.rs.next());

            ResultSetMetaData rsmd = this.rs.getMetaData();

            assertTrue(rsmd.getColumnClassName(1).equals(this.rs.getObject(1).getClass().getName()));
            assertTrue("java.lang.Float".equals(rsmd.getColumnClassName(1)));
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug2855");
        }
    }

    /**
     * Tests fix for BUG#3570 -- inconsistent reporting of column type
     *
     * @throws Exception
     */
    @Test
    public void testBug3570() throws Exception {
        String createTableQuery = " CREATE TABLE testBug3570(field_tinyint TINYINT,field_smallint SMALLINT,field_mediumint MEDIUMINT"
                + ",field_int INT,field_integer INTEGER,field_bigint BIGINT,field_real REAL,field_float FLOAT,field_decimal DECIMAL"
                + ",field_numeric NUMERIC,field_double DOUBLE,field_char CHAR(3),field_varchar VARCHAR(255),field_date DATE"
                + ",field_time TIME,field_year YEAR,field_timestamp TIMESTAMP,field_datetime DATETIME,field_tinyblob TINYBLOB"
                + ",field_blob BLOB,field_mediumblob MEDIUMBLOB,field_longblob LONGBLOB,field_tinytext TINYTEXT,field_text TEXT"
                + ",field_mediumtext MEDIUMTEXT,field_longtext LONGTEXT,field_enum ENUM('1','2','3'),field_set SET('1','2','3'))";

        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3570");
            this.stmt.executeUpdate(createTableQuery);

            ResultSet dbmdRs = this.conn.getMetaData().getColumns(this.conn.getCatalog(), null, "testBug3570", "%");

            this.rs = this.stmt.executeQuery("SELECT * FROM testBug3570");

            ResultSetMetaData rsmd = this.rs.getMetaData();

            while (dbmdRs.next()) {
                String columnName = dbmdRs.getString(4);
                int typeFromGetColumns = dbmdRs.getInt(5);
                int typeFromRSMD = rsmd.getColumnType(this.rs.findColumn(columnName));

                //
                // TODO: Server needs to send these types correctly....
                //
                if (!"field_tinyblob".equals(columnName) && !"field_tinytext".equals(columnName)) {
                    assertTrue(typeFromGetColumns == typeFromRSMD,
                            columnName + " -> type from DBMD.getColumns(" + typeFromGetColumns + ") != type from RSMD.getColumnType(" + typeFromRSMD + ")");
                }
            }
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug3570");
        }
    }

    /**
     * Tests char/varchar bug
     *
     * @throws Exception
     */
    @Test
    public void testCharVarchar() throws Exception {
        try {
            this.stmt.execute("DROP TABLE IF EXISTS charVarCharTest");
            this.stmt.execute("CREATE TABLE charVarCharTest (  TableName VARCHAR(64),  FieldName VARCHAR(64),  NextCounter INTEGER);");

            String query = "SELECT TableName, FieldName, NextCounter FROM charVarCharTest";
            this.rs = this.stmt.executeQuery(query);

            ResultSetMetaData rsmeta = this.rs.getMetaData();

            assertTrue(rsmeta.getColumnTypeName(1).equalsIgnoreCase("VARCHAR"));

            // is "CHAR", expected "VARCHAR"
            assertTrue(rsmeta.getColumnType(1) == 12);

            // is 1 (java.sql.Types.CHAR), expected 12 (java.sql.Types.VARCHAR)
        } finally {
            this.stmt.execute("DROP TABLE IF EXISTS charVarCharTest");
        }
    }

    /**
     * Tests fix for BUG#1673, where DatabaseMetaData.getColumns() is not returning correct column ordinal info for non '%' column name patterns.
     *
     * @throws Exception
     */
    @Test
    public void testFixForBug1673() throws Exception {
        createTable("testBug1673", "(field_1 INT, field_2 INT)");

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

            DatabaseMetaData dbmd = con.getMetaData();

            int ordinalPosOfCol2Full = 0;

            this.rs = dbmd.getColumns(con.getCatalog(), null, "testBug1673", null);

            while (this.rs.next()) {
                if (this.rs.getString(4).equals("field_2")) {
                    ordinalPosOfCol2Full = this.rs.getInt(17);
                }
            }

            int ordinalPosOfCol2Scoped = 0;

            this.rs = dbmd.getColumns(con.getCatalog(), null, "testBug1673", "field_2");

            while (this.rs.next()) {
                if (this.rs.getString(4).equals("field_2")) {
                    ordinalPosOfCol2Scoped = this.rs.getInt(17);
                }
            }

            assertTrue(ordinalPosOfCol2Full != 0 && ordinalPosOfCol2Scoped != 0 && ordinalPosOfCol2Scoped == ordinalPosOfCol2Full,
                    "Ordinal position in full column list of '" + ordinalPosOfCol2Full + "' != ordinal position in pattern search, '" + ordinalPosOfCol2Scoped
                            + "'.");

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

    /**
     * Tests bug reported by OpenOffice team with getColumns and LONGBLOB
     *
     * @throws Exception
     */
    @Test
    public void testGetColumns() throws Exception {
        try {
            this.stmt.execute("CREATE TABLE IF NOT EXISTS longblob_regress(field_1 longblob)");

            DatabaseMetaData dbmd = this.conn.getMetaData();
            ResultSet dbmdRs = null;

            try {
                dbmdRs = dbmd.getColumns("", "", "longblob_regress", "%");

                while (dbmdRs.next()) {
                    dbmdRs.getInt(7);
                }
            } finally {
                if (dbmdRs != null) {
                    try {
                        dbmdRs.close();
                    } catch (SQLException ex) {
                    }
                }
            }
        } finally {
            this.stmt.execute("DROP TABLE IF EXISTS longblob_regress");
        }
    }

    /**
     * Tests fix for Bug#1099
     *
     * @throws Exception
     */
    @Test
    public void testGetColumnsBug1099() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testGetColumnsBug1099");

            DatabaseMetaData dbmd = this.conn.getMetaData();

            this.rs = dbmd.getTypeInfo();

            StringBuilder types = new StringBuilder();

            HashMap<String, String> alreadyDoneTypes = new HashMap<>();

            while (this.rs.next()) {
                String typeName = this.rs.getString("TYPE_NAME");
                //String createParams = this.rs.getString("CREATE_PARAMS");

                if (typeName.equals("VECTOR") && !versionMeetsMinimum(9, 0)) {
                    continue;
                }
                if (typeName.indexOf("BINARY") == -1 && !typeName.equals("LONG VARCHAR")) {
                    if (!alreadyDoneTypes.containsKey(typeName)) {
                        alreadyDoneTypes.put(typeName, null);

                        if (types.length() != 0) {
                            types.append(", \n");
                        }

                        int typeNameLength = typeName.length();
                        StringBuilder safeTypeName = new StringBuilder(typeNameLength);

                        for (int i = 0; i < typeNameLength; i++) {
                            char c = typeName.charAt(i);

                            if (Character.isWhitespace(c)) {
                                safeTypeName.append("_");
                            } else {
                                safeTypeName.append(c);
                            }
                        }

                        types.append(safeTypeName.toString());
                        types.append("Column ");
                        types.append(typeName);

                        if (typeName.indexOf("CHAR") != -1) {
                            types.append(" (1)");
                        } else if (typeName.equalsIgnoreCase("enum") || typeName.equalsIgnoreCase("set")) {
                            types.append("('a', 'b', 'c')");
                        }
                    }
                }
            }

            this.stmt.executeUpdate("CREATE TABLE testGetColumnsBug1099(" + types.toString() + ")");

            dbmd.getColumns(null, this.conn.getCatalog(), "testGetColumnsBug1099", "%");
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testGetColumnsBug1099");
        }
    }

    /**
     * Tests whether or not unsigned columns are reported correctly in DBMD.getColumns
     *
     * @throws Exception
     */
    @Test
    public void testGetColumnsUnsigned() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testGetUnsignedCols");
            this.stmt.executeUpdate("CREATE TABLE testGetUnsignedCols (field1 BIGINT, field2 BIGINT UNSIGNED)");

            DatabaseMetaData dbmd = this.conn.getMetaData();

            this.rs = dbmd.getColumns(this.conn.getCatalog(), null, "testGetUnsignedCols", "%");

            assertTrue(this.rs.next());
            // This row doesn't have 'unsigned' attribute
            assertTrue(this.rs.next());
            assertTrue(StringUtils.indexOfIgnoreCase(this.rs.getString(6), "unsigned") != -1);
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testGetUnsignedCols");
        }
    }

    /**
     * Tests whether bogus parameters break Driver.getPropertyInfo().
     *
     * @throws Exception
     */
    @Test
    public void testGetPropertyInfo() throws Exception {
        new Driver().getPropertyInfo("", null);
    }

    /**
     * Tests whether ResultSetMetaData returns correct info for CHAR/VARCHAR columns.
     *
     * @throws Exception
     */
    @Test
    public void testIsCaseSensitive() throws Exception {
        createSchemaObject(this.stmt, "DATABASE", "testIsCaseSensitive", "DEFAULT CHARACTER SET utf8mb4");
        createTable("testIsCaseSensitive.testIsCaseSensitive",
                "(bin_char CHAR(1) BINARY, bin_varchar VARCHAR(64) BINARY, ci_char CHAR(1), ci_varchar VARCHAR(64))");
        createTable("testIsCaseSensitive.testIsCaseSensitiveCs",
                "(bin_char CHAR(1) CHARACTER SET latin1 COLLATE latin1_general_cs, bin_varchar VARCHAR(64) CHARACTER SET latin1 COLLATE latin1_general_cs,"
                        + "ci_char CHAR(1) CHARACTER SET latin1 COLLATE latin1_general_ci,"
                        + "ci_varchar VARCHAR(64) CHARACTER SET latin1 COLLATE latin1_general_ci, "
                        + "bin_tinytext TINYTEXT CHARACTER SET latin1 COLLATE latin1_general_cs, bin_text TEXT CHARACTER SET latin1 COLLATE latin1_general_cs,"
                        + "bin_med_text MEDIUMTEXT CHARACTER SET latin1 COLLATE latin1_general_cs,"
                        + "bin_long_text LONGTEXT CHARACTER SET latin1 COLLATE latin1_general_cs,"
                        + "ci_tinytext TINYTEXT CHARACTER SET latin1 COLLATE latin1_general_ci, ci_text TEXT CHARACTER SET latin1 COLLATE latin1_general_ci,"
                        + "ci_med_text MEDIUMTEXT CHARACTER SET latin1 COLLATE latin1_general_ci,"
                        + "ci_long_text LONGTEXT CHARACTER SET latin1 COLLATE latin1_general_ci)");

        this.rs = this.stmt.executeQuery("SELECT bin_char, bin_varchar, ci_char, ci_varchar FROM testIsCaseSensitive.testIsCaseSensitive");
        ResultSetMetaData rsmd = this.rs.getMetaData();
        assertTrue(rsmd.isCaseSensitive(1));
        assertTrue(rsmd.isCaseSensitive(2));
        assertTrue(!rsmd.isCaseSensitive(3));
        assertTrue(!rsmd.isCaseSensitive(4));

        this.rs = this.stmt.executeQuery("SELECT bin_char, bin_varchar, ci_char, ci_varchar, bin_tinytext, bin_text, bin_med_text, bin_long_text, "
                + "ci_tinytext, ci_text, ci_med_text, ci_long_text FROM testIsCaseSensitive.testIsCaseSensitiveCs");

        rsmd = this.rs.getMetaData();
        assertTrue(rsmd.isCaseSensitive(1));
        assertTrue(rsmd.isCaseSensitive(2));
        assertTrue(!rsmd.isCaseSensitive(3));
        assertTrue(!rsmd.isCaseSensitive(4));

        assertTrue(rsmd.isCaseSensitive(5));
        assertTrue(rsmd.isCaseSensitive(6));
        assertTrue(rsmd.isCaseSensitive(7));
        assertTrue(rsmd.isCaseSensitive(8));

        assertTrue(!rsmd.isCaseSensitive(9));
        assertTrue(!rsmd.isCaseSensitive(10));
        assertTrue(!rsmd.isCaseSensitive(11));
        assertTrue(!rsmd.isCaseSensitive(12));
    }

    /**
     * Tests whether or not DatabaseMetaData.getColumns() returns the correct java.sql.Types info.
     *
     * @throws Exception
     */
    @Test
    public void testLongText() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testLongText");
            this.stmt.executeUpdate("CREATE TABLE testLongText (field1 LONGTEXT)");

            this.rs = this.conn.getMetaData().getColumns(this.conn.getCatalog(), null, "testLongText", "%");

            assertTrue(this.rs.next());

            assertTrue(this.rs.getInt("DATA_TYPE") == java.sql.Types.LONGVARCHAR);
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testLongText");
        }
    }

    /**
     * Tests for types being returned correctly
     *
     * @throws Exception
     */
    @Test
    public void testTypes() throws Exception {
        try {
            this.stmt.execute("DROP TABLE IF EXISTS typesRegressTest");
            this.stmt.execute("CREATE TABLE typesRegressTest (varcharField VARCHAR(32), charField CHAR(2), enumField ENUM('1','2'),"
                    + "setField  SET('1','2','3'), tinyblobField TINYBLOB, mediumBlobField MEDIUMBLOB, longblobField LONGBLOB, blobField BLOB)");

            this.rs = this.stmt.executeQuery("SELECT * from typesRegressTest");

            Map<String, String> expectedTypes = new HashMap<>();
            expectedTypes.put("varcharField", "VARCHAR");
            expectedTypes.put("charField", "CHAR");
            expectedTypes.put("enumField", "CHAR");
            expectedTypes.put("setField", "CHAR");
            expectedTypes.put("tinyblobField", "TINYBLOB");
            expectedTypes.put("mediumBlobField", "MEDIUMBLOB");
            expectedTypes.put("longblobField", "LONGBLOB");
            expectedTypes.put("blobField", "BLOB");

            ResultSetMetaData rsmd = this.rs.getMetaData();

            int numCols = rsmd.getColumnCount();

            for (int i = 0; i < numCols; i++) {
                String columnName = rsmd.getColumnName(i + 1);
                String columnTypeName = rsmd.getColumnTypeName(i + 1);
                assertEquals(expectedTypes.get(columnName), columnTypeName);
            }
        } finally {
            this.stmt.execute("DROP TABLE IF EXISTS typesRegressTest");
        }
    }

    /**
     * Tests fix for BUG#4742, 'DOUBLE' mapped twice in getTypeInfo().
     *
     * @throws Exception
     */
    @Test
    public void testBug4742() throws Exception {
        HashMap<String, String> clashMap = new HashMap<>();

        this.rs = this.conn.getMetaData().getTypeInfo();

        while (this.rs.next()) {
            String name = this.rs.getString(1);
            assertTrue(!clashMap.containsKey(name), "Type represented twice in type info, '" + name + "'.");
            clashMap.put(name, name);
        }
    }

    /**
     * Tests fix for BUG#4138, getColumns() returns incorrect JDBC type for unsigned columns.
     *
     * @throws Exception
     */
    @Test
    public void testBug4138() throws Exception {
        try {
            String[] typesToTest = new String[] { "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT", "FLOAT", "DOUBLE", "DECIMAL" };

            short[] jdbcMapping = new short[] { Types.TINYINT, Types.SMALLINT, Types.INTEGER, Types.INTEGER, Types.BIGINT, Types.REAL, Types.DOUBLE,
                    Types.DECIMAL };

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

            StringBuilder createBuf = new StringBuilder();

            createBuf.append("CREATE TABLE testBug4138 (");

            boolean firstColumn = true;

            for (int i = 0; i < typesToTest.length; i++) {
                if (!firstColumn) {
                    createBuf.append(", ");
                } else {
                    firstColumn = false;
                }

                createBuf.append("field");
                createBuf.append(i + 1);
                createBuf.append(" ");
                createBuf.append(typesToTest[i]);
                createBuf.append(" UNSIGNED");
            }
            createBuf.append(")");
            this.stmt.executeUpdate(createBuf.toString());

            DatabaseMetaData dbmd = this.conn.getMetaData();
            this.rs = dbmd.getColumns(this.conn.getCatalog(), null, "testBug4138", "field%");

            assertTrue(this.rs.next());

            for (int i = 0; i < typesToTest.length; i++) {
                assertTrue(jdbcMapping[i] == this.rs.getShort("DATA_TYPE"), "JDBC Data Type of " + this.rs.getShort("DATA_TYPE") + " for MySQL type '"
                        + this.rs.getString("TYPE_NAME") + "' from 'DATA_TYPE' column does not match expected value of " + jdbcMapping[i] + ".");
                this.rs.next();
            }

            this.rs.close();

            StringBuilder queryBuf = new StringBuilder("SELECT ");
            firstColumn = true;

            for (int i = 0; i < typesToTest.length; i++) {
                if (!firstColumn) {
                    queryBuf.append(", ");
                } else {
                    firstColumn = false;
                }

                queryBuf.append("field");
                queryBuf.append(i + 1);
            }

            queryBuf.append(" FROM testBug4138");

            this.rs = this.stmt.executeQuery(queryBuf.toString());

            ResultSetMetaData rsmd = this.rs.getMetaData();

            for (int i = 0; i < typesToTest.length; i++) {

                assertTrue(jdbcMapping[i] == rsmd.getColumnType(i + 1));
                String desiredTypeName = typesToTest[i] + " unsigned";

                assertTrue(desiredTypeName.equalsIgnoreCase(rsmd.getColumnTypeName(i + 1)), rsmd.getColumnTypeName(i + 1) + " != " + desiredTypeName);
            }
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug4138");
        }
    }

    /**
     * Here for housekeeping only, the test is actually in testBug4138().
     *
     * @throws Exception
     */
    @Test
    public void testBug4860() throws Exception {
        testBug4138();
    }

    /**
     * Tests fix for BUG#4880 - RSMD.getPrecision() returns '0' for non-numeric types.
     *
     * Why-oh-why is this not in the spec, nor the api-docs, but in some 'optional' book, _and_ it is a variance from both ODBC and the ANSI SQL standard :p
     *
     * (from the CTS testsuite)....
     *
     * The getPrecision(int colindex) method returns an integer value representing the number of decimal digits for number types,maximum length in characters
     * for character types,maximum length in bytes for JDBC binary datatypes.
     *
     * (See Section 27.3 of JDBC 2.0 API Reference & Tutorial 2nd edition)
     *
     * @throws Exception
     */
    @Test
    public void testBug4880() throws Exception {
        createSchemaObject(this.stmt, "DATABASE", "testBug4880Db", "DEFAULT CHARACTER SET latin1");
        createTable("testBug4880Db.testBug4880", "(field1 VARCHAR(80), field2 TINYBLOB, field3 BLOB, field4 MEDIUMBLOB, field5 LONGBLOB)");

        this.rs = this.stmt.executeQuery("SELECT field1, field2, field3, field4, field5 FROM testBug4880Db.testBug4880");
        ResultSetMetaData rsmd = this.rs.getMetaData();

        assertEquals(80, rsmd.getPrecision(1));
        assertEquals(Types.VARCHAR, rsmd.getColumnType(1));
        assertEquals(80, rsmd.getColumnDisplaySize(1));

        assertEquals(255, rsmd.getPrecision(2));
        assertEquals(Types.VARBINARY, rsmd.getColumnType(2));
        assertTrue("TINYBLOB".equalsIgnoreCase(rsmd.getColumnTypeName(2)));
        assertEquals(255, rsmd.getColumnDisplaySize(2));

        assertEquals(65535, rsmd.getPrecision(3));
        assertEquals(Types.LONGVARBINARY, rsmd.getColumnType(3));
        assertTrue("BLOB".equalsIgnoreCase(rsmd.getColumnTypeName(3)));
        assertEquals(65535, rsmd.getColumnDisplaySize(3));

        assertEquals(16777215, rsmd.getPrecision(4));
        assertEquals(Types.LONGVARBINARY, rsmd.getColumnType(4));
        assertTrue("MEDIUMBLOB".equalsIgnoreCase(rsmd.getColumnTypeName(4)));
        assertEquals(16777215, rsmd.getColumnDisplaySize(4));

        // Server doesn't send us enough information to detect LONGBLOB type
        assertEquals(Integer.MAX_VALUE, rsmd.getPrecision(5));
        assertEquals(Types.LONGVARBINARY, rsmd.getColumnType(5));
        assertTrue("LONGBLOB".equalsIgnoreCase(rsmd.getColumnTypeName(5)));
        assertEquals(Integer.MAX_VALUE, rsmd.getColumnDisplaySize(5));
    }

    /**
     * Tests fix for BUG#6399, ResultSetMetaData.getDisplaySize() is wrong for multi-byte charsets.
     *
     * @throws Exception
     */
    @Test
    public void testBug6399() throws Exception {
        try {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug6399");
            this.stmt.executeUpdate(
                    "CREATE TABLE testBug6399 (field1 CHAR(3) CHARACTER SET UTF8, field2 CHAR(3) CHARACTER SET LATIN1, field3 CHAR(3) CHARACTER SET SJIS)");
            this.stmt.executeUpdate("INSERT INTO testBug6399 VALUES ('a', 'a', 'a')");

            this.rs = this.stmt.executeQuery("SELECT field1, field2, field3 FROM testBug6399");
            ResultSetMetaData rsmd = this.rs.getMetaData();

            assertEquals(3, rsmd.getColumnDisplaySize(1));
            assertEquals(3, rsmd.getColumnDisplaySize(2));
            assertEquals(3, rsmd.getColumnDisplaySize(3));
        } finally {
            this.stmt.executeUpdate("DROP TABLE IF EXISTS testBug6399");
        }
    }

    /**
     * Tests fix for BUG#7081, DatabaseMetaData.getIndexInfo() ignoring 'unique' parameters.
     *
     * @throws Exception
     */
    @Test
    public void testBug7081() throws Exception {
        String tableName = "testBug7081";

        try {
            createTable(tableName, "(field1 INT, INDEX(field1))");

            DatabaseMetaData dbmd = this.conn.getMetaData();
            this.rs = dbmd.getIndexInfo(this.conn.getCatalog(), null, tableName, true, false);
            assertTrue(!this.rs.next()); // there should be no rows that meet this requirement

            this.rs = dbmd.getIndexInfo(this.conn.getCatalog(), null, tableName, false, false);
            assertTrue(this.rs.next()); // there should be one row that meets this requirement
            assertTrue(!this.rs.next());

        } finally {
            dropTable(tableName);
        }
    }

    /**
     * Tests fix for BUG#7033 - PreparedStatements don't encode Big5 (and other multibyte) character sets correctly in static SQL strings.
     *
     * @throws Exception
     */
    @Test
    public void testBug7033() throws Exception {
        Connection big5Conn = null;
        Statement big5Stmt = null;
        PreparedStatement big5PrepStmt = null;

        String testString = "\u5957 \u9910";

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

            big5Conn = getConnectionWithProps(props);
            big5Stmt = big5Conn.createStatement();

            this.rs = big5Stmt.executeQuery("select 1 as '\u5957 \u9910'");
            String retrString = this.rs.getMetaData().getColumnName(1);
            assertTrue(testString.equals(retrString));

            big5PrepStmt = big5Conn.prepareStatement("select 1 as '\u5957 \u9910'");
            this.rs = big5PrepStmt.executeQuery();
            retrString = this.rs.getMetaData().getColumnName(1);
            assertTrue(testString.equals(retrString));
        } finally {
            if (this.rs != null) {
                this.rs.close();
                this.rs = null;
            }

            if (big5Stmt != null) {
                big5Stmt.close();

            }

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

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

    /**
     * Tests fix for Bug#8812, DBMD.getIndexInfo() returning inverted values for 'NON_UNIQUE' column.
     *
     * @throws Exception
     */
    @Test
    public void testBug8812() throws Exception {
        String tableName = "testBug8812";

        try {
            createTable(tableName, "(field1 INT, field2 INT, INDEX(field1), UNIQUE INDEX(field2))");

            DatabaseMetaData dbmd = this.conn.getMetaData();
            this.rs = dbmd.getIndexInfo(this.conn.getCatalog(), null, tableName, true, false);
            assertTrue(this.rs.next()); // there should be one row that meets this requirement
            assertEquals(this.rs.getBoolean("NON_UNIQUE"), false);

            this.rs = dbmd.getIndexInfo(this.conn.getCatalog(), null, tableName, false, false);
            assertTrue(this.rs.next()); // there should be two rows that meets this requirement
            assertEquals(this.rs.getBoolean("NON_UNIQUE"), false);
            assertTrue(this.rs.next());
            assertEquals(this.rs.getBoolean("NON_UNIQUE"), true);

        } finally {
            dropTable(tableName);
        }
    }

    /**
     * Tests fix for BUG#8800 - supportsMixedCase*Identifiers() returns wrong value on servers running on case-sensitive filesystems.
     *
     * @throws Exception
     */
    @Test
    public void testBug8800() throws Exception {
        assertEquals(((com.mysql.cj.jdbc.JdbcConnection) this.conn).lowerCaseTableNames(), !this.conn.getMetaData().supportsMixedCaseIdentifiers());
        assertEquals(((com.mysql.cj.jdbc.JdbcConnection) this.conn).lowerCaseTableNames(), !this.conn.getMetaData().supportsMixedCaseQuotedIdentifiers());
    }

    /**
     * Tests fix for BUG#8792 - DBMD.supportsResultSetConcurrency() not returning true for forward-only/read-only result sets (we obviously support this).
     *
     * @throws Exception
     */
    @Test
    public void testBug8792() throws Exception {
        DatabaseMetaData dbmd = this.conn.getMetaData();

        assertTrue(dbmd.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY));

        assertTrue(dbmd.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE));

        assertTrue(dbmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY));

        assertTrue(dbmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE));

        assertTrue(!dbmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY));

        assertTrue(!dbmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE));

        // Check error conditions
        try {
            dbmd.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, Integer.MIN_VALUE);
            fail("Exception should've been raised for bogus concurrency value");
        } catch (SQLException sqlEx) {
            assertTrue(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT.equals(sqlEx.getSQLState()));
        }

        try {
            assertTrue(dbmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, Integer.MIN_VALUE));
            fail("Exception should've been raised for bogus concurrency value");
        } catch (SQLException sqlEx) {
            assertTrue(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT.equals(sqlEx.getSQLState()));
        }

        try {
            assertTrue(dbmd.supportsResultSetConcurrency(Integer.MIN_VALUE, Integer.MIN_VALUE));
            fail("Exception should've been raised for bogus concurrency value");
        } catch (SQLException sqlEx) {
            assertTrue(MysqlErrorNumbers.SQLSTATE_CONNJ_ILLEGAL_ARGUMENT.equals(sqlEx.getSQLState()));
        }
    }

    /**
     * Tests fix for BUG#8803, 'DATA_TYPE' column from DBMD.getBestRowIdentifier() causes ArrayIndexOutOfBoundsException when accessed (and in fact, didn't
     * return any value).
     *
     * @throws Exception
     */
    @Test
    public void testBug8803() throws Exception {
        String tableName = "testBug8803";
        createTable(tableName, "(field1 INT NOT NULL PRIMARY KEY)");
        DatabaseMetaData metadata = this.conn.getMetaData();
        try {
            this.rs = metadata.getBestRowIdentifier(this.conn.getCatalog(), null, tableName, DatabaseMetaData.bestRowNotPseudo, true);

            assertTrue(this.rs.next());

            this.rs.getInt("DATA_TYPE"); // **** Fails here *****
        } finally {
            if (this.rs != null) {
                this.rs.close();

                this.rs = null;
            }
        }
    }

    /**
     * Tests fix for BUG#9320 - PreparedStatement.getMetaData() inserts blank row in database under certain conditions when not using server-side prepared
     * statements.
     *
     * @throws Exception
     */
    @Test
    public void testBug9320() throws Exception {
        createTable("testBug9320", "(field1 int)");

        testAbsenceOfMetadataForQuery("INSERT INTO testBug9320 VALUES (?)");
        testAbsenceOfMetadataForQuery("UPDATE testBug9320 SET field1=?");
        testAbsenceOfMetadataForQuery("DELETE FROM testBug9320 WHERE field1=?");
    }

    /**
     * Tests fix for BUG#9778, DBMD.getTables() shouldn't return tables if views are asked for, even if the database version doesn't support views.
     *
     * @throws Exception
     */
    @Test
    public void testBug9778() throws Exception {
        String tableName = "testBug9778";

        try {
            createTable(tableName, "(field1 int)");
            this.rs = this.conn.getMetaData().getTables(null, null, tableName, new String[] { "VIEW" });
            assertEquals(false, this.rs.next());

            this.rs = this.conn.getMetaData().getTables(null, null, tableName, new String[] { "TABLE" });
            assertEquals(true, this.rs.next());
        } finally {
            if (this.rs != null) {
                this.rs.close();
                this.rs = null;
            }
        }
    }

    /**
     * Tests fix for BUG#9769 - Should accept null for procedureNamePattern, even though it isn't JDBC compliant, for legacy's sake.
     *
     * @throws Exception
     */
    @Test
    public void testBug9769() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        Connection con = getConnectionWithProps(props);
        try {

            con.getMetaData().getProcedures(con.getCatalog(), "%", null);

            // TODO: Check results

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

        // FIXME: Other methods to test
        // getColumns();
        // getTablePrivileges();
        // getTables();
    }

    /**
     * Tests fix for BUG#9917 - Should accept null for catalog in DBMD methods, even though it's not JDBC-compliant for legacy's sake.
     *
     * @throws Exception
     */
    @Test
    public void testBug9917() throws Exception {
        String dbname = "testBug9917db";
        String tableName = "testBug9917table";
        try {
            // test defaults
            this.stmt.executeUpdate("DROP DATABASE IF EXISTS " + dbname);
            this.stmt.executeUpdate("CREATE DATABASE " + dbname);

            boolean defaultDbConfig = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).getPropertySet().getBooleanProperty(PropertyKey.nullDatabaseMeansCurrent)
                    .getValue();
            assertEquals(false, defaultDbConfig);

            // we use the table name which also exists in 'mysql' database
            createTable(dbname + "." + tableName, "(field1 int)");
            createTable(tableName, "(field1 int)");
            String currentDb = ((JdbcConnection) this.conn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                    .getValue() == DatabaseTerm.SCHEMA ? this.conn.getSchema() : this.conn.getCatalog();

            // default 'false' means 'any database'
            // we should get at least two rows here
            this.rs = this.conn.getMetaData().getTables(null, null, tableName, null);
            int totalCnt = 0;
            int expectedCnt = 0;
            while (this.rs.next()) {
                String currDb = ((JdbcConnection) this.conn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                        .getValue() == DatabaseTerm.SCHEMA ? this.rs.getString("TABLE_SCHEM") : this.rs.getString("TABLE_CAT");
                if (currentDb.equalsIgnoreCase(currDb) || dbname.equalsIgnoreCase(currDb)) {
                    expectedCnt++;
                }
                totalCnt++;
            }
            assertEquals(2, expectedCnt);
            assertTrue(totalCnt >= 2);
            this.rs.close();

            // 'true' means only current database to be checked
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");
            Connection con = getConnectionWithProps(props);
            try {
                this.rs = con.getMetaData().getTables(null, null, tableName, null);
                while (this.rs.next()) {
                    String currDb = ((JdbcConnection) this.conn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                            .getValue() == DatabaseTerm.SCHEMA ? this.rs.getString("TABLE_SCHEM") : this.rs.getString("TABLE_CAT");
                    assertTrue(currentDb.equalsIgnoreCase(currDb));
                }

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

        } finally {
            this.stmt.executeUpdate("DROP DATABASE IF EXISTS " + dbname);
        }

        // FIXME: Other methods to test
        //
        // getBestRowIdentifier()
        // getColumns()
        // getCrossReference()
        // getExportedKeys()
        // getImportedKeys()
        // getIndexInfo()
        // getPrimaryKeys()
        // getProcedures()
    }

    /**
     * Tests fix for BUG#11575 -- DBMD.storesLower/Mixed/UpperIdentifiers() reports incorrect values for servers deployed on Windows.
     *
     * @throws Exception
     */
    @Test
    public void testBug11575() throws Exception {
        DatabaseMetaData dbmd = this.conn.getMetaData();

        if (isServerRunningOnWindows()) {
            assertEquals(true, dbmd.storesLowerCaseIdentifiers());
            assertEquals(true, dbmd.storesLowerCaseQuotedIdentifiers());
            assertEquals(false, dbmd.storesMixedCaseIdentifiers());
            assertEquals(false, dbmd.storesMixedCaseQuotedIdentifiers());
            assertEquals(false, dbmd.storesUpperCaseIdentifiers());
            assertEquals(false, dbmd.storesUpperCaseQuotedIdentifiers());
        } else {
            assertEquals(false, dbmd.storesLowerCaseIdentifiers());
            assertEquals(false, dbmd.storesLowerCaseQuotedIdentifiers());
            assertEquals(true, dbmd.storesMixedCaseIdentifiers());
            assertEquals(true, dbmd.storesMixedCaseQuotedIdentifiers());
            assertEquals(false, dbmd.storesUpperCaseIdentifiers());
            assertEquals(false, dbmd.storesUpperCaseQuotedIdentifiers());
        }
    }

    /**
     * Tests fix for BUG#11781, foreign key information that is quoted is parsed incorrectly.
     *
     * @throws Exception
     */
    @Test
    public void testBug11781() throws Exception {
        createTable("`app tab`", "(C1 INT(11) NOT NULL PRIMARY KEY, C2 INT(11) NULL, INDEX NEWINX1 (C1), INDEX NEWINX2 (C1, C2))", "InnoDB");

        this.stmt.executeUpdate("ALTER TABLE `app tab` ADD CONSTRAINT APPFK FOREIGN KEY (C2) REFERENCES `app tab` (C1)");

        /*
         * this.rs = this.conn.getMetaData().getCrossReference(
         * this.conn.getCatalog(), null, "app tab", this.conn.getCatalog(),
         * null, "app tab");
         */

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        for (boolean useIS : new boolean[] { false, true }) {
            for (String databaseTerm : new String[] { "CATALOG", "SCHEMA" }) {
                props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
                props.setProperty(PropertyKey.databaseTerm.getKeyName(), databaseTerm);

                Connection con = getConnectionWithProps(props);

                String db = databaseTerm.contentEquals("SCHEMA") ? con.getSchema() : con.getCatalog();
                this.rs = ((com.mysql.cj.jdbc.DatabaseMetaData) con.getMetaData()).extractForeignKeyFromCreateTable(db, "app tab");
                assertTrue(this.rs.next(), "must return a row");

                assertEquals(("Key info; APPFK(`C2`) REFER `" + db + "`/`app tab`(`C1`)").toUpperCase(), this.rs.getString(3).toUpperCase());

                this.rs.close();

                this.rs = databaseTerm.contentEquals("SCHEMA") ? con.getMetaData().getImportedKeys(null, con.getSchema(), "app tab")
                        : con.getMetaData().getImportedKeys(con.getCatalog(), null, "app tab");

                assertTrue(this.rs.next());

                this.rs = databaseTerm.contentEquals("SCHEMA") ? con.getMetaData().getExportedKeys(null, con.getSchema(), "app tab")
                        : con.getMetaData().getExportedKeys(con.getCatalog(), null, "app tab");

                assertTrue(this.rs.next());

            }
        }
    }

    /**
     * Tests fix for BUG#12970 - java.sql.Types.OTHER returned for binary and varbinary columns.
     *
     * @throws Exception
     */
    @Test
    public void testBug12970() throws Exception {
        String tableName = "testBug12970";

        createTable(tableName, "(binary_field BINARY(32), varbinary_field VARBINARY(64))");

        try {
            this.rs = this.conn.getMetaData().getColumns(this.conn.getCatalog(), null, tableName, "%");
            assertTrue(this.rs.next());
            assertEquals(Types.BINARY, this.rs.getInt("DATA_TYPE"));
            assertEquals(32, this.rs.getInt("COLUMN_SIZE"));
            assertTrue(this.rs.next());
            assertEquals(Types.VARBINARY, this.rs.getInt("DATA_TYPE"));
            assertEquals(64, this.rs.getInt("COLUMN_SIZE"));
            this.rs.close();

            this.rs = this.stmt.executeQuery("SELECT binary_field, varbinary_field FROM " + tableName);
            ResultSetMetaData rsmd = this.rs.getMetaData();
            assertEquals(Types.BINARY, rsmd.getColumnType(1));
            assertEquals(32, rsmd.getPrecision(1));
            assertEquals(Types.VARBINARY, rsmd.getColumnType(2));
            assertEquals(64, rsmd.getPrecision(2));
            this.rs.close();
        } finally {
            if (this.rs != null) {
                this.rs.close();
            }
        }
    }

    /**
     * Tests fix for BUG#12975 - OpenOffice expects DBMD.supportsIEF() to return "true" if foreign keys are supported by the datasource, even though this method
     * also covers support for check constraints, which MySQL _doesn't_ have.
     *
     * @throws Exception
     */
    @Test
    public void testBug12975() throws Exception {
        assertEquals(false, this.conn.getMetaData().supportsIntegrityEnhancementFacility());

        Connection overrideConn = null;

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

            overrideConn = getConnectionWithProps(props);
            assertEquals(true, overrideConn.getMetaData().supportsIntegrityEnhancementFacility());
        } finally {
            if (overrideConn != null) {
                overrideConn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#13277 - RSMD for generated keys has NPEs when a connection is referenced.
     *
     * @throws Exception
     */
    @Test
    public void testBug13277() throws Exception {
        createTable("testBug13277", "(field1 INT NOT NULL PRIMARY KEY AUTO_INCREMENT, field2 VARCHAR(32))");

        try {
            this.stmt.executeUpdate("INSERT INTO testBug13277 (field2) VALUES ('abcdefg')", Statement.RETURN_GENERATED_KEYS);

            this.rs = this.stmt.getGeneratedKeys();

            ResultSetMetaData rsmd = this.rs.getMetaData();
            checkRsmdForBug13277(rsmd);
            this.rs.close();

            for (int i = 0; i < 5; i++) {
                this.stmt.addBatch("INSERT INTO testBug13277 (field2) VALUES ('abcdefg')");
            }

            this.stmt.executeBatch();

            this.rs = this.stmt.getGeneratedKeys();

            rsmd = this.rs.getMetaData();
            checkRsmdForBug13277(rsmd);
            this.rs.close();

            this.pstmt = this.conn.prepareStatement("INSERT INTO testBug13277 (field2) VALUES ('abcdefg')", Statement.RETURN_GENERATED_KEYS);
            this.pstmt.executeUpdate();

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

            rsmd = this.rs.getMetaData();
            checkRsmdForBug13277(rsmd);
            this.rs.close();

            this.pstmt.addBatch();
            this.pstmt.addBatch();

            this.pstmt.executeUpdate();

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

            rsmd = this.rs.getMetaData();
            checkRsmdForBug13277(rsmd);
            this.rs.close();

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

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

    /**
     * Tests BUG13601 (which doesn't seem to be present in 3.1.11, but we'll leave it in here for regression's-sake).
     *
     * @throws Exception
     */
    @Test
    public void testBug13601() throws Exception {
        createTable("testBug13601", "(field1 BIGINT NOT NULL, field2 BIT default 0 NOT NULL) ENGINE=MyISAM");

        this.rs = this.stmt.executeQuery("SELECT field1, field2 FROM testBug13601 WHERE 1=-1");
        ResultSetMetaData rsmd = this.rs.getMetaData();
        assertEquals(Types.BIT, rsmd.getColumnType(2));
        assertEquals(Boolean.class.getName(), rsmd.getColumnClassName(2));

        this.rs = this.conn.prepareStatement("SELECT field1, field2 FROM testBug13601 WHERE 1=-1").executeQuery();
        rsmd = this.rs.getMetaData();
        assertEquals(Types.BIT, rsmd.getColumnType(2));
        assertEquals(Boolean.class.getName(), rsmd.getColumnClassName(2));
    }

    /**
     * Tests fix for BUG#14815 - DBMD.getColumns() doesn't return TABLE_NAME correctly.
     *
     * @throws Exception
     */
    @Test
    public void testBug14815() throws Exception {
        try {
            createTable("testBug14815_1", "(field_1_1 int)");
            createTable("testBug14815_2", "(field_2_1 int)");

            boolean lcTableNames = this.conn.getMetaData().storesLowerCaseIdentifiers();

            String tableName1 = lcTableNames ? "testbug14815_1" : "testBug14815_1";
            String tableName2 = lcTableNames ? "testbug14815_2" : "testBug14815_2";

            this.rs = this.conn.getMetaData().getColumns(this.conn.getCatalog(), null, "testBug14815%", "%");

            assertTrue(this.rs.next());
            assertEquals(tableName1, this.rs.getString("TABLE_NAME"));
            assertEquals("field_1_1", this.rs.getString("COLUMN_NAME"));

            assertTrue(this.rs.next());
            assertEquals(tableName2, this.rs.getString("TABLE_NAME"));
            assertEquals("field_2_1", this.rs.getString("COLUMN_NAME"));

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

    /**
     * Tests fix for BUG#15854 - DBMD.getColumns() returns wrong type for BIT.
     *
     * @throws Exception
     */
    @Test
    public void testBug15854() throws Exception {
        createTable("testBug15854", "(field1 BIT)");
        try {
            this.rs = this.conn.getMetaData().getColumns(this.conn.getCatalog(), null, "testBug15854", "field1");
            assertTrue(this.rs.next());
            assertEquals(Types.BIT, this.rs.getInt("DATA_TYPE"));
        } finally {
            if (this.rs != null) {
                ResultSet toClose = this.rs;
                this.rs = null;
                toClose.close();
            }
        }
    }

    /**
     * Tests fix for BUG#16277 - Invalid classname returned for RSMD.getColumnClassName() for BIGINT type.
     *
     * @throws Exception
     */
    @Test
    public void testBug16277() throws Exception {
        createTable("testBug16277", "(field1 BIGINT, field2 BIGINT UNSIGNED)");
        ResultSetMetaData rsmd = this.stmt.executeQuery("SELECT field1, field2 FROM testBug16277").getMetaData();
        assertEquals("java.lang.Long", rsmd.getColumnClassName(1));
        assertEquals("java.math.BigInteger", rsmd.getColumnClassName(2));
    }

    /**
     * Tests fix for BUG#18554 - Aliased column names where length of name > 251 are corrupted.
     *
     * @throws Exception
     */
    @Test
    public void testBug18554() throws Exception {
        testBug18554(249);
        testBug18554(250);
        testBug18554(251);
        testBug18554(252);
        testBug18554(253);
        testBug18554(254);
        testBug18554(255);
    }

    private void testBug18554(int columnNameLength) throws Exception {
        StringBuilder buf = new StringBuilder(columnNameLength + 2);

        for (int i = 0; i < columnNameLength; i++) {
            buf.append((char) (Math.random() * 26 + 65));
        }

        String colName = buf.toString();
        this.rs = this.stmt.executeQuery("select curtime() as `" + colName + "`");
        ResultSetMetaData meta = this.rs.getMetaData();

        assertEquals(colName, meta.getColumnLabel(1));
    }

    private void checkRsmdForBug13277(ResultSetMetaData rsmd) throws SQLException {
        int i = ((com.mysql.cj.jdbc.ConnectionImpl) this.conn).getSession().getServerSession().getCharsetSettings()
                .getMaxBytesPerChar(CharsetMappingWrapper.getStaticJavaEncodingForMysqlCharset(
                        ((NativeCharsetSettings) ((com.mysql.cj.jdbc.JdbcConnection) this.conn).getSession().getServerSession().getCharsetSettings())
                                .getServerDefaultCharset()));
        if (i == 1) {
            // This is INT field but still processed in
            // ResultsetMetaData.getColumnDisplaySize
            assertEquals(20, rsmd.getColumnDisplaySize(1));
        }

        assertEquals(false, rsmd.isDefinitelyWritable(1));
        assertEquals(true, rsmd.isReadOnly(1));
        assertEquals(false, rsmd.isWritable(1));
    }

    @Test
    public void testSupportsCorrelatedSubqueries() throws Exception {
        DatabaseMetaData dbmd = this.conn.getMetaData();

        assertEquals(true, dbmd.supportsCorrelatedSubqueries());
    }

    @Test
    public void testSupportesGroupByUnrelated() throws Exception {
        DatabaseMetaData dbmd = this.conn.getMetaData();

        assertEquals(true, dbmd.supportsGroupByUnrelated());
    }

    /**
     * Tests fix for BUG#21267, ParameterMetaData throws NullPointerException when prepared SQL actually has a syntax error
     *
     * @throws Exception
     */
    @Test
    public void testBug21267() throws Exception {
        createTable("bug21267", "(`Col1` int(11) NOT NULL,`Col2` varchar(45) default NULL,`Col3` varchar(45) default NULL,PRIMARY KEY  (`Col1`))");

        this.pstmt = this.conn.prepareStatement("SELECT Col1, Col2,Col4 FROM bug21267 WHERE Col1=?");
        this.pstmt.setInt(1, 1);

        java.sql.ParameterMetaData psMeta = this.pstmt.getParameterMetaData();

        try {
            assertEquals(0, psMeta.getParameterType(1));
        } catch (SQLException sqlEx) {
            assertEquals(MysqlErrorNumbers.SQLSTATE_CONNJ_DRIVER_NOT_CAPABLE, sqlEx.getSQLState());
        }

        this.pstmt.close();

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

        this.pstmt = getConnectionWithProps(props).prepareStatement("SELECT Col1, Col2,Col4 FROM bug21267 WHERE Col1=?");

        psMeta = this.pstmt.getParameterMetaData();

        assertEquals(Types.VARCHAR, psMeta.getParameterType(1));
    }

    /**
     * Tests fix for BUG#21544 - When using information_schema for metadata, COLUMN_SIZE for getColumns() is not clamped to range
     * of java.lang.Integer as is the case when not using information_schema, thus leading to a truncation exception that
     * isn't present when not using information_schema.
     *
     * @throws Exception
     */
    @Test
    public void testBug21544() throws Exception {
        createTable("testBug21544", "(foo_id INT NOT NULL, stuff LONGTEXT, PRIMARY KEY (foo_id))", "INNODB");

        Connection infoSchemConn = null;

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

        infoSchemConn = getConnectionWithProps(props);

        try {
            this.rs = infoSchemConn.getMetaData().getColumns(null, null, "testBug21544", null);

            while (this.rs.next()) {
                this.rs.getInt("COLUMN_SIZE");
            }
        } finally {
            if (infoSchemConn != null) {
                infoSchemConn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#22613 - DBMD.getColumns() does not return expected COLUMN_SIZE for the SET type (fixed to be consistent with the ODBC driver)
     *
     * @throws Exception
     */
    @Test
    public void testBug22613() throws Exception {
        createTable("bug22613",
                "( s set('a','bc','def','ghij') default NULL, t enum('a', 'ab', 'cdef'), s2 SET('1','2','3','4','1585','ONE','TWO','Y','N','THREE'))");

        checkMetadataForBug22613(this.conn);

        Connection infoSchemConn = null;

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

            infoSchemConn = getConnectionWithProps(props);

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

    private void checkMetadataForBug22613(Connection c) throws Exception {
        String maxValue = "a,bc,def,ghij";
        String maxValue2 = "1,2,3,4,1585,ONE,TWO,Y,N,THREE";

        DatabaseMetaData meta = c.getMetaData();
        this.rs = meta.getColumns(null, this.conn.getCatalog(), "bug22613", "s");
        this.rs.first();

        assertEquals(maxValue.length(), this.rs.getInt("COLUMN_SIZE"));

        this.rs = meta.getColumns(null, this.conn.getCatalog(), "bug22613", "s2");
        this.rs.first();

        assertEquals(maxValue2.length(), this.rs.getInt("COLUMN_SIZE"));

        this.rs = meta.getColumns(null, c.getCatalog(), "bug22613", "t");
        this.rs.first();

        assertEquals(4, this.rs.getInt("COLUMN_SIZE"));
    }

    /**
     * Fix for BUG#22628 - Driver.getPropertyInfo() throws NullPointerException for URL that only specifies host and/or port.
     *
     * @throws Exception
     */
    @Test
    public void testBug22628() throws Exception {
        DriverPropertyInfo[] dpi = new NonRegisteringDriver().getPropertyInfo("jdbc:mysql://bogus:9999", new Properties());

        boolean foundHost = false;
        boolean foundPort = false;

        for (int i = 0; i < dpi.length; i++) {
            if ("bogus".equals(dpi[i].value)) {
                foundHost = true;
            }

            if ("9999".equals(dpi[i].value)) {
                foundPort = true;
            }
        }

        assertTrue(foundHost && foundPort);
    }

    private void testAbsenceOfMetadataForQuery(String query) throws Exception {
        try {
            this.pstmt = this.conn.prepareStatement(query);
            ResultSetMetaData rsmd = this.pstmt.getMetaData();

            assertNull(rsmd);

            this.pstmt = ((com.mysql.cj.jdbc.JdbcConnection) this.conn).clientPrepareStatement(query);
            rsmd = this.pstmt.getMetaData();

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

    @Test
    public void testRSMDToStringFromDBMD() throws Exception {
        this.rs = this.conn.getMetaData().getTypeInfo();

        this.rs.getMetaData().toString(); // used to cause NPE
    }

    @Test
    public void testCharacterSetForDBMD() throws Exception {
        String quoteChar = this.conn.getMetaData().getIdentifierQuoteString();

        String tableName = "\u00e9\u0074\u00e9";
        String quotedTableName = quoteChar + tableName + quoteChar;
        createTable(quotedTableName, "(field1 int)");
        this.rs = this.conn.getMetaData().getTables(this.conn.getCatalog(), null, "%", new String[] { "TABLE" });
        boolean tableFound = false;
        while (this.rs.next() && !tableFound) {
            tableFound = tableName.equals(this.rs.getString("TABLE_NAME")) && tableName.equals(new String(this.rs.getBytes("TABLE_NAME"), "UTF-8"));
        }
        assertTrue(tableFound);
        this.rs = this.conn.getMetaData().getTables(this.conn.getCatalog(), null, quotedTableName, new String[] { "TABLE" });
        assertEquals(true, this.rs.next());
        assertEquals(tableName, this.rs.getString("TABLE_NAME"));
        assertEquals(tableName, new String(this.rs.getBytes("TABLE_NAME"), "UTF-8"));
    }

    /**
     * Tests fix for BUG#18258 - Nonexistent catalog/database causes SQLException to be raised, rather than returning empty result set.
     *
     * @throws Exception
     */
    @Test
    public void testBug18258() throws Exception {
        String bogusDatabaseName = "abcdefghijklmnopqrstuvwxyz";
        this.conn.getMetaData().getTables(bogusDatabaseName, "%", "%", new String[] { "TABLE", "VIEW" });
        this.conn.getMetaData().getColumns(bogusDatabaseName, "%", "%", "%");
        this.conn.getMetaData().getProcedures(bogusDatabaseName, "%", "%");
    }

    /**
     * Tests fix for BUG#23303 - DBMD.getSchemas() doesn't return a TABLE_CATALOG column.
     *
     * @throws Exception
     */
    @Test
    public void testBug23303() throws Exception {
        this.rs = this.conn.getMetaData().getSchemas();
        this.rs.findColumn("TABLE_CATALOG");
    }

    /**
     * Tests fix for BUG#23304 - DBMD using "show" and DBMD using information_schema do not return results consistent with each other.
     *
     * (note this fix only addresses the inconsistencies, not the issue that the driver is treating schemas differently than some users expect.
     *
     * We will revisit this behavior when there is full support for schemas in MySQL).
     *
     * @throws Exception
     */
    @Test
    public void testBug23304() throws Exception {
        Connection connShow = null;
        Connection connInfoSchema = null;

        ResultSet rsShow = null;
        ResultSet rsInfoSchema = null;

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

            Properties infoSchemaProps = new Properties();
            infoSchemaProps.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            infoSchemaProps.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            infoSchemaProps.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
            infoSchemaProps.setProperty(PropertyKey.dumpQueriesOnException.getKeyName(), "true");

            connShow = getConnectionWithProps(noInfoSchemaProps);
            connInfoSchema = getConnectionWithProps(infoSchemaProps);

            DatabaseMetaData dbmdUsingShow = connShow.getMetaData();
            DatabaseMetaData dbmdUsingInfoSchema = connInfoSchema.getMetaData();

            assertNotSame(dbmdUsingShow.getClass(), dbmdUsingInfoSchema.getClass());

            rsShow = dbmdUsingShow.getSchemas();
            rsInfoSchema = dbmdUsingInfoSchema.getSchemas();

            compareResultSets(rsShow, rsInfoSchema);

            /*
             * rsShow = dbmdUsingShow.getTables(connShow.getCatalog(), null,
             * "%", new String[] {"TABLE", "VIEW"}); rsInfoSchema =
             * dbmdUsingInfoSchema.getTables(connInfoSchema.getCatalog(), null,
             * "%", new String[] {"TABLE", "VIEW"});
             *
             * compareResultSets(rsShow, rsInfoSchema);
             *
             * rsShow = dbmdUsingShow.getTables(null, null, "%", new String[]
             * {"TABLE", "VIEW"}); rsInfoSchema =
             * dbmdUsingInfoSchema.getTables(null, null, "%", new String[]
             * {"TABLE", "VIEW"});
             *
             * compareResultSets(rsShow, rsInfoSchema);
             */

            StringBuilder sb = new StringBuilder("(");
            sb.append("field01 int primary key not null, ");
            sb.append("field02 int unsigned, ");
            sb.append("field03 tinyint, ");
            sb.append("field04 tinyint unsigned, ");
            sb.append("field05 smallint, ");
            sb.append("field06 smallint unsigned, ");
            sb.append("field07 mediumint, ");
            sb.append("field08 mediumint unsigned, ");
            sb.append("field09 bigint, ");
            sb.append("field10 bigint unsigned, ");
            sb.append("field11 float, ");
            sb.append("field12 float unsigned, ");
            sb.append("field13 double, ");
            sb.append("field14 double unsigned, ");
            sb.append("field15 decimal, ");
            sb.append("field16 decimal unsigned, ");
            sb.append("field17 char(32), ");
            sb.append("field18 varchar(32), ");
            sb.append("field19 binary(32), ");
            sb.append("field20 varbinary(16384), ");
            sb.append("field21 tinyblob, ");
            sb.append("field22 blob, ");
            sb.append("field23 mediumblob, ");
            sb.append("field24 longblob, ");
            sb.append("field25 tinytext, ");
            sb.append("field26 text, ");
            sb.append("field27 mediumtext, ");
            sb.append("field28 longtext, ");
            sb.append("field29 date, ");
            sb.append("field30 time, ");
            sb.append("field31 datetime, ");
            sb.append("field32 timestamp, ");
            sb.append("field33 year, ");
            if (versionMeetsMinimum(5, 7)) {
                sb.append("field34 json, ");
            }
            sb.append("field35 boolean, ");
            sb.append("field36 bit, ");
            sb.append("field37 bit(64), ");
            sb.append("field38 enum('a','b', 'c' ), ");
            sb.append("field39 set('d', 'e', 'f' ), ");

            sb.append("field40 geometry, ");
            sb.append("field41 POINT, ");
            sb.append("field42 LINESTRING, ");
            sb.append("field43 POLYGON, ");
            sb.append("field44 MULTIPOINT, ");
            sb.append("field45 MULTILINESTRING, ");
            sb.append("field46 MULTIPOLYGON, ");
            sb.append("field47 GEOMETRYCOLLECTION ");
            if (versionMeetsMinimum(8, 0, 5)) {
                sb.append(", field48 GEOMCOLLECTION ");
            }

            sb.append(")");
            createTable("t_testBug23304", sb.toString());

            rsShow = dbmdUsingShow.getColumns(connShow.getCatalog(), null, "t_testBug23304", "%");
            rsInfoSchema = dbmdUsingInfoSchema.getColumns(connInfoSchema.getCatalog(), null, "t_testBug23304", "%");

            compareResultSets(rsShow, rsInfoSchema);
        } finally {
            if (rsShow != null) {
                rsShow.close();
            }

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

    private void compareResultSets(ResultSet expected, ResultSet actual) throws Exception {
        if (expected == null) {
            assertTrue(actual == null, "Expected null result set, actual was not null.");
            return;
        }
        assertFalse(actual == null, "Expected non-null actual result set.");

        expected.last();

        int expectedRows = expected.getRow();

        actual.last();

        int actualRows = actual.getRow();

        assertEquals(expectedRows, actualRows);

        ResultSetMetaData metadataExpected = expected.getMetaData();
        ResultSetMetaData metadataActual = actual.getMetaData();

        assertEquals(metadataExpected.getColumnCount(), metadataActual.getColumnCount());

        for (int i = 0; i < metadataExpected.getColumnCount(); i++) {
            assertEquals(metadataExpected.getColumnName(i + 1), metadataActual.getColumnName(i + 1));
            assertEquals(metadataExpected.getColumnType(i + 1), metadataActual.getColumnType(i + 1));
            assertEquals(metadataExpected.getColumnClassName(i + 1), metadataActual.getColumnClassName(i + 1));
        }

        expected.beforeFirst();
        actual.beforeFirst();

        StringBuilder messageBuf = null;

        while (expected.next() && actual.next()) {

            if (messageBuf != null) {
                messageBuf.append("\n");
            }

            for (int i = 0; i < metadataExpected.getColumnCount(); i++) {
                if (expected.getObject(i + 1) == null && actual.getObject(i + 1) == null) {
                    continue;
                }

                if (expected.getObject(i + 1) == null && actual.getObject(i + 1) != null || expected.getObject(i + 1) != null && actual.getObject(i + 1) == null
                        || !expected.getObject(i + 1).equals(actual.getObject(i + 1))) {
                    if ("COLUMN_DEF".equals(metadataExpected.getColumnName(i + 1)) && expected.getObject(i + 1) == null && actual.getString(i + 1).length() == 0
                            || (expected.getString(i + 1) == null || expected.getString(i + 1).length() == 0) && actual.getObject(i + 1) == null) {
                        continue; // known bug with SHOW FULL COLUMNS, and we
                                 // can't distinguish between null and ''
                                 // for a default
                    }

                    if ("CHAR_OCTET_LENGTH".equals(metadataExpected.getColumnName(i + 1))) {
                        if (((com.mysql.cj.jdbc.ConnectionImpl) this.conn).getSession().getServerSession().getCharsetSettings()
                                .getMaxBytesPerChar(CharsetMappingWrapper
                                        .getStaticJavaEncodingForMysqlCharset(((NativeCharsetSettings) ((com.mysql.cj.jdbc.JdbcConnection) this.conn)
                                                .getSession().getServerSession().getCharsetSettings()).getServerDefaultCharset())) > 1) {
                            continue; // SHOW CREATE and CHAR_OCT *will* differ
                        }
                    }

                    if (messageBuf == null) {
                        messageBuf = new StringBuilder();
                    } else {
                        messageBuf.append("\n");
                    }

                    messageBuf.append("On row " + expected.getRow() + " ,for column named " + metadataExpected.getColumnName(i + 1) + ", expected '"
                            + expected.getObject(i + 1) + "', found '" + actual.getObject(i + 1) + "'");

                }
            }
        }

        if (messageBuf != null) {
            fail(messageBuf.toString());
        }
    }

    /**
     * Tests fix for BUG#25624 - Whitespace surrounding storage/size specifiers in stored procedure declaration causes NumberFormatException to be thrown when
     * calling stored procedure.
     *
     * @throws Exception
     */
    @Test
    public void testBug25624() throws Exception {
        //
        // we changed up the parameters to get coverage of the fixes,
        // also note that whitespace _is_ significant in the DDL...
        //
        createProcedure("testBug25624", "(in _par1 decimal( 10 , 2 ) , in _par2 varchar( 4 )) BEGIN select 1; END");

        this.conn.prepareCall("{call testBug25624(?,?)}").close();
    }

    /**
     * Tests fix for BUG#27867 - Schema objects with identifiers other than the connection character aren't retrieved correctly in ResultSetMetadata.
     *
     * @throws Exception
     */
    @Test
    public void testBug27867() throws Exception {
        String gbkColumnName = "\u00e4\u00b8\u00ad\u00e6\u2013\u2021\u00e6\u00b5\u2039\u00e8\u00af\u2022";
        createTable("ColumnNameEncoding",
                "(`" + gbkColumnName + "` varchar(1) default NULL, `ASCIIColumn` varchar(1) default NULL" + ")ENGINE=MyISAM DEFAULT CHARSET=utf8");

        this.rs = this.stmt.executeQuery("SELECT * FROM ColumnNameEncoding");
        java.sql.ResultSetMetaData tblMD = this.rs.getMetaData();

        assertEquals(gbkColumnName, tblMD.getColumnName(1));
        assertEquals("ASCIIColumn", tblMD.getColumnName(2));
    }

    /**
     * Fixed BUG#27915 - DatabaseMetaData.getColumns() doesn't contain SCOPE_* or IS_AUTOINCREMENT columns.
     *
     * @throws Exception
     */
    @Test
    public void testBug27915() throws Exception {
        createTable("testBug27915", "(field1 int not null primary key auto_increment, field2 int)");
        DatabaseMetaData dbmd = this.conn.getMetaData();

        this.rs = dbmd.getColumns(this.conn.getCatalog(), null, "testBug27915", "%");
        this.rs.next();

        checkBug27915();

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        this.rs = getConnectionWithProps(props).getMetaData().getColumns(this.conn.getCatalog(), null, "testBug27915", "%");
        this.rs.next();

        checkBug27915();
    }

    private void checkBug27915() throws SQLException {
        assertNull(this.rs.getString("SCOPE_CATALOG"));
        assertNull(this.rs.getString("SCOPE_SCHEMA"));
        assertNull(this.rs.getString("SCOPE_TABLE"));
        assertNull(this.rs.getString("SOURCE_DATA_TYPE"));
        assertEquals("YES", this.rs.getString("IS_AUTOINCREMENT"));

        this.rs.next();

        assertNull(this.rs.getString("SCOPE_CATALOG"));
        assertNull(this.rs.getString("SCOPE_SCHEMA"));
        assertNull(this.rs.getString("SCOPE_TABLE"));
        assertNull(this.rs.getString("SOURCE_DATA_TYPE"));
        assertEquals("NO", this.rs.getString("IS_AUTOINCREMENT"));
    }

    /**
     * Tests fix for BUG#27916 - UNSIGNED types not reported via DBMD.getTypeInfo(), and capitalization of types is not consistent between DBMD.getColumns(),
     * RSMD.getColumnTypeName() and DBMD.getTypeInfo().
     *
     * This fix also ensures that the precision of UNSIGNED MEDIUMINT and UNSIGNED BIGINT is reported correctly via DBMD.getColumns().
     *
     * Second fix ensures that list values of ENUM and SET types containing 'unsigned' are not taken in account.
     *
     * @throws Exception
     */
    @Test
    public void testBug27916() throws Exception {
        createTable("testBug27916",
                "(field1 TINYINT UNSIGNED, field2 SMALLINT UNSIGNED, field3 INT UNSIGNED, field4 INTEGER UNSIGNED, field5 MEDIUMINT UNSIGNED, field6 BIGINT UNSIGNED)");

        ResultSetMetaData rsmd = this.stmt.executeQuery("SELECT * FROM testBug27916").getMetaData();

        HashMap<String, Object> typeNameToPrecision = new HashMap<>();
        this.rs = this.conn.getMetaData().getTypeInfo();

        while (this.rs.next()) {
            typeNameToPrecision.put(this.rs.getString("TYPE_NAME"), this.rs.getObject("PRECISION"));
        }

        this.rs = this.conn.getMetaData().getColumns(this.conn.getCatalog(), null, "testBug27916", "%");

        for (int i = 0; i < rsmd.getColumnCount(); i++) {
            this.rs.next();
            String typeName = this.rs.getString("TYPE_NAME");

            assertEquals(typeName, rsmd.getColumnTypeName(i + 1));
            assertEquals(this.rs.getInt("COLUMN_SIZE"), rsmd.getPrecision(i + 1), typeName);
            assertEquals(new Integer(rsmd.getPrecision(i + 1)), typeNameToPrecision.get(typeName), typeName);
        }

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
        ArrayList<String> types = new ArrayList<>();
        Connection PropConn = getConnectionWithProps(props);
        try {
            DatabaseMetaData dbmd = PropConn.getMetaData();
            this.rs = dbmd.getTypeInfo();
            while (this.rs.next()) {
                types.add(this.rs.getString("TYPE_NAME"));
            }
            this.rs.close();

            this.rs = dbmd.getColumns("mysql", null, "time_zone_transition", "%");
            while (this.rs.next()) {
                String typeName = this.rs.getString("TYPE_NAME");
                assertTrue(types.contains(typeName), typeName);
            }
            this.rs.close();
            this.rs = dbmd.getColumns("mysql", null, "proc", "%");
            while (this.rs.next()) {
                String typeName = this.rs.getString("TYPE_NAME");
                assertTrue(types.contains(typeName), typeName);
            }
            this.rs.close();
            PropConn.close();
            props.clear();

            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
            PropConn = getConnectionWithProps(props);
            dbmd = PropConn.getMetaData();

            this.rs = dbmd.getColumns("mysql", null, "time_zone_transition", "%");
            while (this.rs.next()) {
                String typeName = this.rs.getString("TYPE_NAME");
                assertTrue(types.contains(typeName), typeName);
            }
            this.rs.close();
            this.rs = dbmd.getColumns("mysql", null, "proc", "%");
            while (this.rs.next()) {
                String typeName = this.rs.getString("TYPE_NAME");
                assertTrue(types.contains(typeName), typeName);
            }
            this.rs.close();
            PropConn.close();
            props.clear();
        } finally {
            if (PropConn != null) {
                PropConn.close();
            }
        }
    }

    @Test
    public void testBug20491() throws Exception {
        String[] fields = { "field1_ae_\u00e4", "field2_ue_\u00fc", "field3_oe_\u00f6", "field4_sz_\u00df" };

        createTable("tst", "(`" + fields[0] + "` int(10) unsigned NOT NULL default '0', `" + fields[1] + "` varchar(45) default '', `" + fields[2]
                + "` varchar(45) default '', `" + fields[3] + "` varchar(45) default '', PRIMARY KEY  (`" + fields[0] + "`))");

        // demonstrate that these are all in the Cp1252 encoding

        for (int i = 0; i < fields.length; i++) {
            try {
                assertEquals(fields[i], new String(fields[i].getBytes("Cp1252"), "Cp1252"));
            } catch (AssertionFailedError afEr) {
                if (i == 3) {
                    // If we're on a mac, we're out of luck swe can't store this in the filesystem...

                    if (!Constants.OS_NAME.startsWith("Mac")) {
                        throw afEr;
                    }
                }
            }
        }

        @SuppressWarnings("unused")
        byte[] asBytes = fields[0].getBytes("utf-8");

        DatabaseMetaData md = this.conn.getMetaData();

        this.rs = md.getColumns(this.dbName, "%", "tst", "%");

        int j = 0;

        while (this.rs.next()) {
            try {
                assertEquals(fields[j++], this.rs.getString(4), "Wrong column name:" + this.rs.getString(4));
            } catch (AssertionFailedError afEr) {
                if (j == 3) {
                    // If we're on a mac, we're out of luck we can't store this in the filesystem...

                    if (!Constants.OS_NAME.startsWith("Mac")) {
                        throw afEr;
                    }
                }
            }
        }

        this.rs.close();

        this.rs = this.stmt.executeQuery("SELECT * FROM tst");

        ResultSetMetaData rsmd = this.rs.getMetaData();

        for (int i = 1; i <= rsmd.getColumnCount(); i++) {
            try {
                assertEquals(fields[i - 1], rsmd.getColumnName(i), "Wrong column name:" + rsmd.getColumnName(i));
            } catch (AssertionFailedError afEr) {
                if (i - 1 == 3) {
                    // If we're on a mac, we're out of luck we can't store this in the filesystem...

                    if (!Constants.OS_NAME.startsWith("Mac")) {
                        throw afEr;
                    }
                }
            }
        }
    }

    /**
     * Tests fix for Bug#33594 - When cursor fetch is enabled, wrong metadata is returned from DBMD.
     *
     * The fix is two parts.
     *
     * First, when asking for the first column value twice from a cursor-fetched row, the driver didn't re-position, and thus the "next" column was returned.
     *
     * Second, metadata statements and internal statements the driver uses shouldn't use cursor-based fetching at all, so we've ensured that internal statements
     * have their fetch size set to "0".
     *
     * @throws Exception
     */
    @Test
    public void testBug33594() throws Exception {
        boolean max_key_l_bug = false;

        try {
            createTable("bug33594", "(fid varchar(255) not null primary key, id INT, geom linestring, name varchar(255))");
        } catch (SQLException sqlEx) {
            if (sqlEx.getMessage().indexOf("max key length") != -1) {
                createTable("bug33594", "(fid varchar(180) not null primary key, id INT, geom linestring, name varchar(255))");
                max_key_l_bug = true;
            }
        }

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
        props.setProperty(PropertyKey.useCursorFetch.getKeyName(), "false");
        props.setProperty(PropertyKey.defaultFetchSize.getKeyName(), "100");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");
        Connection conn1 = null;
        try {
            conn1 = getConnectionWithProps(props);
            DatabaseMetaData metaData = conn1.getMetaData();
            this.rs = metaData.getColumns(null, null, "bug33594", null);
            this.rs.next();
            assertEquals("bug33594", this.rs.getString("TABLE_NAME"));
            assertEquals("fid", this.rs.getString("COLUMN_NAME"));
            assertEquals("VARCHAR", this.rs.getString("TYPE_NAME"));
            if (!max_key_l_bug) {
                assertEquals("255", this.rs.getString("COLUMN_SIZE"));
            } else {
                assertEquals("180", this.rs.getString("COLUMN_SIZE"));
            }

            Properties props2 = new Properties();
            props2.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props2.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props2.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
            props2.setProperty(PropertyKey.useCursorFetch.getKeyName(), "true");
            props2.setProperty(PropertyKey.defaultFetchSize.getKeyName(), "100");
            props2.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");

            Connection conn2 = null;

            try {
                conn2 = getConnectionWithProps(props2);
                DatabaseMetaData metaData2 = conn2.getMetaData();
                this.rs = metaData2.getColumns(null, null, "bug33594", null);
                this.rs.next();
                assertEquals("bug33594", this.rs.getString("TABLE_NAME"));
                assertEquals("fid", this.rs.getString("COLUMN_NAME"));
                assertEquals("VARCHAR", this.rs.getString("TYPE_NAME"));
                if (!max_key_l_bug) {
                    assertEquals("255", this.rs.getString("COLUMN_SIZE"));
                } else {
                    assertEquals("180", this.rs.getString("COLUMN_SIZE"));
                }

                // we should only see one server-side prepared statement, and
                // that's
                // caused by us going off to ask about the count!
                assertEquals("1", getSingleIndexedValueWithQuery(conn2, 2, "SHOW SESSION STATUS LIKE 'Com_stmt_prepare'").toString());
            } finally {
                if (conn2 != null) {
                    conn2.close();
                }
            }
        } finally {
            if (conn1 != null) {
                conn1.close();
            }
        }
    }

    @Test
    public void testBug34194() throws Exception {
        createTable("bug34194", "(id integer,geom geometry)");

        if (!versionMeetsMinimum(5, 6)) {
            this.stmt.execute("insert into bug34194 values('1', GeomFromText('POINT(622572.881 5156121.034)'))");
        } else {
            this.stmt.execute("insert into bug34194 values('1', ST_GeomFromText('POINT(622572.881 5156121.034)'))");
        }
        this.rs = this.stmt.executeQuery("select * from bug34194");
        ResultSetMetaData RSMD = this.rs.getMetaData();
        assertEquals("GEOMETRY", RSMD.getColumnTypeName(2));
    }

    @Test
    public void testNoSystemTablesReturned() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        for (boolean useIS : new boolean[] { false, true }) {
            for (boolean dbMapsToSchema : new boolean[] { false, true }) {
                props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
                props.setProperty(PropertyKey.databaseTerm.getKeyName(), dbMapsToSchema ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name());

                Connection conn1 = null;
                try {
                    conn1 = getConnectionWithProps(props);

                    this.rs = dbMapsToSchema ? conn1.getMetaData().getTables("null", "information_schema", "%", new String[] { "SYSTEM VIEW" })
                            : conn1.getMetaData().getTables("information_schema", "null", "%", new String[] { "SYSTEM VIEW" });
                    assertTrue(this.rs.next());

                    this.rs = dbMapsToSchema ? conn1.getMetaData().getTables("null", "information_schema", "%", new String[] { "SYSTEM TABLE" })
                            : conn1.getMetaData().getTables("information_schema", "null", "%", new String[] { "SYSTEM TABLE" });
                    assertFalse(this.rs.next());

                    this.rs = dbMapsToSchema ? conn1.getMetaData().getTables("null", "information_schema", "%", new String[] { "TABLE" })
                            : conn1.getMetaData().getTables("information_schema", "null", "%", new String[] { "TABLE" });
                    assertFalse(this.rs.next());

                    this.rs = dbMapsToSchema ? conn1.getMetaData().getTables("null", "information_schema", "%", new String[] { "VIEW" })
                            : conn1.getMetaData().getTables("information_schema", "null", "%", new String[] { "VIEW" });
                    assertFalse(this.rs.next());

                    this.rs = dbMapsToSchema
                            ? conn1.getMetaData().getTables("null", "information_schema", "%", new String[] { "SYSTEM TABLE", "SYSTEM VIEW", "TABLE", "VIEW" })
                            : conn1.getMetaData().getTables("information_schema", "null", "%", new String[] { "SYSTEM TABLE", "SYSTEM VIEW", "TABLE", "VIEW" });
                    assertTrue(this.rs.next());

                    this.rs = dbMapsToSchema ? conn1.getMetaData().getColumns(null, "information_schema", "TABLES", "%")
                            : conn1.getMetaData().getColumns("information_schema", null, "TABLES", "%");
                    assertTrue(this.rs.next());
                } finally {
                    if (conn1 != null) {
                        conn1.close();
                    }
                }
            }
        }
    }

    @Test
    public void testABunchOfReturnTypes() throws Exception {
        checkABunchOfReturnTypesForConnection(this.conn);
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        checkABunchOfReturnTypesForConnection(getConnectionWithProps(props));
    }

    private void checkABunchOfReturnTypesForConnection(Connection mdConn) throws Exception {
        // Setup.
        final Map<Integer, String> sqlTypesMap = new HashMap<>();
        Field[] typeFields = Types.class.getFields();

        for (int i = 0; i < typeFields.length; i++) {
            if (Modifier.isStatic(typeFields[i].getModifiers())) {
                try {
                    sqlTypesMap.put(new Integer(typeFields[i].getInt(null)), "java.sql.Types." + typeFields[i].getName());
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    // ignore
                }
            }
        }

        // Test.
        DatabaseMetaData md = mdConn.getMetaData();

        // Bug#44862 - getBestRowIdentifier does not return resultset as per JDBC API specifications
        this.rs = md.getBestRowIdentifier(this.conn.getCatalog(), null, "returnTypesTest", DatabaseMetaData.bestRowSession, false);

        int[] types = new int[] { Types.SMALLINT, // 1. SCOPE short => actual scope of result
                Types.CHAR, // 2. COLUMN_NAME String => column name
                Types.INTEGER, // 3. DATA_TYPE int => SQL data type from java.sql.Types
                Types.CHAR, // 4. TYPE_NAME String => Data source dependent type name, for a UDT the type name is fully qualified
                Types.INTEGER, // 5. COLUMN_SIZE int => precision
                Types.INTEGER, // 6. BUFFER_LENGTH int => not used
                Types.SMALLINT, // 7. DECIMAL_DIGITS short => scale
                Types.SMALLINT, // 8. PSEUDO_COLUMN short => is this a pseudo column like an Oracle ROWID
        };

        checkTypes(this.rs, types, sqlTypesMap);

        // Bug#44683 - getVersionColumns does not return resultset as per JDBC API specifications
        this.rs = md.getVersionColumns(this.conn.getCatalog(), null, "returnTypesTest");

        types = new int[] { Types.SMALLINT, // SCOPE short => is not used
                Types.CHAR, // COLUMN_NAME String => column name
                Types.INTEGER, // DATA_TYPE int => SQL data type from java.sql.Types
                Types.CHAR, // TYPE_NAME String => Data source-dependent type name
                Types.INTEGER, // COLUMN_SIZE int => precision
                Types.INTEGER, // BUFFER_LENGTH int => length of column value in bytes
                Types.SMALLINT, // DECIMAL_DIGITS short => scale
                Types.SMALLINT // PSEUDO_COLUMN short => whether this is pseudo column like an Oracle ROWID
        };

        checkTypes(this.rs, types, sqlTypesMap);

        // Bug#44865 - getColumns does not return resultset as per JDBC API specifications
        this.rs = md.getColumns(this.conn.getCatalog(), null, "returnTypesTest", "foo");

        types = new int[] { Types.CHAR, // 1. TABLE_CAT String => table catalog (may be null)
                Types.CHAR, // 2. TABLE_SCHEM String => table schema (may be null)
                Types.CHAR, // 3. TABLE_NAME String => table name
                Types.CHAR, // 4. COLUMN_NAME String => column name
                Types.INTEGER, // 5. DATA_TYPE int => SQL type from java.sql.Types
                Types.CHAR, // 6. TYPE_NAME String => Data source dependent type name, for a UDT the type name is fully qualified
                Types.INTEGER, // 7. COLUMN_SIZE int => column size. For char or date types this is the maximum number of characters, for numeric or decimal
                // types this is precision.
                Types.INTEGER, // 8. BUFFER_LENGTH is not used.
                Types.INTEGER, // 9. DECIMAL_DIGITS int => the number of fractional digits
                Types.INTEGER, // 10. NUM_PREC_RADIX int => Radix (typically either 10 or 2)
                Types.INTEGER, // 11. NULLABLE int => is NULL allowed.
                Types.CHAR, // 12. REMARKS String => comment describing column (may be null)
                Types.CHAR, // 13. COLUMN_DEF String => default value (may be null)
                Types.INTEGER, // 14. SQL_DATA_TYPE int => unused
                Types.INTEGER, // 15. SQL_DATETIME_SUB int => unused
                Types.INTEGER, // 16. CHAR_OCTET_LENGTH int => for char types the maximum number of bytes in the column
                Types.INTEGER, // 17. ORDINAL_POSITION int => index of column in table (starting at 1)
                Types.CHAR, // 18. IS_NULLABLE String => "NO" means column definitely does not allow NULL values; "YES" means the column might allow NULL
                // values. An empty string means nobody knows.
                Types.CHAR, // 19. SCOPE_CATLOG String => catalog of table that is the scope of a reference attribute (null if DATA_TYPE isn't REF)
                Types.CHAR, // 20. SCOPE_SCHEMA String => schema of table that is the scope of a reference attribute (null if the DATA_TYPE isn't REF)
                Types.CHAR, // 21. SCOPE_TABLE String => table name that this the scope of a reference attribute (null if the DATA_TYPE isn't REF)
                Types.SMALLINT, // 22. SOURCE_DATA_TYPE short => source type of a distinct type or user-generated Ref type, SQL type from java.sql.Types (null
                // if DATA_TYPE isn't DISTINCT or user-generated REF)
                Types.CHAR, // 23. IS_AUTOINCREMENT String => Indicates whether this column is auto incremented
                Types.CHAR // 24. IS_GENERATEDCOLUMN String => Indicates whether this is a generated column
        };

        checkTypes(this.rs, types, sqlTypesMap);

        // Bug#44868 - getTypeInfo does not return resultset as per JDBC API specifications
        this.rs = md.getTypeInfo();

        types = new int[] { Types.CHAR, // 1. TYPE_NAME String => Type name
                Types.INTEGER, // 2. DATA_TYPE int => SQL data type from java.sql.Types
                Types.INTEGER, // 3. PRECISION int => maximum precision
                Types.CHAR, // 4. LITERAL_PREFIX String => prefix used to quote a literal (may be null)
                Types.CHAR, // 5. LITERAL_SUFFIX String => suffix used to quote a literal (may be null)
                Types.CHAR, // 6. CREATE_PARAMS String => parameters used in creating the type (may be null)
                Types.SMALLINT, // 7. NULLABLE short => can you use NULL for this type.
                Types.BOOLEAN, // 8. CASE_SENSITIVE boolean=> is it case sensitive.
                Types.SMALLINT, // 9. SEARCHABLE short => can you use "WHERE" based on this type:
                Types.BOOLEAN, // 10. UNSIGNED_ATTRIBUTE boolean => is it unsigned.
                Types.BOOLEAN, // 11. FIXED_PREC_SCALE boolean => can it be a money value.
                Types.BOOLEAN, // 12. AUTO_INCREMENT boolean => can it be used for an auto-increment value.
                Types.CHAR, // 13. LOCAL_TYPE_NAME String => localized version of type name (may be null)
                Types.SMALLINT, // 14. MINIMUM_SCALE short => minimum scale supported
                Types.SMALLINT, // 15. MAXIMUM_SCALE short => maximum scale supported
                Types.INTEGER, // 16. SQL_DATA_TYPE int => unused
                Types.INTEGER, // 17. SQL_DATETIME_SUB int => unused
                Types.INTEGER // 18. NUM_PREC_RADIX int => usually 2 or 10
        };

        checkTypes(this.rs, types, sqlTypesMap);

        // Bug#44869 - getIndexInfo does not return resultset as per JDBC API specifications
        this.rs = md.getIndexInfo(this.conn.getCatalog(), null, "returnTypesTest", false, false);

        types = new int[] { Types.CHAR, // 1. TABLE_CAT String => table catalog (may be null)
                Types.CHAR, // 2. TABLE_SCHEM String => table schema (may be null)
                Types.CHAR, // 3. TABLE_NAME String => table name
                Types.BOOLEAN, // 4. NON_UNIQUE boolean => Can index values be non-unique. false when TYPE is tableIndexStatistic
                Types.CHAR, // 5. INDEX_QUALIFIER String => index catalog (may be null); null when TYPE is tableIndexStatistic
                Types.CHAR, // 6. INDEX_NAME String => index name; null when TYPE is tableIndexStatistic
                Types.SMALLINT, // 7. TYPE short => index type:
                Types.SMALLINT, // 8. ORDINAL_POSITION short => column sequence number within index; zero when TYPE is tableIndexStatistic
                Types.CHAR, // 9. COLUMN_NAME String => column name; null when TYPE is tableIndexStatistic
                Types.CHAR, // 10. ASC_OR_DESC String => column sort sequence, "A" => ascending, "D" => descending, may be null if sort sequence is not
                // supported; null when TYPE is tableIndexStatistic
                Types.BIGINT, // 11. CARDINALITY int/long => When TYPE is tableIndexStatistic, then this is the number of rows
                // in the table; otherwise, it is the number of unique values in the index.
                Types.BIGINT, // 12. PAGES int/long => When TYPE is tableIndexStatistic then this is the number of pages used
                // for the table, otherwise it is the number of pages used for the current index.
                Types.CHAR // 13. FILTER_CONDITION String => Filter condition, if any. (may be null)
        };

        checkTypes(this.rs, types, sqlTypesMap);

        // Bug#44867 - getImportedKeys/exportedKeys/crossReference doesn't have correct type for DEFERRABILITY
        this.rs = md.getImportedKeys(this.conn.getCatalog(), null, "returnTypesTest");

        types = new int[] { Types.CHAR, // PKTABLE_CAT String => primary key table catalog being imported (may be null)
                Types.CHAR, // PKTABLE_SCHEM String => primary key table schema being imported (may be null)
                Types.CHAR, // PKTABLE_NAME String => primary key table name being imported
                Types.CHAR, // PKCOLUMN_NAME String => primary key column name being imported
                Types.CHAR, // FKTABLE_CAT String => foreign key table catalog (may be null)
                Types.CHAR, // FKTABLE_SCHEM String => foreign key table schema (may be null)
                Types.CHAR, // FKTABLE_NAME String => foreign key table name
                Types.CHAR, // FKCOLUMN_NAME String => foreign key column name
                Types.SMALLINT, // KEY_SEQ short => sequence number within a foreign key
                Types.SMALLINT, // UPDATE_RULE short => What happens to a foreign key when the primary key is updated:
                Types.SMALLINT, // DELETE_RULE short => What happens to the foreign key when primary is deleted
                Types.CHAR, // FK_NAME String => foreign key name (may be null)
                Types.CHAR, // PK_NAME String => primary key name (may be null)
                Types.SMALLINT // DEFERRABILITY short => can the evaluation of foreign key constraints be deferred until commit
        };

        checkTypes(this.rs, types, sqlTypesMap);

        this.rs = md.getExportedKeys(this.conn.getCatalog(), null, "returnTypesTest");

        types = new int[] { Types.CHAR, // PKTABLE_CAT String => primary key table catalog being imported (may be null)
                Types.CHAR, // PKTABLE_SCHEM String => primary key table schema being imported (may be null)
                Types.CHAR, // PKTABLE_NAME String => primary key table name being imported
                Types.CHAR, // PKCOLUMN_NAME String => primary key column name being imported
                Types.CHAR, // FKTABLE_CAT String => foreign key table catalog (may be null)
                Types.CHAR, // FKTABLE_SCHEM String => foreign key table schema (may be null)
                Types.CHAR, // FKTABLE_NAME String => foreign key table name
                Types.CHAR, // FKCOLUMN_NAME String => foreign key column name
                Types.SMALLINT, // KEY_SEQ short => sequence number within a foreign key
                Types.SMALLINT, // UPDATE_RULE short => What happens to a foreign key when the primary key is updated:
                Types.SMALLINT, // DELETE_RULE short => What happens to the foreign key when primary is deleted
                Types.CHAR, // FK_NAME String => foreign key name (may be null)
                Types.CHAR, // PK_NAME String => primary key name (may be null)
                Types.SMALLINT // DEFERRABILITY short => can the evaluation of foreign key constraints be deferred until commit
        };

        checkTypes(this.rs, types, sqlTypesMap);

        this.rs = md.getCrossReference(this.conn.getCatalog(), null, "returnTypesTest", this.conn.getCatalog(), null, "bar");

        types = new int[] { Types.CHAR, // PKTABLE_CAT String => primary key table catalog being imported (may be null)
                Types.CHAR, // PKTABLE_SCHEM String => primary key table schema being imported (may be null)
                Types.CHAR, // PKTABLE_NAME String => primary key table name being imported
                Types.CHAR, // PKCOLUMN_NAME String => primary key column name being imported
                Types.CHAR, // FKTABLE_CAT String => foreign key table catalog (may be null)
                Types.CHAR, // FKTABLE_SCHEM String => foreign key table schema (may be null)
                Types.CHAR, // FKTABLE_NAME String => foreign key table name
                Types.CHAR, // FKCOLUMN_NAME String => foreign key column name
                Types.SMALLINT, // KEY_SEQ short => sequence number within a foreign key
                Types.SMALLINT, // UPDATE_RULE short => What happens to a foreign key when the primary key is updated:
                Types.SMALLINT, // DELETE_RULE short => What happens to the foreign key when primary is deleted
                Types.CHAR, // FK_NAME String => foreign key name (may be null)
                Types.CHAR, // PK_NAME String => primary key name (may be null)
                Types.SMALLINT // DEFERRABILITY short => can the evaluation of foreign key constraints be deferred until commit
        };

        checkTypes(this.rs, types, sqlTypesMap);
    }

    private void checkTypes(ResultSet rsToCheck, int[] types, Map<Integer, String> sqlTypesMap) throws Exception {
        ResultSetMetaData rsmd = rsToCheck.getMetaData();
        assertEquals(types.length, rsmd.getColumnCount());
        for (int i = 0; i < types.length; i++) {
            String expectedType = sqlTypesMap.get(new Integer(types[i]));
            String actualType = sqlTypesMap.get(new Integer(rsmd.getColumnType(i + 1)));
            assertNotNull(expectedType);
            assertNotNull(actualType);
            assertEquals(expectedType, actualType, "Unexpected type in column " + (i + 1));
        }
    }

    /**
     * Bug #43714 - useInformationSchema with DatabaseMetaData.getExportedKeys() throws exception
     *
     * @throws Exception
     */
    @Test
    public void testBug43714() throws Exception {
        Connection c_IS = null;
        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
            c_IS = getConnectionWithProps(props);
            DatabaseMetaData dbmd = c_IS.getMetaData();
            this.rs = dbmd.getExportedKeys("x", "y", "z");
        } finally {
            if (c_IS != null) {
                try {
                    c_IS.close();
                } catch (SQLException ex) {
                }
            }
        }
    }

    /**
     * Bug #41269 - DatabaseMetadata.getProcedureColumns() returns wrong value for column length
     *
     * @throws Exception
     */
    @Test
    public void testBug41269() throws Exception {
        createProcedure("bug41269", "(in param1 int, out result varchar(197)) BEGIN select 1, ''; END");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");
        Connection con = getConnectionWithProps(props);
        try {
            ResultSet procMD = con.getMetaData().getProcedureColumns(null, null, "bug41269", "%");
            assertTrue(procMD.next());
            assertEquals(10, procMD.getInt(9), "Int param length");
            assertTrue(procMD.next());
            assertEquals(197, procMD.getInt(9), "String param length");
            assertFalse(procMD.next());
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    @Test
    public void testBug31187() throws Exception {
        createTable("testBug31187", "(field1 int)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "false");
        Connection nullCatConn = getConnectionWithProps(props);
        DatabaseMetaData dbmd = nullCatConn.getMetaData();
        ResultSet dbTblCols = dbmd.getColumns(null, null, "testBug31187", "%");

        boolean found = false;
        boolean dbMapsToSchema = ((JdbcConnection) nullCatConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                .getValue() == DatabaseTerm.SCHEMA;

        String expectedDb = dbMapsToSchema ? nullCatConn.getSchema() : nullCatConn.getCatalog();
        while (dbTblCols.next()) {
            String db = dbMapsToSchema ? dbTblCols.getString("TABLE_SCHEM") : dbTblCols.getString("TABLE_CAT");
            String table = dbTblCols.getString("TABLE_NAME");
            boolean useLowerCaseTableNames = dbmd.storesLowerCaseIdentifiers();

            if (useLowerCaseTableNames && db.equalsIgnoreCase(expectedDb) && "testBug31187".equalsIgnoreCase(table)
                    || db.equalsIgnoreCase(expectedDb) && "testBug31187".equals(table)) {
                found = true;
            }
        }

        assertTrue(found, "Didn't find any columns for table named 'testBug31187' in database " + this.conn.getCatalog());
    }

    @Test
    public void testBug44508() throws Exception {
        DatabaseMetaData dbmd = this.conn.getMetaData();

        this.rs = dbmd.getSuperTypes("", "", "");
        ResultSetMetaData rsmd = this.rs.getMetaData();

        assertEquals("TYPE_CAT", rsmd.getColumnName(1)); // Gives TABLE_CAT
        assertEquals("TYPE_SCHEM", rsmd.getColumnName(2)); // Gives TABLE_SCHEM
    }

    /**
     * Tests fix for BUG#52167 - Can't parse parameter list with special characters inside
     *
     * @throws Exception
     */
    @Test
    public void testBug52167() throws Exception {
        // DatabaseMetaData.java (~LN 1730)
        // + //Bug#52167, tokenizer will break if declaration contains special
        // characters like \n
        // + declaration = declaration.replaceAll("[\\t\\n\\x0B\\f\\r]", " ");
        // StringTokenizer declarationTok = new StringTokenizer(
        // declaration, " \t");
        createProcedure("testBug52167", "(in _par1 decimal( 10 , 2 ) , in _par2\n varchar( 4 )) BEGIN select 1; END");

        this.conn.prepareCall("{call testBug52167(?,?)}").close();
    }

    /**
     * Tests fix for BUG#51912 - Passing NULL as cat. param to getProcedureColumns with nullCatalogMeansCurrent = false
     *
     * @throws Exception
     */
    @Test
    public void testBug51912() throws Exception {
        Connection overrideConn = null;
        try {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "false");
            overrideConn = getConnectionWithProps(props);

            DatabaseMetaData dbmd = overrideConn.getMetaData();
            this.rs = dbmd.getProcedureColumns(null, null, "%", null);
            this.rs.close();

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

    /**
     * Tests fix for BUG#38367 - DatabaseMetaData dbMeta = this.conn.getMetaData();
     * this.rs = dbMeta.getProcedureColumns("test", null, "nullableParameterTest", null);
     * ...
     * Short columnNullable = new Short(this.rs.getShort(12));
     * assertTrue("Parameter " + columnName + " do not allow null arguments",
     * columnNullable.intValue() == java.sql.DatabaseMetaData.procedureNullable);
     * was failing for no good reason.
     *
     * @throws Exception
     */
    @Test
    public void testBug38367() throws Exception {
        createProcedure("sptestBug38367",
                "(OUT nfact VARCHAR(100), IN ccuenta VARCHAR(100),\nOUT ffact VARCHAR(100),\nOUT fdoc VARCHAR(100))" + "\nBEGIN\nEND");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        Connection con = getConnectionWithProps(props);
        try {
            DatabaseMetaData dbMeta = con.getMetaData();
            this.rs = dbMeta.getProcedureColumns(con.getCatalog(), null, "sptestBug38367", null);
            while (this.rs.next()) {
                String columnName = this.rs.getString(4);
                Short columnNullable = new Short(this.rs.getShort(12));
                assertTrue(columnNullable.intValue() == java.sql.DatabaseMetaData.procedureNullable,
                        "Parameter " + columnName + " is not java.sql.DatabaseMetaData.procedureNullable.");
            }
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for BUG#57808 - wasNull not set for DATE field with value 0000-00-00 in getDate() although zeroDateTimeBehavior is CONVERT_TO_NULL.
     *
     * @throws Exception
     */
    @Test
    public void testBug57808() throws Exception {
        try {
            createTable("bug57808", "(ID INT(3) NOT NULL PRIMARY KEY, ADate DATE NOT NULL)");
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            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 + "'");
                }
            }
            props.setProperty(PropertyKey.zeroDateTimeBehavior.getKeyName(), "CONVERT_TO_NULL");
            Connection conn1 = null;

            conn1 = getConnectionWithProps(props);
            this.stmt = conn1.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            this.stmt.executeUpdate("INSERT INTO bug57808(ID, ADate) VALUES(1, 0000-00-00)");

            this.rs = this.stmt.executeQuery("SELECT ID, ADate FROM bug57808 WHERE ID = 1");
            if (this.rs.first()) {
                Date theDate = this.rs.getDate("ADate");
                assertNull(theDate, "Original date was not NULL!");
                assertTrue(this.rs.wasNull(), "wasNull is FALSE");
            }
        } finally {
        }
    }

    /**
     * Tests fix for BUG#61150 - First call to SP fails with "No Database Selected"
     * The workaround introduced in DatabaseMetaData.getCallStmtParameterTypes to fix the bug in server where SHOW CREATE PROCEDURE was not respecting
     * lower-case table names is misbehaving when connection is not attached to database and on non-casesensitive OS.
     *
     * @throws Exception
     */
    @Test
    public void testBug61150() throws Exception {
        StringBuilder newUrlToTestNoDB = new StringBuilder("jdbc:mysql://");
        newUrlToTestNoDB.append(getEncodedHostPortPairFromTestsuiteUrl()).append("/");

        Statement savedSt = this.stmt;

        Properties props = getHostFreePropertiesFromTestsuiteUrl();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.remove(PropertyKey.DBNAME.getKeyName());
        Connection conn1 = DriverManager.getConnection(newUrlToTestNoDB.toString(), props);

        this.stmt = conn1.createStatement();
        createDatabase("TST1");
        createProcedure("TST1.PROC", "(x int, out y int)\nbegin\ndeclare z int;\nset z = x+1, y = z;\nend\n");

        CallableStatement cStmt = null;
        cStmt = conn1.prepareCall("{call `TST1`.`PROC`(?, ?)}");
        cStmt.setInt(1, 5);
        cStmt.registerOutParameter(2, Types.INTEGER);

        cStmt.execute();
        assertEquals(6, cStmt.getInt(2));
        cStmt.clearParameters();
        cStmt.close();

        conn1.setCatalog("TST1");
        cStmt = null;
        cStmt = conn1.prepareCall("{call TST1.PROC(?, ?)}");
        cStmt.setInt(1, 5);
        cStmt.registerOutParameter(2, Types.INTEGER);

        cStmt.execute();
        assertEquals(6, cStmt.getInt(2));
        cStmt.clearParameters();
        cStmt.close();

        conn1.setCatalog("mysql");
        cStmt = null;
        cStmt = conn1.prepareCall("{call `TST1`.`PROC`(?, ?)}");
        cStmt.setInt(1, 5);
        cStmt.registerOutParameter(2, Types.INTEGER);

        cStmt.execute();
        assertEquals(6, cStmt.getInt(2));
        cStmt.clearParameters();
        cStmt.close();

        this.stmt = savedSt;
    }

    /**
     * Tests fix for BUG#61332 - Check if "LIKE" or "=" is sent to server in I__S query when no wildcards are supplied for schema parameter.
     *
     * @throws Exception
     */
    @Test
    public void testBug61332() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        props.setProperty(PropertyKey.queryInterceptors.getKeyName(), QueryInterceptorBug61332.class.getName());

        createDatabase("dbbug61332");
        Connection testConn = getConnectionWithProps(props);

        try {
            createTable("dbbug61332.bug61332", "(c1 char(1))");
            DatabaseMetaData metaData = testConn.getMetaData();

            this.rs = ((JdbcConnection) this.conn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                    ? metaData.getColumns(null, "dbbug61332", "bug61332", null)
                    : metaData.getColumns("dbbug61332", null, "bug61332", null);
            this.rs.next();
        } finally {
        }
    }

    public static class QueryInterceptorBug61332 extends BaseQueryInterceptor {

        @Override
        public <T extends Resultset> T preProcess(Supplier<String> str, Query interceptedQuery) {
            String sql = str.get();
            if (interceptedQuery instanceof ClientPreparedStatement) {
                sql = ((ClientPreparedStatement) interceptedQuery).getPreparedSql();
                assertTrue(StringUtils.indexOfIgnoreCase(0, sql, "WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?") > -1, "Failed on: " + sql);
            }
            return null;
        }

    }

    @Test
    public void testQuotedGunk() throws Exception {
        createTable("testQuotedGunk", "(field1 int)");

        String quotedCatalog = "`" + this.conn.getCatalog() + "`";
        String unquotedCatalog = this.conn.getCatalog();

        DatabaseMetaData dbmd = this.conn.getMetaData();
        this.rs = dbmd.getTables(quotedCatalog, null, "testQuotedGunk", new String[] { "TABLE" });
        assertTrue(this.rs.next());
        this.rs = dbmd.getTables(unquotedCatalog, null, "testQuotedGunk", new String[] { "TABLE" });
        assertTrue(this.rs.next());
        this.rs = dbmd.getColumns(quotedCatalog, null, "testQuotedGunk", "field1");
        assertTrue(this.rs.next());
        this.rs = dbmd.getColumns(unquotedCatalog, null, "testQuotedGunk", "field1");
        assertTrue(this.rs.next());
    }

    /**
     * Tests fix for BUG#61203 - noAccessToProcedureBodies does not work anymore.
     *
     * @throws Exception
     */
    @Test
    public void testBug61203() throws Exception {
        Connection rootConn = null;
        Connection userConn = null;
        CallableStatement cStmt = null;

        try {
            Properties props = getPropertiesFromTestsuiteUrl();
            String dbname = props.getProperty(PropertyKey.DBNAME.getKeyName());
            if (dbname == null) {
                assertTrue(false, "No database selected");
            }

            createUser("'bug61203user'@'%'", "identified by 'foo'");
            this.stmt.executeUpdate("delete from mysql.db where user='bug61203user'");
            this.stmt.executeUpdate("insert into mysql.db (Host, Db, User, Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv,Drop_priv, "
                    + "Grant_priv, References_priv, Index_priv, Alter_priv, Create_tmp_table_priv, Lock_tables_priv, Create_view_priv,"
                    + "Show_view_priv, Create_routine_priv, Alter_routine_priv, Execute_priv, Event_priv, Trigger_priv) VALUES ('%', '" + dbname
                    + "', 'bug61203user', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'N')");
            this.stmt.executeUpdate("insert into mysql.db (Host, Db, User, Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv,Drop_priv, "
                    + "Grant_priv, References_priv, Index_priv, Alter_priv, Create_tmp_table_priv, Lock_tables_priv, Create_view_priv,"
                    + "Show_view_priv, Create_routine_priv, Alter_routine_priv, Execute_priv, Event_priv, Trigger_priv) VALUES "
                    + "('%', 'information\\_schema', 'bug61203user', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', "
                    + "'Y', 'N', 'N')");
            this.stmt.executeUpdate("flush privileges");

            // 1. underprivileged user is the creator
            this.stmt.executeUpdate("DROP FUNCTION IF EXISTS testbug61203fn;");
            this.stmt.executeUpdate("CREATE DEFINER='bug61203user'@'%' FUNCTION testbug61203fn(a float) RETURNS INT NO SQL BEGIN RETURN a; END");
            this.stmt.executeUpdate("DROP PROCEDURE IF EXISTS testbug61203pr;");
            this.stmt.executeUpdate(
                    "CREATE DEFINER='bug61203user'@'%' PROCEDURE testbug61203pr(INOUT a float, b bigint, c int) " + "NO SQL BEGIN SET @a = b + c; END");
            testBug61203checks(rootConn, userConn);
            this.stmt.executeUpdate("DROP FUNCTION IF EXISTS testbug61203fn;");
            this.stmt.executeUpdate("DROP PROCEDURE IF EXISTS testbug61203pr;");

            // 2. root user is the creator
            createFunction("testbug61203fn", "(a float) RETURNS INT NO SQL BEGIN RETURN a; END");
            createProcedure("testbug61203pr", "(INOUT a float, b bigint, c int) NO SQL BEGIN SET @a = b + c; END");
            testBug61203checks(rootConn, userConn);

        } finally {
            dropFunction("testbug61203fn");
            dropProcedure("testbug61203pr");

            if (cStmt != null) {
                cStmt.close();
            }
            if (rootConn != null) {
                rootConn.close();
            }
            if (userConn != null) {
                userConn.close();
            }
        }
    }

    private void testBug61203checks(Connection rootConn, Connection userConn) throws SQLException {
        CallableStatement cStmt = null;

        // 1.1. with information schema
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.noAccessToProcedureBodies.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        rootConn = getConnectionWithProps(props);
        props.setProperty(PropertyKey.USER.getKeyName(), "bug61203user");
        props.setProperty(PropertyKey.PASSWORD.getKeyName(), "foo");
        userConn = getConnectionWithProps(props);
        // 1.1.1. root call;
        callFunction(cStmt, rootConn);
        callProcedure(cStmt, rootConn);
        // 1.1.2. underprivileged user call;
        callFunction(cStmt, userConn);
        callProcedure(cStmt, userConn);

        // 1.2. no information schema
        props.clear();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.noAccessToProcedureBodies.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
        rootConn = getConnectionWithProps(props);
        props.setProperty(PropertyKey.USER.getKeyName(), "bug61203user");
        props.setProperty(PropertyKey.PASSWORD.getKeyName(), "foo");
        userConn = getConnectionWithProps(props);
        // 1.2.1. root call;
        callFunction(cStmt, rootConn);
        callProcedure(cStmt, rootConn);
        // 1.2.2. underprivileged user call;
        callFunction(cStmt, userConn);
        callProcedure(cStmt, userConn);
    }

    private void callFunction(CallableStatement cStmt, Connection c) throws SQLException {
        cStmt = c.prepareCall("{? = CALL testbug61203fn(?)}");
        cStmt.registerOutParameter(1, Types.INTEGER);
        cStmt.setFloat(2, 2);
        cStmt.execute();
        assertEquals(cStmt.getInt(1), .001, 2f);
    }

    private void callProcedure(CallableStatement cStmt, Connection c) throws SQLException {
        cStmt = c.prepareCall("{CALL testbug61203pr(?,?,?)}");
        cStmt.setFloat(1, 2);
        cStmt.setInt(2, 1);
        cStmt.setInt(3, 1);
        cStmt.registerOutParameter(1, Types.INTEGER);
        cStmt.execute();
        assertEquals(cStmt.getInt(1), .001, 2f);
    }

    /**
     * Tests fix for BUG#63456 - MetaData precision is different when using UTF8 or Latin1 tables
     *
     * @throws Exception
     */
    @Test
    public void testBug63456() throws Exception {
        //createTable("testBug63456_custom1", "(TEST VARCHAR(10)) ENGINE = MyISAM CHARACTER SET custom1 COLLATE custom1_general_ci");
        createTable("testBug63456_latin1", "(TEST VARCHAR(10)) DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci");
        createTable("testBug63456_utf8", "(TEST VARCHAR(10)) DEFAULT CHARACTER SET utf8");
        createTable("testBug63456_utf8_bin", "(TEST VARCHAR(10)) DEFAULT CHARACTER SET utf8 COLLATE utf8_bin");

        //this.rs = this.stmt.executeQuery("select * from testBug63456_custom1");
        //int precision_custom1 = this.rs.getMetaData().getPrecision(1);
        //assertEquals(10, precision_custom1);

        this.rs = this.stmt.executeQuery("select * from testBug63456_latin1");
        int precision_latin1 = this.rs.getMetaData().getPrecision(1);

        this.rs = this.stmt.executeQuery("select * from testBug63456_utf8");
        int precision_utf8 = this.rs.getMetaData().getPrecision(1);

        this.rs = this.stmt.executeQuery("select * from testBug63456_utf8_bin");
        int precision_utf8bin = this.rs.getMetaData().getPrecision(1);

        assertEquals(precision_latin1, precision_utf8);
        assertEquals(precision_utf8, precision_utf8bin);
    }

    /**
     * Tests fix for BUG#63800 - getVersionColumns() does not return timestamp fields; always empty.
     *
     * @throws Exception
     */
    @Test
    public void testBug63800() throws Exception {
        try {
            Properties props = getPropertiesFromTestsuiteUrl();
            String dbname = props.getProperty(PropertyKey.DBNAME.getKeyName());
            assertFalse(StringUtils.isNullOrEmpty(dbname), "No database selected");

            for (boolean useIS : new boolean[] { false, true }) {
                for (boolean dbMapsToSchema : new boolean[] { false, true }) {
                    props = new Properties();
                    props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
                    props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
                    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 + "'");
                        }
                    }
                    props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
                    props.setProperty(PropertyKey.databaseTerm.getKeyName(), dbMapsToSchema ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name());
                    Connection conn2 = getConnectionWithProps(props);
                    Statement stmt2 = null;

                    try {
                        stmt2 = conn2.createStatement();
                        testTimestamp(conn2, stmt2, dbname);
                        if (versionMeetsMinimum(5, 6, 5)) {
                            testDatetime(conn2, stmt2, dbname);
                        }
                    } finally {
                        if (stmt2 != null) {
                            stmt2.close();
                        }
                        if (conn2 != null) {
                            conn2.close();
                        }
                    }
                }
            }
        } finally {
            dropTable("testBug63800");
        }
    }

    private void testTimestamp(Connection con, Statement st, String dbname) throws SQLException {
        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)");
        DatabaseMetaData dmd = con.getMetaData();

        if (((JdbcConnection) con).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA) {
            String dbPattern = con.getSchema().substring(0, con.getSchema().length() - 1) + "%";
            this.rs = dmd.getVersionColumns(null, dbPattern, "testBug63800");
            assertFalse(this.rs.next(), "Schema pattern " + dbPattern + " should not be recognized.");
        } else {
            String dbPattern = con.getCatalog().substring(0, con.getCatalog().length() - 1) + "%";
            this.rs = dmd.getVersionColumns(dbPattern, null, "testBug63800");
            assertFalse(this.rs.next(), "Catalog pattern " + dbPattern + " should not be recognized.");
        }

        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertTrue(this.rs.next(), "1 column must be found");
        assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");
        assertEquals(19, this.rs.getInt("COLUMN_SIZE"));

        if (versionMeetsMinimum(5, 6, 4)) {
            // fractional seconds are not supported in previous versions
            st.execute("DROP  TABLE IF EXISTS testBug63800");
            st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))");
            dmd = con.getMetaData();
            this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
            assertTrue(this.rs.next(), "1 column must be found");
            assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");
            assertEquals(23, this.rs.getInt("COLUMN_SIZE"));
        }

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        if ("ON".equals(getMysqlVariable("explicit_defaults_for_timestamp"))) {
            assertFalse(this.rs.next(), "0 column must be found");
        } else {
            assertTrue(this.rs.next(), "1 column must be found");
            assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");
        }

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertFalse(this.rs.next(), "0 column must be found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP DEFAULT 0)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertFalse(this.rs.next(), "0 column must be found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP DEFAULT 0 ON UPDATE CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertTrue(this.rs.next(), "1 column must be found");
        assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertTrue(this.rs.next(), "1 column must be found");
        assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP NULL, f2 TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertTrue(this.rs.next(), "1 column must be found");
        assertEquals("f2", this.rs.getString(2), "Wrong column or single column not found");

        // ALTER test
        st.execute("ALTER TABLE testBug63800 CHANGE COLUMN `f2` `f2` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', "
                + "ADD COLUMN `f3` TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP  AFTER `f2`");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertTrue(this.rs.next(), "1 column must be found");
        assertEquals("f3", this.rs.getString(2), "Wrong column or single column not found");
    }

    private void testDatetime(Connection con, Statement st, String dbname) throws SQLException {
        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)");
        DatabaseMetaData dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertTrue(this.rs.next(), "1 column must be found");
        assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");
        assertEquals(19, this.rs.getInt("COLUMN_SIZE"));

        if (versionMeetsMinimum(5, 6, 4)) {
            // fractional seconds are not supported in previous versions
            st.execute("DROP  TABLE IF EXISTS testBug63800");
            st.execute("CREATE TABLE testBug63800(f1 DATETIME(4) DEFAULT CURRENT_TIMESTAMP(4) ON UPDATE CURRENT_TIMESTAMP(4))");
            dmd = con.getMetaData();
            this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
            assertTrue(this.rs.next(), "1 column must be found");
            assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");
            assertEquals(24, this.rs.getInt("COLUMN_SIZE"));
        }

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 DATETIME)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertFalse(this.rs.next(), "0 column must be found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 DATETIME DEFAULT CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertFalse(this.rs.next(), "0 column must be found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 DATETIME DEFAULT 0)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertFalse(this.rs.next(), "0 column must be found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 DATETIME DEFAULT 0 ON UPDATE CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertTrue(this.rs.next(), "1 column must be found");
        assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 DATETIME ON UPDATE CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        assertTrue(this.rs.next(), "1 column must be found");
        assertEquals("f1", this.rs.getString(2), "Wrong column or single column not found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 DATETIME NULL, f2 DATETIME ON UPDATE CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        int cnt = 0;
        while (this.rs.next()) {
            cnt++;
            assertEquals(1, cnt, "1 column must be found");
            assertEquals("f2", this.rs.getString(2), "Wrong column or single column not found");
        }

        // ALTER 1 test
        st.execute("ALTER TABLE testBug63800 CHANGE COLUMN `f2` `f2` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', "
                + "ADD COLUMN `f3` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP  AFTER `f2`");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        cnt = 0;
        while (this.rs.next()) {
            cnt++;
            assertEquals(1, cnt, "1 column must be found");
            assertEquals("f3", this.rs.getString(2), "Wrong column or single column not found");
        }

        // ALTER 2 test
        st.execute("ALTER TABLE testBug63800 CHANGE COLUMN `f2` `f2` TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        cnt = 0;
        while (this.rs.next()) {
            cnt++;
        }
        assertEquals(2, cnt, "2 column must be found");

        st.execute("DROP  TABLE IF EXISTS testBug63800");
        st.execute("CREATE TABLE testBug63800(f1 TIMESTAMP, f2 DATETIME ON UPDATE CURRENT_TIMESTAMP, f3 TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP)");
        dmd = con.getMetaData();
        this.rs = dmd.getVersionColumns(dbname, dbname, "testBug63800");
        cnt = 0;
        while (this.rs.next()) {
            cnt++;
        }
        if ("ON".equals(getMysqlVariable("explicit_defaults_for_timestamp"))) {
            assertEquals(2, cnt, "2 column must be found");
        } else {
            assertEquals(3, cnt, "3 column must be found");
        }
    }

    /**
     * Tests fix for BUG#16436511 - getDriverName() returns a string with company name "MySQL-AB"
     *
     * @throws Exception
     */
    @Test
    public void testBug16436511() throws Exception {
        DatabaseMetaData dbmd = this.conn.getMetaData();
        assertEquals("MySQL Connector/J", dbmd.getDriverName());
    }

    /**
     * Test fix for BUG#68098 - DatabaseMetaData.getIndexInfo sorts results incorrectly.
     *
     * @throws Exception
     */
    @Test
    public void testBug68098() throws Exception {
        String[] expectedIndexesOrder = new String[] { "index_1", "index_1", "index_3", "PRIMARY", "index_2", "index_2", "index_4" };

        createTable("testBug68098", "(column_1 INT NOT NULL, column_2 INT NOT NULL, column_3 INT NOT NULL, PRIMARY KEY (column_1))");
        this.stmt.execute("CREATE INDEX index_4 ON testBug68098 (column_2)");
        this.stmt.execute("CREATE UNIQUE INDEX index_3 ON testBug68098 (column_3)");
        this.stmt.execute("CREATE INDEX index_2 ON testBug68098 (column_2, column_1)");
        this.stmt.execute("CREATE UNIQUE INDEX index_1 ON testBug68098 (column_3, column_2)");

        boolean useIS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), Boolean.toString(useIS));

            Connection testConn = getConnectionWithProps(props);
            DatabaseMetaData testDbMetaData = testConn.getMetaData();
            this.rs = testDbMetaData.getIndexInfo(this.dbName, null, "testBug68098", false, false);
            int ind = 0;
            while (this.rs.next()) {
                assertEquals(expectedIndexesOrder[ind++], this.rs.getString("INDEX_NAME"),
                        (useIS ? "I_S MetaData" : "MySQL MetaData") + ", sort order is wrong");
            }
            this.rs.close();
            testConn.close();
        } while (useIS = !useIS);
    }

    /**
     * Tests fix for BUG#65871 - DatabaseMetaData.getColumns() throws an MySQLSyntaxErrorException.
     * Delimited names of databases and tables are handled correctly now. The edge case is ANSI quoted identifiers with leading and trailing "`" symbols, for
     * example CREATE DATABASE "`dbname`". Methods like DatabaseMetaData.getColumns() allow parameters passed both in unquoted and quoted form, quoted form is
     * not JDBC-compliant but used by third party tools. So when you pass the identifier "`dbname`" in unquoted form (`dbname`) driver handles it as quoted by
     * "`" symbol. To handle such identifiers correctly a new behavior was added to pedantic mode (connection property pedantic=true), now if it set to true
     * methods like DatabaseMetaData.getColumns() treat all parameters as unquoted.
     *
     * @throws Exception
     */
    @Test
    public void testBug65871() throws Exception {
        createTable("testbug65871_foreign",
                "(cpd_foreign_1_id int(8) not null, cpd_foreign_2_id int(8) not null, primary key (cpd_foreign_1_id, cpd_foreign_2_id)) ", "InnoDB");

        Connection pedanticConn = null;
        Connection pedanticConn_IS = null;
        Connection nonPedanticConn = null;
        Connection nonPedanticConn_IS = 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=ansi");

            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
            props.setProperty(PropertyKey.pedantic.getKeyName(), "false");
            nonPedanticConn = getConnectionWithProps(props);

            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
            nonPedanticConn_IS = getConnectionWithProps(props);

            props.setProperty(PropertyKey.pedantic.getKeyName(), "true");
            pedanticConn_IS = getConnectionWithProps(props);

            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
            pedanticConn = getConnectionWithProps(props);

            // 1. Non-pedantic, without I_S.
            testBug65871_testCatalogs(nonPedanticConn);

            // 2. Pedantic, without I_S.
            testBug65871_testCatalogs(pedanticConn);

            // 3. Non-pedantic, with I_S.
            testBug65871_testCatalogs(nonPedanticConn_IS);

            // 4. Pedantic, with I_S.
            testBug65871_testCatalogs(pedanticConn_IS);

        } finally {
            if (nonPedanticConn != null) {
                nonPedanticConn.close();
            }
            if (nonPedanticConn_IS != null) {
                nonPedanticConn_IS.close();
            }
            if (pedanticConn != null) {
                pedanticConn.close();
            }
            if (pedanticConn_IS != null) {
                pedanticConn_IS.close();
            }
        }
    }

    private void testBug65871_testCatalogs(Connection conn1) throws Exception {
        boolean pedantic = ((MysqlConnection) conn1).getPropertySet().getBooleanProperty(PropertyKey.pedantic).getValue();

        testBug65871_testCatalog("db1`testbug65871", StringUtils.quoteIdentifier("db1`testbug65871", pedantic), conn1);

        testBug65871_testCatalog("db2`testbug65871", StringUtils.quoteIdentifier("db2`testbug65871", "\"", pedantic), conn1);

        testBug65871_testCatalog("`db3`testbug65871`", StringUtils.quoteIdentifier("`db3`testbug65871`", "\"", pedantic), conn1);
    }

    private void testBug65871_testCatalog(String unquotedDbName, String quotedDbName, Connection conn1) throws Exception {
        Statement st1 = null;

        try {
            st1 = conn1.createStatement();

            // 1. catalog
            st1.executeUpdate("DROP DATABASE IF EXISTS " + quotedDbName);
            st1.executeUpdate("CREATE DATABASE " + quotedDbName);
            this.rs = st1.executeQuery("show databases like '" + unquotedDbName + "'");
            assertTrue(this.rs.next(), "Database " + unquotedDbName + " (quoted " + quotedDbName + ") not found.");
            assertEquals(unquotedDbName, this.rs.getString(1));

            boolean pedantic = ((MysqlConnection) conn1).getPropertySet().getBooleanProperty(PropertyKey.pedantic).getValue();

            testBug65871_testTable(unquotedDbName, quotedDbName, "table1`testbug65871", StringUtils.quoteIdentifier("table1`testbug65871", pedantic), conn1,
                    st1);

            testBug65871_testTable(unquotedDbName, quotedDbName, "table2`testbug65871", StringUtils.quoteIdentifier("table2`testbug65871", "\"", pedantic),
                    conn1, st1);

            testBug65871_testTable(unquotedDbName, quotedDbName, "table3\"testbug65871", StringUtils.quoteIdentifier("table3\"testbug65871", "\"", pedantic),
                    conn1, st1);

            testBug65871_testTable(unquotedDbName, quotedDbName, "`table4`testbug65871`", StringUtils.quoteIdentifier("`table4`testbug65871`", "\"", pedantic),
                    conn1, st1);

        } finally {
            if (st1 != null) {
                st1.executeUpdate("DROP DATABASE IF EXISTS " + quotedDbName);
                st1.close();
            }
        }
    }

    private void testBug65871_testTable(String unquotedDbName, String quotedDbName, String unquotedTableName, String quotedTableName, Connection conn1,
            Statement st1) throws Exception {
        StringBuilder failedTests = new StringBuilder();
        try {

            String sql = "CREATE  TABLE " + quotedDbName + "." + quotedTableName + "(\"`B`EST`\" INT NOT NULL PRIMARY KEY, `C\"1` int(11) DEFAULT NULL,"
                    + " TS TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, \"cpd_f\"\"oreign_1_id\" int(8) not null,"
                    + " \"`cpd_f\"\"oreign_2_id`\" int(8) not null, KEY `NEWINX` (`C\"1`), KEY `NEWINX2` (`C\"1`, `TS`),"
                    + " foreign key (\"cpd_f\"\"oreign_1_id\", \"`cpd_f\"\"oreign_2_id`\")  references "
                    + (((JdbcConnection) st1.getConnection()).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                            .getValue() == DatabaseTerm.SCHEMA ? this.conn.getSchema() : this.conn.getCatalog())
                    + ".testbug65871_foreign(cpd_foreign_1_id, cpd_foreign_2_id),  CONSTRAINT `APPFK` FOREIGN KEY (`C\"1`) REFERENCES " + quotedDbName + "."
                    + quotedTableName + " (\"`B`EST`\")) ENGINE=InnoDB";

            st1.executeUpdate(sql);

            // 1. Create table
            try {
                this.rs = st1.executeQuery("SHOW TABLES FROM " + quotedDbName + " LIKE '" + unquotedTableName + "'");
                if (!this.rs.next() || !unquotedTableName.equals(this.rs.getString(1))) {
                    failedTests.append(sql + "\n");
                }
            } catch (Exception e) {
                failedTests.append(sql + "\n");
            }

            // 2. extractForeignKeyFromCreateTable(...)
            try {
                this.rs = ((com.mysql.cj.jdbc.DatabaseMetaData) conn1.getMetaData()).extractForeignKeyFromCreateTable(unquotedDbName, unquotedTableName);
                if (!this.rs.next()) {
                    failedTests.append("conn.getMetaData.extractForeignKeyFromCreateTable(unquotedDbName, unquotedTableName);\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.extractForeignKeyFromCreateTable(unquotedDbName, unquotedTableName);\n");
            }

            // 3. getColumns(...)
            try {
                boolean found = false;
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getColumns(null, unquotedDbName, unquotedTableName, "`B`EST`")
                        : conn1.getMetaData().getColumns(unquotedDbName, null, unquotedTableName, "`B`EST`");
                while (this.rs.next()) {
                    if ("`B`EST`".equals(this.rs.getString("COLUMN_NAME"))) {
                        found = true;
                    }
                }
                if (!found) {
                    failedTests.append("conn.getMetaData.getColumns(unquotedDbName, null, unquotedTableName, null);\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.getColumns(unquotedDbName, null, unquotedTableName, null);\n");
            }

            // 4. getBestRowIdentifier(...)
            try {
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getBestRowIdentifier(null, unquotedDbName, unquotedTableName, DatabaseMetaData.bestRowNotPseudo, true)
                        : conn1.getMetaData().getBestRowIdentifier(unquotedDbName, null, unquotedTableName, DatabaseMetaData.bestRowNotPseudo, true);
                if (!this.rs.next() || !"`B`EST`".equals(this.rs.getString("COLUMN_NAME"))) {
                    failedTests.append(
                            "conn.getMetaData.getBestRowIdentifier(unquotedDbName, null, unquotedTableName, DatabaseMetaData.bestRowNotPseudo, " + "true);\n");
                }
            } catch (Exception e) {
                failedTests
                        .append("conn.getMetaData.getBestRowIdentifier(unquotedDbName, null, unquotedTableName, DatabaseMetaData.bestRowNotPseudo, true);\n");
            }

            // 5. getCrossReference(...)
            try {
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getCrossReference(null, this.conn.getCatalog(), "testbug65871_foreign", null, unquotedDbName, unquotedTableName)
                        : conn1.getMetaData().getCrossReference(this.conn.getCatalog(), null, "testbug65871_foreign", unquotedDbName, null, unquotedTableName);
                if (!this.rs.next()) {
                    failedTests.append("conn.getMetaData.getCrossReference(this.conn.getCatalog(), null, \"testbug65871_foreign\", unquotedDbName, null, "
                            + "unquotedTableName);\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.getCrossReference(this.conn.getCatalog(), null, \"testbug65871_foreign\", unquotedDbName, null, "
                        + "unquotedTableName);\n");
            }

            // 6.getExportedKeys(...)
            try {
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getExportedKeys(unquotedDbName, null, unquotedTableName)
                        : conn1.getMetaData().getExportedKeys(unquotedDbName, null, unquotedTableName);
                if (!this.rs.next()) {
                    failedTests.append("conn.getMetaData.getExportedKeys(unquotedDbName, null, unquotedTableName);\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.getExportedKeys(unquotedDbName, null, unquotedTableName);\n");
            }

            // 7. getImportedKeys(...)
            try {
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getImportedKeys(null, unquotedDbName, unquotedTableName)
                        : conn1.getMetaData().getImportedKeys(unquotedDbName, null, unquotedTableName);
                if (!this.rs.next()) {
                    failedTests.append("conn.getMetaData.getImportedKeys(unquotedDbName, null, unquotedTableName);\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.getImportedKeys(unquotedDbName, null, unquotedTableName);\n");
            }

            // 8. getIndexInfo(...)
            try {
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getIndexInfo(null, unquotedDbName, unquotedTableName, true, false)
                        : conn1.getMetaData().getIndexInfo(unquotedDbName, null, unquotedTableName, true, false);
                if (!this.rs.next()) {
                    failedTests.append("conn.getMetaData.getIndexInfo(unquotedDbName, null, unquotedTableName, true, false);\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.getIndexInfo(unquotedDbName, null, unquotedTableName, true, false);\n");
            }

            // 9. getPrimaryKeys(...)
            try {
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getPrimaryKeys(null, unquotedDbName, unquotedTableName)
                        : conn1.getMetaData().getPrimaryKeys(unquotedDbName, null, unquotedTableName);
                if (!this.rs.next()) {
                    failedTests.append("conn.getMetaData.getPrimaryKeys(unquotedDbName, null, unquotedTableName);\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.getPrimaryKeys(unquotedDbName, null, unquotedTableName);\n");
            }

            // 10. getTables(...)
            try {
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getTables(null, unquotedDbName, unquotedTableName, new String[] { "TABLE" })
                        : conn1.getMetaData().getTables(unquotedDbName, null, unquotedTableName, new String[] { "TABLE" });
                if (!this.rs.next()) {
                    failedTests.append("conn.getMetaData.getTables(unquotedDbName, null, unquotedTableName, new String[] {\"TABLE\"});\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.getTables(unquotedDbName, null, unquotedTableName, new String[] {\"TABLE\"});\n");
            }

            // 11. getVersionColumns(...)
            try {
                this.rs = ((JdbcConnection) conn1).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm).getValue() == DatabaseTerm.SCHEMA
                        ? conn1.getMetaData().getVersionColumns(null, unquotedDbName, unquotedTableName)
                        : conn1.getMetaData().getVersionColumns(unquotedDbName, null, unquotedTableName);
                if (!this.rs.next() || !"TS".equals(this.rs.getString(2))) {
                    failedTests.append("conn.getMetaData.getVersionColumns(unquotedDbName, null, unquotedTableName);\n");
                }
            } catch (Exception e) {
                failedTests.append("conn.getMetaData.getVersionColumns(unquotedDbName, null, unquotedTableName);\n");
            }

        } finally {
            try {
                st1.executeUpdate("DROP TABLE IF EXISTS " + quotedDbName + "." + quotedTableName);
            } catch (Exception e) {
                failedTests.append("DROP TABLE IF EXISTS " + quotedDbName + "." + quotedTableName + "\n");
            }
        }

        if (failedTests.length() > 0) {
            boolean pedantic = ((MysqlConnection) conn1).getPropertySet().getBooleanProperty(PropertyKey.pedantic).getValue();
            throw new Exception("Failed tests for catalog " + quotedDbName + " and table " + quotedTableName + " ("
                    + (pedantic ? "pedantic mode" : "non-pedantic mode") + "):\n" + failedTests.toString());
        }
    }

    /**
     * Tests fix for BUG#69298 - Different outcome from DatabaseMetaData.getFunctions() when using I__S.
     *
     * @throws Exception
     */
    @Test
    public void testBug69298() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");
        Connection testConn;

        createFunction("testBug69298_func", "(param_func INT) RETURNS INT COMMENT 'testBug69298_func comment' DETERMINISTIC RETURN 1");
        createProcedure("testBug69298_proc", "(IN param_proc INT) COMMENT 'testBug69298_proc comment' SELECT 1");

        boolean useIS = false;
        boolean procRetFunc = false;
        boolean dbIsSch = false;
        do {
            final String testCase = String.format("Case: [useIS: %s, procRetFunc: %s, DbTerm: %s]", useIS ? "Y" : "N", procRetFunc ? "Y" : "N",
                    dbIsSch ? "SCHEMA" : "CATALOG");

            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), Boolean.toString(useIS));
            props.setProperty(PropertyKey.getProceduresReturnsFunctions.getKeyName(), Boolean.toString(procRetFunc));
            props.setProperty(PropertyKey.databaseTerm.getKeyName(), dbIsSch ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name());
            testConn = getConnectionWithProps(props);
            assertEquals(useIS, ((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.useInformationSchema).getValue(),
                    "Property useInformationSchema should be " + useIS);
            assertEquals(procRetFunc, ((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.getProceduresReturnsFunctions).getValue(),
                    "Property getProceduresReturnsFunctions should be " + procRetFunc);
            assertEquals(dbIsSch ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name(),
                    ((JdbcConnection) testConn).getPropertySet().getEnumProperty(PropertyKey.databaseTerm).getValue().name(),
                    "Property databaseTerm should be " + (dbIsSch ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name()));
            checkGetFunctionsForBug69298(testCase, testConn);
            checkGetFunctionColumnsForBug69298(testCase, testConn);
            checkGetProceduresForBug69298(testCase, testConn);
            checkGetProcedureColumnsForBug69298(testCase, testConn);
            testConn.close();
        } while ((useIS = !useIS) || (procRetFunc = !procRetFunc) || (dbIsSch = !dbIsSch));
    }

    private void checkGetFunctionsForBug69298(String stepDescription, Connection testConn) throws Exception {
        DatabaseMetaData testDbMetaData = testConn.getMetaData();
        boolean dbMapsToSchema = ((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                .getValue() == DatabaseTerm.SCHEMA;
        boolean lowerCaseIds = testDbMetaData.storesLowerCaseIdentifiers();
        ResultSet functionsMD = testDbMetaData.getFunctions(null, null, "testBug69298_%");
        String sd = stepDescription + " getFunctions() ";

        assertTrue(functionsMD.next(), sd + "one row expected.");

        // function: testBug69298_func

        assertEquals(dbMapsToSchema ? "def" : lowerCaseIds ? testConn.getCatalog().toLowerCase() : testConn.getCatalog(), functionsMD.getString("FUNCTION_CAT"),
                sd + "-> FUNCTION_CAT");
        assertEquals(dbMapsToSchema ? lowerCaseIds ? testConn.getSchema().toLowerCase() : testConn.getSchema() : null, functionsMD.getString("FUNCTION_SCHEM"),
                sd + "-> FUNCTION_SCHEM");
        assertEquals("testBug69298_func", functionsMD.getString("FUNCTION_NAME"), sd + "-> FUNCTION_NAME");
        assertEquals("testBug69298_func comment", functionsMD.getString("REMARKS"), sd + "-> REMARKS");
        assertEquals(DatabaseMetaData.functionNoTable, functionsMD.getShort("FUNCTION_TYPE"), sd + "-> FUNCTION_TYPE");
        assertEquals("testBug69298_func", functionsMD.getString("SPECIFIC_NAME"), sd + "-> SPECIFIC_NAME");

        assertFalse(functionsMD.next(), stepDescription + "no more rows expected.");
    }

    private void checkGetFunctionColumnsForBug69298(String stepDescription, Connection testConn) throws Exception {
        DatabaseMetaData testDbMetaData = testConn.getMetaData();
        boolean dbMapsToSchema = ((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                .getValue() == DatabaseTerm.SCHEMA;
        boolean lowerCaseIds = testDbMetaData.storesLowerCaseIdentifiers();
        ResultSet funcColsMD = testDbMetaData.getFunctionColumns(null, null, "testBug69298_%", "%");
        String sd = stepDescription + " getFunctionColumns() ";

        assertTrue(funcColsMD.next(), sd + "1st of 2 rows expected.");

        // function column: testBug69298_func return
        assertEquals(dbMapsToSchema ? "def" : lowerCaseIds ? testConn.getCatalog().toLowerCase() : testConn.getCatalog(), funcColsMD.getString("FUNCTION_CAT"),
                sd + "-> FUNCTION_CAT");
        assertEquals(dbMapsToSchema ? lowerCaseIds ? testConn.getSchema().toLowerCase() : testConn.getSchema() : null, funcColsMD.getString("FUNCTION_SCHEM"),
                sd + "-> FUNCTION_SCHEM");
        assertEquals("testBug69298_func", funcColsMD.getString("FUNCTION_NAME"), sd + "-> FUNCTION_NAME");
        assertEquals("", funcColsMD.getString("COLUMN_NAME"), sd + "-> COLUMN_NAME");
        assertEquals(DatabaseMetaData.functionReturn, funcColsMD.getShort("COLUMN_TYPE"), sd + "-> COLUMN_TYPE");
        assertEquals(Types.INTEGER, funcColsMD.getInt("DATA_TYPE"), sd + "-> DATA_TYPE");
        assertEquals("INT", funcColsMD.getString("TYPE_NAME"), sd + "-> TYPE_NAME");
        assertEquals(10, funcColsMD.getInt("PRECISION"), sd + "-> PRECISION");
        assertEquals(10, funcColsMD.getInt("LENGTH"), sd + "-> LENGTH");
        assertEquals(0, funcColsMD.getShort("SCALE"), sd + "-> SCALE");
        assertEquals(10, funcColsMD.getShort("RADIX"), sd + "-> RADIX");
        assertEquals(DatabaseMetaData.functionNullable, funcColsMD.getShort("NULLABLE"), sd + "-> NULLABLE");
        assertEquals(null, funcColsMD.getString("REMARKS"), sd + "-> REMARKS");
        assertEquals(0, funcColsMD.getInt("CHAR_OCTET_LENGTH"), sd + "-> CHAR_OCTET_LENGTH");
        assertEquals(0, funcColsMD.getInt("ORDINAL_POSITION"), sd + "-> ORDINAL_POSITION");
        assertEquals("YES", funcColsMD.getString("IS_NULLABLE"), sd + "-> IS_NULLABLE");
        assertEquals("testBug69298_func", funcColsMD.getString("SPECIFIC_NAME"), sd + "-> SPECIFIC_NAME");

        assertTrue(funcColsMD.next(), sd + "2nd of 2 rows expected.");

        // function column: testBug69298_func.param_func
        assertEquals(dbMapsToSchema ? "def" : lowerCaseIds ? testConn.getCatalog().toLowerCase() : testConn.getCatalog(), funcColsMD.getString("FUNCTION_CAT"),
                sd + "-> FUNCTION_CAT");
        assertEquals(dbMapsToSchema ? lowerCaseIds ? testConn.getSchema().toLowerCase() : testConn.getSchema() : null, funcColsMD.getString("FUNCTION_SCHEM"),
                sd + "-> FUNCTION_SCHEM");
        assertEquals("testBug69298_func", funcColsMD.getString("FUNCTION_NAME"), sd + "-> FUNCTION_NAME");
        assertEquals("param_func", funcColsMD.getString("COLUMN_NAME"), sd + "-> COLUMN_NAME");
        assertEquals(DatabaseMetaData.functionColumnIn, funcColsMD.getShort("COLUMN_TYPE"), sd + "-> COLUMN_TYPE");
        assertEquals(Types.INTEGER, funcColsMD.getInt("DATA_TYPE"), sd + "-> DATA_TYPE");
        assertEquals("INT", funcColsMD.getString("TYPE_NAME"), sd + "-> TYPE_NAME");
        assertEquals(10, funcColsMD.getInt("PRECISION"), sd + "-> PRECISION");
        assertEquals(10, funcColsMD.getInt("LENGTH"), sd + "-> LENGTH");
        assertEquals(0, funcColsMD.getShort("SCALE"), sd + "-> SCALE");
        assertEquals(10, funcColsMD.getShort("RADIX"), sd + "-> RADIX");
        assertEquals(DatabaseMetaData.functionNullable, funcColsMD.getShort("NULLABLE"), sd + "-> NULLABLE");
        assertEquals(null, funcColsMD.getString("REMARKS"), sd + "-> REMARKS");
        assertEquals(0, funcColsMD.getInt("CHAR_OCTET_LENGTH"), sd + "-> CHAR_OCTET_LENGTH");
        assertEquals(1, funcColsMD.getInt("ORDINAL_POSITION"), sd + "-> ORDINAL_POSITION");
        assertEquals("YES", funcColsMD.getString("IS_NULLABLE"), sd + "-> IS_NULLABLE");
        assertEquals("testBug69298_func", funcColsMD.getString("SPECIFIC_NAME"), sd + "-> SPECIFIC_NAME");

        assertFalse(funcColsMD.next(), sd + "no more rows expected.");
    }

    private void checkGetProceduresForBug69298(String stepDescription, Connection testConn) throws Exception {
        DatabaseMetaData testDbMetaData = testConn.getMetaData();
        boolean dbMapsToSchema = ((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                .getValue() == DatabaseTerm.SCHEMA;
        boolean lowerCaseIds = testDbMetaData.storesLowerCaseIdentifiers();
        ResultSet proceduresMD = testDbMetaData.getProcedures(null, null, "testBug69298_%");
        String sd = stepDescription + " getProcedures() ";
        boolean isGetProceduresReturnsFunctions = ((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.getProceduresReturnsFunctions)
                .getValue();

        if (isGetProceduresReturnsFunctions) {
            assertTrue(proceduresMD.next(), sd + "1st of 2 rows expected.");

            // function: testBug69298_func
            assertEquals(dbMapsToSchema ? "def" : lowerCaseIds ? testConn.getCatalog().toLowerCase() : testConn.getCatalog(),
                    proceduresMD.getString("PROCEDURE_CAT"), sd + "-> PROCEDURE_CAT");
            assertEquals(dbMapsToSchema ? lowerCaseIds ? testConn.getSchema().toLowerCase() : testConn.getSchema() : null,
                    proceduresMD.getString("PROCEDURE_SCHEM"), sd + "-> PROCEDURE_SCHEM");
            assertEquals("testBug69298_func", proceduresMD.getString("PROCEDURE_NAME"), sd + "-> PROCEDURE_NAME");
            assertEquals("testBug69298_func comment", proceduresMD.getString("REMARKS"), sd + "-> REMARKS");
            assertEquals(DatabaseMetaData.procedureReturnsResult, proceduresMD.getShort("PROCEDURE_TYPE"), sd + "-> PROCEDURE_TYPE");
            assertEquals("testBug69298_func", proceduresMD.getString("SPECIFIC_NAME"), sd + "-> SPECIFIC_NAME");

            assertTrue(proceduresMD.next(), sd + "2nd of 2 rows expected.");
        } else {
            assertTrue(proceduresMD.next(), sd + "one row expected.");
        }

        // procedure: testBug69298_proc
        assertEquals(dbMapsToSchema ? "def" : lowerCaseIds ? testConn.getCatalog().toLowerCase() : testConn.getCatalog(),
                proceduresMD.getString("PROCEDURE_CAT"), sd + "-> PROCEDURE_CAT");
        assertEquals(dbMapsToSchema ? lowerCaseIds ? testConn.getSchema().toLowerCase() : testConn.getSchema() : null,
                proceduresMD.getString("PROCEDURE_SCHEM"), sd + "-> PROCEDURE_SCHEM");
        assertEquals("testBug69298_proc", proceduresMD.getString("PROCEDURE_NAME"), sd + "-> PROCEDURE_NAME");
        assertEquals("testBug69298_proc comment", proceduresMD.getString("REMARKS"), sd + "-> REMARKS");
        assertEquals(DatabaseMetaData.procedureNoResult, proceduresMD.getShort("PROCEDURE_TYPE"), sd + "-> PROCEDURE_TYPE");
        assertEquals("testBug69298_proc", proceduresMD.getString("SPECIFIC_NAME"), sd + "-> SPECIFIC_NAME");

        assertFalse(proceduresMD.next(), stepDescription + "no more rows expected.");
    }

    private void checkGetProcedureColumnsForBug69298(String stepDescription, Connection testConn) throws Exception {
        DatabaseMetaData testDbMetaData = testConn.getMetaData();
        boolean dbMapsToSchema = ((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                .getValue() == DatabaseTerm.SCHEMA;
        boolean lowerCaseIds = testDbMetaData.storesLowerCaseIdentifiers();
        ResultSet procColsMD = testDbMetaData.getProcedureColumns(null, null, "testBug69298_%", "%");
        String sd = stepDescription + " getProcedureColumns() ";
        boolean isGetProceduresReturnsFunctions = ((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.getProceduresReturnsFunctions)
                .getValue();

        if (isGetProceduresReturnsFunctions) {
            assertTrue(procColsMD.next(), sd + "1st of 3 rows expected.");

            // function column: testBug69298_func return
            assertEquals(dbMapsToSchema ? "def" : lowerCaseIds ? testConn.getCatalog().toLowerCase() : testConn.getCatalog(),
                    procColsMD.getString("PROCEDURE_CAT"), sd + "-> PROCEDURE_CAT");
            assertEquals(dbMapsToSchema ? lowerCaseIds ? testConn.getSchema().toLowerCase() : testConn.getSchema() : null,
                    procColsMD.getString("PROCEDURE_SCHEM"), sd + "-> PROCEDURE_SCHEM");
            assertEquals("testBug69298_func", procColsMD.getString("PROCEDURE_NAME"), sd + "-> PROCEDURE_NAME");
            assertEquals("", procColsMD.getString("COLUMN_NAME"), sd + "-> COLUMN_NAME");
            assertEquals(DatabaseMetaData.procedureColumnReturn, procColsMD.getShort("COLUMN_TYPE"), sd + "-> COLUMN_TYPE");
            assertEquals(Types.INTEGER, procColsMD.getInt("DATA_TYPE"), sd + "-> DATA_TYPE");
            assertEquals("INT", procColsMD.getString("TYPE_NAME"), sd + "-> TYPE_NAME");
            assertEquals(10, procColsMD.getInt("PRECISION"), sd + "-> PRECISION");
            assertEquals(10, procColsMD.getInt("LENGTH"), sd + "-> LENGTH");
            assertEquals(0, procColsMD.getShort("SCALE"), sd + "-> SCALE");
            assertEquals(10, procColsMD.getShort("RADIX"), sd + "-> RADIX");
            assertEquals(DatabaseMetaData.procedureNullable, procColsMD.getShort("NULLABLE"), sd + "-> NULLABLE");
            assertEquals(null, procColsMD.getString("REMARKS"), sd + "-> REMARKS");
            assertEquals(null, procColsMD.getString("COLUMN_DEF"), sd + "-> COLUMN_DEF");
            assertEquals(0, procColsMD.getInt("SQL_DATA_TYPE"), sd + "-> SQL_DATA_TYPE");
            assertEquals(0, procColsMD.getInt("SQL_DATETIME_SUB"), sd + "-> SQL_DATETIME_SUB");
            assertEquals(0, procColsMD.getInt("CHAR_OCTET_LENGTH"), sd + "-> CHAR_OCTET_LENGTH");
            assertEquals(0, procColsMD.getInt("ORDINAL_POSITION"), sd + "-> ORDINAL_POSITION");
            assertEquals("YES", procColsMD.getString("IS_NULLABLE"), sd + "-> IS_NULLABLE");
            assertEquals("testBug69298_func", procColsMD.getString("SPECIFIC_NAME"), sd + "-> SPECIFIC_NAME");

            assertTrue(procColsMD.next(), sd + "2nd of 3 rows expected.");

            // function column: testBug69298_func.param_func
            assertEquals(dbMapsToSchema ? "def" : lowerCaseIds ? testConn.getCatalog().toLowerCase() : testConn.getCatalog(),
                    procColsMD.getString("PROCEDURE_CAT"), sd + "-> PROCEDURE_CAT");
            assertEquals(dbMapsToSchema ? lowerCaseIds ? testConn.getSchema().toLowerCase() : testConn.getSchema() : null,
                    procColsMD.getString("PROCEDURE_SCHEM"), sd + "-> PROCEDURE_SCHEM");
            assertEquals("testBug69298_func", procColsMD.getString("PROCEDURE_NAME"), sd + "-> PROCEDURE_NAME");
            assertEquals("param_func", procColsMD.getString("COLUMN_NAME"), sd + "-> COLUMN_NAME");
            assertEquals(DatabaseMetaData.procedureColumnIn, procColsMD.getShort("COLUMN_TYPE"), sd + "-> COLUMN_TYPE");
            assertEquals(Types.INTEGER, procColsMD.getInt("DATA_TYPE"), sd + "-> DATA_TYPE");
            assertEquals("INT", procColsMD.getString("TYPE_NAME"), sd + "-> TYPE_NAME");
            assertEquals(10, procColsMD.getInt("PRECISION"), sd + "-> PRECISION");
            assertEquals(10, procColsMD.getInt("LENGTH"), sd + "-> LENGTH");
            assertEquals(0, procColsMD.getShort("SCALE"), sd + "-> SCALE");
            assertEquals(10, procColsMD.getShort("RADIX"), sd + "-> RADIX");
            assertEquals(DatabaseMetaData.procedureNullable, procColsMD.getShort("NULLABLE"), sd + "-> NULLABLE");
            assertEquals(null, procColsMD.getString("REMARKS"), sd + "-> REMARKS");
            assertEquals(null, procColsMD.getString("COLUMN_DEF"), sd + "-> COLUMN_DEF");
            assertEquals(0, procColsMD.getInt("SQL_DATA_TYPE"), sd + "-> SQL_DATA_TYPE");
            assertEquals(0, procColsMD.getInt("SQL_DATETIME_SUB"), sd + "-> SQL_DATETIME_SUB");
            assertEquals(0, procColsMD.getInt("CHAR_OCTET_LENGTH"), sd + "-> CHAR_OCTET_LENGTH");
            assertEquals(1, procColsMD.getInt("ORDINAL_POSITION"), sd + "-> ORDINAL_POSITION");
            assertEquals("YES", procColsMD.getString("IS_NULLABLE"), sd + "-> IS_NULLABLE");
            assertEquals("testBug69298_func", procColsMD.getString("SPECIFIC_NAME"), sd + "-> SPECIFIC_NAME");

            assertTrue(procColsMD.next(), sd + "3rd of 3 rows expected.");
        } else {
            assertTrue(procColsMD.next(), sd + "one row expected.");
        }

        // procedure column: testBug69298_proc.param_proc
        assertEquals(dbMapsToSchema ? "def" : lowerCaseIds ? testConn.getCatalog().toLowerCase() : testConn.getCatalog(), procColsMD.getString("PROCEDURE_CAT"),
                sd + "-> PROCEDURE_CAT");
        assertEquals(dbMapsToSchema ? lowerCaseIds ? testConn.getSchema().toLowerCase() : testConn.getSchema() : null, procColsMD.getString("PROCEDURE_SCHEM"),
                sd + "-> PROCEDURE_SCHEM");
        assertEquals("testBug69298_proc", procColsMD.getString("PROCEDURE_NAME"), sd + "-> PROCEDURE_NAME");
        assertEquals("param_proc", procColsMD.getString("COLUMN_NAME"), sd + "-> COLUMN_NAME");
        assertEquals(DatabaseMetaData.procedureColumnIn, procColsMD.getShort("COLUMN_TYPE"), sd + "-> COLUMN_TYPE");
        assertEquals(Types.INTEGER, procColsMD.getInt("DATA_TYPE"), sd + "-> DATA_TYPE");
        assertEquals("INT", procColsMD.getString("TYPE_NAME"), sd + "-> TYPE_NAME");
        assertEquals(10, procColsMD.getInt("PRECISION"), sd + "-> PRECISION");
        assertEquals(10, procColsMD.getInt("LENGTH"), sd + "-> LENGTH");
        assertEquals(0, procColsMD.getShort("SCALE"), sd + "-> SCALE");
        assertEquals(10, procColsMD.getShort("RADIX"), sd + "-> RADIX");
        assertEquals(DatabaseMetaData.procedureNullable, procColsMD.getShort("NULLABLE"), sd + "-> NULLABLE");
        assertEquals(null, procColsMD.getString("REMARKS"), sd + "-> REMARKS");
        assertEquals(null, procColsMD.getString("COLUMN_DEF"), sd + "-> COLUMN_DEF");
        assertEquals(0, procColsMD.getInt("SQL_DATA_TYPE"), sd + "-> SQL_DATA_TYPE");
        assertEquals(0, procColsMD.getInt("SQL_DATETIME_SUB"), sd + "-> SQL_DATETIME_SUB");
        assertEquals(0, procColsMD.getInt("CHAR_OCTET_LENGTH"), sd + "-> CHAR_OCTET_LENGTH");
        assertEquals(1, procColsMD.getInt("ORDINAL_POSITION"), sd + "-> ORDINAL_POSITION");
        assertEquals("YES", procColsMD.getString("IS_NULLABLE"), sd + "-> IS_NULLABLE");
        assertEquals("testBug69298_proc", procColsMD.getString("SPECIFIC_NAME"), sd + "-> SPECIFIC_NAME");

        assertFalse(procColsMD.next(), sd + "no more rows expected.");
    }

    /**
     * Tests fix for BUG#17248345 - GETFUNCTIONCOLUMNS() METHOD RETURNS COLUMNS OF PROCEDURE. (this happens when functions and procedures have a common name)
     *
     * @throws Exception
     */
    @Test
    public void testBug17248345() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");

        Connection testConn;

        // create one stored procedure and one function with same name
        createProcedure("testBug17248345", "(IN proccol INT) SELECT 1");
        createFunction("testBug17248345", "(funccol INT) RETURNS INT DETERMINISTIC RETURN 1");

        // test with standard connection (getProceduresReturnsFunctions=true & useInformationSchema=false)
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
        testConn = getConnectionWithProps(props);
        assertFalse(((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.useInformationSchema).getValue(),
                "Property useInformationSchema should be false");
        assertTrue(((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.getProceduresReturnsFunctions).getValue(),
                "Property getProceduresReturnsFunctions should be true");
        checkMetaDataInfoForBug17248345(testConn);
        testConn.close();

        // test with property useInformationSchema=true (getProceduresReturnsFunctions=true)
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        testConn = getConnectionWithProps(props);
        assertTrue(((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.useInformationSchema).getValue(),
                "Property useInformationSchema should be true");
        assertTrue(((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.getProceduresReturnsFunctions).getValue(),
                "Property getProceduresReturnsFunctions should be true");
        checkMetaDataInfoForBug17248345(testConn);
        testConn.close();

        // test with property getProceduresReturnsFunctions=false (useInformationSchema=false)
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
        props.setProperty(PropertyKey.getProceduresReturnsFunctions.getKeyName(), "false");
        testConn = getConnectionWithProps(props);
        assertFalse(((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.useInformationSchema).getValue(),
                "Property useInformationSchema should be false");
        assertFalse(((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.getProceduresReturnsFunctions).getValue(),
                "Property getProceduresReturnsFunctions should be false");
        checkMetaDataInfoForBug17248345(testConn);
        testConn.close();

        // test with property useInformationSchema=true & getProceduresReturnsFunctions=false
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        props.setProperty(PropertyKey.getProceduresReturnsFunctions.getKeyName(), "false");
        testConn = getConnectionWithProps(props);
        assertTrue(((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.useInformationSchema).getValue(),
                "Property useInformationSchema should be true");
        assertFalse(((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.getProceduresReturnsFunctions).getValue(),
                "Property getProceduresReturnsFunctions should be false");
        checkMetaDataInfoForBug17248345(testConn);
        testConn.close();
    }

    private void checkMetaDataInfoForBug17248345(Connection testConn) throws Exception {
        DatabaseMetaData testDbMetaData = testConn.getMetaData();
        ResultSet rsMD;
        boolean useInfoSchema = ((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.useInformationSchema).getValue();
        boolean getProcRetFunc = ((JdbcConnection) testConn).getPropertySet().getBooleanProperty(PropertyKey.getProceduresReturnsFunctions).getValue();
        String stepDescription = "Prop. useInfoSchema(" + (useInfoSchema ? 1 : 0) + ") + getProcRetFunc(" + (getProcRetFunc ? 1 : 0) + "):";
        String sd;

        // getFunctions() must return 1 record.
        sd = stepDescription + " getFunctions() ";
        rsMD = testDbMetaData.getFunctions(null, null, "testBug17248345");
        assertTrue(rsMD.next(), sd + "one row expected.");
        assertEquals("testBug17248345", rsMD.getString("FUNCTION_NAME"), sd + " -> FUNCTION_NAME");
        assertFalse(rsMD.next(), sd + "no more rows expected.");

        // getFunctionColumns() must return 2 records (func return + func param).
        sd = stepDescription + " getFunctionColumns() ";
        rsMD = testDbMetaData.getFunctionColumns(null, null, "testBug17248345", "%");
        assertTrue(rsMD.next(), sd + "1st of 2 rows expected.");
        assertEquals("testBug17248345", rsMD.getString("FUNCTION_NAME"), sd + " -> FUNCTION_NAME");
        assertEquals("", rsMD.getString("COLUMN_NAME"), sd + " -> COLUMN_NAME");
        assertTrue(rsMD.next(), sd + "2nd of 2 rows expected.");
        assertEquals("testBug17248345", rsMD.getString("FUNCTION_NAME"), sd + " -> FUNCTION_NAME");
        assertEquals("funccol", rsMD.getString("COLUMN_NAME"), sd + " -> COLUMN_NAME");
        assertFalse(rsMD.next(), sd + "no more rows expected.");

        // getProcedures() must return 1 or 2 records, depending on if getProceduresReturnsFunctions is false or true
        // respectively. When exists a procedure and a function with same name, function is returned first.
        sd = stepDescription + " getProcedures() ";
        rsMD = testDbMetaData.getProcedures(null, null, "testBug17248345");
        if (getProcRetFunc) {
            assertTrue(rsMD.next(), sd + "1st of 2 rows expected.");
            assertEquals("testBug17248345", rsMD.getString("PROCEDURE_NAME"), sd + " -> PROCEDURE_NAME");
            assertTrue(rsMD.next(), sd + "2nd of 2 rows expected.");
        } else {
            assertTrue(rsMD.next(), sd + "one row expected.");
        }
        assertEquals("testBug17248345", rsMD.getString("PROCEDURE_NAME"), sd + " -> PROCEDURE_NAME");
        assertFalse(rsMD.next(), sd + "no more rows expected.");

        // getProcedureColumns() must return 1 or 3 records, depending on if getProceduresReturnsFunctions is false or
        // true respectively. When exists a procedure and a function with same name, function is returned first.
        sd = stepDescription + " getProcedureColumns() ";
        rsMD = testDbMetaData.getProcedureColumns(null, null, "testBug17248345", "%");
        if (getProcRetFunc) {
            assertTrue(rsMD.next(), sd + "1st of 3 rows expected.");
            assertEquals("testBug17248345", rsMD.getString("PROCEDURE_NAME"), sd + " -> PROCEDURE_NAME");
            assertEquals("", rsMD.getString("COLUMN_NAME"), sd + " -> COLUMN_NAME");
            assertTrue(rsMD.next(), sd + "2nd of 3 rows expected.");
            assertEquals("testBug17248345", rsMD.getString("PROCEDURE_NAME"), sd + " -> PROCEDURE_NAME");
            assertEquals("funccol", rsMD.getString("COLUMN_NAME"), sd + " -> COLUMN_NAME");
            assertTrue(rsMD.next(), sd + "3rd of 3 rows expected.");
        } else {
            assertTrue(rsMD.next(), sd + "one row expected.");
        }
        assertEquals("testBug17248345", rsMD.getString("PROCEDURE_NAME"), sd + " -> PROCEDURE_NAME");
        assertEquals("proccol", rsMD.getString("COLUMN_NAME"), sd + " -> COLUMN_NAME");
        assertFalse(rsMD.next(), sd + "no more rows expected.");
    }

    /**
     * Tests fix for BUG#69290 - JDBC Table type "SYSTEM TABLE" is used inconsistently.
     *
     * Tests DatabaseMetaData.getTableTypes() and DatabaseMetaData.getTables() against schemas: mysql, information_schema, performance_schema, test.
     *
     * @throws Exception
     */
    @Test
    public void testBug69290() throws Exception {
        String[] testStepDescription = new String[] { "MySQL MetaData", "I__S MetaData" };

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");
        Connection connUseISAndNullCurr = getConnectionWithProps(props);

        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "false");
        Connection connUseISAndNullAll = getConnectionWithProps(props);

        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");
        Connection connUseMySqlAndNullCurr = getConnectionWithProps(props);

        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "false");
        Connection connUseMySqlAndNullAll = getConnectionWithProps(props);

        boolean dbMapsToSchema = ((JdbcConnection) this.conn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                .getValue() == DatabaseTerm.SCHEMA;

        final String testDb = dbMapsToSchema ? this.conn.getSchema() : this.conn.getCatalog();

        Connection[] testConnections = new Connection[] { connUseMySqlAndNullCurr, connUseISAndNullCurr };

        // check table types returned in getTableTypes()
        final List<String> tableTypes = Arrays.asList(new String[] { "LOCAL TEMPORARY", "SYSTEM TABLE", "SYSTEM VIEW", "TABLE", "VIEW" });

        for (int i = 0; i < testStepDescription.length; i++) {
            DatabaseMetaData testDbMetaData = testConnections[i].getMetaData();
            this.rs = testDbMetaData.getTableTypes();

            int idx = 0;
            while (this.rs.next()) {
                String message = testStepDescription[i] + ", table type '" + this.rs.getString("TABLE_TYPE") + "'";
                assertFalse(idx >= tableTypes.size(), message + " not expected.");
                assertEquals(tableTypes.get(idx++), this.rs.getString("TABLE_TYPE"), message);
            }
        }

        // create table and view in '(test)' schema
        createTable("testBug69290_table", "(c1 INT)");
        createView("testBug69290_view", "AS SELECT * FROM testBug69290_table WHERE c1 > 1");

        int[][] countResults = new int[][] { { 0, 0, 0 }, { 0, 0, 0 } };

        // check table types returned in getTables() for each catalog/schema
        for (int i = 0; i < testStepDescription.length; i++) {
            DatabaseMetaData testDbMetaData = testConnections[i].getMetaData();

            // check catalog/schema 'information_schema'
            this.rs = dbMapsToSchema ? testDbMetaData.getTables(null, "information_schema", "%", null)
                    : testDbMetaData.getTables("information_schema", null, "%", null);
            while (this.rs.next()) {
                assertEquals("SYSTEM VIEW", this.rs.getString("TABLE_TYPE"),
                        testStepDescription[i] + ", 'information_schema' catalog/schema, wrong table type for '" + this.rs.getString("TABLE_NAME") + "'.");
                countResults[i][0]++;
            }

            // check catalog/schema 'mysql'
            this.rs = dbMapsToSchema ? testDbMetaData.getTables(null, "mysql", "%", null) : testDbMetaData.getTables("mysql", null, "%", null);
            while (this.rs.next()) {
                assertEquals("SYSTEM TABLE", this.rs.getString("TABLE_TYPE"),
                        testStepDescription[i] + ", 'mysql' catalog/schema, wrong table type for '" + this.rs.getString("TABLE_NAME") + "'.");
                countResults[i][1]++;
            }

            // check catalog/schema 'performance_schema'
            this.rs = dbMapsToSchema ? testDbMetaData.getTables(null, "performance_schema", "%", null)
                    : testDbMetaData.getTables("performance_schema", null, "%", null);
            while (this.rs.next()) {
                assertEquals("SYSTEM TABLE", this.rs.getString("TABLE_TYPE"),
                        testStepDescription[i] + ", 'performance_schema' catalog/schema, wrong table type for '" + this.rs.getString("TABLE_NAME") + "'.");
                countResults[i][2]++;
            }

            // check catalog/schema '(test)'
            this.rs = dbMapsToSchema ? testDbMetaData.getTables(null, testDb, "testBug69290_%", null)
                    : testDbMetaData.getTables(testDb, null, "testBug69290_%", null);
            assertTrue(this.rs.next(), testStepDescription[i] + ", '" + testDb + "' catalog/schema, expected row from getTables().");
            assertEquals("TABLE", this.rs.getString("TABLE_TYPE"),
                    testStepDescription[i] + ", '" + testDb + "' catalog/schema, wrong table type for '" + this.rs.getString("TABLE_NAME") + "'.");
            assertTrue(this.rs.next(), testStepDescription[i] + ", '" + testDb + "' catalog/schema, expected row from getTables().");
            assertEquals("VIEW", this.rs.getString("TABLE_TYPE"),
                    testStepDescription[i] + ", '" + testDb + "' catalog/schema, wrong table type for '" + this.rs.getString("TABLE_NAME") + "'.");
        }

        // compare results count
        assertTrue(countResults[0][0] == countResults[1][0], "The number of results from getTables() MySQl(" + countResults[0][0] + ") and I__S("
                + countResults[1][0] + ") should be the same for 'information_schema' catalog/schema.");
        assertTrue(countResults[0][1] == countResults[1][1], "The number of results from getTables() MySQl(" + countResults[0][1] + ") and I__S("
                + countResults[1][1] + ") should be the same for 'mysql' catalog/schema.");
        assertTrue(countResults[0][2] == countResults[1][2], "The number of results from getTables() MySQl(" + countResults[0][2] + ") and I__S("
                + countResults[1][2] + ") should be the same for 'performance_schema' catalog/schema.");

        testConnections = new Connection[] { connUseMySqlAndNullAll, connUseISAndNullAll };
        countResults = new int[][] { { 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0 } };

        // check table types returned in getTables() for all catalogs/schemas and filter by table type (tested with property nullCatalogMeansCurrent=false)
        for (int i = 0; i < testStepDescription.length; i++) {
            DatabaseMetaData testDbMetaData = testConnections[i].getMetaData();
            int j = 0;

            // check table type filters
            for (String tableType : tableTypes) {
                this.rs = testDbMetaData.getTables(null, null, "%", new String[] { tableType });
                while (this.rs.next()) {
                    assertEquals(tableType, this.rs.getString("TABLE_TYPE"), testStepDescription[i] + ", table type filter '" + tableType
                            + "', wrong table type for '" + this.rs.getString("TABLE_NAME") + "'.");
                    countResults[i][j]++;
                }
                j++;
            }
        }

        // compare results count
        int i = 0;
        for (String tableType : tableTypes) {
            assertTrue(countResults[0][i] == countResults[1][i], "The number of results from getTables() MySQl(" + countResults[0][i] + ") and I__S("
                    + countResults[1][i] + ") should be the same for '" + tableType + "' table type filter.");
            i++;
        }
    }

    /**
     * Tests fix for BUG#35115 - yearIsDateType=false has no effect on result's column type and class.
     *
     * @throws Exception
     */
    @Test
    public void testBug35115() throws Exception {
        Connection testConnection = null;
        ResultSetMetaData rsMetaData = null;

        createTable("testBug35115", "(year YEAR)");
        this.stmt.executeUpdate("INSERT INTO testBug35115 VALUES ('2002'), ('2013')");

        /*
         * test connection with property 'yearIsDateType=false'
         */
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.yearIsDateType.getKeyName(), "false");
        testConnection = getConnectionWithProps(props);
        Statement st = testConnection.createStatement();
        this.rs = st.executeQuery("SELECT * FROM testBug35115");
        rsMetaData = this.rs.getMetaData();

        assertTrue(this.rs.next());
        assertEquals(Types.SMALLINT, rsMetaData.getColumnType(1), "YEAR columns should be treated as java.sql.Types.SMALLINT");
        assertEquals("YEAR", rsMetaData.getColumnTypeName(1), "YEAR columns should be identified as 'YEAR'");
        assertEquals(java.lang.Short.class.getName(), rsMetaData.getColumnClassName(1), "YEAR columns should be mapped to java.lang.Short");
        assertEquals(java.lang.Short.class.getName(), this.rs.getObject(1).getClass().getName(), "YEAR columns should be returned as java.lang.Short");

        testConnection.close();

        /*
         * test connection with property 'yearIsDateType=true'
         */
        props.setProperty(PropertyKey.yearIsDateType.getKeyName(), "true");
        testConnection = getConnectionWithProps(props);
        st = testConnection.createStatement();
        this.rs = st.executeQuery("SELECT * FROM testBug35115");
        rsMetaData = this.rs.getMetaData();

        assertTrue(this.rs.next());
        assertEquals(Types.DATE, rsMetaData.getColumnType(1), "YEAR columns should be treated as java.sql.Types.DATE");
        assertEquals("YEAR", rsMetaData.getColumnTypeName(1), "YEAR columns should be identified as 'YEAR'");
        assertEquals(java.sql.Date.class.getName(), rsMetaData.getColumnClassName(1), "YEAR columns should be mapped to java.sql.Date");
        assertEquals(java.sql.Date.class.getName(), this.rs.getObject(1).getClass().getName(), "YEAR columns should be returned as java.sql.Date");

        testConnection.close();
    }

    /**
     * Tests fix for BUG#68307 - getFunctionColumns() returns incorrect "COLUMN_TYPE" information. This is a JDBC4 feature.
     *
     * @throws Exception
     */
    @Test
    public void testBug68307() throws Exception {
        createFunction("testBug68307_func", "(func_param_in INT) RETURNS INT DETERMINISTIC RETURN 1");

        createProcedure("testBug68307_proc", "(IN proc_param_in INT, OUT proc_param_out INT, INOUT proc_param_inout INT) SELECT 1");

        // test metadata from MySQL
        DatabaseMetaData testDbMetaData = this.conn.getMetaData();
        checkFunctionColumnTypeForBug68307("MySQL", testDbMetaData);
        checkProcedureColumnTypeForBug68307("MySQL", testDbMetaData);

        // test metadata from I__S
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        Connection connUseIS = getConnectionWithProps(props);
        testDbMetaData = connUseIS.getMetaData();
        checkFunctionColumnTypeForBug68307("I__S", testDbMetaData);
        checkProcedureColumnTypeForBug68307("I__S", testDbMetaData);
        connUseIS.close();
    }

    private void checkFunctionColumnTypeForBug68307(String testAgainst, DatabaseMetaData testDbMetaData) throws Exception {
        this.rs = testDbMetaData.getFunctionColumns(null, null, "testBug68307_%", "%");

        while (this.rs.next()) {
            String message = testAgainst + ", function <" + this.rs.getString("FUNCTION_NAME") + "." + this.rs.getString("COLUMN_NAME") + ">";
            if (this.rs.getString("COLUMN_NAME") == null || this.rs.getString("COLUMN_NAME").length() == 0) {
                assertEquals(DatabaseMetaData.functionReturn, this.rs.getShort("COLUMN_TYPE"), message);
            } else if (this.rs.getString("COLUMN_NAME").endsWith("_in")) {
                assertEquals(DatabaseMetaData.functionColumnIn, this.rs.getShort("COLUMN_TYPE"), message);
            } else if (this.rs.getString("COLUMN_NAME").endsWith("_inout")) {
                assertEquals(DatabaseMetaData.functionColumnInOut, this.rs.getShort("COLUMN_TYPE"), message);
            } else if (this.rs.getString("COLUMN_NAME").endsWith("_out")) {
                assertEquals(DatabaseMetaData.functionColumnOut, this.rs.getShort("COLUMN_TYPE"), message);
            } else {
                fail("Column '" + this.rs.getString("FUNCTION_NAME") + "." + this.rs.getString("COLUMN_NAME") + "' not expected within test case.");
            }
        }
    }

    private void checkProcedureColumnTypeForBug68307(String testAgainst, DatabaseMetaData testDbMetaData) throws Exception {
        this.rs = testDbMetaData.getProcedureColumns(null, null, "testBug68307_%", "%");

        while (this.rs.next()) {
            String message = testAgainst + ", procedure <" + this.rs.getString("PROCEDURE_NAME") + "." + this.rs.getString("COLUMN_NAME") + ">";
            if (this.rs.getString("COLUMN_NAME") == null || this.rs.getString("COLUMN_NAME").length() == 0) {
                assertEquals(DatabaseMetaData.procedureColumnReturn, this.rs.getShort("COLUMN_TYPE"), message);
            } else if (this.rs.getString("COLUMN_NAME").endsWith("_in")) {
                assertEquals(DatabaseMetaData.procedureColumnIn, this.rs.getShort("COLUMN_TYPE"), message);
            } else if (this.rs.getString("COLUMN_NAME").endsWith("_inout")) {
                assertEquals(DatabaseMetaData.procedureColumnInOut, this.rs.getShort("COLUMN_TYPE"), message);
            } else if (this.rs.getString("COLUMN_NAME").endsWith("_out")) {
                assertEquals(DatabaseMetaData.procedureColumnOut, this.rs.getShort("COLUMN_TYPE"), message);
            } else {
                fail("Column '" + this.rs.getString("FUNCTION_NAME") + "." + this.rs.getString("COLUMN_NAME") + "' not expected within test case.");
            }
        }
    }

    /**
     * Tests fix for BUG#44451 - getTables does not return resultset with expected columns.
     *
     * @throws Exception
     */
    @Test
    public void testBug44451() throws Exception {
        boolean useIS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), Boolean.toString(useIS));
            Connection testConn = getConnectionWithProps(props);
            DatabaseMetaData testDbMetaData = testConn.getMetaData();

            String methodName = "getClientInfoProperties()";
            List<String> expectedFields = Arrays.asList("NAME", "MAX_LEN", "DEFAULT_VALUE", "DESCRIPTION");
            this.rs = testDbMetaData.getClientInfoProperties();
            checkReturnedColumnsForBug44451(useIS ? "I_S MetaData" : "MySQL MetaData", methodName, expectedFields, this.rs);
            this.rs.close();

            methodName = "getFunctions()";
            expectedFields = Arrays.asList("FUNCTION_CAT", "FUNCTION_SCHEM", "FUNCTION_NAME", "REMARKS", "FUNCTION_TYPE", "SPECIFIC_NAME");
            this.rs = testDbMetaData.getFunctions(null, null, "%");
            checkReturnedColumnsForBug44451(useIS ? "I_S MetaData" : "MySQL MetaData", methodName, expectedFields, this.rs);
            this.rs.close();

            testConn.close();
        } while (useIS = !useIS);
    }

    private void checkReturnedColumnsForBug44451(String stepDescription, String methodName, List<String> expectedFields, ResultSet resultSetToCheck)
            throws Exception {
        ResultSetMetaData rsMetaData = resultSetToCheck.getMetaData();
        int numberOfColumns = rsMetaData.getColumnCount();

        assertEquals(expectedFields.size(), numberOfColumns, stepDescription + ", wrong column count in method '" + methodName + "'.");
        for (int i = 0; i < numberOfColumns; i++) {
            int position = i + 1;
            assertEquals(expectedFields.get(i), rsMetaData.getColumnName(position),
                    stepDescription + ", wrong column at position '" + position + "' in method '" + methodName + "'.");
        }
    }

    /**
     * Tests fix for BUG#20504139 - GETFUNCTIONCOLUMNS() AND GETPROCEDURECOLUMNS() RETURNS ERROR FOR VALID INPUTS.
     *
     * @throws Exception
     */
    @Test
    public void testBug20504139() throws Exception {
        createFunction("testBug20504139f", "(namef CHAR(20)) RETURNS CHAR(50) DETERMINISTIC RETURN CONCAT('Hello, ', namef, '!')");
        createFunction("`testBug20504139``f`", "(namef CHAR(20)) RETURNS CHAR(50) DETERMINISTIC RETURN CONCAT('Hello, ', namef, '!')");
        createProcedure("testBug20504139p", "(INOUT namep CHAR(50)) SELECT  CONCAT('Hello, ', namep, '!') INTO namep");
        createProcedure("`testBug20504139``p`", "(INOUT namep CHAR(50)) SELECT  CONCAT('Hello, ', namep, '!') INTO namep");

        for (int testCase = 0; testCase < 8; testCase++) {// 3 props, 8 combinations: 2^3 = 8
            boolean usePedantic = (testCase & 1) == 1;
            boolean useInformationSchema = (testCase & 2) == 2;
            boolean useFuncsInProcs = (testCase & 4) == 4;

            String connProps = String.format("pedantic=%s,useInformationSchema=%s,getProceduresReturnsFunctions=%s", usePedantic, useInformationSchema,
                    useFuncsInProcs);
            String testDesc = String.format("testBug20504139_%d: %s%n", testCase, connProps);

            Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.pedantic.getKeyName(), "" + usePedantic);
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useInformationSchema);
            props.setProperty(PropertyKey.getProceduresReturnsFunctions.getKeyName(), "" + useFuncsInProcs);

            Connection testConn = getConnectionWithProps(props);
            DatabaseMetaData dbmd = testConn.getMetaData();
            boolean dbMapsToSchema = ((JdbcConnection) testConn).getPropertySet().<DatabaseTerm>getEnumProperty(PropertyKey.databaseTerm)
                    .getValue() == DatabaseTerm.SCHEMA;

            ResultSet testRs = null;

            try {
                /*
                 * test DatabaseMetadata.getProcedureColumns for function
                 */
                int i = 1;
                try {
                    for (String name : new String[] { "testBug20504139f", "testBug20504139`f" }) {
                        testRs = dbMapsToSchema ? dbmd.getProcedureColumns("", this.dbName, name, "%") : dbmd.getProcedureColumns(this.dbName, "", name, "%");

                        if (useFuncsInProcs) {
                            assertTrue(testRs.next(), testDesc);
                            assertEquals("", testRs.getString(4), testDesc + "; " + testCase + "." + i + ". expected function column name (empty)");
                            assertEquals(DatabaseMetaData.procedureColumnReturn, testRs.getInt(5),
                                    testDesc + "; " + testCase + "." + i + ". expected function column type (empty)");
                            assertTrue(testRs.next(), testDesc);
                            assertEquals("namef", testRs.getString(4), testDesc + "; " + testCase + "." + i + ". expected function column name");
                            assertEquals(DatabaseMetaData.procedureColumnIn, testRs.getInt(5),
                                    testDesc + "; " + testCase + "." + i + ". expected function column type (empty)");
                            assertFalse(testRs.next(), testDesc);
                        } else {
                            assertFalse(testRs.next(), testDesc);
                        }

                        testRs.close();
                        i++;
                    }
                } catch (SQLException e) {
                    assertFalse(e.getMessage().matches("FUNCTION `testBug20504139(:?`{2})?[fp]` does not exist"), testDesc + "; " + testCase + "." + i
                            + ". failed to retrieve function columns, with getProcedureColumns(), from database meta data.");
                    throw e;
                }

                /*
                 * test DatabaseMetadata.getProcedureColumns for procedure
                 */
                i = 1;
                try {
                    for (String name : new String[] { "testBug20504139p", "testBug20504139`p" }) {
                        testRs = dbMapsToSchema ? dbmd.getProcedureColumns("", this.dbName, name, "%") : dbmd.getProcedureColumns(this.dbName, "", name, "%");

                        assertTrue(testRs.next());
                        assertEquals("namep", testRs.getString(4), testCase + ". expected procedure column name");
                        assertEquals(DatabaseMetaData.procedureColumnInOut, testRs.getInt(5), testCase + ". expected procedure column type (empty)");
                        assertFalse(testRs.next());

                        testRs.close();
                        i++;
                    }
                } catch (SQLException e) {
                    assertFalse(e.getMessage().matches("PROCEDURE `testBug20504139(:?`{2})?[fp]` does not exist"),
                            testCase + "." + i + ". failed to retrieve prodedure columns, with getProcedureColumns(), from database meta data.");
                    throw e;
                }

                /*
                 * test DatabaseMetadata.getFunctionColumns for function
                 */
                i = 1;
                try {
                    for (String name : new String[] { "testBug20504139f", "testBug20504139`f" }) {
                        testRs = dbMapsToSchema ? dbmd.getFunctionColumns("", this.dbName, name, "%") : dbmd.getFunctionColumns(this.dbName, "", name, "%");

                        assertTrue(testRs.next());
                        assertEquals("", testRs.getString(4), testCase + ". expected function column name (empty)");
                        assertEquals(DatabaseMetaData.functionReturn, testRs.getInt(5), testCase + ". expected function column type (empty)");
                        assertTrue(testRs.next());
                        assertEquals("namef", testRs.getString(4), testCase + ". expected function column name");
                        assertEquals(DatabaseMetaData.functionColumnIn, testRs.getInt(5), testCase + ". expected function column type (empty)");
                        assertFalse(testRs.next());

                        testRs.close();
                        i++;
                    }
                } catch (SQLException e) {
                    assertFalse(e.getMessage().matches("FUNCTION `testBug20504139(:?`{2})?[fp]` does not exist"),
                            testCase + "." + i + ". failed to retrieve function columns, with getFunctionColumns(), from database meta data.");
                    throw e;
                }

                /*
                 * test DatabaseMetadata.getFunctionColumns for procedure
                 */
                i = 1;
                try {
                    for (String name : new String[] { "testBug20504139p", "testBug20504139`p" }) {
                        testRs = dbMapsToSchema ? dbmd.getFunctionColumns("", this.dbName, name, "%") : dbmd.getFunctionColumns(this.dbName, "", name, "%");

                        assertFalse(testRs.next());

                        testRs.close();
                        i++;
                    }
                } catch (SQLException e) {
                    assertFalse(e.getMessage().matches("PROCEDURE `testBug20504139(:?`{2})?[fp]` does not exist"),
                            testCase + "." + i + ". failed to retrieve procedure columns, with getFunctionColumns(), from database meta data.");
                    throw e;
                }
            } finally {
                testConn.close();
            }
        }
    }

    /**
     * Tests fix for BUG#21215151 - DATABASEMETADATA.GETCATALOGS() FAILS TO SORT RESULTS.
     *
     * DatabaseMetaData.GetCatalogs() relies on the results of 'SHOW DATABASES' which deliver a sorted list of databases except for 'information_schema' which
     * is always returned in the first position.
     * This test creates set of databases around the relative position of 'information_schema' and checks the ordering of the final ResultSet.
     *
     * @throws Exception
     */
    @Test
    public void testBug21215151() throws Exception {
        createDatabase("z_testBug21215151");
        createDatabase("j_testBug21215151");
        createDatabase("h_testBug21215151");
        createDatabase("i_testBug21215151");
        createDatabase("a_testBug21215151");

        DatabaseMetaData dbmd = this.conn.getMetaData();
        this.rs = dbmd.getCatalogs();

        // check the relative position of each element in the result set compared to the previous element.
        String previousDb = "";
        while (this.rs.next()) {
            assertTrue(previousDb.compareTo(this.rs.getString(1)) < 0,
                    "'" + this.rs.getString(1) + "' is lexicographically lower than the previous catalog. Check the system output to see the catalogs list.");
            previousDb = this.rs.getString(1);
        }
    }

    /**
     * Tests fix for BUG#19803348 - GETPROCEDURES() RETURNS INCORRECT O/P WHEN USEINFORMATIONSCHEMA=FALSE.
     *
     * Composed by two parts:
     * 1. Confirm that getProcedures() and getProcedureColumns() aren't returning more results than expected (as per reported bug).
     * 2. Confirm that the results from getProcedures() and getProcedureColumns() are in the right order (secondary bug).
     *
     * @throws Exception
     */
    @Test
    public void testBug19803348() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.getProceduresReturnsFunctions.getKeyName(), "false");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "false");

        for (boolean useIS : new boolean[] { false, true }) {
            for (boolean dbMapsToSchema : new boolean[] { false, true }) {
                props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
                props.setProperty(PropertyKey.databaseTerm.getKeyName(), dbMapsToSchema ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name());

                String testCase = "useIS=" + useIS + ", dbMapsToSchema=" + dbMapsToSchema;

                Connection conn1 = null;
                try {
                    conn1 = getConnectionWithProps(props);
                    DatabaseMetaData dbmd = conn1.getMetaData();

                    String testDb1 = "testBug19803348_db1";
                    String testDb2 = "testBug19803348_db2";

                    if (!dbmd.supportsMixedCaseIdentifiers()) {
                        testDb1 = testDb1.toLowerCase();
                        testDb2 = testDb2.toLowerCase();
                    }

                    createDatabase(testDb1);
                    createDatabase(testDb2);

                    // 1. Check if getProcedures() and getProcedureColumns() aren't returning more results than expected (as per reported bug).
                    createFunction(testDb1 + ".testBug19803348_f", "(d INT) RETURNS INT DETERMINISTIC BEGIN RETURN d; END");
                    createProcedure(testDb1 + ".testBug19803348_p", "(d int) BEGIN SELECT d; END");

                    this.rs = dbmd.getFunctions(null, null, "testBug19803348_%");
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_f", this.rs.getString(3), testCase);
                    assertFalse(this.rs.next(), testCase);

                    this.rs = dbmd.getFunctionColumns(null, null, "testBug19803348_%", "%");
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_f", this.rs.getString(3), testCase);
                    assertEquals("", this.rs.getString(4), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_f", this.rs.getString(3), testCase);
                    assertEquals("d", this.rs.getString(4), testCase);
                    assertFalse(this.rs.next(), testCase);

                    this.rs = dbmd.getProcedures(null, null, "testBug19803348_%");
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_p", this.rs.getString(3), testCase);
                    assertFalse(this.rs.next(), testCase);

                    this.rs = dbmd.getProcedureColumns(null, null, "testBug19803348_%", "%");
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_p", this.rs.getString(3), testCase);
                    assertEquals("d", this.rs.getString(4), testCase);
                    assertFalse(this.rs.next(), testCase);

                    dropFunction(testDb1 + ".testBug19803348_f");
                    dropProcedure(testDb1 + ".testBug19803348_p");

                    // 2. Check if the results from getProcedures() and getProcedureColumns() are in the right order (secondary bug).
                    createFunction(testDb1 + ".testBug19803348_B_f", "(d INT) RETURNS INT DETERMINISTIC BEGIN RETURN d; END");
                    createProcedure(testDb1 + ".testBug19803348_B_p", "(d int) BEGIN SELECT d; END");
                    createFunction(testDb2 + ".testBug19803348_A_f", "(d INT) RETURNS INT DETERMINISTIC BEGIN RETURN d; END");
                    createProcedure(testDb2 + ".testBug19803348_A_p", "(d int) BEGIN SELECT d; END");

                    this.rs = dbmd.getFunctions(null, null, "testBug19803348_%");
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_B_f", this.rs.getString(3), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb2, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_A_f", this.rs.getString(3), testCase);
                    assertFalse(this.rs.next(), testCase);

                    this.rs = dbmd.getFunctionColumns(null, null, "testBug19803348_%", "%");
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_B_f", this.rs.getString(3), testCase);
                    assertEquals("", this.rs.getString(4), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_B_f", this.rs.getString(3), testCase);
                    assertEquals("d", this.rs.getString(4), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb2, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_A_f", this.rs.getString(3), testCase);
                    assertEquals("", this.rs.getString(4), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb2, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_A_f", this.rs.getString(3), testCase);
                    assertEquals("d", this.rs.getString(4), testCase);
                    assertFalse(this.rs.next(), testCase);

                    this.rs = dbmd.getProcedures(null, null, "testBug19803348_%");
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_B_p", this.rs.getString(3), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb2, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_A_p", this.rs.getString(3), testCase);
                    assertFalse(this.rs.next(), testCase);

                    this.rs = dbmd.getProcedureColumns(null, null, "testBug19803348_%", "%");
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb1, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_B_p", this.rs.getString(3), testCase);
                    assertEquals("d", this.rs.getString(4), testCase);
                    assertTrue(this.rs.next(), testCase);
                    assertEquals(testDb2, this.rs.getString(dbMapsToSchema ? 2 : 1), testCase);
                    assertEquals("testBug19803348_A_p", this.rs.getString(3), testCase);
                    assertEquals("d", this.rs.getString(4), testCase);
                    assertFalse(this.rs.next(), testCase);

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

    /**
     * Tests fix for BUG#20727196 - GETPROCEDURECOLUMNS() RETURNS EXCEPTION FOR FUNCTION WHICH RETURNS ENUM/SET TYPE.
     *
     * @throws Exception
     */
    @Test
    public void testBug20727196() throws Exception {
        createFunction("testBug20727196_f1",
                "(p ENUM ('Yes', 'No')) RETURNS VARCHAR(10) DETERMINISTIC BEGIN RETURN IF(p='Yes', 'Yay!', if(p='No', 'Ney!', 'What?')); END");
        createFunction("testBug20727196_f2", "(p CHAR(1)) RETURNS ENUM ('Yes', 'No') DETERMINISTIC BEGIN RETURN IF(p='y', 'Yes', if(p='n', 'No', '?')); END");
        createFunction("testBug20727196_f3",
                "(p ENUM ('Yes', 'No')) RETURNS ENUM ('Yes', 'No') DETERMINISTIC BEGIN RETURN IF(p='Yes', 'Yes', if(p='No', 'No', '?')); END");
        createProcedure("testBug20727196_p1", "(p ENUM ('Yes', 'No')) BEGIN SELECT IF(p='Yes', 'Yay!', if(p='No', 'Ney!', 'What?')); END");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.nullDatabaseMeansCurrent.getKeyName(), "true");
        props.setProperty(PropertyKey.getProceduresReturnsFunctions.getKeyName(), "false");
        for (boolean useInformationSchema : new boolean[] { false, true }) {
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useInformationSchema);

            Connection testConn = null;
            try {
                testConn = getConnectionWithProps(props);
                DatabaseMetaData dbmd = testConn.getMetaData();

                this.rs = dbmd.getFunctionColumns(null, null, "testBug20727196_%", "%");

                // testBug20727196_f1 columns:
                assertTrue(this.rs.next());
                assertEquals("testBug20727196_f1", this.rs.getString(3));
                assertEquals("", this.rs.getString(4));
                assertEquals("VARCHAR", this.rs.getString(7));
                assertTrue(this.rs.next());
                assertEquals("testBug20727196_f1", this.rs.getString(3));
                assertEquals("p", this.rs.getString(4));
                assertEquals("ENUM", this.rs.getString(7));

                // testBug20727196_f2 columns:
                assertTrue(this.rs.next());
                assertEquals("testBug20727196_f2", this.rs.getString(3));
                assertEquals("", this.rs.getString(4));
                assertEquals("ENUM", this.rs.getString(7));
                assertTrue(this.rs.next());
                assertEquals("testBug20727196_f2", this.rs.getString(3));
                assertEquals("p", this.rs.getString(4));
                assertEquals("CHAR", this.rs.getString(7));

                // testBug20727196_f3 columns:
                assertTrue(this.rs.next());
                assertEquals("testBug20727196_f3", this.rs.getString(3));
                assertEquals("", this.rs.getString(4));
                assertEquals("ENUM", this.rs.getString(7));
                assertTrue(this.rs.next());
                assertEquals("testBug20727196_f3", this.rs.getString(3));
                assertEquals("p", this.rs.getString(4));
                assertEquals("ENUM", this.rs.getString(7));

                assertFalse(this.rs.next());

                this.rs = dbmd.getProcedureColumns(null, null, "testBug20727196_%", "%");

                // testBug20727196_p1 columns:
                assertTrue(this.rs.next());
                assertEquals("testBug20727196_p1", this.rs.getString(3));
                assertEquals("p", this.rs.getString(4));
                assertEquals("ENUM", this.rs.getString(7));

                assertFalse(this.rs.next());
            } finally {
                if (testConn != null) {
                    testConn.close();
                }
            }
        }
    }

    /**
     * Tests fix for BUG#76187 (20675539), getTypeInfo report maximum precision of 255 for varchar.
     *
     * @throws Exception
     */
    @Test
    public void testBug76187() throws Exception {
        DatabaseMetaData meta = this.conn.getMetaData();
        this.rs = meta.getTypeInfo();
        while (this.rs.next()) {
            if (this.rs.getString("TYPE_NAME").equals("VARCHAR")) {
                if (versionMeetsMinimum(5, 0, 3)) {
                    assertEquals(65535, this.rs.getInt("PRECISION"));
                } else {
                    assertEquals(255, this.rs.getInt("PRECISION"));
                }
            }
        }
    }

    /**
     * Tests fix for BUG#21978216, GETTYPEINFO REPORT MAXIMUM PRECISION OF 255 FOR VARBINARY
     *
     * @throws Exception
     */
    @Test
    public void testBug21978216() throws Exception {
        DatabaseMetaData meta = this.conn.getMetaData();
        this.rs = meta.getTypeInfo();
        while (this.rs.next()) {
            if (this.rs.getString("TYPE_NAME").equals("VARBINARY")) {
                if (versionMeetsMinimum(5, 0, 3)) {
                    assertEquals(65535, this.rs.getInt("PRECISION"));
                } else {
                    assertEquals(255, this.rs.getInt("PRECISION"));
                }
            }
        }
    }

    /**
     * Tests fix for Bug#23212347, ALL API CALLS ON RESULTSET METADATA RESULTS IN NPE WHEN USESERVERPREPSTMTS=TRUE.
     *
     * @throws Exception
     */
    @Test
    public void testBug23212347() throws Exception {
        boolean useSPS = false;
        do {
            String testCase = String.format("Case [SPS: %s]", useSPS ? "Y" : "N");
            createTable("testBug23212347", "(id INT)");

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

            Connection testConn = getConnectionWithProps(props);
            Statement testStmt = testConn.createStatement();
            testStmt.execute("INSERT INTO testBug23212347 VALUES (1)");

            this.pstmt = testConn.prepareStatement("SELECT * FROM testBug23212347 WHERE id = 1");
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertFalse(this.rs.next(), testCase);
            ResultSetMetaData rsmd = this.pstmt.getMetaData();
            assertEquals("id", rsmd.getColumnName(1), testCase);

            this.pstmt = testConn.prepareStatement("SELECT * FROM testBug23212347 WHERE id = ?");
            this.pstmt.setInt(1, 1);
            this.rs = this.pstmt.executeQuery();
            assertTrue(this.rs.next(), testCase);
            assertEquals(1, this.rs.getInt(1), testCase);
            assertFalse(this.rs.next());
            rsmd = this.pstmt.getMetaData();
            assertEquals("id", rsmd.getColumnName(1), testCase);
        } while (useSPS = !useSPS);
    }

    /**
     * Tests fix for Bug#73775 - DBMD.getProcedureColumns()/.getFunctionColumns() fail to filter by columnPattern
     *
     * @throws Exception
     */
    @Test
    public void testBug73775() throws Exception {
        createFunction("testBug73775f", "(param1 CHAR(20), param2 CHAR(20)) RETURNS CHAR(40) DETERMINISTIC RETURN CONCAT(param1, param2)");
        createProcedure("testBug73775p", "(INOUT param1 CHAR(20), IN param2 CHAR(20)) BEGIN  SELECT CONCAT(param1, param2) INTO param1; END");

        boolean useIS = false;
        boolean inclFuncs = false;
        boolean dbMapsToSchema = false;
        do {
            final String testCase = String.format("Case: [useIS: %s, inclFuncs: %s, dbMapsToSchema: %s]", useIS ? "Y" : "N", inclFuncs ? "Y" : "N",
                    dbMapsToSchema ? "Y" : "N");

            final Properties props = new Properties();
            props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
            props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), Boolean.toString(useIS));
            props.setProperty(PropertyKey.getProceduresReturnsFunctions.getKeyName(), Boolean.toString(inclFuncs));
            props.setProperty(PropertyKey.databaseTerm.getKeyName(), dbMapsToSchema ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name());
            final Connection testConn = getConnectionWithProps(props);
            final DatabaseMetaData dbmd = testConn.getMetaData();

            /*
             * Test getProcedureColumns()
             */
            this.rs = dbMapsToSchema ? dbmd.getProcedureColumns("", null, "testBug73775%", "%") : dbmd.getProcedureColumns(null, "", "testBug73775%", "%");
            if (inclFuncs) {
                assertTrue(this.rs.next(), testCase);
                assertEquals("testBug73775f", this.rs.getString(3), testCase);
                assertEquals("", this.rs.getString(4), testCase); // Function return param is always returned.
                assertEquals(DatabaseMetaData.procedureColumnReturn, this.rs.getInt(5));
                assertTrue(this.rs.next(), testCase);
                assertEquals("testBug73775f", this.rs.getString(3), testCase);
                assertEquals("param1", this.rs.getString(4), testCase);
                assertTrue(this.rs.next(), testCase);
                assertEquals("testBug73775f", this.rs.getString(3), testCase);
                assertEquals("param2", this.rs.getString(4), testCase);
            }
            assertTrue(this.rs.next(), testCase);
            assertEquals("testBug73775p", this.rs.getString(3), testCase);
            assertEquals("param1", this.rs.getString(4), testCase);
            assertTrue(this.rs.next(), testCase);
            assertEquals("testBug73775p", this.rs.getString(3), testCase);
            assertEquals("param2", this.rs.getString(4), testCase);
            assertFalse(this.rs.next(), testCase);

            for (String ptn : new String[] { "param1", "_____1", "%1", "p_r_m%1" }) {
                this.rs = dbMapsToSchema ? dbmd.getProcedureColumns("", null, "testBug73775%", ptn) : dbmd.getProcedureColumns(null, "", "testBug73775%", ptn);
                if (inclFuncs) {
                    assertTrue(this.rs.next());
                    assertEquals("testBug73775f", this.rs.getString(3), testCase);
                    assertEquals("", this.rs.getString(4), testCase); // Function return param is always returned.
                    assertEquals(DatabaseMetaData.procedureColumnReturn, this.rs.getInt(5));
                    assertTrue(this.rs.next(), testCase);
                    assertEquals("testBug73775f", this.rs.getString(3), testCase);
                    assertEquals("param1", this.rs.getString(4), testCase);
                }
                assertTrue(this.rs.next(), testCase);
                assertEquals("testBug73775p", this.rs.getString(3), testCase);
                assertEquals("param1", this.rs.getString(4), testCase);
                assertFalse(this.rs.next(), testCase);
            }

            for (String ptn : new String[] { "param2", "_____2", "%2", "p_r_m%2" }) {
                this.rs = dbMapsToSchema ? dbmd.getProcedureColumns("", null, "testBug73775%", ptn) : dbmd.getProcedureColumns(null, "", "testBug73775%", ptn);
                if (inclFuncs) {
                    assertTrue(this.rs.next());
                    assertEquals("testBug73775f", this.rs.getString(3), testCase);
                    assertEquals("", this.rs.getString(4), testCase); // Function return param is always returned.
                    assertEquals(DatabaseMetaData.procedureColumnReturn, this.rs.getInt(5));
                    assertTrue(this.rs.next(), testCase);
                    assertEquals("testBug73775f", this.rs.getString(3), testCase);
                    assertEquals("param2", this.rs.getString(4), testCase);
                }
                assertTrue(this.rs.next(), testCase);
                assertEquals("testBug73775p", this.rs.getString(3), testCase);
                assertEquals("param2", this.rs.getString(4), testCase);
                assertFalse(this.rs.next(), testCase);
            }

            this.rs = dbMapsToSchema ? dbmd.getProcedureColumns("", null, "testBug73775%", "") : dbmd.getProcedureColumns(null, "", "testBug73775%", "");
            if (inclFuncs) {
                assertTrue(this.rs.next(), testCase);
                assertEquals("testBug73775f", this.rs.getString(3), testCase);
                assertEquals("", this.rs.getString(4), testCase); // Function return param is always returned.
                assertEquals(DatabaseMetaData.procedureColumnReturn, this.rs.getInt(5), testCase);
            }
            assertFalse(this.rs.next(), testCase);

            /*
             * Test getFunctionColumns()
             */
            this.rs = dbMapsToSchema ? dbmd.getFunctionColumns("", null, "testBug73775%", "%") : dbmd.getFunctionColumns(null, "", "testBug73775%", "%");
            assertTrue(this.rs.next(), testCase);
            assertEquals("testBug73775f", this.rs.getString(3), testCase);
            assertEquals("", this.rs.getString(4), testCase); // Function return param is always returned.
            assertEquals(DatabaseMetaData.functionReturn, this.rs.getInt(5), testCase);
            assertTrue(this.rs.next(), testCase);
            assertEquals("testBug73775f", this.rs.getString(3), testCase);
            assertEquals("param1", this.rs.getString(4), testCase);
            assertTrue(this.rs.next(), testCase);
            assertEquals("testBug73775f", this.rs.getString(3), testCase);
            assertEquals("param2", this.rs.getString(4), testCase);
            assertFalse(this.rs.next(), testCase);

            for (String ptn : new String[] { "param1", "_____1", "%1", "p_r_m%1" }) {
                this.rs = dbMapsToSchema ? dbmd.getFunctionColumns("", null, "testBug73775%", ptn) : dbmd.getFunctionColumns(null, "", "testBug73775%", ptn);
                assertTrue(this.rs.next());
                assertEquals("testBug73775f", this.rs.getString(3), testCase);
                assertEquals("", this.rs.getString(4), testCase); // Function return param is always returned.
                assertEquals(DatabaseMetaData.functionReturn, this.rs.getInt(5), testCase);
                assertTrue(this.rs.next(), testCase);
                assertEquals("testBug73775f", this.rs.getString(3), testCase);
                assertEquals("param1", this.rs.getString(4), testCase);
                assertFalse(this.rs.next(), testCase);
            }

            for (String ptn : new String[] { "param2", "_____2", "%2", "p_r_m%2" }) {
                this.rs = dbMapsToSchema ? dbmd.getFunctionColumns("", null, "testBug73775%", ptn) : dbmd.getFunctionColumns(null, "", "testBug73775%", ptn);
                assertTrue(this.rs.next());
                assertEquals("testBug73775f", this.rs.getString(3), testCase);
                assertEquals("", this.rs.getString(4), testCase); // Function return param is always returned.
                assertEquals(DatabaseMetaData.functionReturn, this.rs.getInt(5), testCase);
                assertTrue(this.rs.next(), testCase);
                assertEquals("testBug73775f", this.rs.getString(3), testCase);
                assertEquals("param2", this.rs.getString(4), testCase);
                assertFalse(this.rs.next(), testCase);
            }

            this.rs = dbMapsToSchema ? dbmd.getFunctionColumns("", null, "testBug73775%", "") : dbmd.getFunctionColumns(null, "", "testBug73775%", "");
            assertTrue(this.rs.next(), testCase);
            assertEquals("testBug73775f", this.rs.getString(3), testCase);
            assertEquals("", this.rs.getString(4), testCase); // Function return param is always returned.
            assertEquals(DatabaseMetaData.functionReturn, this.rs.getInt(5), testCase);
            assertFalse(this.rs.next(), testCase);

            testConn.close();
        } while ((useIS = !useIS) || (inclFuncs = !inclFuncs) || (dbMapsToSchema = !dbMapsToSchema));
    }

    /**
     * Tests fix for BUG#87826 (26846249), MYSQL JDBC CONNECTOR/J DATABASEMETADATA NULL PATTERN HANDLING IS NON-COMPLIANT.
     *
     * @throws Exception
     */
    @Test
    public void testBug87826() throws Exception {
        createTable("testBug87826", "(id INT)");
        createProcedure("bug87826", "(in param1 int, out result varchar(197)) BEGIN select 1, ''; END");

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

        for (String useIS : new String[] { "false", "true" }) {
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), useIS);
            Connection con = null;
            try {
                con = getConnectionWithProps(props);
                DatabaseMetaData md = con.getMetaData();

                ResultSet procMD = md.getProcedureColumns(null, null, "bug87826", null);
                assertTrue(procMD.next());
                assertEquals(10, procMD.getInt(9), "Int param length");
                assertTrue(procMD.next());
                assertEquals(197, procMD.getInt(9), "String param length");
                assertFalse(procMD.next());

                // at least one table should exist in current catalog
                this.rs = md.getTables(null, null, null, null);
                int cnt = 0;
                while (this.rs.next()) {
                    cnt++;
                }
                assertTrue(cnt > 0);

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

    /**
     * Tests fix for BUG#90887 (28034570), DATABASEMETADATAUSINGINFOSCHEMA#GETTABLES FAILS IF METHOD ARGUMENTS ARE NULL.
     *
     * @throws Exception
     */
    @Test
    public void testBug90887() throws Exception {
        List<String> resNames = new ArrayList<>();

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "false");
        Connection con = getConnectionWithProps(props);
        DatabaseMetaData metaData = con.getMetaData();
        ResultSet res = metaData.getTables(null, null, null, null);
        while (res.next()) {
            resNames.add(res.getString("TABLE_NAME"));
        }

        assertTrue(resNames.size() > 0);

        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "true");
        con = getConnectionWithProps(props);
        metaData = con.getMetaData();
        res = metaData.getTables(null, null, null, null);
        while (res.next()) {
            resNames.remove(res.getString("TABLE_NAME"));
        }

        assertTrue(resNames.size() == 0);
    }

    /**
     * Tests fix for BUG#29186870, CONNECTOR/J REGRESSION: NOT RETURNING PRECISION GETPROCEDURECOLUMNS.
     *
     * @throws Exception
     */
    @Test
    public void testBug29186870() throws Exception {
        Connection con = null;
        ResultSet rst = null;
        DatabaseMetaData dmd = null;

        Map<String, String> fieldToType = new HashMap<>();
        fieldToType.put("I", "INTEGER");
        fieldToType.put("D", "DATE");
        fieldToType.put("T", "TIME");
        fieldToType.put("DT", "DATETIME");
        fieldToType.put("TS", "TIMESTAMP");

        Map<String, Integer> fieldToExpectedLength = new HashMap<>();
        fieldToExpectedLength.put("I", 10);
        fieldToExpectedLength.put("D", 10);
        fieldToExpectedLength.put("T", 8);
        fieldToExpectedLength.put("DT", 19);
        fieldToExpectedLength.put("TS", 19);

        Map<String, Integer> fieldToExpectedPrecision = new HashMap<>();
        fieldToExpectedPrecision.put("I", 10);
        fieldToExpectedPrecision.put("D", 0);
        fieldToExpectedPrecision.put("T", 0);
        fieldToExpectedPrecision.put("DT", 0);
        fieldToExpectedPrecision.put("TS", 0);

        if (versionMeetsMinimum(5, 6, 4)) {
            // fractional seconds are not supported in previous versions
            fieldToType.put("T0", "TIME(0)");
            fieldToType.put("T3", "TIME(3)");
            fieldToType.put("T6", "TIME(6)");
            fieldToType.put("DT3", "DATETIME(3)");
            fieldToType.put("TS0", "TIMESTAMP(0)");
            fieldToType.put("TS6", "TIMESTAMP(6)");

            fieldToExpectedLength.put("T0", 8);
            fieldToExpectedLength.put("T3", 12);
            fieldToExpectedLength.put("T6", 15);
            fieldToExpectedLength.put("DT3", 23);
            fieldToExpectedLength.put("TS0", 19);
            fieldToExpectedLength.put("TS6", 26);

            fieldToExpectedPrecision.put("T0", 0);
            fieldToExpectedPrecision.put("T3", 3);
            fieldToExpectedPrecision.put("T6", 6);
            fieldToExpectedPrecision.put("DT3", 3);
            fieldToExpectedPrecision.put("TS0", 0);
            fieldToExpectedPrecision.put("TS6", 6);
        }

        String tname = "testBug29186870tab";
        String pname = "testBug29186870proc";
        String fname = "testBug29186870func";

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

        try {
            for (String useIS : new String[] { "false", "true" }) {
                props.setProperty(PropertyKey.useInformationSchema.getKeyName(), useIS);

                con = getConnectionWithProps(props);

                // 1. Test COLUMN_SIZE in getColumns()
                dmd = con.getMetaData();

                String str = "";
                for (String k : fieldToType.keySet()) {
                    if (str.length() > 0) {
                        str += ",";
                    }
                    str += k + " " + fieldToType.get(k);
                    if (fieldToType.get(k).startsWith("TIMESTAMP")) {
                        str += " NULL";
                    }

                }
                createTable(tname, "(" + str + ")");

                rst = dmd.getColumns(null, null, tname, null);

                assertNotNull(rst, "No records fetched");
                int cnt = 0;
                while (rst.next()) {
                    assertEquals(fieldToExpectedLength.get(rst.getString("COLUMN_NAME")), (Integer) rst.getInt("COLUMN_SIZE"));
                    cnt++;
                }
                assertEquals(fieldToType.size(), cnt);

                // 2. Test PRECISION and LENGTH in getProcedureColumns()
                str = "";
                for (String k : fieldToType.keySet()) {
                    if (str.length() > 0) {
                        str += ",";
                    }
                    str += "IN " + k + " " + fieldToType.get(k);
                }
                createProcedure(pname, "(" + str + ")" + "\n BEGIN\n SELECT I, DT, TS FROM TDT WHERE PDT = DT;" + "\n END");

                rst = dmd.getProcedureColumns(null, null, pname, null);

                assertNotNull(rst, "No records fetched");
                cnt = 0;
                while (rst.next()) {
                    String paramName = rst.getString("COLUMN_NAME");
                    assertEquals(fieldToExpectedPrecision.get(paramName), (Integer) rst.getInt("PRECISION"), paramName);
                    assertEquals(fieldToExpectedLength.get(paramName), (Integer) rst.getInt("LENGTH"), paramName);
                    cnt++;
                }
                assertEquals(fieldToType.size(), cnt);

                // 3. Test PRECISION and LENGTH in getFunctionColumns()
                str = "";
                for (String k : fieldToType.keySet()) {
                    if (str.length() > 0) {
                        str += ",";
                    }
                    str += k + " " + fieldToType.get(k);
                }
                createFunction(fname, "(" + str + ") RETURNS CHAR(1) DETERMINISTIC RETURN 'a'");

                rst = dmd.getFunctionColumns(null, null, fname, null);

                assertNotNull(rst, "No records fetched");
                cnt = 0;
                while (rst.next()) {
                    String paramName = rst.getString("COLUMN_NAME");
                    if (!StringUtils.isNullOrEmpty(paramName)) { // ignore the out parameter
                        assertEquals(fieldToExpectedPrecision.get(paramName), (Integer) rst.getInt("PRECISION"));
                        assertEquals(fieldToExpectedLength.get(paramName), (Integer) rst.getInt("LENGTH"));
                        cnt++;
                    }
                }
                assertEquals(fieldToType.size(), cnt);
            }
        } finally {
            if (con != null) {
                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#97413 (30477722), DATABASEMETADATA IS BROKEN AFTER SERVER WL#13528.
     *
     * @throws Exception
     */
    @Test
    public void testBug97413() throws Exception {
        createTable("testBug97413",
                "(f1_1 TINYINT, f1_2 TINYINT UNSIGNED, f1_3 TINYINT(1), f1_4 TINYINT(1) UNSIGNED, f1_5 TINYINT(1) ZEROFILL, f1_6 TINYINT(1) UNSIGNED ZEROFILL,"
                        + " f2_1 SMALLINT, f2_2 SMALLINT UNSIGNED, f2_3 SMALLINT(1), f2_4 SMALLINT(1) UNSIGNED, f2_5 SMALLINT(1) ZEROFILL, f2_6 SMALLINT(1) UNSIGNED ZEROFILL,"
                        + " f3_1 MEDIUMINT, f3_2 MEDIUMINT UNSIGNED, f3_3 MEDIUMINT(1), f3_4 MEDIUMINT(1) UNSIGNED, f3_5 MEDIUMINT(1) ZEROFILL, f3_6 MEDIUMINT(1) UNSIGNED ZEROFILL,"
                        + " f4_1 INT, f4_2 INT UNSIGNED, f4_3 INT(1), f4_4 INT(1) UNSIGNED, f4_5 INT(1) ZEROFILL, f4_6 INT(1) UNSIGNED ZEROFILL,"
                        + " f5_1 BIGINT, f5_2 BIGINT UNSIGNED, f5_3 BIGINT(1), f5_4 BIGINT(1) UNSIGNED, f5_5 BIGINT(1) ZEROFILL, f5_6 BIGINT(1) UNSIGNED ZEROFILL,"
                        + " f6_1 FLOAT(7), f6_2 FLOAT(7) UNSIGNED, f6_3 FLOAT(10, 7), f6_4 FLOAT(10, 7) UNSIGNED, f6_5 FLOAT(10, 7) ZEROFILL, f6_6 FLOAT(10, 7) UNSIGNED ZEROFILL,"
                        + " f6_7 FLOAT(30), f6_8 FLOAT(30) UNSIGNED, f6_9 FLOAT(30) ZEROFILL, f6_10 FLOAT(30) UNSIGNED ZEROFILL,"
                        + " f7_1 REAL, f7_2 REAL UNSIGNED, f7_3 REAL(10, 7), f7_4 REAL(10, 7) UNSIGNED, f7_5 REAL(10, 7) ZEROFILL, f7_6 REAL(10, 7) UNSIGNED ZEROFILL,"
                        + " f8_1 DOUBLE, f8_2 DOUBLE UNSIGNED, f8_3 DOUBLE(12, 10), f8_4 DOUBLE(12, 10) UNSIGNED, f8_5 DOUBLE(12, 10) ZEROFILL, f8_6 DOUBLE(12, 10) UNSIGNED ZEROFILL"
                        + ")");

        int cnt1 = 0;
        int cnt2 = 0;
        int cnt3 = 0;
        int cnt4 = 0;
        if (versionMeetsMinimum(8, 0, 17)) {
            SQLWarning warn = this.stmt.getWarnings();
            assertNotNull(warn);
            while (warn != null) {
                if (warn.getMessage().startsWith("Integer display width is deprecated")) {
                    cnt1++;
                } else if (warn.getMessage().startsWith("Specifying number of digits for floating")) {
                    cnt2++;
                } else if (warn.getMessage().startsWith("The ZEROFILL attribute is deprecated")) {
                    cnt3++;
                } else if (warn.getMessage().startsWith("UNSIGNED for decimal and floating point data types is deprecated")) {
                    cnt4++;
                } else {
                    fail("Not expected warning: " + warn.getMessage());
                }
                warn = warn.getNextWarning();
            }
            assertEquals(20, cnt1);
            assertEquals(12, cnt2);
            assertEquals(18, cnt3);
            assertEquals(11, cnt4);
        }

        createProcedure("testBug97413p", "("
                + " INOUT f1_1 TINYINT, INOUT f1_2 TINYINT UNSIGNED, INOUT f1_3 TINYINT(1), INOUT f1_4 TINYINT(1) UNSIGNED, INOUT f1_5 TINYINT(1) ZEROFILL, INOUT f1_6 TINYINT(1) UNSIGNED ZEROFILL,"
                + " INOUT f2_1 SMALLINT, INOUT f2_2 SMALLINT UNSIGNED, INOUT f2_3 SMALLINT(1), INOUT f2_4 SMALLINT(1) UNSIGNED, INOUT f2_5 SMALLINT(1) ZEROFILL, INOUT f2_6 SMALLINT(1) UNSIGNED ZEROFILL,"
                + " INOUT f3_1 MEDIUMINT, INOUT f3_2 MEDIUMINT UNSIGNED, INOUT f3_3 MEDIUMINT(1), INOUT f3_4 MEDIUMINT(1) UNSIGNED, INOUT f3_5 MEDIUMINT(1) ZEROFILL, INOUT f3_6 MEDIUMINT(1) UNSIGNED ZEROFILL,"
                + " INOUT f4_1 INT, INOUT f4_2 INT UNSIGNED, INOUT f4_3 INT(1), INOUT f4_4 INT(1) UNSIGNED, INOUT f4_5 INT(1) ZEROFILL, INOUT f4_6 INT(1) UNSIGNED ZEROFILL,"
                + " INOUT f5_1 BIGINT, INOUT f5_2 BIGINT UNSIGNED, INOUT f5_3 BIGINT(1), INOUT f5_4 BIGINT(1) UNSIGNED, INOUT f5_5 BIGINT(1) ZEROFILL, INOUT f5_6 BIGINT(1) UNSIGNED ZEROFILL,"
                + " INOUT f6_1 FLOAT(7), INOUT f6_2 FLOAT(7) UNSIGNED, INOUT f6_3 FLOAT(10, 7), INOUT f6_4 FLOAT(10, 7) UNSIGNED, INOUT f6_5 FLOAT(10, 7) ZEROFILL, INOUT f6_6 FLOAT(10, 7) UNSIGNED ZEROFILL,"
                + " INOUT f6_7 FLOAT(30), INOUT f6_8 FLOAT(30) UNSIGNED, INOUT f6_9 FLOAT(30) ZEROFILL, INOUT f6_10 FLOAT(30) UNSIGNED ZEROFILL,"
                + " INOUT f7_1 REAL, INOUT f7_2 REAL UNSIGNED, INOUT f7_3 REAL(10, 7), INOUT f7_4 REAL(10, 7) UNSIGNED, INOUT f7_5 REAL(10, 7) ZEROFILL, INOUT f7_6 REAL(10, 7) UNSIGNED ZEROFILL,"
                + " INOUT f8_1 DOUBLE, INOUT f8_2 DOUBLE UNSIGNED, INOUT f8_3 DOUBLE(12, 10), INOUT f8_4 DOUBLE(12, 10) UNSIGNED, INOUT f8_5 DOUBLE(12, 10) ZEROFILL, INOUT f8_6 DOUBLE(12, 10) UNSIGNED ZEROFILL"
                + ") BEGIN SELECT CONCAT(f1_3, f1_4) INTO f1_1; END");

        createFunction("testBug97413f",
                "(f1_1 TINYINT, f1_2 TINYINT UNSIGNED, f1_3 TINYINT(1), f1_4 TINYINT(1) UNSIGNED, f1_5 TINYINT(1) ZEROFILL, f1_6 TINYINT(1) UNSIGNED ZEROFILL,"
                        + " f2_1 SMALLINT, f2_2 SMALLINT UNSIGNED, f2_3 SMALLINT(1), f2_4 SMALLINT(1) UNSIGNED, f2_5 SMALLINT(1) ZEROFILL, f2_6 SMALLINT(1) UNSIGNED ZEROFILL,"
                        + " f3_1 MEDIUMINT, f3_2 MEDIUMINT UNSIGNED, f3_3 MEDIUMINT(1), f3_4 MEDIUMINT(1) UNSIGNED, f3_5 MEDIUMINT(1) ZEROFILL, f3_6 MEDIUMINT(1) UNSIGNED ZEROFILL,"
                        + " f4_1 INT, f4_2 INT UNSIGNED, f4_3 INT(1), f4_4 INT(1) UNSIGNED, f4_5 INT(1) ZEROFILL, f4_6 INT(1) UNSIGNED ZEROFILL,"
                        + " f5_1 BIGINT, f5_2 BIGINT UNSIGNED, f5_3 BIGINT(1), f5_4 BIGINT(1) UNSIGNED, f5_5 BIGINT(1) ZEROFILL, f5_6 BIGINT(1) UNSIGNED ZEROFILL,"
                        + " f6_1 FLOAT(7), f6_2 FLOAT(7) UNSIGNED, f6_3 FLOAT(10, 7), f6_4 FLOAT(10, 7) UNSIGNED, f6_5 FLOAT(10, 7) ZEROFILL, f6_6 FLOAT(10, 7) UNSIGNED ZEROFILL,"
                        + " f6_7 FLOAT(30), f6_8 FLOAT(30) UNSIGNED, f6_9 FLOAT(30) ZEROFILL, f6_10 FLOAT(30) UNSIGNED ZEROFILL,"
                        + " f7_1 REAL, f7_2 REAL UNSIGNED, f7_3 REAL(10, 7), f7_4 REAL(10, 7) UNSIGNED, f7_5 REAL(10, 7) ZEROFILL, f7_6 REAL(10, 7) UNSIGNED ZEROFILL,"
                        + " f8_1 DOUBLE, f8_2 DOUBLE UNSIGNED, f8_3 DOUBLE(12, 10), f8_4 DOUBLE(12, 10) UNSIGNED, f8_5 DOUBLE(12, 10) ZEROFILL, f8_6 DOUBLE(12, 10) UNSIGNED ZEROFILL"
                        + ") RETURNS INT(6) DETERMINISTIC RETURN CONCAT(f1_1, f2_1)");

        Properties props = new Properties();
        for (boolean useIS : new boolean[] { false, true }) {
            for (boolean useSSPS : new boolean[] { false, true }) {
                for (boolean tinyInt1isBit : new boolean[] { false, true }) {
                    for (boolean transformedBitIsBoolean : new boolean[] { false, true }) {
                        String errMsg = "useIS=" + useIS + ", useSSPS=" + useSSPS + ", tinyInt1isBit=" + tinyInt1isBit + ", transformedBitIsBoolean="
                                + transformedBitIsBoolean + "\n";
                        props.clear();
                        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
                        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
                        props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
                        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "" + useSSPS);
                        props.setProperty(PropertyKey.tinyInt1isBit.getKeyName(), "" + tinyInt1isBit);
                        props.setProperty(PropertyKey.transformedBitIsBoolean.getKeyName(), "" + transformedBitIsBoolean);
                        Connection c1 = getConnectionWithProps(props);

                        /*
                         * DatabaseMetaData
                         */
                        ResultSet rs1 = c1.getMetaData().getColumns(c1.getCatalog(), null, "testBug97413", "f%");
                        new TestBug97413Columns(errMsg, tinyInt1isBit, transformedBitIsBoolean, rs1, "DATA_TYPE", "TYPE_NAME", null, "COLUMN_SIZE",
                                "DECIMAL_DIGITS").run();

                        /*
                         * ResultSetMetaData
                         */
                        this.rs = c1.createStatement().executeQuery("SELECT * FROM testBug97413");
                        ResultSetMetaData rm = this.rs.getMetaData();
                        new TestBug97413Columns(errMsg, tinyInt1isBit, transformedBitIsBoolean, rm).run();

                        /*
                         * Procedures
                         */
                        CallableStatement storedProc = c1
                                .prepareCall("{call testBug97413p(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,"
                                        + " ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)}");
                        ParameterMetaData pm = storedProc.getParameterMetaData();
                        new TestBug97413Columns(errMsg, tinyInt1isBit, transformedBitIsBoolean, pm).run();

                        rs1 = c1.getMetaData().getProcedureColumns(null, null, "testBug97413p", null);
                        new TestBug97413Columns(errMsg, tinyInt1isBit, transformedBitIsBoolean, rs1, "DATA_TYPE", "TYPE_NAME", "LENGTH", "PRECISION", "SCALE")
                                .run();

                        /*
                         * Functions
                         */
                        storedProc = c1.prepareCall("{? = call testBug97413f(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,"
                                + " ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)}");
                        pm = storedProc.getParameterMetaData();
                        TestBug97413Columns testBug97413Columns = new TestBug97413Columns(errMsg, tinyInt1isBit, transformedBitIsBoolean, pm);
                        testBug97413Columns.testBug97413CheckMDColumn(Types.INTEGER, "INT", 0, 10, 0); // return type
                        testBug97413Columns.run();

                        rs1 = c1.getMetaData().getFunctionColumns(null, null, "testBug97413f", null);
                        testBug97413Columns = new TestBug97413Columns(errMsg, tinyInt1isBit, transformedBitIsBoolean, rs1, "DATA_TYPE", "TYPE_NAME", "LENGTH",
                                "PRECISION", "SCALE"); // return type
                        testBug97413Columns.testBug97413CheckMDColumn(Types.INTEGER, "INT", 10, 10, 0);
                        testBug97413Columns.run();

                        c1.close();
                    }
                }
            }
        }
    }

    private class TestBug97413Columns {

        String errMsg = null;
        boolean tinyInt1isBit = false;
        boolean transformedBitIsBoolean = false;
        ResultSet rset = null;
        ParameterMetaData pm = null;
        ResultSetMetaData rm = null;
        int id = 0;
        String tidField = null;
        String tnField = null;
        String widthField = null;
        String precisionField = null;
        String decimalField = null;

        TestBug97413Columns(String errMsg, boolean tinyInt1isBit, boolean transformedBitIsBoolean, ResultSet rset, String tidField, String tnField,
                String widthField, String precisionField, String decimalField) {
            this.errMsg = errMsg;
            this.tinyInt1isBit = tinyInt1isBit;
            this.transformedBitIsBoolean = transformedBitIsBoolean;
            this.rset = rset;
            this.tidField = tidField;
            this.tnField = tnField;
            this.widthField = widthField;
            this.precisionField = precisionField;
            this.decimalField = decimalField;
        }

        TestBug97413Columns(String errMsg, boolean tinyInt1isBit, boolean transformedBitIsBoolean, ParameterMetaData pm) {
            this.errMsg = errMsg;
            this.tinyInt1isBit = tinyInt1isBit;
            this.transformedBitIsBoolean = transformedBitIsBoolean;
            this.pm = pm;
        }

        TestBug97413Columns(String errMsg, boolean tinyInt1isBit, boolean transformedBitIsBoolean, ResultSetMetaData rm) {
            this.errMsg = errMsg;
            this.tinyInt1isBit = tinyInt1isBit;
            this.transformedBitIsBoolean = transformedBitIsBoolean;
            this.rm = rm;
        }

        void testBug97413CheckMDColumn(int dataType, String typeName, int width, int precision, int decimalDigits) throws Exception {
            if (this.rset != null) {
                assertTrue(this.rset.next());
                assertEquals(dataType, this.rset.getInt(this.tidField), this.errMsg + "Field name: " + this.rset.getString("COLUMN_NAME") + "\n");
                assertEquals(typeName, this.rset.getString(this.tnField), this.errMsg + "Field name: " + this.rset.getString("COLUMN_NAME") + "\n");
                if (this.widthField != null) {
                    assertEquals(width, this.rset.getInt(this.widthField), this.errMsg + "Field name: " + this.rset.getString("COLUMN_NAME") + "\n");
                }
                assertEquals(precision, this.rset.getInt(this.precisionField), this.errMsg + "Field name: " + this.rset.getString("COLUMN_NAME") + "\n");
                assertEquals(decimalDigits, this.rset.getInt(this.decimalField), this.errMsg + "Field name: " + this.rset.getString("COLUMN_NAME") + "\n");
            } else if (this.pm != null) {
                this.id++;
                assertEquals(dataType, this.pm.getParameterType(this.id), this.errMsg);
                assertEquals(typeName, this.pm.getParameterTypeName(this.id), this.errMsg);
                assertEquals(precision, this.pm.getPrecision(this.id), this.errMsg);
                assertEquals(decimalDigits, this.pm.getScale(this.id), this.errMsg);
            } else {
                this.id++;
                assertEquals(dataType, this.rm.getColumnType(this.id), this.errMsg);
                assertEquals(typeName, this.rm.getColumnTypeName(this.id), this.errMsg);
                assertEquals(precision, this.rm.getPrecision(this.id), this.errMsg);
                assertEquals(decimalDigits, this.rm.getScale(this.id), this.errMsg);
                assertEquals(precision, this.rm.getColumnDisplaySize(this.id), this.errMsg);
            }
        }

        void run() throws Exception {
            testBug97413CheckMDColumn(Types.TINYINT, "TINYINT", 3, 3, 0);
            testBug97413CheckMDColumn(Types.TINYINT, "TINYINT UNSIGNED", 3, 3, 0);

            testBug97413CheckMDColumn(this.tinyInt1isBit ? this.transformedBitIsBoolean ? Types.BOOLEAN : Types.BIT : Types.TINYINT,
                    this.tinyInt1isBit ? this.transformedBitIsBoolean ? "BOOLEAN" : "BIT" : "TINYINT",
                    this.tinyInt1isBit ? this.transformedBitIsBoolean ? 3 : 1 : 3, this.tinyInt1isBit ? this.transformedBitIsBoolean ? 3 : 1 : 3, 0);
            testBug97413CheckMDColumn(Types.TINYINT, "TINYINT UNSIGNED", 3, 3, 0);
            testBug97413CheckMDColumn(Types.TINYINT, "TINYINT UNSIGNED", 3, 3, 0);
            testBug97413CheckMDColumn(Types.TINYINT, "TINYINT UNSIGNED", 3, 3, 0);

            testBug97413CheckMDColumn(Types.SMALLINT, "SMALLINT", 5, 5, 0);
            testBug97413CheckMDColumn(Types.SMALLINT, "SMALLINT UNSIGNED", 5, 5, 0);
            testBug97413CheckMDColumn(Types.SMALLINT, "SMALLINT", 5, 5, 0);
            testBug97413CheckMDColumn(Types.SMALLINT, "SMALLINT UNSIGNED", 5, 5, 0);
            testBug97413CheckMDColumn(Types.SMALLINT, "SMALLINT UNSIGNED", 5, 5, 0);
            testBug97413CheckMDColumn(Types.SMALLINT, "SMALLINT UNSIGNED", 5, 5, 0);

            testBug97413CheckMDColumn(Types.INTEGER, "MEDIUMINT", 7, 7, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "MEDIUMINT UNSIGNED", 8, 8, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "MEDIUMINT", 7, 7, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "MEDIUMINT UNSIGNED", 8, 8, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "MEDIUMINT UNSIGNED", 8, 8, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "MEDIUMINT UNSIGNED", 8, 8, 0);

            testBug97413CheckMDColumn(Types.INTEGER, "INT", 10, 10, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "INT UNSIGNED", 10, 10, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "INT", 10, 10, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "INT UNSIGNED", 10, 10, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "INT UNSIGNED", 10, 10, 0);
            testBug97413CheckMDColumn(Types.INTEGER, "INT UNSIGNED", 10, 10, 0);

            testBug97413CheckMDColumn(Types.BIGINT, "BIGINT", 19, 19, 0);
            testBug97413CheckMDColumn(Types.BIGINT, "BIGINT UNSIGNED", 20, 20, 0);
            testBug97413CheckMDColumn(Types.BIGINT, "BIGINT", 19, 19, 0);
            testBug97413CheckMDColumn(Types.BIGINT, "BIGINT UNSIGNED", 20, 20, 0);
            testBug97413CheckMDColumn(Types.BIGINT, "BIGINT UNSIGNED", 20, 20, 0);
            testBug97413CheckMDColumn(Types.BIGINT, "BIGINT UNSIGNED", 20, 20, 0);

            testBug97413CheckMDColumn(Types.REAL, "FLOAT", 12, 12, 0);
            testBug97413CheckMDColumn(Types.REAL, "FLOAT UNSIGNED", 12, 12, 0);
            testBug97413CheckMDColumn(Types.REAL, "FLOAT", 10, 10, 7);
            testBug97413CheckMDColumn(Types.REAL, "FLOAT UNSIGNED", 10, 10, 7);
            testBug97413CheckMDColumn(Types.REAL, "FLOAT UNSIGNED", 10, 10, 7);
            testBug97413CheckMDColumn(Types.REAL, "FLOAT UNSIGNED", 10, 10, 7);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE", 22, 22, 0);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 22, 22, 0);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 22, 22, 0);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 22, 22, 0);

            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE", 22, 22, 0);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 22, 22, 0);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE", 10, 10, 7);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 10, 10, 7);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 10, 10, 7);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 10, 10, 7);

            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE", 22, 22, 0);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 22, 22, 0);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE", 12, 12, 10);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 12, 12, 10);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 12, 12, 10);
            testBug97413CheckMDColumn(Types.DOUBLE, "DOUBLE UNSIGNED", 12, 12, 10);
        }

    }

    /**
     * Tests fix for Bug#102076 (32329915), CONTRIBUTION: MYSQL JDBC DRIVER RESULTSET.GETLONG() THROWS NUMBEROUTOFRANGE.
     *
     * @throws Exception
     */
    @Test
    public void testBug102076() throws Exception {
        createFunction("testBug102076f", "(x LONGBLOB, y LONGTEXT) RETURNS LONGBLOB DETERMINISTIC RETURN CONCAT(x, y)");
        createProcedure("testBug102076p", "(x LONGBLOB, y LONGTEXT)\nBEGIN\nSELECT 1;end\n");

        DatabaseMetaData dbmd = this.conn.getMetaData();

        this.rs = dbmd.getProcedureColumns(null, null, "testBug102076p", "%");
        assertTrue(this.rs.next());
        do {
            assertEquals(Integer.MAX_VALUE, this.rs.getLong(9)); // LENGTH
            assertEquals(Integer.MAX_VALUE, this.rs.getLong(17)); // CHAR_OCTET_LENGTH
        } while (this.rs.next());

        this.rs = dbmd.getFunctionColumns(null, null, "testBug102076f", "%");
        assertTrue(this.rs.next());
        do {
            assertEquals(Integer.MAX_VALUE, this.rs.getLong(9)); // LENGTH
            assertEquals(Integer.MAX_VALUE, this.rs.getLong(14)); // CHAR_OCTET_LENGTH
        } while (this.rs.next());
    }

    /**
     * Tests fix for Bug#95280 (29757140), DATABASEMETADATA.GETIMPORTEDKEYS RETURNS DOUBLE THE ROWS.
     *
     * @throws Exception
     */
    @Test
    public void testBug95280() throws Exception {
        String databaseName1 = "dbBug95280_1";
        createDatabase(databaseName1);
        createTable(databaseName1 + ".table1",
                "(cat_id int not null auto_increment primary key, cat_name varchar(255) not null, cat_description text) ENGINE=InnoDB;");
        createTable(databaseName1 + ".table2",
                "(prd_id int not null auto_increment primary key, prd_name varchar(355) not null, prd_price decimal, cat_id int not null,"
                        + " FOREIGN KEY fk_cat(cat_id) REFERENCES table1(cat_id) ON UPDATE CASCADE ON DELETE RESTRICT) ENGINE=InnoDB;");

        String databaseName2 = "dbBug95280_2";
        createDatabase(databaseName2);
        createTable(databaseName2 + ".table1",
                "(cat_id int not null auto_increment primary key, cat_name varchar(255) not null, cat_description text) ENGINE=InnoDB;");
        createTable(databaseName2 + ".table2",
                "(prd_id int not null auto_increment primary key, prd_name varchar(355) not null, prd_price decimal, cat_id int not null,"
                        + " FOREIGN KEY fk_cat(cat_id) REFERENCES table1(cat_id) ON UPDATE CASCADE ON DELETE RESTRICT) ENGINE=InnoDB;");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        for (boolean useIS : new boolean[] { false, true }) {
            for (String databaseTerm : new String[] { "CATALOG", "SCHEMA" }) {
                props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
                props.setProperty(PropertyKey.databaseTerm.getKeyName(), databaseTerm);

                Connection con = getConnectionWithProps(props);
                DatabaseMetaData meta = con.getMetaData();

                this.rs = databaseTerm.contentEquals("SCHEMA") ? meta.getImportedKeys(null, databaseName1, "table2")
                        : meta.getImportedKeys(databaseName1, null, "table2");
                assertTrue(this.rs.next());
                assertEquals("table2", this.rs.getString("FKTABLE_NAME"));
                assertEquals("cat_id", this.rs.getString("FKCOLUMN_NAME"));
                assertEquals(1, this.rs.getInt("KEY_SEQ"));
                assertFalse(this.rs.next());
                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#104641 (33237255), DatabaseMetaData.getImportedKeys can return duplicated foreign keys.
     *
     * @throws Exception
     */
    @Test
    public void testBug104641() throws Exception {
        String databaseName1 = "dbBug104641";
        if (isServerRunningOnWindows()) {
            databaseName1 = databaseName1.toLowerCase();
        }
        createDatabase(databaseName1);
        createTable(databaseName1 + ".table1",
                "(`CREATED` datetime DEFAULT NULL,`ID` bigint NOT NULL AUTO_INCREMENT,`LRN_ID` bigint DEFAULT '0',`USERNAME` varchar(50) NOT NULL,"
                        + "PRIMARY KEY (`ID`),UNIQUE KEY `U_table1_LRN_ID` (`LRN_ID`),UNIQUE KEY `U_table1_USERNAME` (`USERNAME`) )");
        createTable(databaseName1 + ".table2",
                "(`AL_ID` varchar(50) DEFAULT NULL,`CREATED` datetime DEFAULT NULL,`ID` bigint NOT NULL AUTO_INCREMENT,`USER_ID` bigint DEFAULT NULL,"
                        + "PRIMARY KEY (`ID`),KEY `fk_table2_user_id` (`USER_ID`),KEY `index_al_id1` (`AL_ID`),"
                        + "CONSTRAINT `fk_table2_user_id` FOREIGN KEY (`USER_ID`) REFERENCES `table1` (`ID`) )");
        createTable(databaseName1 + ".table3",
                "(`AL_ID` varchar(50) DEFAULT NULL,`ID` bigint NOT NULL AUTO_INCREMENT,`USER_ID` bigint DEFAULT NULL,`LRN_ID` bigint DEFAULT '0',"
                        + "PRIMARY KEY (`ID`),KEY `fk_table3_LRN_ID` (`LRN_ID`),KEY `index_al_id2` (`AL_ID`),"
                        + "CONSTRAINT `fk_table3_LRN_ID` FOREIGN KEY `U_table1_LRN_ID` (`LRN_ID`) REFERENCES `table1` (`LRN_ID`) )");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        for (boolean useIS : new boolean[] { false, true }) {
            for (String databaseTerm : new String[] { "CATALOG", "SCHEMA" }) {
                props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
                props.setProperty(PropertyKey.databaseTerm.getKeyName(), databaseTerm);

                boolean dbTermIsSchema = databaseTerm.contentEquals("SCHEMA");

                String err = "useInformationSchema=" + useIS + ", databaseTerm=" + databaseTerm;
                Connection con = getConnectionWithProps(props);
                DatabaseMetaData meta = con.getMetaData();

                this.rs = dbTermIsSchema ? meta.getImportedKeys(null, databaseName1, "table2") : meta.getImportedKeys(databaseName1, null, "table2");
                assertTrue(this.rs.next(), err);
                assertEquals(dbTermIsSchema ? "def" : databaseName1, this.rs.getString("PKTABLE_CAT"), err);
                assertEquals(dbTermIsSchema ? databaseName1 : null, this.rs.getString("PKTABLE_SCHEM"), err);
                assertEquals(dbTermIsSchema ? "def" : databaseName1, this.rs.getString("FKTABLE_CAT"), err);
                assertEquals(dbTermIsSchema ? databaseName1 : null, this.rs.getString("FKTABLE_SCHEM"), err);
                assertEquals("table1", this.rs.getString("PKTABLE_NAME"), err);
                assertEquals("ID", this.rs.getString("PKCOLUMN_NAME"), err);
                assertEquals("table2", this.rs.getString("FKTABLE_NAME"), err);
                assertEquals("USER_ID", this.rs.getString("FKCOLUMN_NAME"), err);
                assertEquals(1, this.rs.getInt("KEY_SEQ"), err);
                assertEquals(1, this.rs.getInt("UPDATE_RULE"), err);
                assertEquals(1, this.rs.getInt("DELETE_RULE"), err);
                assertEquals("fk_table2_user_id", this.rs.getString("FK_NAME"), err);
                assertEquals(useIS ? "PRIMARY" : null, this.rs.getString("PK_NAME"), err);
                assertEquals(7, this.rs.getInt("DEFERRABILITY"), err);
                assertFalse(this.rs.next(), err);

                this.rs = dbTermIsSchema ? meta.getImportedKeys(null, databaseName1, "table3") : meta.getImportedKeys(databaseName1, null, "table3");
                assertTrue(this.rs.next(), err);
                assertEquals(dbTermIsSchema ? "def" : databaseName1, this.rs.getString("PKTABLE_CAT"), err);
                assertEquals(dbTermIsSchema ? databaseName1 : null, this.rs.getString("PKTABLE_SCHEM"), err);
                assertEquals(dbTermIsSchema ? "def" : databaseName1, this.rs.getString("FKTABLE_CAT"), err);
                assertEquals(dbTermIsSchema ? databaseName1 : null, this.rs.getString("FKTABLE_SCHEM"), err);
                assertEquals("table1", this.rs.getString("PKTABLE_NAME"), err);
                assertEquals("LRN_ID", this.rs.getString("PKCOLUMN_NAME"), err);
                assertEquals("table3", this.rs.getString("FKTABLE_NAME"), err);
                assertEquals("LRN_ID", this.rs.getString("FKCOLUMN_NAME"), err);
                assertEquals(1, this.rs.getInt("KEY_SEQ"), err);
                assertEquals(1, this.rs.getInt("UPDATE_RULE"), err);
                assertEquals(1, this.rs.getInt("DELETE_RULE"), err);
                assertEquals("fk_table3_LRN_ID", this.rs.getString("FK_NAME"), err);
                assertEquals(useIS ? "U_table1_LRN_ID" : null, this.rs.getString("PK_NAME"), err);
                assertEquals(7, this.rs.getInt("DEFERRABILITY"), err);
                assertFalse(this.rs.next(), err);

                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#33723611, getDefaultTransactionIsolation must return repeatable read.
     *
     * @throws Exception
     */
    @Test
    public void testBug33723611() throws Exception {
        this.rs = this.stmt.executeQuery(versionMeetsMinimum(5, 7) ? "SELECT @@SESSION.transaction_isolation" : "SELECT @@SESSION.tx_isolation");
        assertTrue(this.rs.next());
        assertEquals("REPEATABLE-READ", this.rs.getString(1));

        assertEquals(Connection.TRANSACTION_REPEATABLE_READ, this.conn.getTransactionIsolation());

        DatabaseMetaData dbmd = this.conn.getMetaData();
        assertEquals(Connection.TRANSACTION_REPEATABLE_READ, dbmd.getDefaultTransactionIsolation());
    }

    /**
     * Tests fix for Bug#82084 (23743938), YEAR DATA TYPE RETURNS INCORRECT VALUE FOR JDBC GETCOLUMNTYPE().
     *
     * @throws Exception
     */
    @Test
    public void testBug82084() throws Exception {
        createProcedure("testBug82084p", "(in param1 int, out result year) BEGIN select 1, ''; END");
        createTable("testBug82084", "(col_tiny SMALLINT, col_year year, col_date date)");
        this.stmt.executeUpdate("INSERT INTO testBug82084 VALUES(1,2006, '2006-01-01')");
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");

        for (boolean useIS : new Boolean[] { true, false }) {
            for (boolean yearIsDate : new Boolean[] { true, false }) {
                props.setProperty(PropertyKey.yearIsDateType.getKeyName(), "" + yearIsDate);
                props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
                Connection con = getConnectionWithProps(props);

                DatabaseMetaData dbmd = con.getMetaData();
                this.rs = dbmd.getColumns(null, null, "testBug82084", "%year");
                assertTrue(this.rs.next());
                assertEquals(MysqlType.YEAR.getName(), this.rs.getString("TYPE_NAME"));
                assertEquals(yearIsDate ? Types.DATE : Types.SMALLINT, this.rs.getInt("DATA_TYPE"));

                this.rs = dbmd.getTypeInfo();
                while (this.rs.next()) {
                    if (this.rs.getString("TYPE_NAME").equals("YEAR")) {
                        assertEquals(yearIsDate ? Types.DATE : Types.SMALLINT, this.rs.getInt("DATA_TYPE"));
                        break;
                    }
                }

                ResultSet procMD = con.getMetaData().getProcedureColumns(null, null, "testBug82084p", "%result");
                assertTrue(procMD.next());
                assertEquals(MysqlType.YEAR.getName(), procMD.getString("TYPE_NAME"));
                assertEquals(yearIsDate ? Types.DATE : Types.SMALLINT, procMD.getInt("DATA_TYPE"));

                this.rs = con.createStatement().executeQuery("select * from testBug82084");
                ResultSetMetaData rsmd = this.rs.getMetaData();

                assertEquals(MysqlType.YEAR.getName(), rsmd.getColumnTypeName(2));
                if (yearIsDate) {
                    assertEquals(Types.DATE, rsmd.getColumnType(2));
                    assertEquals(java.sql.Date.class.getName(), rsmd.getColumnClassName(2));
                } else {
                    assertEquals(Types.SMALLINT, rsmd.getColumnType(2));
                    assertEquals(Short.class.getName(), rsmd.getColumnClassName(2));
                }

                con.close();
            }
        }
    }

    /**
     * Tests fix for Bug#106758 (33973048), DatabaseMetaData.getTypeInfo returns AUTO_INCREMENT = false for all datatypes.
     *
     * @throws Exception
     */
    @Test
    public void testBug106758() throws Exception {
        DatabaseMetaData dbmd = this.conn.getMetaData();

        this.rs = dbmd.getTypeInfo();
        while (this.rs.next()) {
            if (this.rs.getString("TYPE_NAME").equals("VECTOR") && !versionMeetsMinimum(9, 0)) {
                continue;
            }
            StringBuilder sb = new StringBuilder("CREATE TEMPORARY TABLE testBug106758 (col ");
            sb.append(this.rs.getString("TYPE_NAME"));
            if (this.rs.getString("CREATE_PARAMS").startsWith("(M)")) {
                sb.append("(5)");
            } else if (this.rs.getString("CREATE_PARAMS").startsWith("('value")) {
                sb.append(" ('value')");
            }
            sb.append(" AUTO_INCREMENT PRIMARY KEY)"); // Some types don't support primary keys like this, however, the query fails due to AUTO_INCREMENT first.

            if (this.rs.getBoolean("AUTO_INCREMENT")) {
                try {
                    this.stmt.execute(sb.toString());
                } catch (SQLException e) {
                    fail("Type " + this.rs.getString("TYPE_NAME") + " does not support AUTO_INCREMENT.");
                } finally {
                    this.stmt.execute("DROP TABLE IF EXISTS testBug106758");
                }
            } else {
                assertThrows("Type " + this.rs.getString("TYPE_NAME") + " supports AUTO_INCREMENT.", SQLException.class,
                        "Incorrect column specifier for column 'col'", () -> {
                            this.stmt.execute(sb.toString());
                            this.stmt.execute("DROP TABLE IF EXISTS testBug106758");
                            return null;
                        });
            }
        }
    }

    /**
     * Tests fix for Bug#109807 (Bug#35021014), DatabaseMetaData#getTypeInfo should ordered by DATA_TYPE.
     *
     * @throws Exception
     */
    @Test
    void testBug109807() throws Exception {
        boolean useIS = false;
        boolean yearDT = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), Boolean.toString(useIS));
            props.setProperty(PropertyKey.yearIsDateType.getKeyName(), Boolean.toString(yearDT));
            Connection testConn = getConnectionWithProps(props);
            DatabaseMetaData testDbMD = testConn.getMetaData();
            this.rs = testDbMD.getTypeInfo();
            int prevDataType = Integer.MIN_VALUE;
            while (this.rs.next()) {
                assertTrue(prevDataType <= this.rs.getInt("DATA_TYPE"), "DatabaseMetaData#getTypeInfo must be ordered ascending by DATA_TYPE");
                prevDataType = this.rs.getInt("DATA_TYPE");
            }
            testConn.close();
        } while ((useIS = !useIS) || (yearDT = !yearDT));
    }

    /**
     * Tests fix for Bug#109808 (Bug#35021038), DatabaseMetaData#getPrimaryKeys should ordered by COLUMN_NAME.
     *
     * @throws Exception
     */
    @Test
    void testBug109808() throws Exception {
        createTable("testBug109808",
                "(col_A INT NOT NULL, col_b INT NOT NULL, col_C VARCHAR(100) NOT NULL, col_D TIMESTAMP, PRIMARY KEY(col_b, col_C, col_A))");
        boolean useIS = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), Boolean.toString(useIS));
            Connection testConn = getConnectionWithProps(props);
            DatabaseMetaData dbmd = testConn.getMetaData();
            this.rs = dbmd.getPrimaryKeys(null, null, "testBug109808");
            assertTrue(this.rs.next());
            assertEquals("col_A", this.rs.getString("COLUMN_NAME"));
            assertTrue(this.rs.next());
            assertEquals("col_b", this.rs.getString("COLUMN_NAME"));
            assertTrue(this.rs.next());
            assertEquals("col_C", this.rs.getString("COLUMN_NAME"));
            assertFalse(this.rs.next());
        } while (useIS = !useIS);
    }

    /**
     * Tests fix for Bug#96582 (Bug#30222032), jdbc.MysqlParameterMetadata#isNullable doesnt check whether to be simple.
     *
     * @throws Exception
     */
    @Test
    void testBug96582() throws Exception {
        Properties connectionProps = new Properties();
        connectionProps.put(PropertyKey.generateSimpleParameterMetadata.getKeyName(), "true");
        connectionProps.put(PropertyKey.useServerPrepStmts.getKeyName(), "false");
        Connection con = this.getConnectionWithProps(connectionProps);
        createTable("testBug96582", "(c1 INT NOT NULL, c2 INT)");
        this.pstmt = con.prepareStatement("INSERT INTO testBug96582(c1, c2) VALUES(?, ?)");
        this.pstmt.setInt(1, 1);
        this.pstmt.setInt(2, 2);
        assertEquals(ParameterMetaData.parameterNullableUnknown, this.pstmt.getParameterMetaData().isNullable(1));
        assertEquals(ParameterMetaData.parameterNullableUnknown, this.pstmt.getParameterMetaData().isNullable(2));

        con.close();

        connectionProps.put(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        con = this.getConnectionWithProps(connectionProps);
        createTable("testBug96582", "(c1 INT NOT NULL, c2 INT)");
        this.pstmt = con.prepareStatement("INSERT INTO testBug96582(c1, c2) VALUES(?, ?)");
        this.pstmt.setInt(1, 1);
        this.pstmt.setInt(2, 2);
        assertEquals(ParameterMetaData.parameterNullable, this.pstmt.getParameterMetaData().isNullable(1));
        assertEquals(ParameterMetaData.parameterNullable, this.pstmt.getParameterMetaData().isNullable(2));
    }

    /**
     * Tests fix for Bug#91550 (Bug#28297874), DatabaseMetaData specifies incorrect extra name characters.
     *
     * @throws Exception
     */
    @Test
    void testBug91550() throws Exception {
        DatabaseMetaData md = this.conn.getMetaData();
        assertEquals("$", md.getExtraNameCharacters());
    }

    /**
     * Tests fix for Bug#113600 (Bug#36171575), Contribution: Fix join condition for retrieval of imported primary keys.
     *
     * @throws Exception
     */
    @Test
    public void testBug113600() throws Exception {
        String databaseName1 = "dbBug113600_1";
        String databaseName2 = "dbBug113600_2";
        if (isServerRunningOnWindows()) {
            databaseName1 = databaseName1.toLowerCase();
            databaseName2 = databaseName2.toLowerCase();
        }
        String tableName1 = "table1";
        String tableName2 = "table2";
        String constraintName = "fk_table2_fk_id";

        createDatabase(databaseName1);
        createTable(databaseName1 + "." + tableName1, "(`ID` bigint unsigned AUTO_INCREMENT PRIMARY KEY)");
        createTable(databaseName1 + "." + tableName2, "(`ID` bigint unsigned AUTO_INCREMENT PRIMARY KEY, `FK_ID` bigint unsigned NOT NULL," + "CONSTRAINT `"
                + constraintName + "` FOREIGN KEY (`FK_ID`) REFERENCES `" + databaseName1 + "`.`" + tableName1 + "` (`ID`) ON DELETE CASCADE)");
        createDatabase(databaseName2);
        createTable(databaseName2 + "." + tableName1, "(`ID` bigint unsigned AUTO_INCREMENT PRIMARY KEY)");
        createTable(databaseName2 + "." + tableName2, "(`ID` bigint unsigned AUTO_INCREMENT PRIMARY KEY, `FK_ID` bigint unsigned NOT NULL," + "CONSTRAINT `"
                + constraintName + "` FOREIGN KEY (`FK_ID`) REFERENCES `" + databaseName2 + "`.`" + tableName1 + "` (`ID`) ON DELETE RESTRICT)");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), SslMode.DISABLED.name());
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        boolean useIS = false;
        boolean dbTermIsSchema = false;
        do {
            String databaseTerm = dbTermIsSchema ? "SCHEMA" : "CATALOG";
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), "" + useIS);
            props.setProperty(PropertyKey.databaseTerm.getKeyName(), databaseTerm);
            Connection con = getConnectionWithProps(props);
            DatabaseMetaData md = con.getMetaData();

            final String testCase = String.format("Case [useIS: %s, databaseTerm: %s]", useIS ? "Y" : "N", databaseTerm);

            this.rs = dbTermIsSchema ? md.getImportedKeys(null, databaseName1, tableName2) : md.getImportedKeys(databaseName1, null, tableName2);
            assertTrue(this.rs.next(), testCase);
            assertEquals(dbTermIsSchema ? "def" : databaseName1, this.rs.getString("PKTABLE_CAT"), testCase);
            assertEquals(dbTermIsSchema ? databaseName1 : null, this.rs.getString("PKTABLE_SCHEM"), testCase);
            assertEquals(dbTermIsSchema ? "def" : databaseName1, this.rs.getString("FKTABLE_CAT"), testCase);
            assertEquals(dbTermIsSchema ? databaseName1 : null, this.rs.getString("FKTABLE_SCHEM"), testCase);
            assertEquals(tableName1, this.rs.getString("PKTABLE_NAME"), testCase);
            assertEquals("ID", this.rs.getString("PKCOLUMN_NAME"), testCase);
            assertEquals(tableName2, this.rs.getString("FKTABLE_NAME"), testCase);
            assertEquals("FK_ID", this.rs.getString("FKCOLUMN_NAME"), testCase);
            assertEquals(1, this.rs.getInt("KEY_SEQ"), testCase);
            assertEquals(1, this.rs.getInt("UPDATE_RULE"), testCase);
            assertEquals(0, this.rs.getInt("DELETE_RULE"), testCase);
            assertEquals(constraintName, this.rs.getString("FK_NAME"), testCase);
            assertEquals(useIS ? "PRIMARY" : null, this.rs.getString("PK_NAME"), testCase);
            assertEquals(7, this.rs.getInt("DEFERRABILITY"), testCase);
            assertFalse(this.rs.next(), testCase);

            this.rs = dbTermIsSchema ? md.getImportedKeys(null, databaseName2, tableName2) : md.getImportedKeys(databaseName2, null, tableName2);
            assertTrue(this.rs.next(), testCase);
            assertEquals(dbTermIsSchema ? "def" : databaseName2, this.rs.getString("PKTABLE_CAT"), testCase);
            assertEquals(dbTermIsSchema ? databaseName2 : null, this.rs.getString("PKTABLE_SCHEM"), testCase);
            assertEquals(dbTermIsSchema ? "def" : databaseName2, this.rs.getString("FKTABLE_CAT"), testCase);
            assertEquals(dbTermIsSchema ? databaseName2 : null, this.rs.getString("FKTABLE_SCHEM"), testCase);
            assertEquals(tableName1, this.rs.getString("PKTABLE_NAME"), testCase);
            assertEquals("ID", this.rs.getString("PKCOLUMN_NAME"), testCase);
            assertEquals(tableName2, this.rs.getString("FKTABLE_NAME"), testCase);
            assertEquals("FK_ID", this.rs.getString("FKCOLUMN_NAME"), testCase);
            assertEquals(1, this.rs.getInt("KEY_SEQ"), testCase);
            assertEquals(1, this.rs.getInt("UPDATE_RULE"), testCase);
            assertEquals(1, this.rs.getInt("DELETE_RULE"), testCase);
            assertEquals(constraintName, this.rs.getString("FK_NAME"), testCase);
            assertEquals(useIS ? "PRIMARY" : null, this.rs.getString("PK_NAME"), testCase);
            assertEquals(7, this.rs.getInt("DEFERRABILITY"), testCase);
            assertFalse(this.rs.next(), testCase);

            con.close();
        } while ((useIS = !useIS) || (dbTermIsSchema = !dbTermIsSchema));
    }

    /**
     * Tests fix for Bug#116113 (Bug#37063728), DatabaseMetaData#getTables doesn't work properly for delimited identifiers.
     *
     * @throws Exception
     */
    @Test
    void testBug116113() throws Exception {
        createDatabase("```db````Bug116113```");
        createTable("```db````Bug116113```.```table````Bug116113```", "(```col````Bug116113``` INT)");
        createUser("'userBug116113'@'%'", "IDENTIFIED BY 'passBug116113'");
        this.stmt.executeUpdate("GRANT SELECT on ```db````Bug116113```.```table````Bug116113``` to 'userBug116113'@'%'");
        this.stmt.executeUpdate("GRANT INSERT (```col````Bug116113```) on ```db````Bug116113```.```table````Bug116113``` to 'userBug116113'@'%'");
        createProcedure("```db````Bug116113```.```proc````Bug116113```", "(IN ```param````Bug116113``` INT) BEGIN SELECT ```param````Bug116113```; END");
        createFunction("```db````Bug116113```.```func````Bug116113```",
                "(```param````Bug116113``` INT) RETURNS INT DETERMINISTIC RETURN ```param````Bug116113```");

        boolean pedantic = false;
        boolean useIS = false;
        boolean dbIsSchema = false;
        do {
            final String searchDbNamePattern = pedantic ? "`db``Bug116113`" : "```db````Bug116113```";
            final String searchTableNamePattern = pedantic ? "`table``Bug116113`" : "```table````Bug116113```";
            final String searchColumnNamePattern = pedantic ? "`col``Bug116113`" : "```col````Bug116113```";
            final String searchProcNamePattern = pedantic ? "`proc``Bug116113`" : "```proc````Bug116113```";
            final String searchParamNamePattern = pedantic ? "`param``Bug116113`" : "```param````Bug116113```";
            final String searchFuncNamePattern = pedantic ? "`func``Bug116113`" : "```func````Bug116113```";

            Properties props = new Properties();
            props.setProperty(PropertyKey.pedantic.getKeyName(), Boolean.toString(pedantic));
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), Boolean.toString(useIS));
            props.setProperty(PropertyKey.databaseTerm.getKeyName(), dbIsSchema ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name());

            try (Connection testConn = getConnectionWithProps(props)) {
                final String testCase = String.format("Case: [pedantic: %s, useIS: %s, DbTerm: %s]", pedantic ? "Y" : "N", useIS ? "Y" : "N",
                        dbIsSchema ? "SCHEMA" : "CATALOG");
                DatabaseMetaData dbmd = testConn.getMetaData();

                boolean lowerCaseIdentifiers = dbmd.storesLowerCaseQuotedIdentifiers();

                if (dbIsSchema) {
                    this.rs = dbmd.getSchemas(searchDbNamePattern, searchDbNamePattern);
                    assertTrue(this.rs.next());
                    assertEquals(lowerCaseIdentifiers ? "`db``bug116113`" : "`db``Bug116113`", this.rs.getString("TABLE_SCHEM"));
                }

                this.rs = dbmd.getTables(searchDbNamePattern, searchDbNamePattern, searchTableNamePattern, null);
                assertTrue(this.rs.next(), testCase);
                assertEquals(lowerCaseIdentifiers ? "`table``bug116113`" : "`table``Bug116113`", this.rs.getString("TABLE_NAME"), testCase);

                this.rs = dbmd.getTablePrivileges(searchDbNamePattern, searchDbNamePattern, searchTableNamePattern);
                assertTrue(this.rs.next(), testCase);
                assertEquals(lowerCaseIdentifiers ? "`table``bug116113`" : "`table``Bug116113`", this.rs.getString("TABLE_NAME"), testCase);
                assertEquals("SELECT", this.rs.getString("PRIVILEGE"));

                this.rs = dbmd.getColumns(searchDbNamePattern, searchDbNamePattern, searchTableNamePattern, searchColumnNamePattern);
                assertTrue(this.rs.next(), testCase);
                assertEquals(lowerCaseIdentifiers ? "`table``bug116113`" : "`table``Bug116113`", this.rs.getString("TABLE_NAME"), testCase);
                assertEquals("`col``Bug116113`", this.rs.getString("COLUMN_NAME"), testCase);

                this.rs = dbmd.getColumnPrivileges(searchDbNamePattern, searchDbNamePattern, searchTableNamePattern, searchColumnNamePattern);
                assertTrue(this.rs.next(), testCase);
                assertEquals(lowerCaseIdentifiers ? "`table``bug116113`" : "`table``Bug116113`", this.rs.getString("TABLE_NAME"), testCase);
                assertEquals("`col``Bug116113`", this.rs.getString("COLUMN_NAME"), testCase);
                assertEquals("INSERT", this.rs.getString("PRIVILEGE"));

                this.rs = dbmd.getProcedures(searchDbNamePattern, searchDbNamePattern, searchProcNamePattern);
                assertTrue(this.rs.next());
                assertEquals("`proc``Bug116113`", this.rs.getString("PROCEDURE_NAME"));

                this.rs = dbmd.getProcedureColumns(searchDbNamePattern, searchDbNamePattern, searchProcNamePattern, searchParamNamePattern);
                assertTrue(this.rs.next());
                assertEquals("`proc``Bug116113`", this.rs.getString("PROCEDURE_NAME"));
                assertEquals("`param``Bug116113`", this.rs.getString("COLUMN_NAME"));

                this.rs = dbmd.getFunctions(searchDbNamePattern, searchDbNamePattern, searchFuncNamePattern);
                assertTrue(this.rs.next());
                assertEquals("`func``Bug116113`", this.rs.getString("FUNCTION_NAME"));

                this.rs = dbmd.getFunctionColumns(searchDbNamePattern, searchDbNamePattern, searchFuncNamePattern, searchParamNamePattern);
                assertTrue(this.rs.next()); // Function return.
                assertEquals("`func``Bug116113`", this.rs.getString("FUNCTION_NAME"));
                assertTrue(this.rs.next());
                assertEquals("`func``Bug116113`", this.rs.getString("FUNCTION_NAME"));
                assertEquals("`param``Bug116113`", this.rs.getString("COLUMN_NAME"));
            }
        } while ((pedantic = !pedantic) || (useIS = !useIS) || (dbIsSchema = !dbIsSchema));
    }

    /**
     * Tests fix for Bug#103437 (Bug#32807360), Syntax error when calling PreparedStatement.getMetadata() with LIMIT placeholded.
     *
     * @throws Exception
     */
    @Test
    void testBug103437() throws Exception {
        this.pstmt = this.conn.prepareStatement("SELECT ?, ? WHERE 1 = ? LIMIT ? OFFSET ?");
        ResultSetMetaData md = this.pstmt.getMetaData();
        assertEquals(2, md.getColumnCount());
        this.pstmt = this.conn.prepareStatement("SELECT ?, ? WHERE 1 = ? LIMIT ?");
        md = this.pstmt.getMetaData();
        assertEquals(2, md.getColumnCount());
        this.pstmt = this.conn.prepareStatement("SELECT ?, ? WHERE 1 = ? LIMIT ?, ?");
        md = this.pstmt.getMetaData();
        assertEquals(2, md.getColumnCount());

        createTable("testBug103437", "(id INT)");
        Properties props = new Properties();
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        try (Connection testConn = getConnectionWithProps(props)) {
            this.pstmt = testConn.prepareStatement("INSERT INTO testBug103437 VALUES (?)");
            for (int i = 1; i <= 10; i++) {
                this.pstmt.setInt(1, i);
                this.pstmt.addBatch();
            }
            this.pstmt.executeBatch();
            assertNull(this.pstmt.getMetaData());

            this.pstmt = testConn.prepareStatement("SELECT id, ? FROM testBug103437");
            for (int i = 1; i <= 10; i++) {
                this.pstmt.setInt(1, i);
                this.pstmt.addBatch();
            }
            md = this.pstmt.getMetaData();
            assertEquals(2, md.getColumnCount());
        }
    }

    /**
     * Tests fix for Bug#63992 (Bug#14598872), getTables did not return a result for table names with a dot in it.
     *
     * @throws Exception
     */
    @Test
    public void testBug63992() throws Exception {
        createDatabase("testBug63992db");
        createTable("testBug63992db.testBug63992pk", "(id INT PRIMARY KEY, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)");
        createTable("testBug63992db.testBug63992fk", "(id INT, pid INT, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
                + "FOREIGN KEY (pid) REFERENCES testBug63992db.testBug63992pk (id) ON DELETE SET NULL)");
        createTable("testBug63992db.`testBug63992db.testBug63992pk`",
                "(id INT PRIMARY KEY, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)");
        createTable("testBug63992db.`testBug63992db.testBug63992fk`", "(id INT, pid INT, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
                + "FOREIGN KEY (pid) REFERENCES testBug63992db.`testBug63992db.testBug63992pk` (id) ON DELETE SET NULL)");

        final boolean lowerCaseIdentifiers = this.conn.getMetaData().storesLowerCaseQuotedIdentifiers();

        final Map<String, String> searchAndExpectedTables = new LinkedHashMap<>();
        searchAndExpectedTables.put("testBug63992pk", lowerCaseIdentifiers ? "testbug63992pk" : "testBug63992pk");
        searchAndExpectedTables.put("`testBug63992pk`", lowerCaseIdentifiers ? "testbug63992pk" : "testBug63992pk");
        searchAndExpectedTables.put("testBug63992db.testBug63992pk", lowerCaseIdentifiers ? "testbug63992db.testbug63992pk" : "testBug63992db.testBug63992pk");
        searchAndExpectedTables.put("`testBug63992db.testBug63992pk`",
                lowerCaseIdentifiers ? "testbug63992db.testbug63992pk" : "testBug63992db.testBug63992pk");
        final String[] expectedTableNamesForPatterns = lowerCaseIdentifiers ? new String[] { "testbug63992db.testbug63992fk", "testbug63992db.testbug63992pk" }
                : new String[] { "testBug63992db.testBug63992fk", "testBug63992db.testBug63992pk" };
        final String expectedDatabase = lowerCaseIdentifiers ? "testbug63992db" : "testBug63992db";

        boolean pedantic = false;
        boolean useIS = false;
        boolean dbIsSchema = false;
        do {
            Properties props = new Properties();
            props.setProperty(PropertyKey.pedantic.getKeyName(), Boolean.toString(pedantic));
            props.setProperty(PropertyKey.useInformationSchema.getKeyName(), Boolean.toString(useIS));
            props.setProperty(PropertyKey.databaseTerm.getKeyName(), dbIsSchema ? DatabaseTerm.SCHEMA.name() : DatabaseTerm.CATALOG.name());

            try (Connection testConn = getConnectionWithProps(props)) {
                final String testCase = String.format("Case: [pedantic: %s, useIS: %s, DbTerm: %s]", pedantic ? "Y" : "N", useIS ? "Y" : "N",
                        dbIsSchema ? "SCHEMA" : "CATALOG");
                DatabaseMetaData dbmd = testConn.getMetaData();

                // Check DatabaseMetaData.getTables().
                for (Map.Entry<String, String> kv : searchAndExpectedTables.entrySet()) {
                    this.rs = dbmd.getTables(null, null, kv.getKey(), null);
                    if (pedantic && kv.getKey().startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("TABLE_SCHEM") : this.rs.getString("TABLE_CAT"), testCase);
                        assertEquals(kv.getValue(), this.rs.getString("TABLE_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }

                // Check DatabaseMetaData.getTables(), with table name pattern.
                for (String t : new String[] { "testBug63992db.%", "testBug63992db%", "`testBug63992db.%`", "`testBug63992db%`" }) {
                    this.rs = dbmd.getTables(null, null, t, null);
                    if (pedantic && t.startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("TABLE_SCHEM") : this.rs.getString("TABLE_CAT"), testCase);
                        assertEquals(expectedTableNamesForPatterns[0], this.rs.getString("TABLE_NAME"), testCase);
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("TABLE_SCHEM") : this.rs.getString("TABLE_CAT"), testCase);
                        assertEquals(expectedTableNamesForPatterns[1], this.rs.getString("TABLE_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }

                // Check DatabaseMetaData.getPrimaryKeys().
                for (Map.Entry<String, String> kv : searchAndExpectedTables.entrySet()) {
                    this.rs = dbmd.getPrimaryKeys(null, null, kv.getKey());
                    if (pedantic && kv.getKey().startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("TABLE_SCHEM") : this.rs.getString("TABLE_CAT"), testCase);
                        assertEquals(kv.getValue(), this.rs.getString("TABLE_NAME"), testCase);
                        assertEquals("id", this.rs.getString("COLUMN_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }

                // Check DatabaseMetaData.getBestRowIdentifier().
                for (Map.Entry<String, String> kv : searchAndExpectedTables.entrySet()) {
                    this.rs = dbmd.getBestRowIdentifier(null, null, kv.getKey(), DatabaseMetaData.bestRowSession, true);
                    if (pedantic && kv.getKey().startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals("id", this.rs.getString("COLUMN_NAME"), testCase);
                        assertEquals("int", this.rs.getString("TYPE_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }

                // Check DatabaseMetaData.getCrossReference().
                for (Map.Entry<String, String> kv : searchAndExpectedTables.entrySet()) {
                    this.rs = dbmd.getCrossReference(null, null, kv.getKey(), null, null, kv.getKey().replace("pk", "fk"));
                    if (pedantic && kv.getKey().startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("PKTABLE_SCHEM") : this.rs.getString("PKTABLE_CAT"), testCase);
                        assertEquals(kv.getValue(), this.rs.getString("PKTABLE_NAME"), testCase);
                        assertEquals("id", this.rs.getString("PKCOLUMN_NAME"), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("FKTABLE_SCHEM") : this.rs.getString("FKTABLE_CAT"), testCase);
                        assertEquals(kv.getValue().replace("pk", "fk"), this.rs.getString("FKTABLE_NAME"), testCase);
                        assertEquals("pid", this.rs.getString("FKCOLUMN_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }

                // Check DatabaseMetaData.getExportedKeys().
                for (Map.Entry<String, String> kv : searchAndExpectedTables.entrySet()) {
                    this.rs = dbmd.getExportedKeys(null, null, kv.getKey());
                    if (pedantic && kv.getKey().startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("PKTABLE_SCHEM") : this.rs.getString("PKTABLE_CAT"), testCase);
                        assertEquals(kv.getValue(), this.rs.getString("PKTABLE_NAME"), testCase);
                        assertEquals("id", this.rs.getString("PKCOLUMN_NAME"), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("FKTABLE_SCHEM") : this.rs.getString("FKTABLE_CAT"), testCase);
                        assertEquals(kv.getValue().replace("pk", "fk"), this.rs.getString("FKTABLE_NAME"), testCase);
                        assertEquals("pid", this.rs.getString("FKCOLUMN_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }

                // Check DatabaseMetaData.getImportedKeys().
                for (Map.Entry<String, String> kv : searchAndExpectedTables.entrySet()) {
                    this.rs = dbmd.getImportedKeys(null, null, kv.getKey().replace("pk", "fk"));
                    if (pedantic && kv.getKey().startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("PKTABLE_SCHEM") : this.rs.getString("PKTABLE_CAT"), testCase);
                        assertEquals(kv.getValue(), this.rs.getString("PKTABLE_NAME"), testCase);
                        assertEquals("id", this.rs.getString("PKCOLUMN_NAME"), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("FKTABLE_SCHEM") : this.rs.getString("FKTABLE_CAT"), testCase);
                        assertEquals(kv.getValue().replace("pk", "fk"), this.rs.getString("FKTABLE_NAME"), testCase);
                        assertEquals("pid", this.rs.getString("FKCOLUMN_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }

                // Check DatabaseMetaData.getIndexInfo().
                for (Map.Entry<String, String> kv : searchAndExpectedTables.entrySet()) {
                    this.rs = dbmd.getIndexInfo(null, null, kv.getKey(), false, false);
                    if (pedantic && kv.getKey().startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals(expectedDatabase, dbIsSchema ? this.rs.getString("TABLE_SCHEM") : this.rs.getString("TABLE_CAT"), testCase);
                        assertEquals(kv.getValue(), this.rs.getString("TABLE_NAME"), testCase);
                        assertEquals("PRIMARY", this.rs.getString("INDEX_NAME"), testCase);
                        assertEquals("id", this.rs.getString("COLUMN_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }

                // Check DatabaseMetaData.getVersionColumns().
                for (Map.Entry<String, String> kv : searchAndExpectedTables.entrySet()) {
                    this.rs = dbmd.getVersionColumns(null, null, kv.getKey());
                    if (pedantic && kv.getKey().startsWith("`")) {
                        assertFalse(this.rs.next(), testCase);
                    } else {
                        assertTrue(this.rs.next(), testCase);
                        assertEquals("ts", this.rs.getString("COLUMN_NAME"), testCase);
                        assertEquals("TIMESTAMP", this.rs.getString("TYPE_NAME"), testCase);
                        assertFalse(this.rs.next(), testCase);
                    }
                }
            }
        } while ((pedantic = !pedantic) || (useIS = !useIS) || (dbIsSchema = !dbIsSchema));
    }

}
