/*
 * Copyright (c) 2021, 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.simple;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.TimeZone;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.jdbc.JdbcStatement;

import testsuite.BaseTestCase;

public class QueryAttributesTest extends BaseTestCase {

    @BeforeEach
    public void setUp() throws Exception {
        assumeTrue(versionMeetsMinimum(8, 0, 26), "MySQL 8.0.26+ is required to run this test.");

        this.rs = this.stmt.executeQuery("SELECT * FROM mysql.component WHERE component_urn = 'file://component_query_attributes'");
        if (!this.rs.next()) {
            this.stmt.execute("INSTALL COMPONENT 'file://component_query_attributes'");
        }
    }

    @AfterEach
    public void teardown() throws Exception {
        if (versionMeetsMinimum(8, 0, 26)) {
            this.stmt.execute("UNINSTALL COMPONENT 'file://component_query_attributes'");
        }
    }

    /**
     * Tests all supported query attributes types when used in plain statements.
     *
     * @throws Exception
     */
    @Test
    public void queryAttributesTypesInPlainStatement() throws Exception {
        long testInstInMilli = 801216026987l; // Tuesday, May 23, 1995 08:00:26.987654 GMT
        long testInstInSecs = 801216026;
        int testInstHour = 8;
        int testInstMin = 0;
        int testInstSec = 26;
        int testInstNano = 987654321;
        int testOffset = 2;
        String testZoneId = "UTC+2";
        String testTimezone = "Europe/Stockholm";
        Calendar testCal = Calendar.getInstance(TimeZone.getTimeZone(testTimezone));
        testCal.setTimeInMillis(testInstInMilli);
        List<String> testList = Arrays.asList("MySQL", "Connector/J");

        String expectedLocalTime = new SimpleDateFormat("HH:mm:ss.SSS000").format(new Date(testInstInMilli));
        String expectedLocalDatetime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000").format(new Date(testInstInMilli))
                + new SimpleDateFormat("XXX").format(new Date(testInstInMilli)).replaceAll("([+-])0", "$1").replace("Z", "+0:00");

        Statement testStmt = this.conn.createStatement();

        assertTrue(JdbcStatement.class.isInstance(testStmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testStmt;
        testJdbcStmt.setAttribute("qa01", null);
        testJdbcStmt.setAttribute("qa02", "Query Attributes");
        testJdbcStmt.setAttribute("qa03", false);
        testJdbcStmt.setAttribute("qa04", (byte) 42);
        testJdbcStmt.setAttribute("qa05", (short) -42);
        testJdbcStmt.setAttribute("qa06", Integer.MAX_VALUE);
        testJdbcStmt.setAttribute("qa07", Long.MAX_VALUE);
        testJdbcStmt.setAttribute("qa08", new BigInteger("351910092110"));
        testJdbcStmt.setAttribute("qa09", 2.71828182f);
        testJdbcStmt.setAttribute("qa10", 3.141592653589793d);
        testJdbcStmt.setAttribute("qa11", new BigDecimal("1.61803398874989484820"));
        testJdbcStmt.setAttribute("qa12", new java.sql.Date(testInstInMilli));
        testJdbcStmt.setAttribute("qa13", LocalDate.of(1995, 5, 23));
        testJdbcStmt.setAttribute("qa14", new Time(testInstInMilli));
        testJdbcStmt.setAttribute("qa15", LocalTime.of(testInstHour, testInstMin, testInstSec, testInstNano));
        testJdbcStmt.setAttribute("qa16", OffsetTime.of(testInstHour, testInstMin, testInstSec, testInstNano, ZoneOffset.ofHours(testOffset)));
        testJdbcStmt.setAttribute("qa17", Duration.ofDays(-2).plusHours(2).plusMinutes(20));
        testJdbcStmt.setAttribute("qa18", LocalDateTime.ofEpochSecond(testInstInSecs, testInstNano, ZoneOffset.ofHours(testOffset)));
        testJdbcStmt.setAttribute("qa19", new Timestamp(testInstInMilli));
        testJdbcStmt.setAttribute("qa20", Instant.ofEpochMilli(testInstInMilli));
        testJdbcStmt.setAttribute("qa21", OffsetDateTime.ofInstant(Instant.ofEpochMilli(testInstInMilli), ZoneId.of(testZoneId)));
        testJdbcStmt.setAttribute("qa22", ZonedDateTime.ofInstant(Instant.ofEpochMilli(testInstInMilli), ZoneId.of(testTimezone)));
        testJdbcStmt.setAttribute("qa23", new Date(testInstInMilli));
        testJdbcStmt.setAttribute("qa24", testCal);
        testJdbcStmt.setAttribute("qa25", testList);

        this.rs = testStmt.executeQuery("SELECT 'MySQL Connector/J', " + IntStream.range(1, 26)
                .mapToObj(i -> String.format("mysql_query_attribute_string('qa%1$02d') AS qa%1$02d", i)).collect(Collectors.joining(", ")) + ", '8.0.26'");
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertNull(this.rs.getString("qa01"));
        assertTrue(this.rs.wasNull());
        assertEquals("Query Attributes", this.rs.getString("qa02"));
        assertEquals("0", this.rs.getString("qa03"));
        assertEquals("42", this.rs.getString("qa04"));
        assertEquals("-42", this.rs.getString("qa05"));
        assertEquals("2147483647", this.rs.getString("qa06"));
        assertEquals("9223372036854775807", this.rs.getString("qa07"));
        assertEquals("351910092110", this.rs.getString("qa08"));
        assertTrue(this.rs.getString("qa09").startsWith("2.71828"));
        assertTrue(this.rs.getString("qa10").startsWith("3.14159"));
        assertTrue(this.rs.getString("qa11").startsWith("1.61803"));
        assertEquals("1995-05-23", this.rs.getString("qa12"));
        assertEquals("1995-05-23", this.rs.getString("qa13"));
        assertEquals(expectedLocalTime, this.rs.getString("qa14"));
        assertEquals("08:00:26.987654", this.rs.getString("qa15"));
        assertEquals("08:00:26.987654", this.rs.getString("qa16"));
        assertEquals("-45:40:00.000000", this.rs.getString("qa17"));
        assertEquals("1995-05-23 10:00:26.987654", this.rs.getString("qa18"));
        assertEquals(expectedLocalDatetime, this.rs.getString("qa19"));
        assertEquals("1995-05-23 08:00:26.987000+0:00", this.rs.getString("qa20"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa21"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa22"));
        assertEquals(expectedLocalDatetime, this.rs.getString("qa23"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa24"));
        assertEquals("[MySQL, Connector/J]", this.rs.getString("qa25"));
        assertEquals("8.0.26", this.rs.getString(27));
        assertFalse(this.rs.next());
    }

    /**
     * Tests all supported query attributes types when used in client prepared statements.
     *
     * @throws Exception
     */
    @Test
    public void queryAttributesTypesInClientPreparedStatement() throws Exception {
        long testInstInMilli = 801216026987l; // Tuesday, May 23, 1995 08:00:26.987654 GMT
        long testInstInSecs = 801216026;
        int testInstHour = 8;
        int testInstMin = 0;
        int testInstSec = 26;
        int testInstNano = 987654321;
        int testOffset = 2;
        String testZoneId = "UTC+2";
        String testTimezone = "Europe/Stockholm";
        Calendar testCal = Calendar.getInstance(TimeZone.getTimeZone(testTimezone));
        testCal.setTimeInMillis(testInstInMilli);
        List<String> testList = Arrays.asList("MySQL", "Connector/J");

        String expectedLocalTime = new SimpleDateFormat("HH:mm:ss.SSS000").format(new Date(testInstInMilli));
        String expectedLocalDatetime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000").format(new Date(testInstInMilli))
                + new SimpleDateFormat("XXX").format(new Date(testInstInMilli)).replaceAll("([+-])0", "$1").replace("Z", "+0:00");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
        Connection testConn = getConnectionWithProps(props);

        PreparedStatement testPstmt = testConn.prepareStatement("SELECT ?, " + IntStream.range(1, 26)
                .mapToObj(i -> String.format("mysql_query_attribute_string('qa%1$02d') AS qa%1$02d", i)).collect(Collectors.joining(", ")) + ", ?");
        testPstmt.setString(1, "MySQL Connector/J");
        testPstmt.setString(2, "8.0.26");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa01", null);
        testJdbcStmt.setAttribute("qa02", "Query Attributes");
        testJdbcStmt.setAttribute("qa03", false);
        testJdbcStmt.setAttribute("qa04", (byte) 42);
        testJdbcStmt.setAttribute("qa05", (short) -42);
        testJdbcStmt.setAttribute("qa06", Integer.MAX_VALUE);
        testJdbcStmt.setAttribute("qa07", Long.MAX_VALUE);
        testJdbcStmt.setAttribute("qa08", new BigInteger("351910092110"));
        testJdbcStmt.setAttribute("qa09", 2.71828182f);
        testJdbcStmt.setAttribute("qa10", 3.141592653589793d);
        testJdbcStmt.setAttribute("qa11", new BigDecimal("1.61803398874989484820"));
        testJdbcStmt.setAttribute("qa12", new java.sql.Date(testInstInMilli));
        testJdbcStmt.setAttribute("qa13", LocalDate.of(1995, 5, 23));
        testJdbcStmt.setAttribute("qa14", new Time(testInstInMilli));
        testJdbcStmt.setAttribute("qa15", LocalTime.of(testInstHour, testInstMin, testInstSec, testInstNano));
        testJdbcStmt.setAttribute("qa16", OffsetTime.of(testInstHour, testInstMin, testInstSec, testInstNano, ZoneOffset.ofHours(testOffset)));
        testJdbcStmt.setAttribute("qa17", Duration.ofDays(-2).plusHours(2).plusMinutes(20));
        testJdbcStmt.setAttribute("qa18", LocalDateTime.ofEpochSecond(testInstInSecs, testInstNano, ZoneOffset.ofHours(testOffset)));
        testJdbcStmt.setAttribute("qa19", new Timestamp(testInstInMilli));
        testJdbcStmt.setAttribute("qa20", Instant.ofEpochMilli(testInstInMilli));
        testJdbcStmt.setAttribute("qa21", OffsetDateTime.ofInstant(Instant.ofEpochMilli(testInstInMilli), ZoneId.of(testZoneId)));
        testJdbcStmt.setAttribute("qa22", ZonedDateTime.ofInstant(Instant.ofEpochMilli(testInstInMilli), ZoneId.of(testTimezone)));
        testJdbcStmt.setAttribute("qa23", new Date(testInstInMilli));
        testJdbcStmt.setAttribute("qa24", testCal);
        testJdbcStmt.setAttribute("qa25", testList);

        this.rs = testPstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertNull(this.rs.getString("qa01"));
        assertTrue(this.rs.wasNull());
        assertEquals("Query Attributes", this.rs.getString("qa02"));
        assertEquals("0", this.rs.getString("qa03"));
        assertEquals("42", this.rs.getString("qa04"));
        assertEquals("-42", this.rs.getString("qa05"));
        assertEquals("2147483647", this.rs.getString("qa06"));
        assertEquals("9223372036854775807", this.rs.getString("qa07"));
        assertEquals("351910092110", this.rs.getString("qa08"));
        assertTrue(this.rs.getString("qa09").startsWith("2.71828"));
        assertTrue(this.rs.getString("qa10").startsWith("3.14159"));
        assertTrue(this.rs.getString("qa11").startsWith("1.61803"));
        assertEquals("1995-05-23", this.rs.getString("qa12"));
        assertEquals("1995-05-23", this.rs.getString("qa13"));
        assertEquals(expectedLocalTime, this.rs.getString("qa14"));
        assertEquals("08:00:26.987654", this.rs.getString("qa15"));
        assertEquals("08:00:26.987654", this.rs.getString("qa16"));
        assertEquals("-45:40:00.000000", this.rs.getString("qa17"));
        assertEquals("1995-05-23 10:00:26.987654", this.rs.getString("qa18"));
        assertEquals(expectedLocalDatetime, this.rs.getString("qa19"));
        assertEquals("1995-05-23 08:00:26.987000+0:00", this.rs.getString("qa20"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa21"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa22"));
        assertEquals(expectedLocalDatetime, this.rs.getString("qa23"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa24"));
        assertEquals("[MySQL, Connector/J]", this.rs.getString("qa25"));
        assertEquals("8.0.26", this.rs.getString(27));
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests all supported query attributes types when used in server prepared statements.
     *
     * @throws Exception
     */
    @Test
    public void queryAttributesTypesInServerPreparedStatement() throws Exception {
        long testInstInMilli = 801216026987l; // Tuesday, May 23, 1995 08:00:26.987654 GMT
        long testInstInSecs = 801216026;
        int testInstHour = 8;
        int testInstMin = 0;
        int testInstSec = 26;
        int testInstNano = 987654321;
        int testOffset = 2;
        String testZoneId = "UTC+2";
        String testTimezone = "Europe/Stockholm";
        Calendar testCal = Calendar.getInstance(TimeZone.getTimeZone(testTimezone));
        testCal.setTimeInMillis(testInstInMilli);
        List<String> testList = Arrays.asList("MySQL", "Connector/J");

        String expectedLocalTime = new SimpleDateFormat("HH:mm:ss.SSS000").format(new Date(testInstInMilli));
        String expectedLocalDatetime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000").format(new Date(testInstInMilli))
                + new SimpleDateFormat("XXX").format(new Date(testInstInMilli)).replaceAll("([+-])0", "$1").replace("Z", "+0:00");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        PreparedStatement testPstmt = testConn.prepareStatement("SELECT ?, " + IntStream.range(1, 26)
                .mapToObj(i -> String.format("mysql_query_attribute_string('qa%1$02d') AS qa%1$02d", i)).collect(Collectors.joining(", ")) + ", ?");
        testPstmt.setString(1, "MySQL Connector/J");
        testPstmt.setString(2, "8.0.26");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa01", null);
        testJdbcStmt.setAttribute("qa02", "Query Attributes");
        testJdbcStmt.setAttribute("qa03", false);
        testJdbcStmt.setAttribute("qa04", (byte) 42);
        testJdbcStmt.setAttribute("qa05", (short) -42);
        testJdbcStmt.setAttribute("qa06", Integer.MAX_VALUE);
        testJdbcStmt.setAttribute("qa07", Long.MAX_VALUE);
        testJdbcStmt.setAttribute("qa08", new BigInteger("351910092110"));
        testJdbcStmt.setAttribute("qa09", 2.71828182f);
        testJdbcStmt.setAttribute("qa10", 3.141592653589793d);
        testJdbcStmt.setAttribute("qa11", new BigDecimal("1.61803398874989484820"));
        testJdbcStmt.setAttribute("qa12", new java.sql.Date(testInstInMilli));
        testJdbcStmt.setAttribute("qa13", LocalDate.of(1995, 5, 23));
        testJdbcStmt.setAttribute("qa14", new Time(testInstInMilli));
        testJdbcStmt.setAttribute("qa15", LocalTime.of(testInstHour, testInstMin, testInstSec, testInstNano));
        testJdbcStmt.setAttribute("qa16", OffsetTime.of(testInstHour, testInstMin, testInstSec, testInstNano, ZoneOffset.ofHours(testOffset)));
        testJdbcStmt.setAttribute("qa17", Duration.ofDays(-2).plusHours(2).plusMinutes(20));
        testJdbcStmt.setAttribute("qa18", LocalDateTime.ofEpochSecond(testInstInSecs, testInstNano, ZoneOffset.ofHours(testOffset)));
        testJdbcStmt.setAttribute("qa19", new Timestamp(testInstInMilli));
        testJdbcStmt.setAttribute("qa20", Instant.ofEpochMilli(testInstInMilli));
        testJdbcStmt.setAttribute("qa21", OffsetDateTime.ofInstant(Instant.ofEpochMilli(testInstInMilli), ZoneId.of(testZoneId)));
        testJdbcStmt.setAttribute("qa22", ZonedDateTime.ofInstant(Instant.ofEpochMilli(testInstInMilli), ZoneId.of(testTimezone)));
        testJdbcStmt.setAttribute("qa23", new Date(testInstInMilli));
        testJdbcStmt.setAttribute("qa24", testCal);
        testJdbcStmt.setAttribute("qa25", testList);

        this.rs = testPstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertNull(this.rs.getString("qa01"));
        assertTrue(this.rs.wasNull());
        assertEquals("Query Attributes", this.rs.getString("qa02"));
        assertEquals("0", this.rs.getString("qa03"));
        assertEquals("42", this.rs.getString("qa04"));
        assertEquals("-42", this.rs.getString("qa05"));
        assertEquals("2147483647", this.rs.getString("qa06"));
        assertEquals("9223372036854775807", this.rs.getString("qa07"));
        assertEquals("351910092110", this.rs.getString("qa08"));
        assertTrue(this.rs.getString("qa09").startsWith("2.71828"));
        assertTrue(this.rs.getString("qa10").startsWith("3.14159"));
        assertTrue(this.rs.getString("qa11").startsWith("1.61803"));
        assertEquals("1995-05-23", this.rs.getString("qa12"));
        assertEquals("1995-05-23", this.rs.getString("qa13"));
        assertEquals(expectedLocalTime, this.rs.getString("qa14"));
        assertEquals("08:00:26.987654", this.rs.getString("qa15"));
        assertEquals("08:00:26.987654", this.rs.getString("qa16"));
        assertEquals("-45:40:00.000000", this.rs.getString("qa17"));
        assertEquals("1995-05-23 10:00:26.987654", this.rs.getString("qa18"));
        assertEquals(expectedLocalDatetime, this.rs.getString("qa19"));
        assertEquals("1995-05-23 08:00:26.987000+0:00", this.rs.getString("qa20"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa21"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa22"));
        assertEquals(expectedLocalDatetime, this.rs.getString("qa23"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa24"));
        assertEquals("[MySQL, Connector/J]", this.rs.getString("qa25"));
        assertEquals("8.0.26", this.rs.getString(27));
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests all supported query attributes types when used in callable statements.
     *
     * @throws Exception
     */
    @Test
    public void queryAttributesTypesInCallableStatement() throws Exception {
        long testInstInMilli = 801216026987l; // Tuesday, May 23, 1995 08:00:26.987654 GMT
        long testInstInSecs = 801216026;
        int testInstHour = 8;
        int testInstMin = 0;
        int testInstSec = 26;
        int testInstNano = 987654321;
        int testOffset = 2;
        String testZoneId = "UTC+2";
        String testTimezone = "Europe/Stockholm";
        Calendar testCal = Calendar.getInstance(TimeZone.getTimeZone(testTimezone));
        testCal.setTimeInMillis(testInstInMilli);
        List<String> testList = Arrays.asList("MySQL", "Connector/J");

        String expectedLocalTime = new SimpleDateFormat("HH:mm:ss.SSS000").format(new Date(testInstInMilli));
        String expectedLocalDatetime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS000").format(new Date(testInstInMilli))
                + new SimpleDateFormat("XXX").format(new Date(testInstInMilli)).replaceAll("([+-])0", "$1").replace("Z", "+0:00");

        createProcedure("testQueryAttrTypes",
                "(IN p1 VARCHAR(100), IN p2 VARCHAR(100)) BEGIN SELECT p1, " + IntStream.range(1, 26)
                        .mapToObj(i -> String.format("mysql_query_attribute_string('qa%1$02d') AS qa%1$02d", i)).collect(Collectors.joining(", "))
                        + ", p2; END");

        PreparedStatement testCstmt = this.conn.prepareCall("{ CALL testQueryAttrTypes(?, ?) }");
        testCstmt.setString(1, "MySQL Connector/J");
        testCstmt.setString(2, "8.0.26");

        assertTrue(JdbcStatement.class.isInstance(testCstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testCstmt;
        testJdbcStmt.setAttribute("qa01", null);
        testJdbcStmt.setAttribute("qa02", "Query Attributes");
        testJdbcStmt.setAttribute("qa03", false);
        testJdbcStmt.setAttribute("qa04", (byte) 42);
        testJdbcStmt.setAttribute("qa05", (short) -42);
        testJdbcStmt.setAttribute("qa06", Integer.MAX_VALUE);
        testJdbcStmt.setAttribute("qa07", Long.MAX_VALUE);
        testJdbcStmt.setAttribute("qa08", new BigInteger("351910092110"));
        testJdbcStmt.setAttribute("qa09", 2.71828182f);
        testJdbcStmt.setAttribute("qa10", 3.141592653589793d);
        testJdbcStmt.setAttribute("qa11", new BigDecimal("1.61803398874989484820"));
        testJdbcStmt.setAttribute("qa12", new java.sql.Date(testInstInMilli));
        testJdbcStmt.setAttribute("qa13", LocalDate.of(1995, 5, 23));
        testJdbcStmt.setAttribute("qa14", new Time(testInstInMilli));
        testJdbcStmt.setAttribute("qa15", LocalTime.of(testInstHour, testInstMin, testInstSec, testInstNano));
        testJdbcStmt.setAttribute("qa16", OffsetTime.of(testInstHour, testInstMin, testInstSec, testInstNano, ZoneOffset.ofHours(testOffset)));
        testJdbcStmt.setAttribute("qa17", Duration.ofDays(-2).plusHours(2).plusMinutes(20));
        testJdbcStmt.setAttribute("qa18", LocalDateTime.ofEpochSecond(testInstInSecs, testInstNano, ZoneOffset.ofHours(testOffset)));
        testJdbcStmt.setAttribute("qa19", new Timestamp(testInstInMilli));
        testJdbcStmt.setAttribute("qa20", Instant.ofEpochMilli(testInstInMilli));
        testJdbcStmt.setAttribute("qa21", OffsetDateTime.ofInstant(Instant.ofEpochMilli(testInstInMilli), ZoneId.of(testZoneId)));
        testJdbcStmt.setAttribute("qa22", ZonedDateTime.ofInstant(Instant.ofEpochMilli(testInstInMilli), ZoneId.of(testTimezone)));
        testJdbcStmt.setAttribute("qa23", new Date(testInstInMilli));
        testJdbcStmt.setAttribute("qa24", testCal);
        testJdbcStmt.setAttribute("qa25", testList);

        this.rs = testCstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertNull(this.rs.getString("qa01"));
        assertTrue(this.rs.wasNull());
        assertEquals("Query Attributes", this.rs.getString("qa02"));
        assertEquals("0", this.rs.getString("qa03"));
        assertEquals("42", this.rs.getString("qa04"));
        assertEquals("-42", this.rs.getString("qa05"));
        assertEquals("2147483647", this.rs.getString("qa06"));
        assertEquals("9223372036854775807", this.rs.getString("qa07"));
        assertEquals("351910092110", this.rs.getString("qa08"));
        assertTrue(this.rs.getString("qa09").startsWith("2.71828"));
        assertTrue(this.rs.getString("qa10").startsWith("3.14159"));
        assertTrue(this.rs.getString("qa11").startsWith("1.61803"));
        assertEquals("1995-05-23", this.rs.getString("qa12"));
        assertEquals("1995-05-23", this.rs.getString("qa13"));
        assertEquals(expectedLocalTime, this.rs.getString("qa14"));
        assertEquals("08:00:26.987654", this.rs.getString("qa15"));
        assertEquals("08:00:26.987654", this.rs.getString("qa16"));
        assertEquals("-45:40:00.000000", this.rs.getString("qa17"));
        assertEquals("1995-05-23 10:00:26.987654", this.rs.getString("qa18"));
        assertEquals(expectedLocalDatetime, this.rs.getString("qa19"));
        assertEquals("1995-05-23 08:00:26.987000+0:00", this.rs.getString("qa20"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa21"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa22"));
        assertEquals(expectedLocalDatetime, this.rs.getString("qa23"));
        assertEquals("1995-05-23 10:00:26.987000+2:00", this.rs.getString("qa24"));
        assertEquals("[MySQL, Connector/J]", this.rs.getString("qa25"));
        assertEquals("8.0.26", this.rs.getString(27));
        assertFalse(this.rs.next());
    }

    /**
     * Tests if query attributes are preserved between plain statement executions and cleared after calling the 'clearAttributes' method.
     *
     * @throws Exception
     */
    @Test
    public void preserveAndClearAttributesInPlainStatement() throws Exception {
        Statement testStmt = this.conn.createStatement();

        assertTrue(JdbcStatement.class.isInstance(testStmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testStmt;
        testJdbcStmt.setAttribute("qa", "8.0.26");

        for (int c = 0; c < 2; c++) {
            this.rs = testStmt.executeQuery("SELECT 'MySQL Connector/J', mysql_query_attribute_string('qa') AS qa");
            assertTrue(this.rs.next());
            assertEquals("MySQL Connector/J", this.rs.getString(1));
            assertEquals("8.0.26", this.rs.getString("qa"));
            assertFalse(this.rs.next());
        } // Execute twice. Query Attributes must be preserved.

        testJdbcStmt.clearAttributes();

        this.rs = testStmt.executeQuery("SELECT 'MySQL Connector/J', mysql_query_attribute_string('qa') AS qa");
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertNull(this.rs.getString("qa"));
        assertTrue(this.rs.wasNull());
        assertFalse(this.rs.next());
    }

    /**
     * Tests if query attributes are preserved between client prepared statement executions and cleared after calling the 'clearAttributes' method.
     *
     * @throws Exception
     */
    @Test
    public void preserveAndClearAttributesInClientPreparedStatement() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
        Connection testConn = getConnectionWithProps(props);

        PreparedStatement testPstmt = testConn.prepareStatement("SELECT ?, mysql_query_attribute_string('qa') AS qa");
        testPstmt.setString(1, "MySQL Connector/J");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa", "8.0.26");

        for (int c = 0; c < 2; c++) {
            this.rs = testPstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals("MySQL Connector/J", this.rs.getString(1));
            assertEquals("8.0.26", this.rs.getString("qa"));
            assertFalse(this.rs.next());
        } // Execute twice. Query Attributes must be preserved.

        testJdbcStmt.clearAttributes();

        this.rs = testPstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertNull(this.rs.getString("qa"));
        assertTrue(this.rs.wasNull());
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests if query attributes are preserved between server prepared statement executions and cleared after calling the 'clearAttributes' method.
     *
     * @throws Exception
     */
    @Test
    public void preserveAndClearAttributesInServerPreparedStatement() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        PreparedStatement testPstmt = testConn.prepareStatement("SELECT ?, mysql_query_attribute_string('qa') AS qa");
        testPstmt.setString(1, "MySQL Connector/J");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa", "8.0.26");

        for (int c = 0; c < 2; c++) {
            this.rs = testPstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals("MySQL Connector/J", this.rs.getString(1));
            assertEquals("8.0.26", this.rs.getString("qa"));
            assertFalse(this.rs.next());
        } // Execute twice. Query Attributes must be preserved.

        testJdbcStmt.clearAttributes();

        this.rs = testPstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertNull(this.rs.getString("qa"));
        assertTrue(this.rs.wasNull());
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests if query attributes are preserved between callable statement executions and cleared after calling the 'clearAttributes' method.
     *
     * @throws Exception
     */
    @Test
    public void preserveAndClearAttributesInCallableStatement() throws Exception {
        createProcedure("testQueryAttrPreserveAndClear", "(IN p1 VARCHAR(100)) BEGIN SELECT p1, mysql_query_attribute_string('qa') AS qa; END");

        PreparedStatement testCstmt = this.conn.prepareCall("{ CALL testQueryAttrPreserveAndClear(?) }");
        testCstmt.setString(1, "MySQL Connector/J");

        assertTrue(JdbcStatement.class.isInstance(testCstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testCstmt;
        testJdbcStmt.setAttribute("qa", "8.0.26");

        for (int c = 0; c < 2; c++) {
            this.rs = testCstmt.executeQuery();
            assertTrue(this.rs.next());
            assertEquals("MySQL Connector/J", this.rs.getString(1));
            assertEquals("8.0.26", this.rs.getString("qa"));
            assertFalse(this.rs.next());
        } // Execute twice. Query Attributes must be preserved.

        testJdbcStmt.clearAttributes();

        this.rs = testCstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertNull(this.rs.getString("qa"));
        assertTrue(this.rs.wasNull());
        assertFalse(this.rs.next());
    }

    /**
     * Tests if query attributes hold in plain statements with multi-queries.
     *
     * @throws Exception
     */
    @Test
    public void multiQueriesWithAttributesInPlainStatement() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        Statement testStmt = testConn.createStatement();

        assertTrue(JdbcStatement.class.isInstance(testStmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testStmt;
        testJdbcStmt.setAttribute("qa01", "MySQL Connector/J");
        testJdbcStmt.setAttribute("qa02", "8.0.26");

        this.rs = testStmt.executeQuery("SELECT mysql_query_attribute_string('qa01') AS qa01; SELECT mysql_query_attribute_string('qa02') AS qa02;");
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString("qa01"));
        assertFalse(this.rs.next());

        assertTrue(testStmt.getMoreResults());
        this.rs = testStmt.getResultSet();
        assertTrue(this.rs.next());
        assertEquals("8.0.26", this.rs.getString("qa02"));
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests if query attributes hold in prepared statements with multi-queries.
     *
     * @throws Exception
     */
    @Test
    public void multiQueriesWithAttributesInPreparedStatement() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true"); // Will fall-back to client prepared statement, anyway.
        props.setProperty(PropertyKey.allowMultiQueries.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        PreparedStatement testPstmt = testConn
                .prepareStatement("SELECT mysql_query_attribute_string('qa01') AS qa01; SELECT mysql_query_attribute_string('qa02') AS qa02;");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa01", "MySQL Connector/J");
        testJdbcStmt.setAttribute("qa02", "8.0.26");

        this.rs = testPstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString("qa01"));
        assertFalse(this.rs.next());

        assertTrue(testPstmt.getMoreResults());
        this.rs = testPstmt.getResultSet();
        assertTrue(this.rs.next());
        assertEquals("8.0.26", this.rs.getString("qa02"));
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests whether the query attributes are propagated to the internally created statement on query rewrites in plain statements.
     *
     * @throws Exception
     */
    @Test
    void rewriteQueriesWithAttributesInPlainStatement() throws Exception {
        createTable("testRewritePlainStmt", "(c1 VARCHAR(100), c2 VARCHAR(100))");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        Statement testStmt = testConn.createStatement();

        assertTrue(JdbcStatement.class.isInstance(testStmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testStmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        testStmt.addBatch("INSERT INTO testRewritePlainStmt VALUES ('Row 1', mysql_query_attribute_string('qa'))");
        testStmt.addBatch("INSERT INTO testRewritePlainStmt VALUES ('Row 2', mysql_query_attribute_string('qa'))");
        testStmt.addBatch("INSERT INTO testRewritePlainStmt VALUES ('Row 3', mysql_query_attribute_string('qa'))");
        testStmt.addBatch("INSERT INTO testRewritePlainStmt VALUES ('Row 4', mysql_query_attribute_string('qa'))");
        testStmt.addBatch("INSERT INTO testRewritePlainStmt VALUES ('Row 5', mysql_query_attribute_string('qa'))");
        testStmt.executeBatch(); // Need 5 to rewrite as multi-queries.

        this.rs = this.stmt.executeQuery("SELECT * FROM testRewritePlainStmt");
        assertTrue(this.rs.next());
        assertEquals("Row 1", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 2", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 3", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 4", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 5", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests whether the query attributes are propagated to the internally created statement on query rewrites in client prepared statements.
     *
     * @throws Exception
     */
    @Test
    void rewriteQueriesWithAttributesInClientPreparedStatement() throws Exception {
        createTable("testRewriteClientPstmt", "(c1 VARCHAR(100), c2 VARCHAR(100))");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "false");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        PreparedStatement testPstmt = testConn.prepareStatement("INSERT INTO testRewriteClientPstmt VALUES (?, mysql_query_attribute_string('qa'))");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        testPstmt.setString(1, "Row 1");
        testPstmt.addBatch();
        testPstmt.setString(1, "Row 2");
        testPstmt.addBatch();
        testPstmt.setString(1, "Row 3");
        testPstmt.addBatch();
        testPstmt.setString(1, "Row 4");
        testPstmt.addBatch();
        testPstmt.setString(1, "Row 5");
        testPstmt.addBatch();
        testPstmt.executeBatch();

        this.rs = this.stmt.executeQuery("SELECT * FROM testRewriteClientPstmt");
        assertTrue(this.rs.next());
        assertEquals("Row 1", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 2", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 3", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 4", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 5", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests whether the query attributes are propagated to the internally created statement on query rewrites in server prepared statements.
     *
     * @throws Exception
     */
    @Test
    void rewriteQueriesWithAttributesInServerPreparedStatement() throws Exception {
        createTable("testRewriteServerPstmt", "(c1 VARCHAR(100), c2 VARCHAR(100))");

        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.rewriteBatchedStatements.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        PreparedStatement testPstmt = testConn.prepareStatement("INSERT INTO testRewriteServerPstmt VALUES (?, mysql_query_attribute_string('qa'))");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        testPstmt.setString(1, "Row 1");
        testPstmt.addBatch();
        testPstmt.setString(1, "Row 2");
        testPstmt.addBatch();
        testPstmt.setString(1, "Row 3");
        testPstmt.addBatch();
        testPstmt.setString(1, "Row 4");
        testPstmt.addBatch();
        testPstmt.setString(1, "Row 5");
        testPstmt.addBatch();
        testPstmt.executeBatch();

        this.rs = this.stmt.executeQuery("SELECT * FROM testRewriteServerPstmt");
        assertTrue(this.rs.next());
        assertEquals("Row 1", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 2", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 3", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 4", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertTrue(this.rs.next());
        assertEquals("Row 5", this.rs.getString(1));
        assertEquals("MySQL Connector/J", this.rs.getString(2));
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests if server prepared statements get their query attributes cleared automatically when cached.
     *
     * @throws Exception
     */
    @Test
    void cachedServerPreparedStatementsWithQueryAttributes() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");
        props.setProperty(PropertyKey.cachePrepStmts.getKeyName(), "true");
        Connection testConn = getConnectionWithProps(props);

        PreparedStatement testPstmt1 = testConn.prepareStatement("SELECT ?, mysql_query_attribute_string('qa')");
        testPstmt1.setString(1, "Param 1");

        assertTrue(JdbcStatement.class.isInstance(testPstmt1));
        JdbcStatement testJdbcStmt1 = (JdbcStatement) testPstmt1;
        testJdbcStmt1.setAttribute("qa", "QA 1");

        this.rs = testPstmt1.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("Param 1", this.rs.getString(1));
        assertEquals("QA 1", this.rs.getString(2));
        assertFalse(this.rs.next());

        testPstmt1.close();

        PreparedStatement testPstmt2 = testConn.prepareStatement("SELECT ?, mysql_query_attribute_string('qa')");
        assertSame(testPstmt1, testPstmt2);
        testPstmt2.setString(1, "Param 2");

        assertTrue(JdbcStatement.class.isInstance(testPstmt2));
        JdbcStatement testJdbcStmt2 = (JdbcStatement) testPstmt2;
        testJdbcStmt2.setAttribute("qa", "QA 2");

        this.rs = testPstmt2.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("Param 2", this.rs.getString(1));
        assertEquals("QA 2", this.rs.getString(2));
        assertFalse(this.rs.next());

        testPstmt2.close();

        PreparedStatement testPstmt3 = testConn.prepareStatement("SELECT ?, mysql_query_attribute_string('qa')");
        assertSame(testPstmt1, testPstmt3);
        testPstmt3.setString(1, "Param 3");

        assertTrue(JdbcStatement.class.isInstance(testPstmt3));
        JdbcStatement testJdbcStmt3 = (JdbcStatement) testPstmt3;
        testJdbcStmt3.setAttribute("qa_new", "QA 3"); // Don't set or set different query attribute.

        this.rs = testPstmt3.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("Param 3", this.rs.getString(1));
        assertNull(this.rs.getString(2));
        assertTrue(this.rs.wasNull());
        assertFalse(this.rs.next());

        testPstmt3.close();

        testConn.close();
    }

    /**
     * Tests whether proxied plain statement objects created in multi-host connections handle query attributes correctly.
     *
     * @throws Exception
     */
    @Test
    void plainStatementWithQueryAttributesInMultiHost() throws Exception {
        // Failover connection.
        Connection testConn = getFailoverConnection();

        Statement testStmt = testConn.createStatement();

        assertTrue(JdbcStatement.class.isInstance(testStmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testStmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        this.rs = testStmt.executeQuery("SELECT mysql_query_attribute_string('qa')");
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertFalse(this.rs.next());

        testConn.close();

        // Loadbalanced connection.
        testConn = getLoadBalancedConnection();

        testStmt = testConn.createStatement();

        assertTrue(JdbcStatement.class.isInstance(testStmt));
        testJdbcStmt = (JdbcStatement) testStmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        this.rs = testStmt.executeQuery("SELECT mysql_query_attribute_string('qa')");
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertFalse(this.rs.next());

        testConn.close();

        // Replication connection.
        testConn = getSourceReplicaReplicationConnection();

        testStmt = testConn.createStatement();

        assertTrue(JdbcStatement.class.isInstance(testStmt));
        testJdbcStmt = (JdbcStatement) testStmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        this.rs = testStmt.executeQuery("SELECT mysql_query_attribute_string('qa')");
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertFalse(this.rs.next());

        testConn.close();
    }

    /**
     * Tests whether proxied server prepared statement objects created in multi-host connections handle query attributes correctly.
     *
     * @throws Exception
     */
    @Test
    void serverPreparedStatementWithQueryAttributesInMultiHost() throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.sslMode.getKeyName(), "DISABLED");
        props.setProperty(PropertyKey.allowPublicKeyRetrieval.getKeyName(), "true");
        props.setProperty(PropertyKey.useServerPrepStmts.getKeyName(), "true");

        // Failover connection.
        Connection testConn = getFailoverConnection(props);

        PreparedStatement testPstmt = testConn.prepareStatement("SELECT mysql_query_attribute_string('qa')");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        JdbcStatement testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        this.rs = testPstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertFalse(this.rs.next());

        testConn.close();

        // Loadbalanced connection.
        testConn = getLoadBalancedConnection(props);

        testPstmt = testConn.prepareStatement("SELECT mysql_query_attribute_string('qa')");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        this.rs = testPstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertFalse(this.rs.next());

        testConn.close();

        // Replication connection.
        testConn = getSourceReplicaReplicationConnection(props);

        testPstmt = testConn.prepareStatement("SELECT mysql_query_attribute_string('qa')");

        assertTrue(JdbcStatement.class.isInstance(testPstmt));
        testJdbcStmt = (JdbcStatement) testPstmt;
        testJdbcStmt.setAttribute("qa", "MySQL Connector/J");

        this.rs = testPstmt.executeQuery();
        assertTrue(this.rs.next());
        assertEquals("MySQL Connector/J", this.rs.getString(1));
        assertFalse(this.rs.next());

        testConn.close();
    }

}
